aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIván Ávalos <avalos@disroot.org>2022-02-01 03:37:08 -0600
committerIván Ávalos <avalos@disroot.org>2022-02-01 03:37:08 -0600
commit416be1cc70494737efc2fc2409c889db314d7fdd (patch)
tree3efa98840f583facc593169f13f2ad72e6d33f1d
parent749c239d952474979a884bfa664b9529e7f895f7 (diff)
downloadetbsa-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.pbxproj16
-rw-r--r--iosApp/iosApp/Devices/DeviceRow.swift237
-rw-r--r--iosApp/iosApp/Map/BaseMapView.swift330
-rw-r--r--iosApp/iosApp/Map/MapView.swift313
-rw-r--r--iosApp/iosApp/Map/MapWrapperView.swift8
-rw-r--r--iosApp/iosApp/Map/UnitMapView.swift43
-rw-r--r--iosApp/iosApp/Units/UnitsView.swift2
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")