diff options
author | Iván Ávalos <avalos@disroot.org> | 2022-03-02 00:18:30 -0600 |
---|---|---|
committer | Iván Ávalos <avalos@disroot.org> | 2022-03-02 00:18:30 -0600 |
commit | 440399490ec8874ed5295ca6b5770c94d47d4f3d (patch) | |
tree | 1538e563438b551a62d9fad8fe39e6bd36af7948 | |
parent | 7522eaa1cf95820717f6d91a8b5879f5e14cfae2 (diff) | |
parent | 9b8eee4362c368e682bfe18d5cef44d6b9d109bf (diff) | |
download | etbsa-trackermap-mobile-440399490ec8874ed5295ca6b5770c94d47d4f3d.tar.gz etbsa-trackermap-mobile-440399490ec8874ed5295ca6b5770c94d47d4f3d.tar.bz2 etbsa-trackermap-mobile-440399490ec8874ed5295ca6b5770c94d47d4f3d.zip |
Merged 'main' from upstream
21 files changed, 648 insertions, 31 deletions
diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index fd6c044..146f7c5 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -12,6 +12,8 @@ 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; 7555FF83242A565900829871 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* RootView.swift */; }; E312E74F27B366060018C5DE /* DevicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E312E74E27B366060018C5DE /* DevicesViewModel.swift */; }; + E3141B8B27B9E91F00CE777C /* UnitReportsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3141B8A27B9E91F00CE777C /* UnitReportsViewModel.swift */; }; + E31D721527CF159900CDA320 /* SidewaysScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = E31D721427CF159900CDA320 /* SidewaysScroller.swift */; }; E3385A4F27B0D8A10025311C /* UnitCommandsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3385A4E27B0D8A10025311C /* UnitCommandsViewModel.swift */; }; E33A236027A4FD2C00DD647F /* UnitMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A235F27A4FD2C00DD647F /* UnitMapView.swift */; }; E33A236527A530F300DD647F /* SmallLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A236427A530F300DD647F /* SmallLabelStyle.swift */; }; @@ -44,6 +46,8 @@ E39ABC4327A4E88C00965D05 /* UnitsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E39ABC4227A4E88C00965D05 /* UnitsViewModel.swift */; }; E39ABC4627A4EBD500965D05 /* DevicesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E39ABC4527A4EBD500965D05 /* DevicesView.swift */; }; E39ABC4827A4EDEC00965D05 /* DeviceRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E39ABC4727A4EDEC00965D05 /* DeviceRow.swift */; }; + E3C651E727CB5426002F6F4C /* Tabler in Frameworks */ = {isa = PBXBuildFile; productRef = E3C651E627CB5426002F6F4C /* Tabler */; }; + E3C651EA27CB61EE002F6F4C /* GEOSwift in Frameworks */ = {isa = PBXBuildFile; productRef = E3C651E927CB61EE002F6F4C /* GEOSwift */; }; E3E77EE6279E6CE400150070 /* FlowCollector.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3E77EE5279E6CE400150070 /* FlowCollector.swift */; }; /* End PBXBuildFile section */ @@ -69,6 +73,8 @@ 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>"; }; E312E74E27B366060018C5DE /* DevicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevicesViewModel.swift; sourceTree = "<group>"; }; + E3141B8A27B9E91F00CE777C /* UnitReportsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitReportsViewModel.swift; sourceTree = "<group>"; }; + E31D721427CF159900CDA320 /* SidewaysScroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidewaysScroller.swift; sourceTree = "<group>"; }; E3385A4E27B0D8A10025311C /* UnitCommandsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnitCommandsViewModel.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>"; }; @@ -108,8 +114,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E3C651E727CB5426002F6F4C /* Tabler in Frameworks */, E36A5A8627B4BFC40070DED5 /* FirebaseMessaging in Frameworks */, E33A236A27A6898700DD647F /* SwiftUIX in Frameworks */, + E3C651EA27CB61EE002F6F4C /* GEOSwift in Frameworks */, E396281D27AF5723005D070E /* WhirlyGlobe.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -214,6 +222,7 @@ isa = PBXGroup; children = ( E396282527AFBD4E005D070E /* UnitReportsView.swift */, + E3141B8A27B9E91F00CE777C /* UnitReportsViewModel.swift */, ); path = Reports; sourceTree = "<group>"; @@ -261,6 +270,7 @@ E33A236627A64E4500DD647F /* MarkerTransformations.swift */, E33A237227A7581A00DD647F /* Utils.swift */, E34A2F4727A7878200AD8AEB /* HyperlinkText.swift */, + E31D721427CF159900CDA320 /* SidewaysScroller.swift */, ); path = Shared; sourceTree = "<group>"; @@ -286,6 +296,8 @@ packageProductDependencies = ( E33A236927A6898700DD647F /* SwiftUIX */, E36A5A8527B4BFC40070DED5 /* FirebaseMessaging */, + E3C651E627CB5426002F6F4C /* Tabler */, + E3C651E927CB61EE002F6F4C /* GEOSwift */, ); productName = iosApp; productReference = 7555FF7B242A565900829871 /* iosApp.app */; @@ -320,6 +332,8 @@ packageReferences = ( E33A236827A6898700DD647F /* XCRemoteSwiftPackageReference "SwiftUIX" */, E36A5A8427B4BFC40070DED5 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + E3C651E527CB5425002F6F4C /* XCRemoteSwiftPackageReference "SwiftTabler" */, + E3C651E827CB61EE002F6F4C /* XCRemoteSwiftPackageReference "GEOSwift" */, ); productRefGroup = 7555FF7C242A565900829871 /* Products */; projectDirPath = ""; @@ -396,8 +410,10 @@ E34A2F4827A7878200AD8AEB /* HyperlinkText.swift in Sources */, 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, E39ABC4627A4EBD500965D05 /* DevicesView.swift in Sources */, + E3141B8B27B9E91F00CE777C /* UnitReportsViewModel.swift in Sources */, E38F242027A27B550069FC45 /* LoadingView.swift in Sources */, E38F241E27A270D50069FC45 /* UnitsView.swift in Sources */, + E31D721527CF159900CDA320 /* SidewaysScroller.swift in Sources */, 7555FF83242A565900829871 /* RootView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -541,7 +557,6 @@ CODE_SIGN_ENTITLEMENTS = iosApp/iosApp.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_TEAM = 358YRZ9P3L; ENABLE_PREVIEWS = YES; @@ -555,7 +570,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.1; OTHER_LDFLAGS = ( "$(inherited)", "-framework", @@ -593,7 +608,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.1; + MARKETING_VERSION = 1.1; OTHER_LDFLAGS = ( "$(inherited)", "-framework", @@ -648,6 +663,22 @@ minimumVersion = 8.0.0; }; }; + E3C651E527CB5425002F6F4C /* XCRemoteSwiftPackageReference "SwiftTabler" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/openalloc/SwiftTabler"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.6.0; + }; + }; + E3C651E827CB61EE002F6F4C /* XCRemoteSwiftPackageReference "GEOSwift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/GEOSwift/GEOSwift"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 9.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -661,6 +692,16 @@ package = E36A5A8427B4BFC40070DED5 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; productName = FirebaseMessaging; }; + E3C651E627CB5426002F6F4C /* Tabler */ = { + isa = XCSwiftPackageProductDependency; + package = E3C651E527CB5425002F6F4C /* XCRemoteSwiftPackageReference "SwiftTabler" */; + productName = Tabler; + }; + E3C651E927CB61EE002F6F4C /* GEOSwift */ = { + isa = XCSwiftPackageProductDependency; + package = E3C651E827CB61EE002F6F4C /* XCRemoteSwiftPackageReference "GEOSwift" */; + productName = GEOSwift; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 7555FF73242A565900829871 /* Project object */; diff --git a/iosApp/iosApp/Assets.xcassets/MapReportEnd.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/MapReportEnd.imageset/Contents.json new file mode 100644 index 0000000..40c489c --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/MapReportEnd.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map_report_end.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/MapReportEnd.imageset/map_report_end.svg b/iosApp/iosApp/Assets.xcassets/MapReportEnd.imageset/map_report_end.svg new file mode 100644 index 0000000..ec134ba --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/MapReportEnd.imageset/map_report_end.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24"> + <path + fill="#008000" + d="M12.36,6H7v6h7.24l0.4,2H18V8h-5.24z"/> + <path + fill="@android:color/black" + d="M14.4,6L14,4L5,4v17h2v-7h5.6l0.4,2h7L20,6h-5.6zM18,14h-3.36l-0.4,-2L7,12L7,6h5.36l0.4,2L18,8v6z"/> +</svg> diff --git a/iosApp/iosApp/Assets.xcassets/MapReportPosition.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/MapReportPosition.imageset/Contents.json new file mode 100644 index 0000000..3a7979a --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/MapReportPosition.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map_report_position.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/MapReportPosition.imageset/map_report_position.svg b/iosApp/iosApp/Assets.xcassets/MapReportPosition.imageset/map_report_position.svg new file mode 100644 index 0000000..c6d39ac --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/MapReportPosition.imageset/map_report_position.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24"> + <path + fill="#008000" + d="M12,18c3.31,0 6,-2.69 6,-6s-2.69,-6 -6,-6 -6,2.69 -6,6 2.69,6 6,6z"/> + <path + fill="@android:color/black" + d="M12,20c4.42,0 8,-3.58 8,-8s-3.58,-8 -8,-8 -8,3.58 -8,8 3.58,8 8,8zM12,6c3.31,0 6,2.69 6,6s-2.69,6 -6,6 -6,-2.69 -6,-6 2.69,-6 6,-6z"/> +</svg> diff --git a/iosApp/iosApp/Assets.xcassets/MapReportStart.imageset/Contents.json b/iosApp/iosApp/Assets.xcassets/MapReportStart.imageset/Contents.json new file mode 100644 index 0000000..3a1438a --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/MapReportStart.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map_report_start.svg", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/MapReportStart.imageset/map_report_start.svg b/iosApp/iosApp/Assets.xcassets/MapReportStart.imageset/map_report_start.svg new file mode 100644 index 0000000..59a6142 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/MapReportStart.imageset/map_report_start.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" + width="24" + height="24" + viewBox="0 0 24 24"> + <path + fill="#008000" + d="M12,20c4.41,0 8,-3.59 8,-8s-3.59,-8 -8,-8 -8,3.59 -8,8 3.59,8 8,8zM10,7.5l6,4.5 -6,4.5v-9z"/> + <path + fill="@android:color/black" + d="M12,22c5.52,0 10,-4.48 10,-10S17.52,2 12,2 2,6.48 2,12s4.48,10 10,10zM12,4c4.41,0 8,3.59 8,8s-3.59,8 -8,8 -8,-3.59 -8,-8 3.59,-8 8,-8zM10,7.5v9l6,-4.5z"/> +</svg> diff --git a/iosApp/iosApp/Details/DetailsView.swift b/iosApp/iosApp/Details/DetailsView.swift index c869369..9b75aec 100644 --- a/iosApp/iosApp/Details/DetailsView.swift +++ b/iosApp/iosApp/Details/DetailsView.swift @@ -50,7 +50,7 @@ struct DetailsView: View { .navigationBarTitleView( Picker(selection: $action) { Text("details").tag(DeviceRow.Action.details) - //Text("reports").tag(DeviceRow.Action.reports) + Text("reports").tag(DeviceRow.Action.reports) Text("commands").tag(DeviceRow.Action.commands) } label: { EmptyView() diff --git a/iosApp/iosApp/Details/Reports/UnitReportsView.swift b/iosApp/iosApp/Details/Reports/UnitReportsView.swift index 26ddead..2143b3b 100644 --- a/iosApp/iosApp/Details/Reports/UnitReportsView.swift +++ b/iosApp/iosApp/Details/Reports/UnitReportsView.swift @@ -17,11 +17,155 @@ */ import SwiftUI import shared +import SwiftUIX +import Tabler + +extension EventInformation: Identifiable {} struct UnitReportsView: View { + @StateObject var unitReportsViewModel: UnitReportsViewModel + var unit: UnitInformation - + + init (unit: UnitInformation) { + self.unit = unit + self._unitReportsViewModel = StateObject( + wrappedValue: UnitReportsViewModel(deviceId: unit.device.id)) + } + + private var eventsConfig: TablerListConfig<EventInformation> { + TablerListConfig<EventInformation>(gridItems: eventsGridItems) + } + + private var eventsGridItems: [GridItem] = [ + GridItem(.flexible(minimum: 100), alignment: .leading), + GridItem(.flexible(minimum: 100), alignment: .leading), + GridItem(.flexible(minimum: 100, maximum: 150), alignment: .leading), + GridItem(.flexible(minimum: 150, maximum: 250), alignment: .leading), + ] + + @ViewBuilder + private func eventsHeader(_ ctx: Binding<TablerContext<EventInformation>>) -> some View { + Text("events-table-datetime") + Text("events-table-event") + Text("events-table-geofence") + Text("events-table-address") + } + + @ViewBuilder + private func eventsRow(_ element: EventInformation) -> some View { + if let eventTime = element.event.eventTime { + Text(Formatter.companion.formatDate(str: eventTime)) + } else { Text("") } + + if let eventType = element.event.type { + switch EventInformation.companion.stringToReportType(s: eventType) { + case .deviceOnline: Text("event-device-online") + case .deviceUnknown: Text("event-device-unknown") + case .deviceOffline: Text("event-device-offline") + case .deviceInactive: Text("event-device-inactive") + case .deviceMoving: Text("event-device-moving") + case .deviceStopped: Text("event-device-stopped") + case .deviceOverspeed: Text("event-device-overspeed") + case .deviceFuelDrop: Text("event-device-fuel-drop") + case .commandResult: Text("event-command-result") + case .geofenceEnter: Text("event-geofence-enter") + case .geofenceExit: Text("event-geofence-exit") + case .alarm: Text("event-alarm") + case .ignitionOn: Text("event-ignition-on") + case .ignitionOff: Text("event-ignition-off") + case .maintenance: Text("event-maintenance") + case .textMessage: Text("event-text-message") + case .driverChanged: Text("event-driver-changed") + default: Text("event-unknown") + } + } else { Text("") } + + if let geofenceName = element.geofence?.name { + Text(geofenceName) + } else { Text("") } + + if let positionAddress = element.position?.address { + Text(positionAddress) + } else { Text("") } + } + var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + VStack { + if unitReportsViewModel.reportType == .events { + if let report = unitReportsViewModel.report as? ReportController.ReportEventsReport { + SidewaysScroller(minWidth: 1000) { + TablerList(eventsConfig, + headerContent: eventsHeader, + rowContent: eventsRow, + results: report.events) + } + } else { + Spacer() + } + } else { + MapView(layer: $unitReportsViewModel.layer, + markers: $unitReportsViewModel.markers, + geofences: $unitReportsViewModel.flatGeofences, + selected: $unitReportsViewModel.selectedMarker, + isReport: true) + } + + VStack { + HStack { + Text("report-period") + Spacer() + Picker(selection: $unitReportsViewModel.reportPeriod) { + Text("period-today").tag(ReportDates.ReportPeriod.today) + Text("period-last-24").tag(ReportDates.ReportPeriod.last24) + Text("period-yesterday").tag(ReportDates.ReportPeriod.yesterday) + Text("period-this-week").tag(ReportDates.ReportPeriod.thisWeek) + Text("period-last-7").tag(ReportDates.ReportPeriod.last7) + Text("period-this-month").tag(ReportDates.ReportPeriod.thisMonth) + Text("period-last-30").tag(ReportDates.ReportPeriod.last30) + } label: { + switch unitReportsViewModel.reportPeriod { + case .today: + Text("period-today") + case .last24: + Text("period-last-24") + case .yesterday: + Text("period-yesterday") + case .thisWeek: + Text("period-this-week") + case .last7: + Text("period-last-7") + case .thisMonth: + Text("period-this-month") + case .last30: + Text("period-last-30") + default: + Text("period-today") + } + } + }.padding() + Picker(selection: $unitReportsViewModel.reportType) { + Text("report-positions").tag(ReportController.ReportType.positions) + Text("report-events").tag(ReportController.ReportType.events) + Text("report-stops").tag(ReportController.ReportType.stops) + } label: { + EmptyView() + }.pickerStyle(SegmentedPickerStyle()) + + //HStack { + // Group { + // Button {} label: { + // Text("report-save") + // } + // + // Button {} label: { + // Text("report-share") + // } + // }.frame(maxWidth: .infinity) + //}.padding() + }.padding() + }.onAppear { + unitReportsViewModel.fetchReport() + } } } diff --git a/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift b/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift new file mode 100644 index 0000000..4731a57 --- /dev/null +++ b/iosApp/iosApp/Details/Reports/UnitReportsViewModel.swift @@ -0,0 +1,94 @@ +// +// UnitReportsViewModel.swift +// iosApp +// +// Created by Iván on 13/02/22. +// Copyright © 2022 orgName. All rights reserved. +// + +import Foundation +import Combine +import shared + +class UnitReportsViewModel: ObservableObject { + @Inject var reportController: ReportController + @Inject var geofencesController: GeofencesController + + @Published var deviceId: Int32? = nil + @Published var reportType: ReportController.ReportType = .positions { + didSet { + fetchReport() + } + } + @Published var reportPeriod: ReportDates.ReportPeriod = .today { + didSet { + fetchReport() + } + } + @Published var report: ReportController.Report? = nil { + didSet { + switch report { + case let report as ReportController.ReportPositionsReport: + markers = report.positions.compactMap { p in Marker.companion.fromPosition(position: p)} + case is ReportController.ReportEventsReport: + markers = [] + case let report as ReportController.ReportStopsReport: + markers = report.stops.compactMap { s in Marker.companion.fromStop(stop: s) } + default: + break + } + } + } + @Published var layer = MapLayer.companion.defaultLayer + @Published var markers = [Marker]() + @Published var selectedMarker: Marker? = nil + + @Published var geofences: [Int: Geofence] = [:] { + didSet { + flatGeofences = Array(geofences.values) + } + } + @Published var flatGeofences: [Geofence] = [] + + init(deviceId id: Int32?) { + deviceId = id + let collector = Collector<ReportController.Report>(callback: setReport) + reportController.reportFlow.collect(collector: collector) { _, _ in } + + let geofencesCollector = Collector<[Int: Geofence]>(callback: setGeofences) + geofencesController.geofencesFlow.collect(collector: geofencesCollector) { unit, error in } + } + + func setReport (report: ReportController.Report) { + self.report = report + } + + private func setGeofences(geofences: [Int: Geofence]) { + self.geofences = geofences + } + + func fetchReport(xlsx: Bool = false) { + if let id = deviceId { + reportController.fetchReport(deviceId: id, + reportType: reportType, + reportPeriod: reportPeriod, + xlsx: xlsx, eventTypes: [ + .deviceInactive, + .deviceMoving, + .deviceStopped, + .deviceOverspeed, + .deviceFuelDrop, + .commandResult, + .geofenceEnter, + .geofenceExit, + .alarm, + .ignitionOn, + .ignitionOff, + .maintenance, + .textMessage, + .driverChanged, + .unknown + ]) { _, _ in } + } + } +} diff --git a/iosApp/iosApp/Devices/DeviceRow.swift b/iosApp/iosApp/Devices/DeviceRow.swift index 0439fff..ee3298d 100644 --- a/iosApp/iosApp/Devices/DeviceRow.swift +++ b/iosApp/iosApp/Devices/DeviceRow.swift @@ -172,9 +172,9 @@ struct DeviceRow: View { Label("details", systemImage: "info.circle") } - //let reports = Button { callback(.reports) } label: { - // Label("reports", systemImage: "clock") - //} + let reports = Button { callback(.reports) } label: { + Label("reports", systemImage: "clock") + } let commands = Button { callback(.commands) } label: { Label("commands", systemImage: "paperplane") @@ -182,12 +182,12 @@ struct DeviceRow: View { if isCell { commands - // reports + reports details } else { Group { details - // reports + reports commands } .frame(maxWidth: .infinity) diff --git a/iosApp/iosApp/Map/MapView.swift b/iosApp/iosApp/Map/MapView.swift index e52c034..84bdc05 100644 --- a/iosApp/iosApp/Map/MapView.swift +++ b/iosApp/iosApp/Map/MapView.swift @@ -25,12 +25,15 @@ struct MapView: UIViewControllerRepresentable { @Binding var layer: MapLayer @Binding var markers: [Marker] + @Binding var geofences: [Geofence] @Binding var selected: Marker? + var isReport: Bool = false var markerCallback: MarkerCallback? class Coordinator { var shouldCenter: Bool = true var oldMarkers: [Marker] = [] + var oldGeofences: [Geofence] = [] } func makeCoordinator() -> Coordinator { @@ -50,14 +53,19 @@ struct MapView: UIViewControllerRepresentable { // MARK: - Set markers if context.coordinator.oldMarkers != markers { - print("center = \(context.coordinator.shouldCenter)") uiViewController.display(markers: markers, - isReport: false, + isReport: isReport, center: context.coordinator.shouldCenter) - context.coordinator.shouldCenter = false + context.coordinator.shouldCenter = false || isReport } context.coordinator.oldMarkers = markers + // MARK: - Set geofences + if context.coordinator.oldGeofences != geofences { + uiViewController.display(geofences: geofences) + } + context.coordinator.oldGeofences = geofences + // MARK: - Center selected marker if let selected = selected { uiViewController.focusOn(marker: selected) diff --git a/iosApp/iosApp/Map/MapViewController.swift b/iosApp/iosApp/Map/MapViewController.swift index 131a511..3c38b89 100644 --- a/iosApp/iosApp/Map/MapViewController.swift +++ b/iosApp/iosApp/Map/MapViewController.swift @@ -18,9 +18,23 @@ import UIKit import WhirlyGlobe import shared +import GEOSwift typealias MarkerCallback = (Int32?) -> () +extension Geometry { + func getCoordinates() -> [Point] { + switch self { + case let .polygon(polygon): + return polygon.exterior.points + case let .lineString(lineString): + return lineString.points + default: + return [] + } + } +} + class MapViewController: UIViewController { var markerCallback: MarkerCallback? = nil var mapLayer: MapLayer = MapLayer.companion.defaultLayer @@ -84,6 +98,12 @@ class MapViewController: UIViewController { } } + func display(geofences: [Geofence]) { + mapView.runOnInit { + self.mapView.display(geofences: geofences) + } + } + func focusOn(marker: Marker) { mapView.runOnInit { self.mapView.focusOn(marker: marker) @@ -205,13 +225,14 @@ class OurMaplyViewController: MaplyViewController { } let fontSize = 11.0 - let colorReport = UIColor.green - let colorLabel = UIColor.secondaryLabel - let colorLabelOutline = UIColor.systemBackground + let colorReport = UIColor(red: 0.0, green: 0.5, blue: 0.0, alpha: 1.0) + let colorLabel = UIColor.darkGray + let colorLabelOutline = UIColor.white let vectorDesc: [AnyHashable : Any] = [ kMaplyColor: colorReport, - kMaplyVecWidth: 20.0 + kMaplyVecWidth: 15.0, + kMaplyWideVecImpl: kMaplyWideVecImplPerf ] let labelDesc: [AnyHashable : Any] = [ @@ -232,7 +253,7 @@ class OurMaplyViewController: MaplyViewController { // For reports, position, start and end icons must be different switch i { case markers.startIndex: type = .reportStart - case markers.endIndex: type = .reportEnd + case markers.endIndex - 1: type = .reportEnd default: type = .reportPosition } } else { @@ -245,8 +266,8 @@ class OurMaplyViewController: MaplyViewController { // 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 + case markers.endIndex - 1: size = 50.0 + default: size = 26.0 } } screenMarker.size = CGSize(width: size, height: size) @@ -289,15 +310,19 @@ class OurMaplyViewController: MaplyViewController { "type": "FeatureCollection", "features": [ [ - "type": "LineString", - "coordinates": points.map({ point in - [point.x, point.y] - }) + "type": "Feature", + "properties": [], + "geometry": [ + "type": "LineString", + "coordinates": markers.map({ marker in + [marker.longitude, marker.latitude] + }) + ] ] ] ] if let vector = MaplyVectorObject(fromGeoJSONDictionary: geoJSON) { - if let objs = addVectors([vector], desc: vectorDesc, mode: .any) { + if let objs = addWideVectors([vector], desc: vectorDesc, mode: .any) { objects.append(objs) } } @@ -314,6 +339,85 @@ class OurMaplyViewController: MaplyViewController { } } + func display(geofences: [Geofence]) { + clear(geofences: true) + + let fontSize = 11.0 + let colorFill = UIColor(red: 0.10, green: 0.46, blue: 0.82, alpha: 1.00) + let colorLabel = UIColor(red: 0.10, green: 0.46, blue: 0.82, alpha: 1.00) + let colorLabelOutline = UIColor.white + + let vectorDesc: [AnyHashable : Any] = [ + kMaplyColor: colorFill, + kMaplyVecWidth: 12.0, + kMaplyWideVecImpl: kMaplyWideVecImplPerf + ] + + let labelDesc: [AnyHashable : Any] = [ + kMaplyFont: UIFont.boldSystemFont(ofSize: fontSize), + kMaplyTextColor: colorLabel, + kMaplyTextOutlineColor: colorLabelOutline, + kMaplyTextOutlineSize: 3.0 + ] + + let shapes = [MaplyShape]() + var vectors = [MaplyVectorObject]() + var labels = [MaplyScreenLabel]() + + geofences.forEach { geofence in + if let area = geofence.area { + print ("Draw geofence with area = \(area)") + if let geometry = try? Geometry(wkt: area) { + switch geometry { + case .polygon(_): + let geoJSON: [AnyHashable : Any] = [ + "type": "FeatureCollection", + "features": [ + [ + "type": "Feature", + "properties": [], + "geometry": [ + "type": "Polygon", + "coordinates": [ + geometry.getCoordinates().map { coordinate in + [coordinate.y, coordinate.x] + } + ] + ] + ] + ] + ] + if let vector = MaplyVectorObject(fromGeoJSONDictionary: geoJSON) { + vectors.append(vector) + } + default: + break + } + + if let centroid = try? geometry.centroid() { + let label = MaplyScreenLabel() + label.text = geofence.name + label.loc = MaplyCoordinateMakeWithDegrees(Float(centroid.y), + Float(centroid.x)) + label.layoutImportance = .infinity + + labels.append(label) + } + } + } + } + + if let objs = addShapes(shapes, desc: nil, mode: .any) { + geofenceObjects.append(objs) + } + if let objs = addWideVectors(vectors, desc: vectorDesc, mode: .any) { + geofenceObjects.append(objs) + } + if let objs = addScreenLabels(labels, desc: labelDesc, mode: .any) { + geofenceObjects.append(objs) + } + } + func setZoomLimits(minZoom: Int32, maxZoom: Int32) { setZoomLimitsMin( height(forMapScale: Float(truncating: diff --git a/iosApp/iosApp/Map/UnitMapView.swift b/iosApp/iosApp/Map/UnitMapView.swift index 533ac13..f416554 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, + geofences: $unitsViewModel.flatGeofences, selected: $unitsViewModel.selectedMarker, markerCallback: unitsViewModel.selectUnitWith) if let unit = unitsViewModel.selectedUnit { diff --git a/iosApp/iosApp/Shared/MarkerTransformations.swift b/iosApp/iosApp/Shared/MarkerTransformations.swift index 6291823..7291a58 100644 --- a/iosApp/iosApp/Shared/MarkerTransformations.swift +++ b/iosApp/iosApp/Shared/MarkerTransformations.swift @@ -43,6 +43,10 @@ class MarkerTransformations { case .trolleybus: imageName = "MapTrolleybus" case .truck: imageName = "MapTruck" case .van: imageName = "MapVan" + + case .reportStart: imageName = "MapReportStart" + case .reportPosition: imageName = "MapReportPosition" + case .reportEnd: imageName = "MapReportEnd" default: break } return imageName diff --git a/iosApp/iosApp/Shared/SidewaysScroller.swift b/iosApp/iosApp/Shared/SidewaysScroller.swift new file mode 100644 index 0000000..262b121 --- /dev/null +++ b/iosApp/iosApp/Shared/SidewaysScroller.swift @@ -0,0 +1,42 @@ +// +// SidewaysScroller.swift +// +// Copyright 2022 FlowAllocator LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +import SwiftUI + +/// wrap a view in a horizontal scroll view, for display of large tables on compact display area +public struct SidewaysScroller<Content: View>: View { + var minWidth: CGFloat + @ViewBuilder var content: () -> Content + + public init(minWidth: CGFloat, + @ViewBuilder content: @escaping () -> Content) + { + self.minWidth = minWidth + self.content = content + } + + public var body: some View { + GeometryReader { geo in + ScrollView(.horizontal) { + VStack(alignment: .leading) { + content() + } + .frame(minWidth: max(minWidth, geo.size.width)) + } + } + } +} diff --git a/iosApp/iosApp/Units/UnitsView.swift b/iosApp/iosApp/Units/UnitsView.swift index 2d51f53..7ddf5aa 100644 --- a/iosApp/iosApp/Units/UnitsView.swift +++ b/iosApp/iosApp/Units/UnitsView.swift @@ -78,7 +78,7 @@ struct UnitsView: View { } } .navigationViewStyle(StackNavigationViewStyle()) - .sheet(isPresented: $unitsViewModel.showDetails) { + .fullScreenCover(isPresented: $unitsViewModel.showDetails) { print("Dismissed") } content: { DetailsView(isPresented: $unitsViewModel.showDetails, diff --git a/iosApp/iosApp/Units/UnitsViewModel.swift b/iosApp/iosApp/Units/UnitsViewModel.swift index 47adeb7..8ba6a4b 100644 --- a/iosApp/iosApp/Units/UnitsViewModel.swift +++ b/iosApp/iosApp/Units/UnitsViewModel.swift @@ -60,7 +60,12 @@ class UnitsViewModel: ObservableObject { } @Published var selectedMarker: Marker? = nil @Published var mapLayerType: MapLayer = .companion.defaultLayer - @Published var geofences: [Int: Geofence] = [:] + @Published var geofences: [Int: Geofence] = [:] { + didSet { + flatGeofences = Array(geofences.values) + } + } + @Published var flatGeofences: [Geofence] = [] init() { unitsController.fetchUnits(scope: mainScope) diff --git a/iosApp/iosApp/en.lproj/Localizable.strings b/iosApp/iosApp/en.lproj/Localizable.strings index 97070ed..ca8e5a1 100644 --- a/iosApp/iosApp/en.lproj/Localizable.strings +++ b/iosApp/iosApp/en.lproj/Localizable.strings @@ -54,6 +54,45 @@ "open-location-browser" = "Open location in browser"; "maps-url-template" = "https://www.google.com/maps/place/{y},{x}?z=19"; +"report-period" = "Period"; +"report-positions" = "Positions"; +"report-events" = "Events"; +"report-stops" = "Stops"; +"report-save" = "Save"; +"report-share" = "Share"; + +"period-today" = "Today"; +"period-last-24" = "Last 24H"; +"period-yesterday" = "Yesterday"; +"period-this-week" = "Week"; +"period-last-7" = "Last 7d"; +"period-this-month" = "Month"; +"period-last-30" = "Last 30d"; + +"events-table-event" = "Event"; +"events-table-datetime" = "Datetime"; +"events-table-geofence" = "Geofence"; +"events-table-address" = "Address"; + +"event-device-online" = "Status online"; +"event-device-unknown" = "Status unknown"; +"event-device-offline" = "Status offline"; +"event-device-inactive" = "Device inactive"; +"event-device-moving" = "Device moving"; +"event-device-stopped" = "Device stopped"; +"event-device-overspeed" = "Speed limit exceeded"; +"event-device-fuel-drop" = "Fuel drop"; +"event-command-result" = "Command result"; +"event-geofence-enter" = "Geofence entered"; +"event-geofence-exit" = "Geofence exited"; +"event-alarm" = "Alarm"; +"event-ignition-on" = "Ignition on"; +"event-ignition-off" = "Ignition off"; +"event-maintenance" = "Maintenance required"; +"event-text-message" = "Text message received"; +"event-driver-changed" = "Driver changed"; +"event-unknown" = "Unknown event"; + "send-command" = "Send command"; "send-command-confirm" = "Are you sure you want to send the command?"; diff --git a/iosApp/iosApp/es-419.lproj/Localizable.strings b/iosApp/iosApp/es-419.lproj/Localizable.strings index a91787a..9abd96a 100644 --- a/iosApp/iosApp/es-419.lproj/Localizable.strings +++ b/iosApp/iosApp/es-419.lproj/Localizable.strings @@ -54,6 +54,45 @@ "open-location-browser" = "Abrir ubicación en navegador"; "maps-url-template" = "https://www.google.com/maps/place/{y},{x}?z=19"; +"report-period" = "Periodo"; +"report-positions" = "Posiciones"; +"report-events" = "Eventos"; +"report-stops" = "Paradas"; +"report-save" = "Guardar"; +"report-share" = "Compartir"; + +"period-today" = "Hoy"; +"period-last-24" = "Últimas 24H"; +"period-yesterday" = "Ayer"; +"period-this-week" = "Semana"; +"period-last-7" = "Últimos 7d"; +"period-this-month" = "Mes"; +"period-last-30" = "Últimos 30d"; + +"events-table-event" = "Evento"; +"events-table-datetime" = "Fecha y hora"; +"events-table-geofence" = "Geocerca"; +"events-table-address" = "Dirección"; + +"event-device-online" = "Unidad en línea"; +"event-device-unknown" = "Unidad en estado desconocido"; +"event-device-offline" = "Unidad fuera de línea"; +"event-device-inactive" = "Unidad inactiva"; +"event-device-moving" = "Unidad en movimiento"; +"event-device-stopped" = "Unidad detenida"; +"event-device-overspeed" = "Excedido el límite de velocidad"; +"event-device-fuel-drop" = "Pérdida de combustible"; +"event-command-result" = "Resultado del comando"; +"event-geofence-enter" = "Entrada en la geocerca"; +"event-geofence-exit" = "Salida de la geocerca"; +"event-alarm" = "Alarma"; +"event-ignition-on" = "Ignición encendida"; +"event-ignition-off" = "Ignición apagada"; +"event-maintenance" = "Se requiere mantenimiento"; +"event-text-message" = "Mensaje de texto recibido"; +"event-driver-changed" = "El conductor ha cambiado"; +"event-unknown" = "Evento desconocido"; + "send-command" = "Enviar comando"; "send-command-confirm" = "¿Está seguro que desea enviar el comando?"; diff --git a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt index b2e97e6..29c4229 100644 --- a/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt +++ b/shared/src/commonMain/kotlin/mx/trackermap/TrackerMap/controllers/ReportController.kt @@ -49,7 +49,7 @@ class ReportController( val reportFlow: MutableStateFlow<Report> = MutableStateFlow(Report.LoadingReport) - fun fetchReport( + suspend fun fetchReport( deviceId: Int, reportType: ReportType?, reportPeriod: ReportDates.ReportPeriod?, @@ -65,13 +65,13 @@ class ReportController( if (!xlsx) { reportFlow.value = Report.LoadingReport } - GlobalScope.launch { + // GlobalScope.launch { when (reportType) { ReportType.POSITIONS -> fetchPositions(deviceId, previousDate, currentDate, xlsx) ReportType.EVENTS -> fetchEvents(deviceId, previousDate, currentDate, eventTypes, xlsx) ReportType.STOPS -> fetchStops(deviceId, previousDate, currentDate, xlsx) } - } + // } } private suspend fun fetchPositions( |