diff options
Diffstat (limited to 'iosApp/iosApp/Map/MapView.swift')
-rw-r--r-- | iosApp/iosApp/Map/MapView.swift | 313 |
1 files changed, 307 insertions, 6 deletions
diff --git a/iosApp/iosApp/Map/MapView.swift b/iosApp/iosApp/Map/MapView.swift index a34bf63..572db64 100644 --- a/iosApp/iosApp/Map/MapView.swift +++ b/iosApp/iosApp/Map/MapView.swift @@ -16,14 +16,315 @@ * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import SwiftUI +import Combine +import WhirlyGlobeMaplyComponent import shared -struct MapView: View { - @StateObject var unitsViewModel: UnitsViewModel +struct MapView: UIViewControllerRepresentable { + typealias UIViewControllerType = OurMaplyViewController - var body: some View { - MapWrapperView(layer: $unitsViewModel.mapLayerType, - markers: $unitsViewModel.markers, - markerCallback: unitsViewModel.selectUnitWith) + @Binding var mapLayer: MapLayer + @Binding var markers: [Marker] + var markerCallback: ((Int32?) -> Void)? + var link: BaseMapLink + + class Coordinator: NSObject, MaplyViewControllerDelegate { + var parent: MapView + var uiViewController: OurMaplyViewController? + var loader: MaplyQuadImageLoader? = nil + + // Source: https://stackoverflow.com/questions/65923718 + var cancellable: AnyCancellable? + var link: BaseMapLink? { + didSet { + cancellable = link?.$action.sink(receiveValue: { action in + guard let action = action else { + return + } + self.uiViewController?.action(action) + }) + } + } + + init(_ uiViewController: MapView) { + self.parent = uiViewController + } + + func maplyViewController(_ viewC: MaplyViewController, + didTapAt coord: MaplyCoordinate) { + if let callback = parent.markerCallback { + callback(nil) + } + } + + func maplyViewController(_ viewC: MaplyViewController, + didSelect selectedObj: NSObject, + atLoc coord: MaplyCoordinate, + onScreen screenPt: CGPoint) { + if let marker = selectedObj as? MaplyScreenMarker { + if let id = marker.userObject as? Int32 { + if let callback = parent.markerCallback { + callback(id) + } + } + } + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + func makeUIViewController(context: Context) -> OurMaplyViewController { + let mapViewController = OurMaplyViewController(mapType: .typeFlat) + mapViewController.delegate = context.coordinator + + let tileInfo = Utils.tileInfoFrom(layer: mapLayer) + mapViewController.setZoomLimits(minZoom: mapLayer.minZoom, + maxZoom: mapLayer.maxZoom) + + let sampleParams = MaplySamplingParams() + sampleParams.coordSys = MaplySphericalMercator(webStandard: ()) + sampleParams.coverPoles = true + sampleParams.edgeMatching = true + sampleParams.minZoom = tileInfo.minZoom + sampleParams.maxZoom = tileInfo.maxZoom + sampleParams.singleLevel = true + sampleParams.maxTiles = 25 + + let loader = MaplyQuadImageLoader(params: sampleParams, tileInfo: tileInfo, viewC: mapViewController) + loader?.baseDrawPriority = kMaplyImageLayerDrawPriorityDefault + loader?.imageFormat = .imageUShort565 + context.coordinator.loader = loader + + DispatchQueue.main.async { + let point = MaplyCoordinateMakeWithDegrees(-100.36, 23.191) + mapViewController.focusOn(point: point, height: 0.4, animated: false) + } + + return mapViewController + } + + func updateUIViewController(_ uiViewController: OurMaplyViewController, context: Context) { + context.coordinator.uiViewController = uiViewController + context.coordinator.link = link + + // MARK: - Set map layer + context.coordinator.loader?.changeTileInfo(Utils.tileInfoFrom(layer: mapLayer)) + uiViewController.setZoomLimits(minZoom: mapLayer.minZoom, + maxZoom: mapLayer.maxZoom) + + // MARK: - Set markers + uiViewController.display(markers: markers, + isReport: false) + } + + static func dismantleUIViewController(_ uiViewController: MaplyViewController, coordinator: Coordinator) { + coordinator.loader?.shutdown() + uiViewController.teardown() + } +} + +class OurMaplyViewController: MaplyViewController { + enum Action { + case zoomIn + case zoomOut + } + + private var objects = [MaplyComponentObject]() + private var geofenceObjects = [MaplyComponentObject]() + + func action(_ action: Action) { + DispatchQueue.main.async { + switch action { + case .zoomIn: + self.zoomIn() + case .zoomOut: + self.zoomOut() + } + } + } + + func focusOn(point: MaplyCoordinate, + height: Float = 0.0000264, + animated: Bool = true) { + let z = max(height, getMinZoom()) + if animated { + animate(toPosition: point, height: z, time: 0.2) + } else { + setPosition(point, height: z) + } + } + + func zoomIn() { + let pos = getPosition() + let zoom = currentMapScale() / 2 + focusOn(point: pos, height: height(forMapScale: zoom)) + } + + func zoomOut() { + let pos = getPosition() + let zoom = currentMapScale() * 2 + focusOn(point: pos, height: height(forMapScale: zoom)) + } + + func clear(geofences: Bool = false) { + if geofences { + remove(geofenceObjects) + geofenceObjects.removeAll() + } else { + remove(objects) + objects.removeAll() + } + } + + func display(markers: [Marker], + isReport: Bool, + center: Bool = false) { + clear() + + let points = markers.map { marker in + MaplyCoordinateMakeWithDegrees(Float(marker.longitude), + Float(marker.latitude)) + } + + let fontSize = 11.0 + let colorReport = Color.green + let colorLabel = Color.secondary + let colorLabelOutline = Color.systemBackground + + let vectorDesc: [AnyHashable : Any] = [ + kMaplyColor: colorReport, + kMaplyVecWidth: 20.0 + ] + + let labelDesc: [AnyHashable : Any] = [ + kMaplyFont: UIFont.boldSystemFont(ofSize: fontSize), + kMaplyTextColor: colorLabel, + kMaplyTextOutlineColor: colorLabelOutline, + kMaplyTextOutlineSize: 3.0 + ] + + /* MARK: - Draw markers for positions */ + let screenMarkers = markers.enumerated().map { (i, marker) -> MaplyScreenMarker in + let screenMarker = MaplyScreenMarker() + screenMarker.layoutImportance = .greatestFiniteMagnitude + screenMarker.loc = MaplyCoordinateMakeWithDegrees(Float(marker.longitude), + Float(marker.latitude)) + var type: Marker.Type_ = .default_ + if isReport { + // For reports, position, start and end icons must be different + switch i { + case markers.startIndex: type = .reportStart + case markers.endIndex: type = .reportEnd + default: type = .reportPosition + } + } else { + type = marker.type + } + screenMarker.image = getIcon(markerType: type) + + var size = 50.0 + if isReport { + // For reports, position, start and end sizes must be different + switch i { + case markers.startIndex: size = 50.0 + case markers.endIndex: size = 50.0 + default: size = 25.0 + } + } + screenMarker.size = CGSize(width: size, height: size) + screenMarker.userObject = marker.id + screenMarker.selectable = true + + return screenMarker + } + + if let objs = addScreenMarkers(screenMarkers, desc: nil, mode: .any) { + objects.append(objs) + } + + /* MARK: - Add labels for markers */ + if !isReport && !markers.isEmpty { + let screenLabels = markers.map { marker -> MaplyScreenLabel in + let label = MaplyScreenLabel() + label.layoutImportance = .greatestFiniteMagnitude + var text = marker.name + if marker.name.count >= 20 { + let end = marker.name.index(marker.name.startIndex, offsetBy: 20) + text = String(marker.name[..<end]) + } + label.text = text + label.loc = MaplyCoordinateMakeWithDegrees(Float(marker.longitude), + Float(marker.latitude)) + label.offset = CGPoint(x: 0.0, y: 25.0) + + return label + } + + if let objs = addScreenLabels(screenLabels, desc: labelDesc) { + objects.append(objs) + } + } + + /* MARK: - Draw polyline for report */ + if isReport && !markers.isEmpty { + let geoJSON: [AnyHashable : Any] = [ + "type": "FeatureCollection", + "features": [ + [ + "type": "LineString", + "coordinates": points.map({ point in + [point.x, point.y] + }) + ] + ] + ] + if let vector = MaplyVectorObject(fromGeoJSONDictionary: geoJSON) { + if let objs = addVectors([vector], desc: vectorDesc, mode: .any) { + objects.append(objs) + } + } + } + + /* MARK: - Center map to bounds */ + if center && !markers.isEmpty { + let box = MaplyBoundingBoxExpandByFraction( + MaplyBoundingBoxFromCoordinates(points, UInt32(points.count)), 0.1) + let center = MaplyCoordinate(x: (box.ur.x + box.ll.x) / 2, + y: (box.ur.y + box.ll.y) / 2) + let zoom = max(findHeight(toViewBounds: box, pos: center), getMinZoom()) + setPosition(center, height: zoom) + } + } + + func setZoomLimits(minZoom: Int32, maxZoom: Int32) { + setZoomLimitsMin( + height(forMapScale: Float(truncating: + MapCalculus.companion.zoomLevelToScale(zoom: maxZoom) + ?? MapCalculus.companion.zoomLevelToScale(zoom: 21)! + )), + max: height(forMapScale: Float(truncating: + MapCalculus.companion.zoomLevelToScale(zoom: minZoom) + ?? MapCalculus.companion.zoomLevelToScale(zoom: 1)! + ))) + } + + private func getIcon(markerType: Marker.Type_) -> UIImage { + return UIImage(named: MarkerTransformations + .markerTypeToImageName(markerType: markerType))! + } +} + +// Source: https://stackoverflow.com/questions/65923718 +class BaseMapLink: ObservableObject { + @Published var action: OurMaplyViewController.Action? + + func zoomIn() { + action = .zoomIn + } + + func zoomOut() { + action = .zoomOut } } |