diff options
Diffstat (limited to 'iosApp')
-rw-r--r-- | iosApp/iosApp/Map/MapView.swift | 11 | ||||
-rw-r--r-- | iosApp/iosApp/Map/MapViewController.swift | 348 | ||||
-rw-r--r-- | iosApp/iosApp/Map/UnitMapView.swift | 1 | ||||
-rw-r--r-- | iosApp/iosApp/Units/UnitsViewModel.swift | 11 |
4 files changed, 370 insertions, 1 deletions
diff --git a/iosApp/iosApp/Map/MapView.swift b/iosApp/iosApp/Map/MapView.swift index 6b0fa20..989f17a 100644 --- a/iosApp/iosApp/Map/MapView.swift +++ b/iosApp/iosApp/Map/MapView.swift @@ -25,6 +25,7 @@ struct MapView: UIViewControllerRepresentable { @Binding var layer: MapLayer @Binding var markers: [Marker] + @Binding var selected: Marker? var markerCallback: MarkerCallback? class Coordinator { @@ -51,5 +52,15 @@ struct MapView: UIViewControllerRepresentable { isReport: false, center: context.coordinator.shouldCenter) context.coordinator.shouldCenter = false + + // MARK: - Center selected marker + if let selected = selected { + uiViewController.focusOn(marker: selected) + self.selected = nil + } + } + + static func dismantleUIViewController(_ uiViewController: MapViewController, coordinator: Coordinator) { + uiViewController.dismantle() } } diff --git a/iosApp/iosApp/Map/MapViewController.swift b/iosApp/iosApp/Map/MapViewController.swift new file mode 100644 index 0000000..207d0d0 --- /dev/null +++ b/iosApp/iosApp/Map/MapViewController.swift @@ -0,0 +1,348 @@ +/** + * TrackerMap + * Copyright (C) 2021-2022 Iván Ávalos <avalos@disroot.org>, Henoch Ojeda <imhenoch@protonmail.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ +import UIKit +import WhirlyGlobeMaplyComponent +import shared + +typealias MarkerCallback = (Int32?) -> () + +class MapViewController: UIViewController { + var markerCallback: MarkerCallback? = nil + var mapLayer: MapLayer = MapLayer.companion.defaultLayer + + @IBOutlet weak var mapContainer: UIView! + @IBOutlet weak var attributionText: UITextView! + + private var mapView: OurMaplyViewController! + + override func viewDidLoad() { + super.viewDidLoad() + // MARK: - Initialise MapViewController + mapView = OurMaplyViewController(mapType: .typeFlat) + mapView.delegate = self + view.sendSubviewToBack(mapContainer) + mapContainer.addSubview(mapView.view) + mapView.view.frame = mapContainer.bounds + addChild(mapView) + + setAttributionText(mapLayer) + + // MARK: - Configure MaplyViewController + let tileInfo = Utils.tileInfoFrom(layer: mapLayer) + mapView.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 + + if let loader = MaplyQuadImageLoader(params: sampleParams, + tileInfo: tileInfo, + viewC: mapView) { + mapView.setLoader(loader) + } + + DispatchQueue.main.async { + let point = MaplyCoordinateMakeWithDegrees(-100.36, 23.191) + self.mapView.setPosition(point, height: 0.4) + } + } + + func setMapLayer(_ layer: MapLayer) { + setAttributionText(layer) + mapView.setLayer(layer) + } + + func display(markers: [Marker], + isReport: Bool, + center: Bool = false) { + DispatchQueue.main.async { + self.mapView.display(markers: markers, + isReport: isReport, + center: center) + } + } + + func focusOn(marker: Marker) { + DispatchQueue.main.async { + self.mapView.focusOn(marker: marker) + } + } + + func dismantle() { + mapView.dismantle() + } + + private func setAttributionText(_ layer: MapLayer) { + DispatchQueue.main.async { + self.attributionText.attributedText = HtmlString.htmlToAttrStr( + layer.attribution, + size: UIFont.smallSystemFontSize, + color: UIColor.label) + } + attributionText.sizeToFit() + attributionText.layoutIfNeeded() + } + + @IBAction func onZoomInPressed(_ sender: UIButton) { + mapView.zoomIn() + } + + @IBAction func onZoomOutPressed(_ sender: UIButton) { + mapView.zoomOut() + } +} + +extension MapViewController: MaplyViewControllerDelegate { + + func maplyViewController(_ viewC: MaplyViewController, + didTapAt coord: MaplyCoordinate) { + markerCallback?(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 { + markerCallback?(id) + } + } + } + +} + +class OurMaplyViewController: MaplyViewController { + enum Action { + case zoomIn + case zoomOut + } + + private var loader: MaplyQuadImageLoader? = nil + 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 setLoader(_ loader: MaplyQuadImageLoader) { + self.loader = loader + } + + func setLayer(_ layer: MapLayer) { + loader?.changeTileInfo(Utils.tileInfoFrom(layer: layer)) + setZoomLimits(minZoom: layer.minZoom, + maxZoom: layer.maxZoom) + } + + 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 focusOn(marker: Marker, animated: Bool = true) { + let point = MaplyCoordinateMakeWithDegrees(Float(marker.longitude), + Float(marker.latitude)) + focusOn(point: point, animated: animated) + } + + 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 = UIColor.green + let colorLabel = UIColor.secondaryLabel + let colorLabelOutline = UIColor.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)! + ))) + } + + func dismantle() { + loader?.shutdown() + teardown() + } + + private func getIcon(markerType: Marker.Type_) -> UIImage { + return UIImage(named: MarkerTransformations + .markerTypeToImageName(markerType: markerType))! + } +} diff --git a/iosApp/iosApp/Map/UnitMapView.swift b/iosApp/iosApp/Map/UnitMapView.swift index 5f418f1..d2ad8a8 100644 --- a/iosApp/iosApp/Map/UnitMapView.swift +++ b/iosApp/iosApp/Map/UnitMapView.swift @@ -25,6 +25,7 @@ struct UnitMapView: View { ZStack { MapView(layer: $unitsViewModel.mapLayerType, markers: $unitsViewModel.markers, + selected: $unitsViewModel.selectedMarker, markerCallback: unitsViewModel.selectUnitWith) if let unit = unitsViewModel.selectedUnit { VStack { diff --git a/iosApp/iosApp/Units/UnitsViewModel.swift b/iosApp/iosApp/Units/UnitsViewModel.swift index dc6236f..8cf4aae 100644 --- a/iosApp/iosApp/Units/UnitsViewModel.swift +++ b/iosApp/iosApp/Units/UnitsViewModel.swift @@ -55,7 +55,16 @@ class UnitsViewModel: ObservableObject { } } @Published var markers: [Marker] = [] - @Published var selectedUnit: UnitInformation? = nil + @Published var selectedUnit: UnitInformation? = nil { + didSet { + if let unit = selectedUnit { + selectedMarker = Marker.companion.fromUnit(unit: unit) + } else { + selectedMarker = nil + } + } + } + @Published var selectedMarker: Marker? = nil @Published var mapLayerType: MapLayer = .companion.defaultLayer @Published var geofences: [Int: Geofence] = [:] @Published var camera: Camera = Camera() |