diff options
author | Iván Ávalos <avalos@disroot.org> | 2022-02-01 03:37:08 -0600 |
---|---|---|
committer | Iván Ávalos <avalos@disroot.org> | 2022-02-01 03:37:08 -0600 |
commit | 416be1cc70494737efc2fc2409c889db314d7fdd (patch) | |
tree | 3efa98840f583facc593169f13f2ad72e6d33f1d | |
parent | 749c239d952474979a884bfa664b9529e7f895f7 (diff) | |
download | etbsa-trackermap-mobile-416be1cc70494737efc2fc2409c889db314d7fdd.tar.gz etbsa-trackermap-mobile-416be1cc70494737efc2fc2409c889db314d7fdd.tar.bz2 etbsa-trackermap-mobile-416be1cc70494737efc2fc2409c889db314d7fdd.zip |
Added selection card in MapView
-rw-r--r-- | iosApp/iosApp.xcodeproj/project.pbxproj | 16 | ||||
-rw-r--r-- | iosApp/iosApp/Devices/DeviceRow.swift | 237 | ||||
-rw-r--r-- | iosApp/iosApp/Map/BaseMapView.swift | 330 | ||||
-rw-r--r-- | iosApp/iosApp/Map/MapView.swift | 313 | ||||
-rw-r--r-- | iosApp/iosApp/Map/MapWrapperView.swift | 8 | ||||
-rw-r--r-- | iosApp/iosApp/Map/UnitMapView.swift | 43 | ||||
-rw-r--r-- | iosApp/iosApp/Units/UnitsView.swift | 2 |
7 files changed, 496 insertions, 453 deletions
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index c6b2e9a..f155c39 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -11,13 +11,13 @@ 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 7555FF83242A565900829871 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* RootView.swift */; }; - E33A236027A4FD2C00DD647F /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A235F27A4FD2C00DD647F /* MapView.swift */; }; + E33A236027A4FD2C00DD647F /* UnitMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A235F27A4FD2C00DD647F /* UnitMapView.swift */; }; E33A236527A530F300DD647F /* SmallLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A236427A530F300DD647F /* SmallLabelStyle.swift */; }; E33A236727A64E4500DD647F /* MarkerTransformations.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A236627A64E4500DD647F /* MarkerTransformations.swift */; }; E33A236A27A6898700DD647F /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = E33A236927A6898700DD647F /* SwiftUIX */; }; E33A236D27A7545500DD647F /* WhirlyGlobeMaplyComponent.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = E33A236B27A7543700DD647F /* WhirlyGlobeMaplyComponent.xcframework */; }; E33A236E27A7545500DD647F /* WhirlyGlobeMaplyComponent.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E33A236B27A7543700DD647F /* WhirlyGlobeMaplyComponent.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - E33A237127A7553500DD647F /* BaseMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A237027A7553500DD647F /* BaseMapView.swift */; }; + E33A237127A7553500DD647F /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A237027A7553500DD647F /* MapView.swift */; }; E33A237327A7581A00DD647F /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A237227A7581A00DD647F /* Utils.swift */; }; E34A2F4427A77D9500AD8AEB /* MapWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34A2F4327A77D9400AD8AEB /* MapWrapperView.swift */; }; E34A2F4827A7878200AD8AEB /* HyperlinkText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34A2F4727A7878200AD8AEB /* HyperlinkText.swift */; }; @@ -55,11 +55,11 @@ 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF82242A565900829871 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; - E33A235F27A4FD2C00DD647F /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = "<group>"; }; + E33A235F27A4FD2C00DD647F /* UnitMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitMapView.swift; sourceTree = "<group>"; }; E33A236427A530F300DD647F /* SmallLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmallLabelStyle.swift; sourceTree = "<group>"; }; E33A236627A64E4500DD647F /* MarkerTransformations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkerTransformations.swift; sourceTree = "<group>"; }; E33A236B27A7543700DD647F /* WhirlyGlobeMaplyComponent.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = WhirlyGlobeMaplyComponent.xcframework; sourceTree = "<group>"; }; - E33A237027A7553500DD647F /* BaseMapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseMapView.swift; sourceTree = "<group>"; }; + E33A237027A7553500DD647F /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = "<group>"; }; E33A237227A7581A00DD647F /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = "<group>"; }; E34A2F4327A77D9400AD8AEB /* MapWrapperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapWrapperView.swift; sourceTree = "<group>"; }; E34A2F4727A7878200AD8AEB /* HyperlinkText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HyperlinkText.swift; sourceTree = "<group>"; }; @@ -142,8 +142,8 @@ E33A235E27A4FD1C00DD647F /* Map */ = { isa = PBXGroup; children = ( - E33A235F27A4FD2C00DD647F /* MapView.swift */, - E33A237027A7553500DD647F /* BaseMapView.swift */, + E33A235F27A4FD2C00DD647F /* UnitMapView.swift */, + E33A237027A7553500DD647F /* MapView.swift */, E34A2F4327A77D9400AD8AEB /* MapWrapperView.swift */, ); path = Map; @@ -293,9 +293,9 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - E33A237127A7553500DD647F /* BaseMapView.swift in Sources */, + E33A237127A7553500DD647F /* MapView.swift in Sources */, E38F241727A242C70069FC45 /* Resolver.swift in Sources */, - E33A236027A4FD2C00DD647F /* MapView.swift in Sources */, + E33A236027A4FD2C00DD647F /* UnitMapView.swift in Sources */, E33A236727A64E4500DD647F /* MarkerTransformations.swift in Sources */, E38F241527A242870069FC45 /* Inject.swift in Sources */, E39ABC4827A4EDEC00965D05 /* DeviceRow.swift in Sources */, diff --git a/iosApp/iosApp/Devices/DeviceRow.swift b/iosApp/iosApp/Devices/DeviceRow.swift index f93687a..4553a4b 100644 --- a/iosApp/iosApp/Devices/DeviceRow.swift +++ b/iosApp/iosApp/Devices/DeviceRow.swift @@ -27,131 +27,160 @@ struct DeviceRow: View { case commands } var callback: (Action) -> () + var isCell: Bool = true @State var isSheet: Bool = false var body: some View { - let row = HStack { - /* Device icon */ - let category = Marker.companion.categoryToMarkerType(category: unit.device.category) - Image(MarkerTransformations.markerTypeToImageName(markerType: category)) - .padding(5.0) + if isCell { + let row = HStack { + /* MARK: - Device icon */ + let category = Marker.companion.categoryToMarkerType(category: unit.device.category) + Image(MarkerTransformations.markerTypeToImageName(markerType: category)) + .padding(5.0) + + getSharedContent() + } + /* MARK: - Device actions */ + if #available(iOS 15, *) { + row.swipeActions(edge: .trailing, allowsFullSwipe: false) { + getActionButtons() + } + } else { + row.contextMenu { + getActionButtons() + } + } + } else { VStack { + getSharedContent() HStack { - /* Status icon */ - switch (unit.getStatus()) { - case .online: - Image(systemName: "circle.fill") - .foregroundColor(.systemGreen) - .imageScale(.small) - case .offline: - Image(systemName: "circle.fill") - .foregroundColor(.systemRed) - .imageScale(.small) - default: - EmptyView() - } - - /* Engine stop */ - switch (unit.getEngineStop()) { - case .on: - Image(systemName: "lock.fill") - .foregroundColor(.systemRed) - .imageScale(.small) - case .off: - Image(systemName: "lock.open.fill") - .foregroundColor(.systemGreen) - .imageScale(.small) - default: - EmptyView() - } - - /* Device name */ - Text(unit.device.name) - Spacer() - } - .padding(.bottom, 5.0) - - /* Driver */ - if let contact = unit.device.contact { - HStack { - Label(contact, systemImage: "person") - .labelStyle(SmallLabelStyle()) - Spacer() - } + getActionButtons() } - - /* Speed */ - if let speed = unit.position?.speed { - HStack { - Label(Formatter.companion.formatSpeed( - speed: Double(truncating: speed), - unit: .kmh), - systemImage: "speedometer") - .labelStyle(SmallLabelStyle()) - Spacer() - } + } + .padding(2.0) + } + } + + @ViewBuilder + func getSharedContent() -> some View { + VStack { + HStack { + /* MARK: - Status icon */ + switch (unit.getStatus()) { + case .online: + Image(systemName: "circle.fill") + .foregroundColor(.systemGreen) + .imageScale(.small) + case .offline: + Image(systemName: "circle.fill") + .foregroundColor(.systemRed) + .imageScale(.small) + default: + EmptyView() } - /* Address */ - if let address = unit.position?.address { - HStack { - Label(address, systemImage: "mappin.and.ellipse") - .labelStyle(SmallLabelStyle()) - Spacer() - } + /* MARK: - Engine stop */ + switch (unit.getEngineStop()) { + case .on: + Image(systemName: "lock.fill") + .foregroundColor(.systemRed) + .imageScale(.small) + case .off: + Image(systemName: "lock.open.fill") + .foregroundColor(.systemGreen) + .imageScale(.small) + default: + EmptyView() } - /* Hourmeter */ - if let hourmeter = Int64(truncating: unit.getHourmeter() ?? 0), - hourmeter >= 60 * 60 * 1000 { - HStack { - Label(Formatter.companion.formatHours(millis: hourmeter), - systemImage: "timer") - .labelStyle(SmallLabelStyle()) - Spacer() - } + /* MARK: - Device name */ + Text(unit.device.name) + Spacer() + } + .padding(.bottom, 5.0) + + /* MARK: - Driver */ + if let contact = unit.device.contact { + HStack { + Label(contact, systemImage: "person") + .labelStyle(SmallLabelStyle()) + Spacer() } - - /* Date time */ - if let datetime = unit.position?.fixTime { - HStack { - Label(Formatter.companion.formatDate(str: datetime), - systemImage: "calendar") - .labelStyle(SmallLabelStyle()) - Spacer() - } + } + + /* MARK: - Speed */ + if let speed = unit.position?.speed { + HStack { + Label(Formatter.companion.formatSpeed( + speed: Double(truncating: speed), + unit: .kmh), + systemImage: "speedometer") + .labelStyle(SmallLabelStyle()) + Spacer() } } - } - - /* Device actions */ - if #available(iOS 15, *) { - row.swipeActions(edge: .trailing, allowsFullSwipe: false) { - Button { callback(.commands) } label: { - Label("commands", systemImage: "paperplane") + + /* MARK: - Address */ + if let address = unit.position?.address { + HStack { + Label(address, systemImage: "mappin.and.ellipse") + .labelStyle(SmallLabelStyle()) + Spacer() } - - Button { callback(.reports) } label: { - Label("reports", systemImage: "clock") + } + + /* MARK: - Hourmeter */ + if let hourmeter = Int64(truncating: unit.getHourmeter() ?? 0), + hourmeter >= 60 * 60 * 1000 { + HStack { + Label(Formatter.companion.formatHours(millis: hourmeter), + systemImage: "timer") + .labelStyle(SmallLabelStyle()) + Spacer() } - - Button { callback(.details) } label: { - Label("details", systemImage: "info.circle") + } + + /* MARK: - Date time */ + if let datetime = unit.position?.fixTime { + HStack { + Label(Formatter.companion.formatDate(str: datetime), + systemImage: "calendar") + .labelStyle(SmallLabelStyle()) + Spacer() } } + } + } + + @ViewBuilder + func getActionButtons() -> some View { + let details = Button { callback(.details) } label: { + Label("details", systemImage: "info.circle") + } + + let reports = Button { callback(.reports) } label: { + Label("reports", systemImage: "clock") + } + + let commands = Button { callback(.commands) } label: { + Label("commands", systemImage: "paperplane") + } + + if isCell { + commands + reports + details } else { - row.onLongPressGesture { - self.isSheet = true - } - .actionSheet(isPresented: $isSheet) { - ActionSheet(title: Text("select-action"), message: nil, buttons: [ - .default(Text("details")) { callback(.details) }, - .default(Text("reports")) { callback(.reports) }, - .default(Text("commands")) { callback(.commands) } - ]) + Group { + details + reports + commands } + .frame(maxWidth: .infinity) + .labelStyle(.titleOnly) + .padding(5.0) } } } diff --git a/iosApp/iosApp/Map/BaseMapView.swift b/iosApp/iosApp/Map/BaseMapView.swift deleted file mode 100644 index 322ae45..0000000 --- a/iosApp/iosApp/Map/BaseMapView.swift +++ /dev/null @@ -1,330 +0,0 @@ -/** - * 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 SwiftUI -import Combine -import WhirlyGlobeMaplyComponent -import shared - -struct BaseMapView: UIViewControllerRepresentable { - typealias UIViewControllerType = OurMaplyViewController - - @Binding var mapLayer: MapLayer - @Binding var markers: [Marker] - var markerCallback: ((Int32?) -> Void)? - var link: BaseMapLink - - class Coordinator: NSObject, MaplyViewControllerDelegate { - var parent: BaseMapView - 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: BaseMapView) { - 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 - } -} 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 } } diff --git a/iosApp/iosApp/Map/MapWrapperView.swift b/iosApp/iosApp/Map/MapWrapperView.swift index cc56937..7450eed 100644 --- a/iosApp/iosApp/Map/MapWrapperView.swift +++ b/iosApp/iosApp/Map/MapWrapperView.swift @@ -29,10 +29,10 @@ struct MapWrapperView: View { var body: some View { ZStack { // MARK: - Map - BaseMapView(mapLayer: $layer, - markers: $markers, - markerCallback: markerCallback, - link: link) + MapView(mapLayer: $layer, + markers: $markers, + markerCallback: markerCallback, + link: link) // MARK: - Attribution VStack { diff --git a/iosApp/iosApp/Map/UnitMapView.swift b/iosApp/iosApp/Map/UnitMapView.swift new file mode 100644 index 0000000..e1f90a0 --- /dev/null +++ b/iosApp/iosApp/Map/UnitMapView.swift @@ -0,0 +1,43 @@ +/** + * 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 SwiftUI +import shared + +struct UnitMapView: View { + @StateObject var unitsViewModel: UnitsViewModel + + var body: some View { + ZStack { + MapWrapperView(layer: $unitsViewModel.mapLayerType, + markers: $unitsViewModel.markers, + markerCallback: unitsViewModel.selectUnitWith) + if let unit = unitsViewModel.selectedUnit { + VStack { + DeviceRow(unit: unit, callback: { action in + print("Action is \(action)") + }, isCell: false) + .padding() + .background(.systemBackground) + } + .frame(maxWidth: .infinity, + maxHeight: .infinity, + alignment: .bottom) + } + } + } +} diff --git a/iosApp/iosApp/Units/UnitsView.swift b/iosApp/iosApp/Units/UnitsView.swift index 0ad84d9..aedb165 100644 --- a/iosApp/iosApp/Units/UnitsView.swift +++ b/iosApp/iosApp/Units/UnitsView.swift @@ -26,7 +26,7 @@ struct UnitsView: View { var body: some View { NavigationView { TabView(selection: $unitsViewModel.unitsDisplayMode, content: { - MapView(unitsViewModel: unitsViewModel) + UnitMapView(unitsViewModel: unitsViewModel) .tabItem { Image(systemName: "map") Text("map") |