From 0fdfc55d0471a7203b6dcbf9439bedb013b8e645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 15 Oct 2022 01:15:46 +0100 Subject: [PATCH 01/63] Revert "Merge branch 'production' into estimates-providers" This reverts commit a92643f42a05fd54595ac20b841085355eed37d9, reversing changes made to 4c7ca52b76ee32768e375e2ca4ba6b13634b8a16. --- GeoBus.xcodeproj/project.pbxproj | 58 +- GeoBus/App/Animations/Pulse.swift | 61 -- GeoBus/App/Animations/Spinner.swift | 38 - GeoBus/App/Components/About/AboutGeoBus.swift | 1 + GeoBus/App/Components/About/CloseButton.swift | 3 +- .../About/EstimationsProviderCard.swift | 84 ++ GeoBus/App/Components/About/SyncStatus.swift | 5 +- .../App/Components/Map/MapAnnotations.swift | 27 +- GeoBus/App/Components/Map/MapView.swift | 4 +- .../RouteDetailsVehiclesQuantity.swift | 2 +- .../RouteDetails/RouteDetailsView.swift | 7 +- .../SelectRoute/SelectRouteView.swift | 33 +- .../StopDetails/StopEstimations.swift | 6 +- .../VehicleDetails/VehicleDetailsView.swift | 374 +++++++- GeoBus/App/Extensions/Globals.swift | 166 ++++ GeoBus/App/Extensions/Helpers.swift | 15 +- GeoBus/App/GeoBusApp.swift | 18 +- GeoBus/App/Layout/RouteBadgePill.swift | 4 +- GeoBus/App/Layout/RouteBadgeSquare.swift | 4 +- GeoBus/App/Layout/TimeLeft.swift | 4 +- GeoBus/App/Lottie/EstimatedIcon.swift | 31 + GeoBus/App/Lottie/LiveIcon.swift | 30 + GeoBus/App/Lottie/LoadingPulse.swift | 91 ++ GeoBus/App/Lottie/LoadingSpinner.swift | 41 + GeoBus/App/Lottie/LoadingView.swift | 29 + GeoBus/App/Models/Estimations.swift | 37 + GeoBus/App/Models/Network.swift | 108 +++ GeoBus/App/Models/Vehicles.swift | 76 ++ GeoBus/App/State/Appstate.swift | 8 +- GeoBus/App/State/CarrisAuthentication.swift | 2 +- .../App/State/CarrisNetworkController.swift | 843 ++++++++++++++++++ GeoBus/App/State/EstimationsController.swift | 155 +++- GeoBus/App/State/MapController.swift | 12 +- GeoBus/App/State/RoutesController.swift | 2 +- GeoBus/App/State/VehiclesController.swift | 349 +++++++- GeoBus/de.lproj/Localizable.strings | 9 + GeoBus/en.lproj/Localizable.strings | 9 + GeoBus/fa.lproj/Localizable.strings | 9 + GeoBus/fr.lproj/Localizable.strings | 9 + GeoBus/it.lproj/Localizable.strings | 11 +- GeoBus/nl.lproj/Localizable.strings | 11 +- GeoBus/pl.lproj/Localizable.strings | 9 + GeoBus/pt.lproj/Localizable.strings | 9 + GeoBus/tr.lproj/Localizable.strings | 9 + GeoBus/uk.lproj/Localizable.strings | 11 +- GeoBus/zh-Hans.lproj/Localizable.strings | 9 + 46 files changed, 2601 insertions(+), 232 deletions(-) delete mode 100644 GeoBus/App/Animations/Pulse.swift delete mode 100644 GeoBus/App/Animations/Spinner.swift create mode 100644 GeoBus/App/Components/About/EstimationsProviderCard.swift create mode 100644 GeoBus/App/Extensions/Globals.swift create mode 100644 GeoBus/App/Lottie/EstimatedIcon.swift create mode 100644 GeoBus/App/Lottie/LiveIcon.swift create mode 100644 GeoBus/App/Lottie/LoadingPulse.swift create mode 100644 GeoBus/App/Lottie/LoadingSpinner.swift create mode 100644 GeoBus/App/Lottie/LoadingView.swift create mode 100644 GeoBus/App/Models/Network.swift create mode 100644 GeoBus/App/State/CarrisNetworkController.swift diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index 7e9ecd19..3d47d7a9 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -17,6 +17,9 @@ CF181FFD28CCB99E00248F72 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF181FFC28CCB99E00248F72 /* Auth.swift */; }; CF18200728CCBA3500248F72 /* RoutesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18200628CCBA3500248F72 /* RoutesController.swift */; }; CF18200928CCBA4600248F72 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18200828CCBA4600248F72 /* Routes.swift */; }; + CF18201928CCBB6400248F72 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18201828CCBB6400248F72 /* LoadingView.swift */; }; + CF18201B28CCBB7100248F72 /* LiveIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18201A28CCBB7100248F72 /* LiveIcon.swift */; }; + CF18201D28CCBB8000248F72 /* EstimatedIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18201C28CCBB8000248F72 /* EstimatedIcon.swift */; }; CF18202928CCBBDC00248F72 /* TapticEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18202828CCBBDC00248F72 /* TapticEngine.swift */; }; CF18202E28CCBC0100248F72 /* VehiclesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18202D28CCBC0100248F72 /* VehiclesController.swift */; }; CF18203028CCBC0B00248F72 /* EstimationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18202F28CCBC0B00248F72 /* EstimationsController.swift */; }; @@ -46,6 +49,7 @@ CF521A0528F34F5300AD0B76 /* StopDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF521A0428F34F5300AD0B76 /* StopDetailsView.swift */; }; CF548FF328D129B000668CB6 /* VehicleDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF6478FC28D112E800C70691 /* VehicleDetailsView.swift */; }; CF548FF628D14BA400668CB6 /* VehicleIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF548FF528D14BA400668CB6 /* VehicleIdentifier.swift */; }; + CF69994C28E878ED00646582 /* EstimationsProviderCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF69994B28E878ED00646582 /* EstimationsProviderCard.swift */; }; CF6C917E28D3ED0A006C3F61 /* SquareButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF6C917D28D3ED0A006C3F61 /* SquareButton.swift */; }; CF6C918228D3F1C6006C3F61 /* MapController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF6C918128D3F1C6006C3F61 /* MapController.swift */; }; CF6C918428D3F2C9006C3F61 /* UserLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF6C918328D3F2C9006C3F61 /* UserLocation.swift */; }; @@ -58,6 +62,7 @@ CF82BB0928D7F19A007F0CDB /* LocationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF82BB0828D7F19A007F0CDB /* LocationCard.swift */; }; CF82BB0B28D7F1C6007F0CDB /* ShareCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF82BB0A28D7F1C6007F0CDB /* ShareCard.swift */; }; CF82BB0D28D7F202007F0CDB /* ContactsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF82BB0C28D7F202007F0CDB /* ContactsCard.swift */; }; + CFAF0E7628CE586300DDAD5B /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFAF0E7528CE586300DDAD5B /* Globals.swift */; }; CFAF0E7828CE84C200DDAD5B /* Vehicles.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFAF0E7728CE84C200DDAD5B /* Vehicles.swift */; }; CFAF0E7A28CE84F400DDAD5B /* MapAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFAF0E7928CE84F400DDAD5B /* MapAnnotations.swift */; }; CFB5D45728EEFE21002368BC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = CFB5D45528EEFE21002368BC /* InfoPlist.strings */; }; @@ -73,11 +78,13 @@ CFFF2D3D28D7C99C00E035E0 /* PostHog in Frameworks */ = {isa = PBXBuildFile; productRef = CFFF2D3C28D7C99C00E035E0 /* PostHog */; }; CFFFAD7928F4AD0400DFD5FD /* TimeLeft.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD7828F4AD0400DFD5FD /* TimeLeft.swift */; }; CFFFAD7B28F4D8D000DFD5FD /* StopIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD7A28F4D8D000DFD5FD /* StopIcon.swift */; }; + CFFFAD7D28F5040C00DFD5FD /* CarrisNetworkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD7C28F5040C00DFD5FD /* CarrisNetworkController.swift */; }; + CFFFAD7F28F5928900DFD5FD /* Network.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD7E28F5928900DFD5FD /* Network.swift */; }; CFFFAD8128F64E2000DFD5FD /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8028F64E2000DFD5FD /* Analytics.swift */; }; CFFFAD8328F6754400DFD5FD /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8228F6754400DFD5FD /* Helpers.swift */; }; CFFFAD8528F7A21100DFD5FD /* CarrisAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8428F7A21100DFD5FD /* CarrisAuthentication.swift */; }; - CFFFAD8928F8ECDE00DFD5FD /* Spinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8828F8ECDE00DFD5FD /* Spinner.swift */; }; - CFFFAD8B28F8F33200DFD5FD /* Pulse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8A28F8F33200DFD5FD /* Pulse.swift */; }; + CFFFAD8928F8ECDE00DFD5FD /* LoadingSpinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8828F8ECDE00DFD5FD /* LoadingSpinner.swift */; }; + CFFFAD8B28F8F33200DFD5FD /* LoadingPulse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8A28F8F33200DFD5FD /* LoadingPulse.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -92,6 +99,9 @@ CF181FFC28CCB99E00248F72 /* Auth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Auth.swift; sourceTree = ""; }; CF18200628CCBA3500248F72 /* RoutesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutesController.swift; sourceTree = ""; }; CF18200828CCBA4600248F72 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routes.swift; sourceTree = ""; }; + CF18201828CCBB6400248F72 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; + CF18201A28CCBB7100248F72 /* LiveIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveIcon.swift; sourceTree = ""; }; + CF18201C28CCBB8000248F72 /* EstimatedIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedIcon.swift; sourceTree = ""; }; CF18202828CCBBDC00248F72 /* TapticEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapticEngine.swift; sourceTree = ""; }; CF18202D28CCBC0100248F72 /* VehiclesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclesController.swift; sourceTree = ""; }; CF18202F28CCBC0B00248F72 /* EstimationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimationsController.swift; sourceTree = ""; }; @@ -121,6 +131,7 @@ CF521A0428F34F5300AD0B76 /* StopDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopDetailsView.swift; sourceTree = ""; }; CF548FF528D14BA400668CB6 /* VehicleIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleIdentifier.swift; sourceTree = ""; }; CF6478FC28D112E800C70691 /* VehicleDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleDetailsView.swift; sourceTree = ""; }; + CF69994B28E878ED00646582 /* EstimationsProviderCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimationsProviderCard.swift; sourceTree = ""; }; CF6C917D28D3ED0A006C3F61 /* SquareButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SquareButton.swift; sourceTree = ""; }; CF6C918128D3F1C6006C3F61 /* MapController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapController.swift; sourceTree = ""; }; CF6C918328D3F2C9006C3F61 /* UserLocation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserLocation.swift; sourceTree = ""; }; @@ -134,6 +145,7 @@ CF82BB0828D7F19A007F0CDB /* LocationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCard.swift; sourceTree = ""; }; CF82BB0A28D7F1C6007F0CDB /* ShareCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareCard.swift; sourceTree = ""; }; CF82BB0C28D7F202007F0CDB /* ContactsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsCard.swift; sourceTree = ""; }; + CFAF0E7528CE586300DDAD5B /* Globals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; CFAF0E7728CE84C200DDAD5B /* Vehicles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicles.swift; sourceTree = ""; }; CFAF0E7928CE84F400DDAD5B /* MapAnnotations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAnnotations.swift; sourceTree = ""; }; CFAF0E7B28CEC78C00DDAD5B /* GeoBus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GeoBus.entitlements; sourceTree = ""; }; @@ -146,6 +158,8 @@ CFB5D46128EEFF2C002368BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; CFB5D46228EEFF2D002368BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; CFC80FCB28D2C2FF003D059D /* DragAndDrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragAndDrop.swift; sourceTree = ""; }; + CFCED4F428EF2F8300963640 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + CFCED4F528EF2F8600963640 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CFCED4F628EF5CB900963640 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; CFCED4F728EF5CB900963640 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; CFCED4F828EF5CD500963640 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; @@ -157,16 +171,16 @@ CFEF85C128D34E4F00A29526 /* crowdin.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = crowdin.yml; sourceTree = ""; }; CFEF85C328D34E6300A29526 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; CFEF85C428D34E6300A29526 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; - CFFCEC5F28F9F34900F8E271 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; - CFFCEC6028F9F34C00F8E271 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; CFFF2D3E28D7CEE300E035E0 /* Secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Secrets.xcconfig; sourceTree = ""; }; CFFFAD7828F4AD0400DFD5FD /* TimeLeft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeLeft.swift; sourceTree = ""; }; CFFFAD7A28F4D8D000DFD5FD /* StopIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopIcon.swift; sourceTree = ""; }; + CFFFAD7C28F5040C00DFD5FD /* CarrisNetworkController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrisNetworkController.swift; sourceTree = ""; }; + CFFFAD7E28F5928900DFD5FD /* Network.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = ""; }; CFFFAD8028F64E2000DFD5FD /* Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; }; CFFFAD8228F6754400DFD5FD /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; CFFFAD8428F7A21100DFD5FD /* CarrisAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrisAuthentication.swift; sourceTree = ""; }; - CFFFAD8828F8ECDE00DFD5FD /* Spinner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spinner.swift; sourceTree = ""; }; - CFFFAD8A28F8F33200DFD5FD /* Pulse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pulse.swift; sourceTree = ""; }; + CFFFAD8828F8ECDE00DFD5FD /* LoadingSpinner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingSpinner.swift; sourceTree = ""; }; + CFFFAD8A28F8F33200DFD5FD /* LoadingPulse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingPulse.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -235,6 +249,7 @@ CFFFAD8028F64E2000DFD5FD /* Analytics.swift */, CF6C918128D3F1C6006C3F61 /* MapController.swift */, CFFFAD8428F7A21100DFD5FD /* CarrisAuthentication.swift */, + CFFFAD7C28F5040C00DFD5FD /* CarrisNetworkController.swift */, CF6C918928D4A88D006C3F61 /* StopsController.swift */, CF18200628CCBA3500248F72 /* RoutesController.swift */, CF18202D28CCBC0100248F72 /* VehiclesController.swift */, @@ -249,6 +264,7 @@ CF181FFC28CCB99E00248F72 /* Auth.swift */, CF6C918D28D4DABA006C3F61 /* Stops.swift */, CF18200828CCBA4600248F72 /* Routes.swift */, + CFFFAD7E28F5928900DFD5FD /* Network.swift */, CFAF0E7728CE84C200DDAD5B /* Vehicles.swift */, CF18203C28CCBC6000248F72 /* Estimations.swift */, ); @@ -319,7 +335,7 @@ CF05F61628CD08D400B4AD58 /* Components */, CF05F61C28CD1F2800B4AD58 /* Layout */, CF18200C28CCBA7300248F72 /* Extensions */, - CF18201528CCBB4900248F72 /* Animations */, + CF18201528CCBB4900248F72 /* Lottie */, ); path = App; sourceTree = ""; @@ -329,19 +345,23 @@ children = ( CF18202828CCBBDC00248F72 /* TapticEngine.swift */, CFC80FCB28D2C2FF003D059D /* DragAndDrop.swift */, + CFAF0E7528CE586300DDAD5B /* Globals.swift */, CFFFAD8228F6754400DFD5FD /* Helpers.swift */, CFDC15EE28D292FB00A4BE49 /* ViewSize.swift */, ); path = Extensions; sourceTree = ""; }; - CF18201528CCBB4900248F72 /* Animations */ = { + CF18201528CCBB4900248F72 /* Lottie */ = { isa = PBXGroup; children = ( - CFFFAD8A28F8F33200DFD5FD /* Pulse.swift */, - CFFFAD8828F8ECDE00DFD5FD /* Spinner.swift */, + CF18201828CCBB6400248F72 /* LoadingView.swift */, + CF18201A28CCBB7100248F72 /* LiveIcon.swift */, + CF18201C28CCBB8000248F72 /* EstimatedIcon.swift */, + CFFFAD8A28F8F33200DFD5FD /* LoadingPulse.swift */, + CFFFAD8828F8ECDE00DFD5FD /* LoadingSpinner.swift */, ); - path = Animations; + path = Lottie; sourceTree = ""; }; CF18205228CCBCF400248F72 /* StopDetails */ = { @@ -378,6 +398,7 @@ children = ( CF6C918728D3FAF8006C3F61 /* AboutGeoBus.swift */, CFDD014828D5114D0070FE4B /* SyncStatus.swift */, + CF69994B28E878ED00646582 /* EstimationsProviderCard.swift */, CF82BB0628D7F166007F0CDB /* LiveDataCard.swift */, CF82BB0828D7F19A007F0CDB /* LocationCard.swift */, CF82BB0A28D7F1C6007F0CDB /* ShareCard.swift */, @@ -510,6 +531,7 @@ CF03D6AE28F3B00F0077299B /* VehicleDestination.swift in Sources */, CF18207328CCBD2300248F72 /* VariantButton.swift in Sources */, CF18209B28CCBD5000248F72 /* MapView.swift in Sources */, + CFFFAD7D28F5040C00DFD5FD /* CarrisNetworkController.swift in Sources */, CF18207428CCBD2300248F72 /* CircularVariantInfo.swift in Sources */, CF18208228CCBD3A00248F72 /* SelectRouteView.swift in Sources */, CF18208328CCBD3A00248F72 /* SelectRouteSheet.swift in Sources */, @@ -517,8 +539,9 @@ CF18207828CCBD2300248F72 /* RouteDetailsSheet.swift in Sources */, CF18207728CCBD2300248F72 /* RouteDetailsAddToFavorites.swift in Sources */, CF6C918428D3F2C9006C3F61 /* UserLocation.swift in Sources */, - CFFFAD8928F8ECDE00DFD5FD /* Spinner.swift in Sources */, + CFFFAD8928F8ECDE00DFD5FD /* LoadingSpinner.swift in Sources */, CF6C918E28D4DABA006C3F61 /* Stops.swift in Sources */, + CF18201928CCBB6400248F72 /* LoadingView.swift in Sources */, CF18207528CCBD2300248F72 /* VariantWarning.swift in Sources */, CF6C918C28D4B452006C3F61 /* SearchStopInput.swift in Sources */, CF18207628CCBD2300248F72 /* StopsList.swift in Sources */, @@ -527,12 +550,14 @@ CF6C918628D3F8C8006C3F61 /* StopSearch.swift in Sources */, CF18207228CCBD2300248F72 /* VariantPicker.swift in Sources */, CFAF0E7A28CE84F400DDAD5B /* MapAnnotations.swift in Sources */, + CF69994C28E878ED00646582 /* EstimationsProviderCard.swift in Sources */, CF548FF328D129B000668CB6 /* VehicleDetailsView.swift in Sources */, CFFFAD7B28F4D8D000DFD5FD /* StopIcon.swift in Sources */, CF18203028CCBC0B00248F72 /* EstimationsController.swift in Sources */, CF18200928CCBA4600248F72 /* Routes.swift in Sources */, CF6C918A28D4A88D006C3F61 /* StopsController.swift in Sources */, CF82BB0928D7F19A007F0CDB /* LocationCard.swift in Sources */, + CFFFAD7F28F5928900DFD5FD /* Network.swift in Sources */, CF18207928CCBD2300248F72 /* RouteDetailsVehiclesQuantity.swift in Sources */, CF181FFD28CCB99E00248F72 /* Auth.swift in Sources */, CF181FE628CCB7D600248F72 /* GeoBusApp.swift in Sources */, @@ -541,6 +566,7 @@ CF18205728CCBD0400248F72 /* StopEstimations.swift in Sources */, CFFFAD8528F7A21100DFD5FD /* CarrisAuthentication.swift in Sources */, CF05F61A28CD09A000B4AD58 /* NavBar.swift in Sources */, + CFAF0E7628CE586300DDAD5B /* Globals.swift in Sources */, CFDD014B28D535370070FE4B /* Card.swift in Sources */, CF18202928CCBBDC00248F72 /* TapticEngine.swift in Sources */, CF18200728CCBA3500248F72 /* RoutesController.swift in Sources */, @@ -550,12 +576,14 @@ CF18204828CCBCC500248F72 /* RouteBadgeSquare.swift in Sources */, CFDD014928D5114D0070FE4B /* SyncStatus.swift in Sources */, CF18208528CCBD3A00248F72 /* FavoriteRoutes.swift in Sources */, - CFFFAD8B28F8F33200DFD5FD /* Pulse.swift in Sources */, + CF18201D28CCBB8000248F72 /* EstimatedIcon.swift in Sources */, + CFFFAD8B28F8F33200DFD5FD /* LoadingPulse.swift in Sources */, CF6C918828D3FAF9006C3F61 /* AboutGeoBus.swift in Sources */, CF6C917E28D3ED0A006C3F61 /* SquareButton.swift in Sources */, CF82BB0B28D7F1C6007F0CDB /* ShareCard.swift in Sources */, CF47994F28D33E1900B56D4B /* Disclaimer.swift in Sources */, CF47994D28D3315E00B56D4B /* Appstate.swift in Sources */, + CF18201B28CCBB7100248F72 /* LiveIcon.swift in Sources */, CFDD014D28D66D9B0070FE4B /* CloseButton.swift in Sources */, CFDC15EF28D292FB00A4BE49 /* ViewSize.swift in Sources */, CFAF0E7828CE84C200DDAD5B /* Vehicles.swift in Sources */, @@ -574,9 +602,9 @@ CFB5D45C28EEFEA6002368BC /* pt */, CFB5D46028EEFEF1002368BC /* zh-Hans */, CFB5D46228EEFF2D002368BC /* tr */, + CFCED4F428EF2F8300963640 /* en */, CFCED4F728EF5CB900963640 /* pl */, CFCED4F928EF5CD500963640 /* fa */, - CFFCEC6028F9F34C00F8E271 /* en */, ); name = InfoPlist.strings; sourceTree = ""; @@ -588,9 +616,9 @@ CFB5D45B28EEFEA3002368BC /* pt */, CFB5D45F28EEFEF1002368BC /* zh-Hans */, CFB5D46128EEFF2C002368BC /* tr */, + CFCED4F528EF2F8600963640 /* en */, CFCED4F628EF5CB900963640 /* pl */, CFCED4F828EF5CD500963640 /* fa */, - CFFCEC5F28F9F34900F8E271 /* en */, ); name = Localizable.strings; sourceTree = ""; diff --git a/GeoBus/App/Animations/Pulse.swift b/GeoBus/App/Animations/Pulse.swift deleted file mode 100644 index d9863c39..00000000 --- a/GeoBus/App/Animations/Pulse.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Pulse.swift -// GeoBus -// -// Created by João de Vasconcelos on 14/10/2022. -// - -import SwiftUI - - -struct PulseLabel: View { - - let accent: Color - let label: Text - - var body: some View { - HStack(spacing: 2) { - Pulse(size: 15, accent: self.accent) - label - .font(Font.system(size: 11, weight: .medium, design: .default) ) - .foregroundColor(self.accent) - } - } - -} - - - -struct Pulse: View { - - let speed: Double = 3 - - let size: CGFloat - let accent: Color - - @State var scale: Double = 0.0 - @State var opacity: Double = 0.8 - - - var body: some View { - ZStack { - Circle() - .scale(scale) - .fill(accent) - .opacity(opacity) - Circle() - .fill(accent) - .frame(width: size/4, height: size/4, alignment: .center) - } - .frame(width: size, height: size, alignment: .center) - .onAppear { - withAnimation(.easeOut(duration: speed).repeatForever(autoreverses: false)) { - scale = 1.0 - } - withAnimation(.easeIn(duration: speed).repeatForever(autoreverses: false)) { - opacity = 0.0 - } - } - } - -} diff --git a/GeoBus/App/Animations/Spinner.swift b/GeoBus/App/Animations/Spinner.swift deleted file mode 100644 index 98d72e89..00000000 --- a/GeoBus/App/Animations/Spinner.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// Spinner.swift -// GeoBus -// -// Created by João de Vasconcelos on 14/10/2022. -// - -import SwiftUI - -struct Spinner: View { - - @Environment(\.colorScheme) var colorScheme: ColorScheme - - private let timing: Double = 0.5 - private let size: CGFloat = 20.0 - - @State var trim: Double = 0.4 - @State var rotationAngle: Double = 0.0 - - var body: some View { - Circle() - .trim(from: trim, to: 1.0) - .stroke(colorScheme == .dark ? .white : .green, - style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round) - ) - .rotationEffect(.degrees(rotationAngle)) - .frame(width: size, height: size, alignment: .center) - .onAppear { - withAnimation(.linear(duration: timing * 2).repeatForever()) { - trim = 0.8 - } - withAnimation(.linear(duration: timing).repeatForever(autoreverses: false)) { - rotationAngle = 360.0 - } - } - } - -} diff --git a/GeoBus/App/Components/About/AboutGeoBus.swift b/GeoBus/App/Components/About/AboutGeoBus.swift index 305f3387..38976e4d 100644 --- a/GeoBus/App/Components/About/AboutGeoBus.swift +++ b/GeoBus/App/Components/About/AboutGeoBus.swift @@ -47,6 +47,7 @@ struct AboutGeoBus: View { .padding(.top, 70) .padding(.bottom, 15) SyncStatus() + EstimationsProviderCard() } .padding(.horizontal) diff --git a/GeoBus/App/Components/About/CloseButton.swift b/GeoBus/App/Components/About/CloseButton.swift index 45e9843c..6dec1477 100644 --- a/GeoBus/App/Components/About/CloseButton.swift +++ b/GeoBus/App/Components/About/CloseButton.swift @@ -9,7 +9,6 @@ import SwiftUI struct CloseButton: View { - @EnvironmentObject var appstate: Appstate @EnvironmentObject var stopsController: StopsController @EnvironmentObject var routesController: RoutesController @@ -101,7 +100,7 @@ struct CloseButton: View { var body: some View { if (stopsController.allStops.isEmpty || routesController.allRoutes.isEmpty) { - if (appstate.stops == .error || appstate.routes == .error) { + if (Appstate.shared.stops == .error || Appstate.shared.routes == .error) { syncError } else { isSyncing diff --git a/GeoBus/App/Components/About/EstimationsProviderCard.swift b/GeoBus/App/Components/About/EstimationsProviderCard.swift new file mode 100644 index 00000000..ff5b08f6 --- /dev/null +++ b/GeoBus/App/Components/About/EstimationsProviderCard.swift @@ -0,0 +1,84 @@ +// +// AboutLiveData.swift +// GeoBus +// +// Created by João de Vasconcelos on 19/09/2022. +// + +import SwiftUI + +struct EstimationsProviderCard: View { + + @EnvironmentObject var estimationsController: EstimationsController + + private let cardColor: Color = Color(.systemTeal) + + @State var communityProviderIsOn: Bool = false + + + var providerToggle: some View { + Toggle(isOn: $communityProviderIsOn) { + HStack { + Image(systemName: "staroflife.circle") + .renderingMode(.template) + .font(Font.system(size: 25)) + .foregroundColor(cardColor) + Text("Community ETAs") + .font(Font.system(size: 18, weight: .bold)) + .foregroundColor(cardColor) + .padding(.leading, 5) + } + .onAppear() { + if (estimationsController.estimationsProvider == .carris) { + communityProviderIsOn = false + } else { + communityProviderIsOn = true + } + } + } + .padding() + .frame(maxWidth: .infinity) + .tint(cardColor) + .background(cardColor.opacity(0.05)) + .cornerRadius(10) + .onChange(of: estimationsController.estimationsProvider) { value in + if (value == .carris) { + communityProviderIsOn = false + } else { + communityProviderIsOn = true + } + } + .onChange(of: communityProviderIsOn) { value in + if (value) { + estimationsController.setProvider(selection: .community) + } else { + estimationsController.setProvider(selection: .carris) + } + } + } + + + var body: some View { + Card { + Image(systemName: "clock.arrow.2.circlepath") + .font(Font.system(size: 30, weight: .regular)) + .foregroundColor(cardColor) + Text("ETA Provider") + .font(.title) + .fontWeight(.bold) + .foregroundColor(cardColor) + Text("Select your prefered Time of Arrival provider.") + .multilineTextAlignment(.center) + .font(.headline) + .fontWeight(.semibold) + .foregroundColor(Color(.label)) + Text("Text about differences in provider.") + .multilineTextAlignment(.center) + .font(.headline) + .fontWeight(.semibold) + .foregroundColor(Color(.secondaryLabel)) + providerToggle + } + } + +} diff --git a/GeoBus/App/Components/About/SyncStatus.swift b/GeoBus/App/Components/About/SyncStatus.swift index 471a1a7d..7cc018af 100644 --- a/GeoBus/App/Components/About/SyncStatus.swift +++ b/GeoBus/App/Components/About/SyncStatus.swift @@ -9,7 +9,6 @@ import SwiftUI struct SyncStatus: View { - @EnvironmentObject var appstate: Appstate @EnvironmentObject var stopsController: StopsController @EnvironmentObject var routesController: RoutesController @@ -111,9 +110,9 @@ struct SyncStatus: View { var body: some View { - if (appstate.stops == .error || appstate.routes == .error) { + if (Appstate.shared.stops == .error || Appstate.shared.routes == .error) { syncError - } else if (appstate.stops == .loading || appstate.routes == .loading) { + } else if (Appstate.shared.stops == .loading || Appstate.shared.routes == .loading) { isSyncing } else { hasSynced diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index df37c53a..de092e23 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -34,10 +34,10 @@ struct GenericMapAnnotation: Identifiable { } // For Vehicles - var vehicle: VehicleSummary? + var vehicle: Vehicle? let busNumber: Int? - init(lat: Double, lng: Double, format: Format, busNumber: Int, vehicle: VehicleSummary) { + init(lat: Double, lng: Double, format: Format, busNumber: Int, vehicle: Vehicle) { self.location = CLLocationCoordinate2D(latitude: lat, longitude: lng) self.format = format self.stop = nil @@ -99,7 +99,7 @@ struct StopAnnotationView: View { struct VehicleAnnotationView: View { - let vehicle: VehicleSummary + let vehicle: Vehicle let isPresentedOnAppear: Bool @State private var isPresented: Bool = false @@ -123,27 +123,16 @@ struct VehicleAnnotationView: View { Image("RegularService-Active") case .regular: Image("RegularService-Active") + case .none: + Rectangle() + .background(Color.clear) } } } .frame(width: 40, height: 40, alignment: .center) - .rotationEffect(.radians(vehicle.angleInRadians)) + .rotationEffect(.radians(vehicle.angleInRadians ?? 0)) .sheet(isPresented: $isPresented) { - VStack(alignment: .leading) { - VehicleDetailsView( - busNumber: vehicle.busNumber, - routeNumber: vehicle.routeNumber, - lastGpsTime: vehicle.lastGpsTime - ) - .padding(.bottom, 20) - Disclaimer() - .padding(.horizontal) - .padding(.bottom, 10) - } - .readSize { size in - viewSize = size - } - .presentationDetents([.height(viewSize.height)]) + VehicleInfoSheet(busNumber: vehicle.busNumber) } } diff --git a/GeoBus/App/Components/Map/MapView.swift b/GeoBus/App/Components/Map/MapView.swift index 93b94005..27950967 100644 --- a/GeoBus/App/Components/Map/MapView.swift +++ b/GeoBus/App/Components/Map/MapView.swift @@ -46,10 +46,10 @@ struct MapView: View { .onChange(of: routesController.selectedVariant) { newVariant in if (newVariant != nil) { self.mapController.updateAnnotations(with: newVariant!) - self.mapController.updateAnnotations(with: vehiclesController.vehicles, for: routesController.selectedRoute?.number) + self.mapController.updateAnnotations(with: vehiclesController.allVehicles, for: routesController.selectedRoute?.number) } } - .onChange(of: vehiclesController.vehicles) { newVehiclesList in + .onChange(of: vehiclesController.allVehicles) { newVehiclesList in self.mapController.updateAnnotations(with: newVehiclesList, for: routesController.selectedRoute?.number) } } diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsVehiclesQuantity.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsVehiclesQuantity.swift index 0f6d57ad..24d03fa2 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsVehiclesQuantity.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsVehiclesQuantity.swift @@ -15,7 +15,7 @@ struct RouteDetailsVehiclesQuantity: View { var body: some View { ZStack(alignment: .topLeading) { - PulseLabel(accent: .green, label: Text("Live")) + LiveIcon() VStack(alignment: .center) { Text(String(vehiclesQuantity)) diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift index 61341e98..67dd85d2 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift @@ -10,7 +10,6 @@ import SwiftUI struct RouteDetailsView: View { - @EnvironmentObject var appstate: Appstate @EnvironmentObject var routesController: RoutesController @EnvironmentObject var vehiclesController: VehiclesController @@ -71,7 +70,7 @@ struct RouteDetailsView: View { var selectedRouteDetails: some View { VStack(alignment: .leading) { HStack { - PulseLabel(accent: .green, label: Text("Live")) + LiveIcon() Text(vehiclesController.vehicles.count == 1 ? "1 active vehicle" : "\(vehiclesController.vehicles.count) active vehicles") .font(Font.system(size: 11, weight: .medium, design: .default) ) .lineLimit(1) @@ -91,9 +90,9 @@ struct RouteDetailsView: View { // The final view where screens are composed based on appstate var body: some View { VStack { - if (appstate.routes == .loading && routesController.allRoutes.count < 1) { + if (Appstate.shared.routes == .loading && routesController.allRoutes.count < 1) { updatingRoutesScreen - } else if (appstate.global == .error) { + } else if (Appstate.shared.global == .error) { connectionError } else if (routesController.selectedRoute != nil) { selectedRouteDetails diff --git a/GeoBus/App/Components/SelectRoute/SelectRouteView.swift b/GeoBus/App/Components/SelectRoute/SelectRouteView.swift index acd08a29..5e328650 100644 --- a/GeoBus/App/Components/SelectRoute/SelectRouteView.swift +++ b/GeoBus/App/Components/SelectRoute/SelectRouteView.swift @@ -9,33 +9,26 @@ import SwiftUI struct SelectRouteView: View { - - @Environment(\.colorScheme) var colorScheme: ColorScheme - - @EnvironmentObject var appstate: Appstate + @EnvironmentObject var routesController: RoutesController - + var body: some View { - + ZStack { - - if (appstate.global == .loading) { - ZStack { - RoundedRectangle(cornerRadius: 10) - .fill(Color(.systemGray4)) - Spinner() - } - - } else if (appstate.global == .error) { + + if (Appstate.shared.global == .loading) { + LoadingView() + + } else if (Appstate.shared.global == .error) { RoundedRectangle(cornerRadius: 10) .fill(Color(.systemRed).opacity(0.5)) Image(systemName: "wifi.exclamationmark") .font(.title) .foregroundColor(Color(.white)) - + } else { - + if (routesController.selectedRoute != nil) { RouteBadgeSquare(routeNumber: routesController.selectedRoute!.number) @@ -45,11 +38,11 @@ struct SelectRouteView: View { Image(systemName: "plus") .font(.title) .foregroundColor(Color(.white)) - + } - + } - + } .aspectRatio(1, contentMode: .fit) diff --git a/GeoBus/App/Components/StopDetails/StopEstimations.swift b/GeoBus/App/Components/StopDetails/StopEstimations.swift index f08318d7..3c03dfc4 100644 --- a/GeoBus/App/Components/StopDetails/StopEstimations.swift +++ b/GeoBus/App/Components/StopDetails/StopEstimations.swift @@ -10,8 +10,6 @@ import SwiftUI struct StopEstimations: View { - @EnvironmentObject var appstate: Appstate - let estimations: [Estimation]? @@ -22,7 +20,7 @@ struct StopEstimations: View { .textCase(.uppercase) .foregroundColor(Color(.tertiaryLabel)) Spacer() - PulseLabel(accent: .orange, label: Text("Estimated")) + EstimatedIcon() } } @@ -71,7 +69,7 @@ struct StopEstimations: View { } else { noResultsScreen } - } else if (appstate.estimations == .error) { + } else if (Appstate.shared.estimations == .error) { errorScreen } else { loadingScreen diff --git a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift index 75be5cf9..f405efb8 100644 --- a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift +++ b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift @@ -5,12 +5,367 @@ // Created by João on 22/04/2020. // Copyright © 2020 João. All rights reserved. // + import SwiftUI import Combine + + +struct VehicleInfoSheet: View { + + public let busNumber: Int + + @EnvironmentObject var vehiclesController: VehiclesController + + private let refreshTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() + + + var body: some View { + ScrollView { + + // SECTION 1 + VehicleInfoSheetHeader(vehicle: vehiclesController.getVehicle(by: busNumber)) + + // SECTION 2 + VehicleInfoSheetCurrentRouteStatus(vehicle: vehiclesController.getVehicle(by: busNumber)) + .padding() + + Spacer() + + VehicleInfoSheetLastSeenTime(vehicle: vehiclesController.getVehicle(by: busNumber)) + .padding() + + } + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.hidden) + .onAppear() { + self.vehiclesController.update(scope: .detail, for: self.busNumber) + self.vehiclesController.update(scope: .community, for: self.busNumber) + } + .onReceive(refreshTimer) { event in + self.vehiclesController.update(scope: .detail, for: self.busNumber) + self.vehiclesController.update(scope: .community, for: self.busNumber) + } + } + +} + + + +struct VehicleInfoSheetHeader: View { + + public let vehicle: Vehicle? + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 10) { + VehicleDestination(routeNumber: vehicle?.routeNumber ?? "-", destination: vehicle?.lastStopOnVoyageName ?? "-") + Spacer() + VehicleIdentifier(busNumber: vehicle?.busNumber ?? 0, vehiclePlate: vehicle?.vehiclePlate ?? "-") + } + .padding() + Divider() + } + } + +} + + + +struct VehicleInfoSheetLastSeenTime: View { + + public let vehicle: Vehicle? + + private let lastSeenTimeTimer = Timer.publish(every: 1 /* seconds */, on: .main, in: .common).autoconnect() + + @State private var lastSeenTime: String = "-" + + + var body: some View { + VStack(alignment: .leading) { + HStack(alignment: .center, spacing: 5) { + Image(systemName: "antenna.radiowaves.left.and.right") + .font(.system(size: 12, weight: .bold, design: .default)) + .foregroundColor(Color(.secondaryLabel)) + Text("GPS updated \(lastSeenTime) ago") + .font(.system(size: 12, weight: .bold, design: .default)) + .foregroundColor(Color(.secondaryLabel)) + .onAppear() { + self.lastSeenTime = Globals().getTimeString(for: vehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) + } + .onReceive(lastSeenTimeTimer) { event in + self.lastSeenTime = Globals().getTimeString(for: vehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) + } + Spacer() + } + } + } + +} + + + + +struct VehicleInfoSheetCurrentRouteStatus: View { + + public let vehicle: Vehicle? + + let times = [ + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20", + "2022-10-10T23:11:20" + ] + + var body: some View { + VStack(spacing: 0) { + // ForEach(vehicle?.estimatedTimeofArrivalCorrected ?? [], id: \.self) { timeString in + ForEach(self.times, id: \.self) { timeString in + VehicleInfoSheetRouteStop(timeString: timeString) + } + } + } + +} + + + + +struct VehicleInfoSheetRouteStop: View { + + + + public let timeString: String + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 15) { + StopIcon(orderInRoute: 3, direction: .ascending) + Text("Teste") + .fontWeight(.medium) + .foregroundColor(Color(.label)) + .multilineTextAlignment(.leading) + Spacer() +// Text("45678") +// .font(Font.system(size: 12, weight: .medium, design: .default) ) +// .foregroundColor(Color(.secondaryLabel)) +// .padding(.vertical, 2) +// .padding(.horizontal, 7) +// .cornerRadius(10) + TimeLeft(time: timeString) + } + HStack(spacing: 15) { + Rectangle() + .foregroundColor(Color("StopSelectedBackground")) + .frame(width: 5, height: 25) + .padding(.leading, 10) + VStack { + Divider() + } + } + } + Spacer() +// TimeLeft(time: timeString) + } + + } + +} + + + + + + + + +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + + + + + + + + + + + +struct VehicleInfoSheetHeader2: View { + + public let vehicle: Vehicle? + + @EnvironmentObject var vehiclesController: VehiclesController + + private let lastSeenTimeTimer = Timer.publish(every: 1 /* seconds */, on: .main, in: .common).autoconnect() + + @State var lastSeenTime: String = "-" + + + var loadingScreen: some View { + HStack(spacing: 3) { + ProgressView() + .scaleEffect(0.55) + Text("Loading...") + .font(Font.system(size: 13, weight: .medium, design: .default) ) + .foregroundColor(Color(.tertiaryLabel)) + Spacer() + } + } + + var errorScreen: some View { + Text("Carris API is unavailable.") + .font(Font.system(size: 13, weight: .medium, design: .default) ) + .foregroundColor(Color(.secondaryLabel)) + } + + var vehicleDetailsHeader: some View { + HStack(spacing: 15) { + VehicleDestination(routeNumber: vehicle?.routeNumber ?? "-", destination: vehicle?.lastStopOnVoyageName ?? "-") + Spacer() + VehicleIdentifier(busNumber: vehicle?.busNumber ?? 0, vehiclePlate: vehicle?.vehiclePlate ?? "-") + } + } + + + var vehicleDetailsScreen: some View { + VStack(alignment: .leading) { + HStack(alignment: .center, spacing: 5) { + Image(systemName: "antenna.radiowaves.left.and.right") + .font(.system(size: 12, weight: .bold, design: .default)) + .foregroundColor(Color(.secondaryLabel)) + Text("GPS updated \(lastSeenTime) ago") + .font(.system(size: 12, weight: .bold, design: .default)) + .foregroundColor(Color(.secondaryLabel)) + .onAppear() { + self.lastSeenTime = Globals().getTimeString(for: vehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) + } + .onReceive(lastSeenTimeTimer) { event in + self.lastSeenTime = Globals().getTimeString(for: vehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) + } + Spacer() + } + } + } + + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + if (vehicle != nil) { + vehicleDetailsHeader + .padding() + Divider() + vehicleDetailsScreen + .padding() + } else if (Appstate.shared.vehicles == .loading) { + loadingScreen + .padding() + } else { + errorScreen + .padding() + } + } + + } + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + struct VehicleDetailsView: View { - @EnvironmentObject var appstate: Appstate + @EnvironmentObject var vehiclesController: VehiclesController + + let vehicle: VehicleSummary + + @State private var viewSize = CGSize() + + var body: some View { + VStack(alignment: .leading) { + VehicleDetailsView2( + busNumber: vehicle.busNumber, + routeNumber: vehicle.routeNumber, + lastGpsTime: vehicle.lastGpsTime + ) + .padding(.bottom, 20) + Disclaimer() + .padding(.horizontal) + .padding(.bottom, 10) + } + .readSize { size in + viewSize = size + } + .presentationDetents([.height(viewSize.height), .large]) + + } + +} + + + + + + +struct VehicleDetailsView2: View { + @EnvironmentObject var vehiclesController: VehiclesController let refreshTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() @@ -50,7 +405,16 @@ struct VehicleDetailsView: View { var vehicleDetailsHeader: some View { HStack(spacing: 15) { - VehicleDestination(routeNumber: routeNumber, destination: vehicleDetails!.lastStopOnVoyageName) + HStack { + RouteBadgePill(routeNumber: routeNumber) + Text("to") + .font(.footnote) + .foregroundColor(Color(.tertiaryLabel)) + Text(vehicleDetails!.lastStopOnVoyageName) + .font(.body) + .fontWeight(.medium) + .foregroundColor(Color(.label)) + } Spacer() VehicleIdentifier(busNumber: busNumber, vehiclePlate: vehicleDetails!.vehiclePlate) } @@ -67,10 +431,10 @@ struct VehicleDetailsView: View { .font(.system(size: 12, weight: .bold, design: .default)) .foregroundColor(Color(.secondaryLabel)) .onAppear() { - self.lastSeenTime = Helpers.getTimeString(for: lastGpsTime, in: .past, style: .full, units: [.hour, .minute, .second]) + self.lastSeenTime = Globals().getTimeString(for: lastGpsTime, in: .past, style: .full, units: [.hour, .minute, .second]) } .onReceive(lastSeenTimeTimer) { event in - self.lastSeenTime = Helpers.getTimeString(for: lastGpsTime, in: .past, style: .full, units: [.hour, .minute, .second]) + self.lastSeenTime = Globals().getTimeString(for: lastGpsTime, in: .past, style: .full, units: [.hour, .minute, .second]) } Spacer() } @@ -86,7 +450,7 @@ struct VehicleDetailsView: View { Divider() vehicleDetailsScreen .padding() - } else if (appstate.vehicles == .loading) { + } else if (Appstate.shared.vehicles == .loading) { loadingScreen .padding() } else { diff --git a/GeoBus/App/Extensions/Globals.swift b/GeoBus/App/Extensions/Globals.swift new file mode 100644 index 00000000..73cf5885 --- /dev/null +++ b/GeoBus/App/Extensions/Globals.swift @@ -0,0 +1,166 @@ +// +// Helpers.swift +// GeoBus +// +// Created by João de Vasconcelos on 11/09/2022. +// + +import Foundation +import SwiftUI + + + +//open class ExampleClass { +// +// public static let variable: ExampleClass = .init() +// +// public enum ExampleEnum { +// case primary, secondary, tertiary +// } +// +// public func set(value example: ExampleEnum) -> ExampleEnum { +// +// } +// +//} + + +open class Globals { + + + public static let variable: Globals = .init() + + /* MARK: - Get Route Kind */ + + // Discover the Route kind by analysing the route number. + + func getKind(by routeNumber: String) -> Kind { + + if (routeNumber.suffix(1) == "B") { + // Neighborhood buses end with "B" + return .neighborhood + + } else if (routeNumber.suffix(1) == "E") { + // Trams and Elevators end with "E" + if (routeNumber.prefix(1) == "5") { + // and Elevators start with "5" + return .elevator + } else { + // All other options starting with "E" are trams + return .tram + } + + } else if (routeNumber.prefix(1) == "2") { + // Night service starts with "2" + return .night + + } else { + // All other options are regular service + return .regular + + } + + } + + + /* MARK: - Get Theme Colors */ + + // Centralized functions that retrieve theme colors. + + func getBackgroundColor(for routeNumber: String) -> Color { + let routeKind = getKind(by: routeNumber) + switch routeKind { + case .tram: + return Color(red: 1.00, green: 0.85, blue: 0.00) + case .neighborhood: + return Color(red: 1.00, green: 0.55, blue: 0.40) + case .night: + return Color(red: 0.12, green: 0.35, blue: 0.70) + case .elevator: + return Color(red: 0.00, green: 0.60, blue: 0.40) + case .regular: + return Color(red: 1.00, green: 0.75, blue: 0.00) + } + } + + func getForegroundColor(for routeNumber: String) -> Color { + let routeKind = getKind(by: routeNumber) + switch routeKind { + case .tram: + return Color(.black) + case .neighborhood: + return Color(.white) + case .night: + return Color(.white) + case .elevator: + return Color(.white) + case .regular: + return Color(.black) + } + } + + + + /* MARK: - Get Time Interval */ + + // Transform an ISO Timestamp String into relative date components. + + enum TimeRelativeToNow { + case past + case future + } + + + func getTimeString(for isoDateString: String, in timeRelation: TimeRelativeToNow, style: DateComponentsFormatter.UnitsStyle, units: NSCalendar.Unit) -> String { + + // Setup Date Formatter + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + + // Parse ISO Timestamp using the Date Formatter + let now = Date() + let dateObj = dateFormatter.date(from: isoDateString) ?? now + let seconds = now.timeIntervalSince(dateObj) // in seconds + + // Setup Date Components Formatter + let dateComponentsFormatter = DateComponentsFormatter() + dateComponentsFormatter.unitsStyle = style + dateComponentsFormatter.allowedUnits = units + dateComponentsFormatter.includesApproximationPhrase = false + dateComponentsFormatter.includesTimeRemainingPhrase = false + dateComponentsFormatter.allowsFractionalUnits = false + + // Use the configured Date Components Formatter to generate the string. + switch timeRelation { + case .past: + return dateComponentsFormatter.string(from: seconds) ?? "?" + case .future: + return dateComponentsFormatter.string(from: -seconds) ?? "?" + } + + } + + + func getLastSeenTime(since lastGpsTime: String) -> Int { + + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + + let now = Date() + let estimation = formatter.date(from: lastGpsTime) ?? now + + let seconds = now.timeIntervalSince(estimation) + + return Int(seconds) + + } + + + func getSecondsFromISO8601DateString(_ dateString: String) -> Int { + let formattedDateObject = ISO8601DateFormatter().date(from: dateString) + return Int(formattedDateObject?.timeIntervalSinceNow ?? -1) + } + +} diff --git a/GeoBus/App/Extensions/Helpers.swift b/GeoBus/App/Extensions/Helpers.swift index e06b4012..c368ff17 100644 --- a/GeoBus/App/Extensions/Helpers.swift +++ b/GeoBus/App/Extensions/Helpers.swift @@ -11,12 +11,15 @@ import SwiftUI open class Helpers { + + + public static let variable: Helpers = .init() /* MARK: - Get Route Kind */ // Discover the Route kind by analysing the route number. - static func getKind(by routeNumber: String) -> Kind { + func getKind(by routeNumber: String) -> Kind { if (routeNumber.suffix(1) == "B") { // Neighborhood buses end with "B" @@ -49,7 +52,7 @@ open class Helpers { // Centralized functions that retrieve theme colors. - static func getBackgroundColor(for routeNumber: String) -> Color { + func getBackgroundColor(for routeNumber: String) -> Color { let routeKind = getKind(by: routeNumber) switch routeKind { case .tram: @@ -65,7 +68,7 @@ open class Helpers { } } - static func getForegroundColor(for routeNumber: String) -> Color { + func getForegroundColor(for routeNumber: String) -> Color { let routeKind = getKind(by: routeNumber) switch routeKind { case .tram: @@ -93,7 +96,7 @@ open class Helpers { } - static func getTimeString(for isoDateString: String, in timeRelation: TimeRelativeToNow, style: DateComponentsFormatter.UnitsStyle, units: NSCalendar.Unit) -> String { + func getTimeString(for isoDateString: String, in timeRelation: TimeRelativeToNow, style: DateComponentsFormatter.UnitsStyle, units: NSCalendar.Unit) -> String { // Setup Date Formatter let dateFormatter = DateFormatter() @@ -124,7 +127,7 @@ open class Helpers { } - static func getLastSeenTime(since lastGpsTime: String) -> Int { + func getLastSeenTime(since lastGpsTime: String) -> Int { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") @@ -140,7 +143,7 @@ open class Helpers { } - static func getSecondsFromISO8601DateString(_ dateString: String) -> Int { + func getSecondsFromISO8601DateString(_ dateString: String) -> Int { let formattedDateObject = ISO8601DateFormatter().date(from: dateString) return Int(formattedDateObject?.timeIntervalSinceNow ?? -1) } diff --git a/GeoBus/App/GeoBusApp.swift b/GeoBus/App/GeoBusApp.swift index ad345528..f1632963 100644 --- a/GeoBus/App/GeoBusApp.swift +++ b/GeoBus/App/GeoBusApp.swift @@ -13,28 +13,30 @@ struct GeoBusApp: App { /* MARK: - GEOBUS */ - @StateObject private var appstate = Appstate.shared - @StateObject private var mapController = MapController() - @StateObject private var stopsController = StopsController() @StateObject private var routesController = RoutesController() @StateObject private var vehiclesController = VehiclesController() @StateObject private var estimationsController = EstimationsController() + @StateObject private var mapController = MapController() + @StateObject private var carrisNetworkController = CarrisNetworkController() + private let updateIntervalTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() var body: some Scene { WindowGroup { ContentView() - .environmentObject(appstate) - .environmentObject(mapController) + // OLD .environmentObject(stopsController) .environmentObject(routesController) .environmentObject(vehiclesController) .environmentObject(estimationsController) + // NEW + .environmentObject(self.mapController) +// .environmentObject(self.carrisNetworkController) .onAppear(perform: { // Update Carris network model - self.routesController.update() +// self.carrisNetworkController.start() // Capture app open Analytics.shared.capture(event: .App_Session_Start) }) @@ -42,9 +44,7 @@ struct GeoBusApp: App { // Capture session continuation Analytics.shared.capture(event: .App_Session_Ping) // Update vehicles on timer call - Task { - await vehiclesController.fetchVehiclesFromCarrisAPI() - } + self.vehiclesController.update(scope: .summary) } } } diff --git a/GeoBus/App/Layout/RouteBadgePill.swift b/GeoBus/App/Layout/RouteBadgePill.swift index 07214274..3568e9ea 100644 --- a/GeoBus/App/Layout/RouteBadgePill.swift +++ b/GeoBus/App/Layout/RouteBadgePill.swift @@ -19,11 +19,11 @@ struct RouteBadgePill: View { .font(.footnote) .fontWeight(.heavy) .lineLimit(1) - .foregroundColor(Helpers.getForegroundColor(for: routeNumber)) + .foregroundColor(Globals().getForegroundColor(for: routeNumber)) .padding(.horizontal, 7) .padding(.vertical, 2) } - .background(Helpers.getBackgroundColor(for: routeNumber)) + .background(Globals().getBackgroundColor(for: routeNumber)) .cornerRadius(10) } diff --git a/GeoBus/App/Layout/RouteBadgeSquare.swift b/GeoBus/App/Layout/RouteBadgeSquare.swift index ce1789aa..937cf980 100644 --- a/GeoBus/App/Layout/RouteBadgeSquare.swift +++ b/GeoBus/App/Layout/RouteBadgeSquare.swift @@ -16,10 +16,10 @@ struct RouteBadgeSquare: View { ZStack { RoundedRectangle(cornerRadius: 10) - .fill(Helpers.getBackgroundColor(for: routeNumber)) + .fill(Globals().getBackgroundColor(for: routeNumber)) Text(routeNumber) .font(Font.system(size: 22, weight: .heavy, design: .default)) - .foregroundColor(Helpers.getForegroundColor(for: routeNumber)) + .foregroundColor(Globals().getForegroundColor(for: routeNumber)) } .aspectRatio(1, contentMode: .fit) diff --git a/GeoBus/App/Layout/TimeLeft.swift b/GeoBus/App/Layout/TimeLeft.swift index 2b839b5d..eb419c84 100644 --- a/GeoBus/App/Layout/TimeLeft.swift +++ b/GeoBus/App/Layout/TimeLeft.swift @@ -31,10 +31,10 @@ struct TimeLeft: View { .fontWeight(.medium) .foregroundColor(Color(.label)) .onAppear() { - self.timeLeftString = Helpers.getTimeString(for: self.time ?? "", in: .future, style: .short, units: [.hour, .minute]) + self.timeLeftString = Globals().getTimeString(for: self.time ?? "", in: .future, style: .short, units: [.hour, .minute]) } .onReceive(countdownTimer) { event in - self.timeLeftString = Helpers.getTimeString(for: self.time ?? "", in: .future, style: .short, units: [.hour, .minute]) + self.timeLeftString = Globals().getTimeString(for: self.time ?? "", in: .future, style: .short, units: [.hour, .minute]) } } } diff --git a/GeoBus/App/Lottie/EstimatedIcon.swift b/GeoBus/App/Lottie/EstimatedIcon.swift new file mode 100644 index 00000000..d9df715e --- /dev/null +++ b/GeoBus/App/Lottie/EstimatedIcon.swift @@ -0,0 +1,31 @@ +// +// LoadingView.swift +// GeoBus +// +// Created by João on 17/04/2020. +// Copyright © 2020 João de Vasconcelos. All rights reserved. +// + +import SwiftUI + +struct EstimatedIcon: View { + + @State var play: Bool = true + + var body: some View { + HStack { +// LottieView(name: "estimated-icon", loopMode: .loop, aspect: .scaleAspectFit, play: $play) +// .frame(width: 15, height: 15) +// .padding(.leading, -2) + LoadingPulse(color: .orange, size: 15) +// .frame(width: 15, height: 15) + .padding(.leading, -2) + Text("Estimated") + .font(Font.system(size: 11, weight: .medium, design: .default) ) + .foregroundColor(Color(.systemOrange)) + .padding(.leading, -5) + } + } +} + + diff --git a/GeoBus/App/Lottie/LiveIcon.swift b/GeoBus/App/Lottie/LiveIcon.swift new file mode 100644 index 00000000..6b37abac --- /dev/null +++ b/GeoBus/App/Lottie/LiveIcon.swift @@ -0,0 +1,30 @@ +// +// LoadingView.swift +// GeoBus +// +// Created by João on 17/04/2020. +// Copyright © 2020 João de Vasconcelos. All rights reserved. +// + +import SwiftUI + +struct LiveIcon: View { + + @State var play: Bool = true + + var body: some View { + HStack { +// LottieView(name: "live-icon", loopMode: .loop, aspect: .scaleAspectFit, play: $play) +// .frame(width: 15, height: 15) +// .padding(.leading, -2) + LoadingPulse(color: .green, size: 15) + .padding(.leading, -2) + Text("Live") + .font(Font.system(size: 11, weight: .medium, design: .default) ) + .foregroundColor(Color(.systemGreen)) + .padding(.leading, -5) + } + } +} + + diff --git a/GeoBus/App/Lottie/LoadingPulse.swift b/GeoBus/App/Lottie/LoadingPulse.swift new file mode 100644 index 00000000..5396b926 --- /dev/null +++ b/GeoBus/App/Lottie/LoadingPulse.swift @@ -0,0 +1,91 @@ +// +// LoadingPulse.swift +// GeoBus +// +// Created by João de Vasconcelos on 14/10/2022. +// + +import SwiftUI + +struct LoadingPulse: View { + @State var isAnimating: Bool = false + let timing: Double + + let maxCounter: Int = 3 + + let frame: CGSize + let primaryColor: Color + + init(color: Color = .black, size: CGFloat = 50, speed: Double = 0.75) { + timing = speed * 4 + frame = CGSize(width: size, height: size) + primaryColor = color + } + + var body: some View { + ZStack { + + ForEach(0.. network_updateInterval + let savedNetworkDataIsEmpty = network_allRoutes.isEmpty || network_allStops.isEmpty + let updateIsForcedByCaller = forceUpdate + + // Proceed if at least one condition is true + if (lastUpdateIsLongerThanInterval || savedNetworkDataIsEmpty || updateIsForcedByCaller) { + Task { + await fetchStopsFromCarrisAPI() + await fetchRoutesFromCarrisAPI() + } + // Replace timestamp in storage with current time + let timestampOfCurrentUpdate = ISO8601DateFormatter().string(from: Date.now) + UserDefaults.standard.set(timestampOfCurrentUpdate, forKey: network_storageKeyForLastUpdated) + } + + + // Retrieve favorites at app launch + // self.retrieveFavorites() + + } + + + + + + /* * */ + /* MARK: - SECTION F: FETCH & FORMAT ROUTES FROM CARRIS API */ + /* This function first fetches the Routes List, which is an object */ + /* that contains all the available routes from the API. */ + /* The information for each Route is very short, so it is necessary to retrieve */ + /* the details for each route. Here, we only care about the publicy available routes. */ + /* After, for each route, it's details are formatted and transformed into a Route. */ + + func fetchRoutesFromCarrisAPI() async { + + Analytics.shared.capture(event: .Routes_Sync_START) + Appstate.shared.change(to: .loading, for: .routes) + + print("GB: Fetching Routes: Starting...") + + do { + // Request API Routes List + var requestCarrisAPIRoutesList = URLRequest(url: URL(string: "\(api_carrisEndpoint)/Routes")!) + requestCarrisAPIRoutesList.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestCarrisAPIRoutesList.addValue("application/json", forHTTPHeaderField: "Accept") + requestCarrisAPIRoutesList.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") + let (rawDataCarrisAPIRoutesList, rawResponseCarrisAPIRoutesList) = try await URLSession.shared.data(for: requestCarrisAPIRoutesList) + let responseCarrisAPIRoutesList = rawResponseCarrisAPIRoutesList as? HTTPURLResponse + + // Check status of response + if (responseCarrisAPIRoutesList?.statusCode == 401) { + await CarrisAuthentication.shared.authenticate() + await self.fetchRoutesFromCarrisAPI() + return + } else if (responseCarrisAPIRoutesList?.statusCode != 200) { + print(responseCarrisAPIRoutesList as Any) + throw Appstate.ModuleError.carris_unavailable + } + + let decodedCarrisAPIRoutesList = try JSONDecoder().decode([APIRoutesList].self, from: rawDataCarrisAPIRoutesList) + + self.network_updateProgress = decodedCarrisAPIRoutesList.count + + // Define a temporary variable to store routes + // before saving them to the device storage. + var tempAllRoutes: [Route_NEW] = [] + + // For each available route in the API, + for availableRoute in decodedCarrisAPIRoutesList { + + if (availableRoute.isPublicVisible ?? false) { + + print("Route: \(String(describing: availableRoute.routeNumber)) starting...") + + // Request Route Detail for ‹routeNumber› + var requestAPIRouteDetail = URLRequest(url: URL(string: "\(api_carrisEndpoint)/Routes/\(availableRoute.routeNumber ?? "invalid-route-number")")!) + requestAPIRouteDetail.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestAPIRouteDetail.addValue("application/json", forHTTPHeaderField: "Accept") + requestAPIRouteDetail.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") + let (rawDataAPIRouteDetail, rawResponseAPIRouteDetail) = try await URLSession.shared.data(for: requestAPIRouteDetail) + let responseAPIRouteDetail = rawResponseAPIRouteDetail as? HTTPURLResponse + + // Check status of response + if (responseAPIRouteDetail?.statusCode == 401) { + await CarrisAuthentication.shared.authenticate() + await self.fetchRoutesFromCarrisAPI() + return + } else if (responseAPIRouteDetail?.statusCode != 200) { + print(responseAPIRouteDetail as Any) + throw Appstate.ModuleError.carris_unavailable + } + + let decodedAPIRouteDetail = try JSONDecoder().decode(APIRoute.self, from: rawDataAPIRouteDetail) + + // Define a temporary variable to store formatted route variants + var tempFormattedRouteVariants: [Variant_NEW] = [] + + // For each variant in route, + // check if it is currently active, format it + // and append the result to the temporary variable. + for apiRouteVariant in decodedAPIRouteDetail.variants ?? [] { + if (apiRouteVariant.isActive ?? false) { + tempFormattedRouteVariants.append( + formatRawRouteVariant(rawVariant: apiRouteVariant) + ) + } + } + + // Build the formatted route object + let formattedRoute = Route_NEW( + number: decodedAPIRouteDetail.routeNumber ?? "-", + name: decodedAPIRouteDetail.name ?? "-", + kind: Globals().getKind(by: decodedAPIRouteDetail.routeNumber ?? "-"), + variants: tempFormattedRouteVariants + ) + + // Save the formatted route object in the allRoutes temporary variable + tempAllRoutes.append(formattedRoute) + + self.network_updateProgress! -= 1 + + print("Route: Route.\(String(describing: formattedRoute.number)) complete.") + + try await Task.sleep(nanoseconds: 100_000_000) + + } + + } + + // Finally, save the temporary variables into storage, + // while removing the previous, old ones. + self.network_allRoutes.removeAll() + self.network_allRoutes.append(contentsOf: tempAllRoutes) + if let encodedAllRoutes = try? JSONEncoder().encode(self.network_allRoutes) { + UserDefaults.standard.set(encodedAllRoutes, forKey: network_storageKeyForSavedRoutes) + } + + print("Fetching Routes: Complete!") + + Analytics.shared.capture(event: .Routes_Sync_OK) + Appstate.shared.change(to: .idle, for: .routes) + + } catch { + Analytics.shared.capture(event: .Routes_Sync_ERROR) + Appstate.shared.change(to: .error, for: .routes) + print("Fetching Routes: Error!") + print(error) + print("************") + } + + } + + + + + func formatConnections(rawConnections: [APIRouteVariantItineraryConnection]) -> [Connection_NEW] { + + var tempConnections: [Connection_NEW] = [] + + // For each connection, + // convert the nested objects into a simplified RouteStop object + for rawConnection in rawConnections { + + // Append new values to the temporary variable property directly + tempConnections.append( + Connection_NEW( + orderInRoute: rawConnection.orderNum ?? -1, + stop: Stop_NEW( + publicId: rawConnection.busStop?.publicId ?? "-", + name: rawConnection.busStop?.name ?? "-", + lat: rawConnection.busStop?.lat ?? 0, + lng: rawConnection.busStop?.lng ?? 0 + ) + ) + ) + + } + + // Sort the stops + tempConnections.sort(by: { $0.orderInRoute < $1.orderInRoute }) + + return tempConnections + + } + + + /* MARK: - Format Route Variants */ + // Parse and simplify the data model for variants + func formatRawRouteVariant(rawVariant: APIRouteVariant) -> Variant_NEW { + + // For each Itinerary type, + // check if it is defined (not nil) in the raw object + var tempItineraries: [Itinerary_NEW] = [] + + // For UpItinerary: + if (rawVariant.upItinerary != nil) { + tempItineraries.append( + Itinerary_NEW( + direction: .ascending, + connections: formatConnections(rawConnections: rawVariant.upItinerary!.connections ?? []) + ) + ) + } + + // For DownItinerary: + if (rawVariant.downItinerary != nil) { + tempItineraries.append( + Itinerary_NEW( + direction: .descending, + connections: formatConnections(rawConnections: rawVariant.downItinerary!.connections ?? []) + ) + ) + } + + // For CircItinerary: + if (rawVariant.circItinerary != nil) { + tempItineraries.append( + Itinerary_NEW( + direction: .circular, + connections: formatConnections(rawConnections: rawVariant.circItinerary!.connections ?? []) + ) + ) + } + + +// if (formattedVariant.isCircular) { +// formattedVariant.name = getTerminalStopNameForVariant(variant: formattedVariant, direction: .circular) +// } else { +// let firstStop = getTerminalStopNameForVariant(variant: formattedVariant, direction: .ascending) +// let lastStop = getTerminalStopNameForVariant(variant: formattedVariant, direction: .descending) +// formattedVariant.name = "\(firstStop) ⇄ \(lastStop)" +// } + + // Finally, return the temporary variable to the caller + return Variant_NEW( + number: rawVariant.variantNumber ?? -1, + name: "in-progress", + itineraries: tempItineraries + ) + + } + + + /* MARK: - Get Terminal Stop Name for Variant */ + // This function returns the provided variant's terminal stop for the provided direction. + func getTerminalStopNameForVariant(variant: Variant, direction: Direction) -> String { + switch direction { + case .circular: + return variant.circItinerary?.first?.name ?? "-" + case .ascending: + return variant.upItinerary?.last?.name ?? (variant.upItinerary?.first?.name ?? "-") + case .descending: + return variant.downItinerary?.last?.name ?? (variant.downItinerary?.first?.name ?? "-") + } + } + + + func fetchStopsFromCarrisAPI() async { + + Analytics.shared.capture(event: .Stops_Sync_START) + Appstate.shared.change(to: .loading, for: .stops) + + print("Fetching Stops: Starting...") + + do { + // Request API Routes List + var requestCarrisAPIStopsList = URLRequest(url: URL(string: "\(api_carrisEndpoint)/busstops")!) + requestCarrisAPIStopsList.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestCarrisAPIStopsList.addValue("application/json", forHTTPHeaderField: "Accept") + requestCarrisAPIStopsList.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") + let (rawDataCarrisAPIStopsList, rawResponseCarrisAPIStopsList) = try await URLSession.shared.data(for: requestCarrisAPIStopsList) + let responseCarrisAPIStopsList = rawResponseCarrisAPIStopsList as? HTTPURLResponse + + // Check status of response + if (responseCarrisAPIStopsList?.statusCode == 401) { + await CarrisAuthentication.shared.authenticate() + await self.fetchStopsFromCarrisAPI() + return + } else if (responseCarrisAPIStopsList?.statusCode != 200) { + print(responseCarrisAPIStopsList as Any) + throw Appstate.ModuleError.carris_unavailable + } + + let decodedCarrisAPIStopsList = try JSONDecoder().decode([APIStop].self, from: rawDataCarrisAPIStopsList) + + // Define a temporary variable to store routes + // before saving them to the device storage. + var tempAllStops: [Stop_NEW] = [] + + // For each available route in the API, + for availableStop in decodedCarrisAPIStopsList { + if (availableStop.isPublicVisible ?? false) { + // Save the formatted route object in the allRoutes temporary variable + tempAllStops.append( + Stop_NEW( + publicId: availableStop.publicId ?? "0", + name: availableStop.name ?? "-", + lat: availableStop.lat ?? 0, + lng: availableStop.lng ?? 0 + ) + ) + } + } + + // Finally, save the temporary variables into storage, + // while removing the previous, old ones. + self.network_allStops.removeAll() + self.network_allStops.append(contentsOf: tempAllStops) + if let encodedAllStops = try? JSONEncoder().encode(self.network_allStops) { + UserDefaults.standard.set(encodedAllStops, forKey: network_storageKeyForSavedStops) + } + + print("[GB-Debug] Fetching Stops: Complete!") + + Analytics.shared.capture(event: .Stops_Sync_OK) + Appstate.shared.change(to: .idle, for: .stops) + + } catch { + Analytics.shared.capture(event: .Stops_Sync_ERROR) + Appstate.shared.change(to: .error, for: .stops) + print("Fetching Stops: Error!") + print(error) + print("************") + } + + } + + /* MARK: - Find Route by RouteNumber */ + // This function searches for the provided routeNumber in all routes array, + // and returns it if found. If not found, returns nil. + func findRoute(by routeNumber: String) -> Route_NEW? { + + // Find index of route matching requested routeNumber + let indexOfRouteInArray = network_allRoutes.firstIndex(where: { (route) -> Bool in + route.number == routeNumber // test if this is the item we're looking for + }) ?? nil // If the item does not exist, return default value nil + + // If a match is found... + if (indexOfRouteInArray != nil) { + return network_allRoutes[indexOfRouteInArray!] + } else { + return nil + } + + } + + + + + + + + /* * */ + /* MARK: - SECTION B: SELECTORS */ + /* Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia, */ + /* molestiae quas vel sint commodi repudiandae consequuntur voluptatum laborum */ + /* numquam blanditiis harum quisquam eius sed odit fugiat */ + + // Routes + + public func select(route routeNumber: String) { + if let route = self.findRoute(by: routeNumber) { + self.select(route: route) + } + } + + func select(route routeNumber: String, returnResult: Bool) -> Bool { + if let route = self.findRoute(by: routeNumber) { + self.select(route: route) + return true + } else { + return false + } + } + + private func select(route: Route_NEW) { + self.network_selectedRoute = route + self.select(variant: route.variants[0]) + } + + public func select(variant: Variant_NEW) { + self.network_selectedVariant = variant + } + + + public func deselect() { + self.network_selectedRoute = nil + self.network_selectedVariant = nil + self.network_selectedConnection = nil + self.network_selectedStop = nil + } + + + // Stops + + private func select(stop: Stop_NEW) { + self.network_selectedStop = stop + } + + func select(stop stopPublicId: String) { + let stop = self.findStop(by: stopPublicId) + if (stop != nil) { + self.select(stop: stop!) + } + } + + func select(stop stopPublicId: String, returnResult: Bool) -> Bool { + let stop = self.findStop(by: stopPublicId) + if (stop != nil) { + self.select(stop: stop!) + return true + } else { + return false + } + } + + + /* MARK: - Find Stop by Public ID */ + // This function searches for the provided routeNumber in all routes array, + // and returns it if found. If not found, returns nil. + func findStop(by stopPublicId: String) -> Stop_NEW? { + + let parsedStopPublicId = Int(stopPublicId) ?? 0 + + // Find index of route matching requested routeNumber + let indexOfStopInArray = network_allStops.firstIndex(where: { (stop) -> Bool in + stop.publicId == String(parsedStopPublicId) // test if this is the item we're looking for + }) ?? nil // If the item does not exist, return default value -1 + + // If a match is found... + if (indexOfStopInArray != nil) { + return network_allStops[indexOfStopInArray!] + } else { + return nil + } + + } + + + /* MARK: - UPDATE VEHICLES */ + + // This function decides whether to update available routes + + enum VehicleUpdateScope { + case summary + case detail + case community + } + + func update(scope: VehicleUpdateScope, for busNumber: Int? = nil) { + + switch scope { + + case .summary: + Task { + await fetchVehiclesListFromCarrisAPI_NEW() + } + + case .detail: + if (busNumber != nil) { + Task { + await fetchVehicleDetailsFromCarrisAPI_NEW(for: busNumber!) + } + } + + case .community: + if (busNumber != nil) { + Task { + await fetchVehicleFromCommunityAPI_NEW(for: busNumber!) + } + } + + } + + } + + + + /* MARK: - FETCH VEHICLES SUMMARY FROM CARRIS API */ + + // This function calls the GeoBus API and receives vehicle metadata, + // including positions, for the set route number, while storing them + // in the vehicles array. It also formats VehicleAnnotations and stores + // them in the annotations array. It must have @objc flag because Timer + // is written in Objective-C. + + func fetchVehiclesListFromCarrisAPI_NEW() async { + + Appstate.shared.change(to: .loading, for: .vehicles) + + do { + // Request all Vehicles from API + var requestCarrisAPIVehiclesList = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/vehicleStatuses")!) // /routeNumber/\(routeNumber!) + requestCarrisAPIVehiclesList.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestCarrisAPIVehiclesList.addValue("application/json", forHTTPHeaderField: "Accept") + requestCarrisAPIVehiclesList.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") + let (rawDataCarrisAPIVehiclesList, rawResponseCarrisAPIVehiclesList) = try await URLSession.shared.data(for: requestCarrisAPIVehiclesList) + let responseCarrisAPIVehiclesList = rawResponseCarrisAPIVehiclesList as? HTTPURLResponse + + // Check status of response + if (responseCarrisAPIVehiclesList?.statusCode == 401) { + await CarrisAuthentication.shared.authenticate() + await self.fetchVehiclesListFromCarrisAPI_NEW() + return + } else if (responseCarrisAPIVehiclesList?.statusCode != 200) { + print(responseCarrisAPIVehiclesList as Any) + throw Appstate.ModuleError.carris_unavailable + } + + let decodedCarrisAPIVehiclesList = try JSONDecoder().decode([CarrisAPIVehicleSummary].self, from: rawDataCarrisAPIVehiclesList) + + + for vehicleSummary in decodedCarrisAPIVehiclesList { + + let indexOfVehicleInArray = self.network_allVehicles.firstIndex(where: { + $0.id == vehicleSummary.busNumber + }) + + if (indexOfVehicleInArray != nil) { + network_allVehicles[indexOfVehicleInArray!].routeNumber = vehicleSummary.routeNumber ?? "-" + network_allVehicles[indexOfVehicleInArray!].kind = Globals().getKind(by: vehicleSummary.routeNumber ?? "-") + network_allVehicles[indexOfVehicleInArray!].lat = vehicleSummary.lat ?? 0 + network_allVehicles[indexOfVehicleInArray!].lng = vehicleSummary.lng ?? 0 + network_allVehicles[indexOfVehicleInArray!].previousLatitude = vehicleSummary.previousLatitude ?? 0 + network_allVehicles[indexOfVehicleInArray!].previousLongitude = vehicleSummary.previousLongitude ?? 0 + network_allVehicles[indexOfVehicleInArray!].lastGpsTime = vehicleSummary.lastGpsTime ?? "" + network_allVehicles[indexOfVehicleInArray!].angleInRadians = self.getAngleInRadians( + prevLat: vehicleSummary.previousLatitude ?? 0, + prevLng: vehicleSummary.previousLongitude ?? 0, + currLat: vehicleSummary.lat ?? 0, + currLng: vehicleSummary.lng ?? 0 + ) + } else { + self.network_allVehicles.append( + Vehicle( + busNumber: vehicleSummary.busNumber ?? 0, + routeNumber: vehicleSummary.routeNumber ?? "-", + kind: Globals().getKind(by: vehicleSummary.routeNumber ?? "-"), + lat: vehicleSummary.lat ?? 0, + lng: vehicleSummary.lng ?? 0, + previousLatitude: vehicleSummary.previousLatitude ?? 0, + previousLongitude: vehicleSummary.previousLongitude ?? 0, + lastGpsTime: vehicleSummary.lastGpsTime ?? "", + angleInRadians: self.getAngleInRadians( + prevLat: vehicleSummary.previousLatitude ?? 0, + prevLng: vehicleSummary.previousLongitude ?? 0, + currLat: vehicleSummary.lat ?? 0, + currLng: vehicleSummary.lng ?? 0 + ) + ) + ) + } + + } + + Appstate.shared.change(to: .idle, for: .vehicles) + + } catch { + Appstate.shared.change(to: .error, for: .vehicles) + print("ERROR IN VEHICLES: \(error)") + return + } + + } + + + + + + + + + /* MARK: - FETCH VEHICLE DETAILS FROM CARRIS API */ + + // This function calls the GeoBus API and receives vehicle metadata, + // including positions, for the set route number, while storing them + // in the vehicles array. It also formats VehicleAnnotations and stores + // them in the annotations array. It must have @objc flag because Timer + // is written in Objective-C. + + func fetchVehicleDetailsFromCarrisAPI_NEW(for busNumber: Int) async { + + // 1. Check if Vehicle exists in array + guard let indexOfVehicleInArray = network_allVehicles.firstIndex(where: { $0.id == busNumber }) else { + return + } + + Appstate.shared.change(to: .loading, for: .vehicles) + + do { + + // Request Vehicle Detail (SGO) + var requestCarrisAPIVehicleDetail = URLRequest(url: URL(string: "\(api_carrisEndpoint)/SGO/busNumber/\(busNumber)")!) + requestCarrisAPIVehicleDetail.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestCarrisAPIVehicleDetail.addValue("application/json", forHTTPHeaderField: "Accept") + requestCarrisAPIVehicleDetail.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") + let (rawDataCarrisAPIVehicleDetail, rawResponseCarrisAPIVehicleDetail) = try await URLSession.shared.data(for: requestCarrisAPIVehicleDetail) + let responseCarrisAPIVehicleDetail = rawResponseCarrisAPIVehicleDetail as? HTTPURLResponse + + // Check status of response + if (responseCarrisAPIVehicleDetail?.statusCode == 401) { + await CarrisAuthentication.shared.authenticate() + await self.fetchVehicleDetailsFromCarrisAPI_NEW(for: busNumber) + return + } else if (responseCarrisAPIVehicleDetail?.statusCode != 200) { + print(responseCarrisAPIVehicleDetail as Any) + throw Appstate.ModuleError.carris_unavailable + } + + let decodedCarrisAPIVehicleDetail = try JSONDecoder().decode(CarrisAPIVehicleDetail.self, from: rawDataCarrisAPIVehicleDetail) + + // Update details of Vehicle + network_allVehicles[indexOfVehicleInArray].vehiclePlate = decodedCarrisAPIVehicleDetail.vehiclePlate ?? "-" + network_allVehicles[indexOfVehicleInArray].lastStopOnVoyageName = decodedCarrisAPIVehicleDetail.lastStopOnVoyageName ?? "-" + + Appstate.shared.change(to: .idle, for: .vehicles) + + } catch { + Appstate.shared.change(to: .error, for: .vehicles) + print("ERROR IN VEHICLE DETAILS: \(error)") + return + } + + } + + + + /* MARK: - FETCH VEHICLE FROM COMMUNITY API */ + + // This function calls the GeoBus API and receives vehicle metadata, + // including positions, for the set route number, while storing them + // in the vehicles array. It also formats VehicleAnnotations and stores + // them in the annotations array. It must have @objc flag because Timer + // is written in Objective-C. + + func fetchVehicleFromCommunityAPI_NEW(for busNumber: Int) async { + + // 1. Check if Vehicle exists in array + guard let indexOfVehicleInArray = network_allVehicles.firstIndex(where: { $0.id == busNumber }) else { + return + } + + Appstate.shared.change(to: .loading, for: .vehicles) + + do { + + // Request Vehicle Detail (SGO) + var requestCommunityAPIVehicle = URLRequest(url: URL(string: "\(api_communityEndpoint)/estbus?busNumber=\(busNumber)")!) + requestCommunityAPIVehicle.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestCommunityAPIVehicle.addValue("application/json", forHTTPHeaderField: "Accept") + let (rawDataCommunityAPIVehicle, rawResponseCommunityAPIVehicle) = try await URLSession.shared.data(for: requestCommunityAPIVehicle) + let responseCommunityAPIVehicle = rawResponseCommunityAPIVehicle as? HTTPURLResponse + + // Check status of response + if (responseCommunityAPIVehicle?.statusCode != 200) { + print(responseCommunityAPIVehicle as Any) + throw Appstate.ModuleError.community_unavailable + } + + let decodedCommunityAPIVehicle = try JSONDecoder().decode([CommunityAPIVehicle].self, from: rawDataCommunityAPIVehicle) + + // Update details of Vehicle + network_allVehicles[indexOfVehicleInArray].estimatedTimeofArrivalCorrected = decodedCommunityAPIVehicle[0].estimatedTimeofArrivalCorrected + + Appstate.shared.change(to: .idle, for: .vehicles) + + } catch { + Appstate.shared.change(to: .error, for: .vehicles) + print("GB: ERROR IN 'fetchVehicleFromCommunityAPI_NEW': \(error)") + return + } + + } + + + + + + func getVehicle(by busNumber: Int) -> Vehicle? { + let indexInArray = self.network_allVehicles.firstIndex(where: { + $0.busNumber == busNumber + }) + + if (indexInArray != nil) { + return network_allVehicles[indexInArray!] + } else { + return nil + } + } + + + + /* MARK: - CALCULATE ANGLE IN RADIANS FOR VEHICLE DIRECTION */ + + // This function calls the GeoBus API and receives vehicle metadata, + + func getAngleInRadians(prevLat: Double, prevLng: Double, currLat: Double, currLng: Double) -> Double { + // and return response to the caller + let x = currLat - prevLat; + let y = currLng - prevLng; + + var teta: Double; + // Angle is calculated with the arctan of ( y / x ) + if (x == 0){ teta = .pi / 2 } + else { teta = atan(y / x) } + + // If x is negative, then the angle is in the symetric quadrant + if (x < 0) { teta += .pi } + + return teta - (.pi / 2) // Correction cuz Apple rotates clockwise + + } + + + +} diff --git a/GeoBus/App/State/EstimationsController.swift b/GeoBus/App/State/EstimationsController.swift index 41853a6f..057d9cba 100644 --- a/GeoBus/App/State/EstimationsController.swift +++ b/GeoBus/App/State/EstimationsController.swift @@ -5,6 +5,7 @@ // Created by João on 20/04/2020. // Copyright © 2020 João. All rights reserved. // + import Foundation import Combine @@ -13,46 +14,97 @@ class EstimationsController: ObservableObject { /* MARK: - Variables */ - @Published var estimations: [Estimation] = [] + private let storageKeyForEstimationsProvider: String = "estimations_estimationsProvider" + @Published var estimationsProvider: EstimationsProvider = .community - /* MARK: - Get Estimations */ + /* MARK: - INITIALIZER */ - // This function calls the API to retrieve estimations for the provided stop 'publicId'. + // Retrieve data from UserDefaults on init. + + init() { + self.getProviderFromStorage() + } + + + + /* MARK: - GET ESTIMATIONS PROVIDER FROM STORAGE */ + + // Retrieve Estimations Provider from device storage. + + private func getProviderFromStorage() { + if let unwrappedEstimationsProvider = UserDefaults.standard.string(forKey: storageKeyForEstimationsProvider) { + self.estimationsProvider = EstimationsProvider(rawValue: unwrappedEstimationsProvider) ?? .carris + } + } + + + + /* MARK: - SET ESTIMATIONS PROVIDER */ + + // Set Estimations Provider for current session and save it to device storage. + + public func setProvider(selection: EstimationsProvider) { + self.estimationsProvider = selection + UserDefaults.standard.set(estimationsProvider.rawValue, forKey: storageKeyForEstimationsProvider) + print("GB: Provider is \(selection)") + } + + + + /* MARK: - GET ESTIMATIONS */ + + // This function initiates the correct API calls according to the set Estimations provider. + + public func get(for publicId: String) async -> [Estimation] { + switch estimationsProvider { + case .carris: + return await self.getCarrisEstimation(for: publicId) + case .community: + return await self.getCommunityEstimation(for: publicId) + } + } + + + + /* MARK: - GET ESTIMATIONS › CARRIS */ + + // This function calls Carris API to retrieve estimations for the given stop 'publicId'. // It formats and returns the results to the caller. - func get(for publicId: String) async -> [Estimation] { + + private func getCarrisEstimation(for publicId: String) async -> [Estimation] { Appstate.shared.change(to: .loading, for: .estimations) do { // Request API Routes List - var requestAPIEstimations = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/Estimations/busStop/\(publicId)/top/5")!) - requestAPIEstimations.addValue("application/json", forHTTPHeaderField: "Content-Type") - requestAPIEstimations.addValue("application/json", forHTTPHeaderField: "Accept") - requestAPIEstimations.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") - let (rawDataAPIEstimations, rawResponseAPIEstimations) = try await URLSession.shared.data(for: requestAPIEstimations) - let responseAPIEstimations = rawResponseAPIEstimations as? HTTPURLResponse + var requestCarrisAPIEstimations = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/Estimations/busStop/\(publicId)/top/5")!) + requestCarrisAPIEstimations.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestCarrisAPIEstimations.addValue("application/json", forHTTPHeaderField: "Accept") + requestCarrisAPIEstimations.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") + let (rawDataCarrisAPIEstimations, rawResponseCarrisAPIEstimations) = try await URLSession.shared.data(for: requestCarrisAPIEstimations) + let responseCarrisAPIEstimations = rawResponseCarrisAPIEstimations as? HTTPURLResponse // Check status of response - if (responseAPIEstimations?.statusCode == 401) { + if (responseCarrisAPIEstimations?.statusCode == 401) { Task { await CarrisAuthentication.shared.authenticate() - return await self.get(for: publicId) + return await self.getCarrisEstimation(for: publicId) } - } else if (responseAPIEstimations?.statusCode != 200) { - print(responseAPIEstimations as Any) + } else if (responseCarrisAPIEstimations?.statusCode != 200) { + print(responseCarrisAPIEstimations as Any) throw Appstate.ModuleError.carris_unavailable } - let decodedAPIEstimations = try JSONDecoder().decode([CarrisAPIEstimation].self, from: rawDataAPIEstimations) + let decodedCarrisAPIEstimations = try JSONDecoder().decode([CarrisAPIEstimation].self, from: rawDataCarrisAPIEstimations) // Define a temporary variable to store vehicles // before publishing and displaying them in the map. var tempAllEstimations: [Estimation] = [] // For each available vehicles in the API - for estimation in decodedAPIEstimations { + for estimation in decodedCarrisAPIEstimations { // Format and append each estimation // to the temporary variable. @@ -61,8 +113,8 @@ class EstimationsController: ObservableObject { routeNumber: estimation.routeNumber ?? "-", destination: estimation.destination ?? "-", publicId: estimation.publicId ?? "-", - busNumber: estimation.busNumber, - eta: Helpers.getTimeString(for: estimation.time ?? "", in: .future, style: .short, units: [.hour, .minute]) + busNumber: estimation.busNumber ?? "-", + eta: estimation.time ?? "" ) ) @@ -73,6 +125,73 @@ class EstimationsController: ObservableObject { // Return the formatted estimations. return tempAllEstimations + } catch { + Appstate.shared.change(to: .error, for: .estimations) + print("GB: ERROR IN ESTIMATIONS: \(error)") + return [] + } + + } + + + /* MARK: - GET ESTIMATIONS › COMMUNITY */ + + // This function calls the API to retrieve estimations for the provided stop 'publicId'. + // It formats and returns the results to the caller. + + func getCommunityEstimation(for publicId: String) async -> [Estimation] { + + Appstate.shared.change(to: .loading, for: .estimations) + + do { + // Request API Routes List + var requestCommunityAPIVehicle = URLRequest(url: URL(string: "https://api.carril.workers.dev/eststop?busStop=\(publicId)")!) + requestCommunityAPIVehicle.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestCommunityAPIVehicle.addValue("application/json", forHTTPHeaderField: "Accept") + let (rawDataCommunityAPIVehicle, rawResponseCommunityAPIVehicle) = try await URLSession.shared.data(for: requestCommunityAPIVehicle) + let responseCommunityAPIVehicle = rawResponseCommunityAPIVehicle as? HTTPURLResponse + + // Check status of response + if (responseCommunityAPIVehicle?.statusCode != 200) { + print(responseCommunityAPIVehicle as Any) + throw Appstate.ModuleError.community_unavailable + } + + let decodedCommunityAPIVehicle = try JSONDecoder().decode([CommunityAPIVehicle].self, from: rawDataCommunityAPIVehicle) + + // Define a temporary variable to store vehicles + // before publishing and displaying them in the map. + var tempAllEstimations: [Estimation] = [] + + // For each available vehicles in the API + for communityVehicle in decodedCommunityAPIVehicle { + + // If the vehicle is not expected to have arrived + if (!(communityVehicle.estimatedRecentlyArrived ?? false)) { + + let carrisVehicleDetails = await VehiclesController().fetchVehicleDetailsFromCarrisAPI(for: communityVehicle.busNumber ?? 0) + + // Format and append each estimation + // to the temporary variable. + tempAllEstimations.append( + Estimation( + routeNumber: communityVehicle.routeNumber ?? "-", + destination: carrisVehicleDetails?.lastStopOnVoyageName ?? "-", + publicId: publicId, + busNumber: String(communityVehicle.busNumber ?? 0), + eta: "" // communityVehicle.estimatedTimeofArrivalCorrected ?? "" + ) + ) + + } + + } + + Appstate.shared.change(to: .idle, for: .estimations) + + // Return the formatted estimations. + return tempAllEstimations + } catch { Appstate.shared.change(to: .error, for: .estimations) print("ERROR IN ESTIMATIONS: \(error)") diff --git a/GeoBus/App/State/MapController.swift b/GeoBus/App/State/MapController.swift index e0fa7252..b611f25c 100644 --- a/GeoBus/App/State/MapController.swift +++ b/GeoBus/App/State/MapController.swift @@ -138,7 +138,7 @@ class MapController: ObservableObject { // ..... - func updateAnnotations(with vehiclesList: [VehicleSummary], for routeNumber: String?) { + func updateAnnotations(with vehiclesList: [Vehicle], for routeNumber: String?) { if (routeNumber != nil) { @@ -151,7 +151,7 @@ class MapController: ObservableObject { // CONDITION 2: // Vehicle was last seen no longer than 3 minutes - let isNotZombieVehicle = Helpers.getLastSeenTime(since: vehicle.lastGpsTime) < 180 + let isNotZombieVehicle = Globals().getLastSeenTime(since: vehicle.lastGpsTime ?? "") < 180 // Find index of Annotation matching this vehicle busNumber @@ -164,16 +164,16 @@ class MapController: ObservableObject { if (indexOfVisibleAnnotation != nil) { // If annotation already exists, update it's values withAnimation(.easeIn(duration: 0.5)) { - self.visibleAnnotations[indexOfVisibleAnnotation!].location.latitude = vehicle.lat - self.visibleAnnotations[indexOfVisibleAnnotation!].location.longitude = vehicle.lng + self.visibleAnnotations[indexOfVisibleAnnotation!].location.latitude = vehicle.lat ?? 0 + self.visibleAnnotations[indexOfVisibleAnnotation!].location.longitude = vehicle.lng ?? 0 self.visibleAnnotations[indexOfVisibleAnnotation!].vehicle = vehicle } } else { // If annotation does not already exist, create a new one visibleAnnotations.append( GenericMapAnnotation( - lat: vehicle.lat, - lng: vehicle.lng, + lat: vehicle.lat ?? 0, + lng: vehicle.lng ?? 0, format: .vehicle, busNumber: vehicle.busNumber, vehicle: vehicle diff --git a/GeoBus/App/State/RoutesController.swift b/GeoBus/App/State/RoutesController.swift index fab4a584..01a2410b 100644 --- a/GeoBus/App/State/RoutesController.swift +++ b/GeoBus/App/State/RoutesController.swift @@ -307,7 +307,7 @@ class RoutesController: ObservableObject { let formattedRoute = Route( number: decodedAPIRouteDetail.routeNumber ?? "-", name: decodedAPIRouteDetail.name ?? "-", - kind: Helpers.getKind(by: decodedAPIRouteDetail.routeNumber ?? "-"), + kind: Globals().getKind(by: decodedAPIRouteDetail.routeNumber ?? "-"), variants: formattedRouteVariants ) diff --git a/GeoBus/App/State/VehiclesController.swift b/GeoBus/App/State/VehiclesController.swift index c0d500b4..1b6e3d1a 100644 --- a/GeoBus/App/State/VehiclesController.swift +++ b/GeoBus/App/State/VehiclesController.swift @@ -16,6 +16,7 @@ class VehiclesController: ObservableObject { private var routeNumber: String? + @Published var allVehicles: [Vehicle] = [] @Published var vehicles: [VehicleSummary] = [] @@ -65,8 +66,11 @@ class VehiclesController: ObservableObject { // Check status of response if (responseAPIVehiclesList?.statusCode == 401) { - await CarrisAuthentication.shared.authenticate() - await self.fetchVehiclesFromCarrisAPI() + Task { + await CarrisAuthentication.shared.authenticate() + await self.fetchVehiclesFromCarrisAPI() + } + return } else if (responseAPIVehiclesList?.statusCode != 200) { print(responseAPIVehiclesList as Any) throw Appstate.ModuleError.carris_unavailable @@ -84,7 +88,7 @@ class VehiclesController: ObservableObject { // Discard vehicles with outdated location, // here decided to be 180 seconds (3 minutes). - if (Helpers.getLastSeenTime(since: vehicleSummary.lastGpsTime ?? "") < 180) { + if (Globals().getLastSeenTime(since: vehicleSummary.lastGpsTime ?? "") < 180) { // Format and append each vehicle // to the temporary variable. @@ -93,7 +97,7 @@ class VehiclesController: ObservableObject { busNumber: vehicleSummary.busNumber ?? -1, state: vehicleSummary.state ?? "", routeNumber: vehicleSummary.routeNumber ?? "-", - kind: Helpers.getKind(by: vehicleSummary.routeNumber ?? "-"), + kind: Globals().getKind(by: vehicleSummary.routeNumber ?? "-"), lat: vehicleSummary.lat ?? 0, lng: vehicleSummary.lng ?? 0, previousLatitude: vehicleSummary.previousLatitude ?? 0, @@ -153,8 +157,11 @@ class VehiclesController: ObservableObject { // Check status of response if (responseAPIVehicleDetail?.statusCode == 401) { - await CarrisAuthentication.shared.authenticate() - return await self.fetchVehicleDetailsFromCarrisAPI(for: busNumber) + Task { + await CarrisAuthentication.shared.authenticate() + return await self.fetchVehicleDetailsFromCarrisAPI(for: busNumber) + } + return nil } else if (responseAPIVehicleDetail?.statusCode != 200) { print(responseAPIVehicleDetail as Any) throw Appstate.ModuleError.carris_unavailable @@ -206,4 +213,334 @@ class VehiclesController: ObservableObject { } + + /* MARK: - FETCH VEHICLE FROM COMMUNITY API */ + + // This function calls the GeoBus API and receives vehicle metadata, + // including positions, for the set route number, while storing them + // in the vehicles array. It also formats VehicleAnnotations and stores + // them in the annotations array. It must have @objc flag because Timer + // is written in Objective-C. + + func fetchVehicleFromCommunityAPI(for busNumber: Int) async -> VehicleDetails? { + + Appstate.shared.change(to: .loading, for: .vehicles) + + do { + + // Request Vehicle Detail (SGO) + var requestAPIVehicleDetail = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/SGO/busNumber/\(busNumber)")!) + requestAPIVehicleDetail.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestAPIVehicleDetail.addValue("application/json", forHTTPHeaderField: "Accept") + requestAPIVehicleDetail.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") + let (rawDataAPIVehicleDetail, rawResponseAPIVehicleDetail) = try await URLSession.shared.data(for: requestAPIVehicleDetail) + let responseAPIVehicleDetail = rawResponseAPIVehicleDetail as? HTTPURLResponse + + // Check status of response + if (responseAPIVehicleDetail?.statusCode == 401) { + Task { + await CarrisAuthentication.shared.authenticate() + return await self.fetchVehicleDetailsFromCarrisAPI(for: busNumber) + } + return nil + } else if (responseAPIVehicleDetail?.statusCode != 200) { + print(responseAPIVehicleDetail as Any) + throw Appstate.ModuleError.carris_unavailable + } + + let decodedAPIVehicleDetail = try JSONDecoder().decode(CarrisAPIVehicleDetail.self, from: rawDataAPIVehicleDetail) + + // Format and append each vehicle + // to the temporary variable. + let result = VehicleDetails( + busNumber: busNumber, + vehiclePlate: decodedAPIVehicleDetail.vehiclePlate ?? "", + lastStopOnVoyageName: decodedAPIVehicleDetail.lastStopOnVoyageName ?? "-" + ) + + Appstate.shared.change(to: .idle, for: .vehicles) + + return result + + } catch { + Appstate.shared.change(to: .error, for: .vehicles) + print("ERROR IN VEHICLE DETAILS: \(error)") + return nil + } + + } + + + + + // + // + // + // + // N E W V E R S I O N + // + // + // + // + + + + + + /* MARK: - UPDATE VEHICLES */ + + // This function decides whether to update available routes + + enum VehicleUpdateScope { + case summary + case detail + case community + } + + func update(scope: VehicleUpdateScope, for busNumber: Int? = nil) { + + switch scope { + + case .summary: + Task { + await fetchVehiclesListFromCarrisAPI_NEW() + } + + case .detail: + if (busNumber != nil) { + Task { + await fetchVehicleDetailsFromCarrisAPI_NEW(for: busNumber!) + } + } + + case .community: + if (busNumber != nil) { + Task { + await fetchVehicleFromCommunityAPI_NEW(for: busNumber!) + } + } + + } + + } + + + + /* MARK: - FETCH VEHICLES SUMMARY FROM CARRIS API */ + + // This function calls the GeoBus API and receives vehicle metadata, + // including positions, for the set route number, while storing them + // in the vehicles array. It also formats VehicleAnnotations and stores + // them in the annotations array. It must have @objc flag because Timer + // is written in Objective-C. + + func fetchVehiclesListFromCarrisAPI_NEW() async { + + Appstate.shared.change(to: .loading, for: .vehicles) + + do { + // Request all Vehicles from API + var requestCarrisAPIVehiclesList = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/vehicleStatuses")!) // /routeNumber/\(routeNumber!) + requestCarrisAPIVehiclesList.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestCarrisAPIVehiclesList.addValue("application/json", forHTTPHeaderField: "Accept") + requestCarrisAPIVehiclesList.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") + let (rawDataCarrisAPIVehiclesList, rawResponseCarrisAPIVehiclesList) = try await URLSession.shared.data(for: requestCarrisAPIVehiclesList) + let responseCarrisAPIVehiclesList = rawResponseCarrisAPIVehiclesList as? HTTPURLResponse + + // Check status of response + if (responseCarrisAPIVehiclesList?.statusCode == 401) { + await CarrisAuthentication.shared.authenticate() + await self.fetchVehiclesListFromCarrisAPI_NEW() + return + } else if (responseCarrisAPIVehiclesList?.statusCode != 200) { + print(responseCarrisAPIVehiclesList as Any) + throw Appstate.ModuleError.carris_unavailable + } + + let decodedCarrisAPIVehiclesList = try JSONDecoder().decode([CarrisAPIVehicleSummary].self, from: rawDataCarrisAPIVehiclesList) + + + for vehicleSummary in decodedCarrisAPIVehiclesList { + + let indexOfVehicleInArray = self.allVehicles.firstIndex(where: { + $0.id == vehicleSummary.busNumber + }) + + if (indexOfVehicleInArray != nil) { + allVehicles[indexOfVehicleInArray!].routeNumber = vehicleSummary.routeNumber ?? "-" + allVehicles[indexOfVehicleInArray!].kind = Globals().getKind(by: vehicleSummary.routeNumber ?? "-") + allVehicles[indexOfVehicleInArray!].lat = vehicleSummary.lat ?? 0 + allVehicles[indexOfVehicleInArray!].lng = vehicleSummary.lng ?? 0 + allVehicles[indexOfVehicleInArray!].previousLatitude = vehicleSummary.previousLatitude ?? 0 + allVehicles[indexOfVehicleInArray!].previousLongitude = vehicleSummary.previousLongitude ?? 0 + allVehicles[indexOfVehicleInArray!].lastGpsTime = vehicleSummary.lastGpsTime ?? "" + allVehicles[indexOfVehicleInArray!].angleInRadians = self.getAngleInRadians( + prevLat: vehicleSummary.previousLatitude ?? 0, + prevLng: vehicleSummary.previousLongitude ?? 0, + currLat: vehicleSummary.lat ?? 0, + currLng: vehicleSummary.lng ?? 0 + ) + } else { + self.allVehicles.append( + Vehicle( + busNumber: vehicleSummary.busNumber ?? 0, + routeNumber: vehicleSummary.routeNumber ?? "-", + kind: Globals().getKind(by: vehicleSummary.routeNumber ?? "-"), + lat: vehicleSummary.lat ?? 0, + lng: vehicleSummary.lng ?? 0, + previousLatitude: vehicleSummary.previousLatitude ?? 0, + previousLongitude: vehicleSummary.previousLongitude ?? 0, + lastGpsTime: vehicleSummary.lastGpsTime ?? "", + angleInRadians: self.getAngleInRadians( + prevLat: vehicleSummary.previousLatitude ?? 0, + prevLng: vehicleSummary.previousLongitude ?? 0, + currLat: vehicleSummary.lat ?? 0, + currLng: vehicleSummary.lng ?? 0 + ) + ) + ) + } + + } + + Appstate.shared.change(to: .idle, for: .vehicles) + + } catch { + Appstate.shared.change(to: .error, for: .vehicles) + print("ERROR IN VEHICLES: \(error)") + return + } + + } + + + + + + + + + /* MARK: - FETCH VEHICLE DETAILS FROM CARRIS API */ + + // This function calls the GeoBus API and receives vehicle metadata, + // including positions, for the set route number, while storing them + // in the vehicles array. It also formats VehicleAnnotations and stores + // them in the annotations array. It must have @objc flag because Timer + // is written in Objective-C. + + func fetchVehicleDetailsFromCarrisAPI_NEW(for busNumber: Int) async { + + // 1. Check if Vehicle exists in array + guard let indexOfVehicleInArray = allVehicles.firstIndex(where: { $0.id == busNumber }) else { + return + } + + Appstate.shared.change(to: .loading, for: .vehicles) + + do { + + // Request Vehicle Detail (SGO) + var requestCarrisAPIVehicleDetail = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/SGO/busNumber/\(busNumber)")!) + requestCarrisAPIVehicleDetail.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestCarrisAPIVehicleDetail.addValue("application/json", forHTTPHeaderField: "Accept") + requestCarrisAPIVehicleDetail.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") + let (rawDataCarrisAPIVehicleDetail, rawResponseCarrisAPIVehicleDetail) = try await URLSession.shared.data(for: requestCarrisAPIVehicleDetail) + let responseCarrisAPIVehicleDetail = rawResponseCarrisAPIVehicleDetail as? HTTPURLResponse + + // Check status of response + if (responseCarrisAPIVehicleDetail?.statusCode == 401) { + await CarrisAuthentication.shared.authenticate() + await self.fetchVehicleDetailsFromCarrisAPI_NEW(for: busNumber) + return + } else if (responseCarrisAPIVehicleDetail?.statusCode != 200) { + print(responseCarrisAPIVehicleDetail as Any) + throw Appstate.ModuleError.carris_unavailable + } + + let decodedCarrisAPIVehicleDetail = try JSONDecoder().decode(CarrisAPIVehicleDetail.self, from: rawDataCarrisAPIVehicleDetail) + + // Update details of Vehicle + allVehicles[indexOfVehicleInArray].vehiclePlate = decodedCarrisAPIVehicleDetail.vehiclePlate ?? "-" + allVehicles[indexOfVehicleInArray].lastStopOnVoyageName = decodedCarrisAPIVehicleDetail.lastStopOnVoyageName ?? "-" + + Appstate.shared.change(to: .idle, for: .vehicles) + + } catch { + Appstate.shared.change(to: .error, for: .vehicles) + print("ERROR IN VEHICLE DETAILS: \(error)") + return + } + + } + + + + /* MARK: - FETCH VEHICLE FROM COMMUNITY API */ + + // This function calls the GeoBus API and receives vehicle metadata, + // including positions, for the set route number, while storing them + // in the vehicles array. It also formats VehicleAnnotations and stores + // them in the annotations array. It must have @objc flag because Timer + // is written in Objective-C. + + func fetchVehicleFromCommunityAPI_NEW(for busNumber: Int) async { + + // 1. Check if Vehicle exists in array + guard let indexOfVehicleInArray = allVehicles.firstIndex(where: { $0.id == busNumber }) else { + return + } + + Appstate.shared.change(to: .loading, for: .vehicles) + + do { + + // Request Vehicle Detail (SGO) + var requestCommunityAPIVehicle = URLRequest(url: URL(string: "https://api.carril.workers.dev/estbus?busNumber=\(busNumber)")!) + requestCommunityAPIVehicle.addValue("application/json", forHTTPHeaderField: "Content-Type") + requestCommunityAPIVehicle.addValue("application/json", forHTTPHeaderField: "Accept") + requestCommunityAPIVehicle.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") + let (rawDataCommunityAPIVehicle, rawResponseCommunityAPIVehicle) = try await URLSession.shared.data(for: requestCommunityAPIVehicle) + let responseCommunityAPIVehicle = rawResponseCommunityAPIVehicle as? HTTPURLResponse + + // Check status of response + if (responseCommunityAPIVehicle?.statusCode != 200) { + print(responseCommunityAPIVehicle as Any) + throw Appstate.ModuleError.community_unavailable + } + + let decodedCommunityAPIVehicle = try JSONDecoder().decode([CommunityAPIVehicle].self, from: rawDataCommunityAPIVehicle) + + // Update details of Vehicle + allVehicles[indexOfVehicleInArray].estimatedTimeofArrivalCorrected = decodedCommunityAPIVehicle[0].estimatedTimeofArrivalCorrected + + Appstate.shared.change(to: .idle, for: .vehicles) + + } catch { + Appstate.shared.change(to: .error, for: .vehicles) + print("GB: ERROR IN 'fetchVehicleFromCommunityAPI_NEW': \(error)") + return + } + + } + + + + + + func getVehicle(by busNumber: Int) -> Vehicle? { + let indexInArray = self.allVehicles.firstIndex(where: { + $0.busNumber == busNumber + }) + + if (indexInArray != nil) { + return allVehicles[indexInArray!] + } else { + return nil + } + } + + + + + } diff --git a/GeoBus/de.lproj/Localizable.strings b/GeoBus/de.lproj/Localizable.strings index 409b771f..f6470b3f 100644 --- a/GeoBus/de.lproj/Localizable.strings +++ b/GeoBus/de.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Wenn der Fehler weiterhin besteht, versuchen sie es später erneut. Leider funktioniert die App ohne diesen ersten Schritt nicht."; +/* No comment provided by engineer. */ +"in ±" = "in ca."; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Wahrscheinlich, weil deren Server nicht erreichbar sind. In diesem Fall ist es das Beste, wenn sie es zu einem anderen Zeitpunkt erneut versuchen. :/"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Deine Meinung zählt"; +/* No comment provided by engineer. */ +"P" = "P"; + /* No comment provided by engineer. */ "Please try again." = "Bitte versuchen sie es erneut."; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "Das wird nur einmal geschehen."; +/* No comment provided by engineer. */ +"to" = "nach"; + /* No comment provided by engineer. */ "to: %@" = "nach: %@"; diff --git a/GeoBus/en.lproj/Localizable.strings b/GeoBus/en.lproj/Localizable.strings index f89111b0..b250fbc5 100644 --- a/GeoBus/en.lproj/Localizable.strings +++ b/GeoBus/en.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step."; +/* No comment provided by engineer. */ +"in ±" = "in ±"; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "It's probably because their servers are down. In this case, the best option is to wait and try again later :/"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Open to Feedback"; +/* No comment provided by engineer. */ +"P" = "P"; + /* No comment provided by engineer. */ "Please try again." = "Please try again."; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "This will only happen once."; +/* No comment provided by engineer. */ +"to" = "to"; + /* No comment provided by engineer. */ "to: %@" = "to: %@"; diff --git a/GeoBus/fa.lproj/Localizable.strings b/GeoBus/fa.lproj/Localizable.strings index f449f97a..6113940e 100644 --- a/GeoBus/fa.lproj/Localizable.strings +++ b/GeoBus/fa.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "اگر خطا ادامه داشت، تنها گزینه این است که صبر کنید و بعداً دوباره امتحان کنید. متأسفانه برنامه بدون این مرحله اول نمی تواند کار کند."; +/* No comment provided by engineer. */ +"in ±" = "در ±"; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "احتمالاً به این دلیل است که سرورهای آنها از کار افتاده است. در این مورد، بهترین گزینه این است که صبر کنید و بعداً دوباره امتحان کنید"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "برای بازخورد باز کنید"; +/* No comment provided by engineer. */ +"P" = "پ"; + /* No comment provided by engineer. */ "Please try again." = "لطفا دوباره امتحان کنید."; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "این فقط یک بار اتفاق می افتد."; +/* No comment provided by engineer. */ +"to" = "به"; + /* No comment provided by engineer. */ "to: %@" = "به: %@"; diff --git a/GeoBus/fr.lproj/Localizable.strings b/GeoBus/fr.lproj/Localizable.strings index 5754d647..9ce82ff5 100644 --- a/GeoBus/fr.lproj/Localizable.strings +++ b/GeoBus/fr.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Si l'erreur persiste, la seule option est d'attendre et de réessayer plus tard. Malheureusement, l'application ne peut pas fonctionner sans cette première étape."; +/* No comment provided by engineer. */ +"in ±" = "en ±"; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "C'est probablement parce que leurs serveurs sont en panne. Dans ce cas, la meilleure option est d'attendre et de réessayer plus tard :/"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Ouvert à la rétroaction"; +/* No comment provided by engineer. */ +"P" = "P"; + /* No comment provided by engineer. */ "Please try again." = "Veuillez réessayer."; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "Cela ne se fera qu'une seule fois."; +/* No comment provided by engineer. */ +"to" = "à"; + /* No comment provided by engineer. */ "to: %@" = "à: %@"; diff --git a/GeoBus/it.lproj/Localizable.strings b/GeoBus/it.lproj/Localizable.strings index 0aa549e8..97775d71 100644 --- a/GeoBus/it.lproj/Localizable.strings +++ b/GeoBus/it.lproj/Localizable.strings @@ -59,7 +59,7 @@ "Favorites" = "Preferiti"; /* No comment provided by engineer. */ -"Find Routes" = "Trova Itinerari"; +"Find Routes" = "Trova percorsi"; /* No comment provided by engineer. */ "GeoBus can show you where you are in the map, if you allow it." = "GeoBus può mostrarti dove ti trovi nella mappa, se lo consenti."; @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Se l'errore persiste, l'unica opzione è attendere e riprovare più tardi. Sfortunatamente l'app non può funzionare senza questo primo passo."; +/* No comment provided by engineer. */ +"in ±" = "in ±"; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Probabilmente perché i loro server sono giù. In questo caso, l'opzione migliore è aspettare e riprovare più tardi :/"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Apri a Feedback"; +/* No comment provided by engineer. */ +"P" = "P"; + /* No comment provided by engineer. */ "Please try again." = "Per favore riprova."; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "Questo accadrà solo una volta."; +/* No comment provided by engineer. */ +"to" = "a"; + /* No comment provided by engineer. */ "to: %@" = "a: %@"; diff --git a/GeoBus/nl.lproj/Localizable.strings b/GeoBus/nl.lproj/Localizable.strings index d195c7aa..46ab1194 100644 --- a/GeoBus/nl.lproj/Localizable.strings +++ b/GeoBus/nl.lproj/Localizable.strings @@ -59,7 +59,7 @@ "Favorites" = "Favorieten"; /* No comment provided by engineer. */ -"Find Routes" = "Zoek routes"; +"Find Routes" = "Vind routes"; /* No comment provided by engineer. */ "GeoBus can show you where you are in the map, if you allow it." = "GeoBus kan je laten zien waar je bent in de kaart, als je het toestaat."; @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Als de fout zich blijft voordoen, is de enige optie wachten en het later opnieuw proberen. Helaas kan de app niet functioneren zonder deze eerste stap."; +/* No comment provided by engineer. */ +"in ±" = "in ±"; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Het is waarschijnlijk omdat hun servers offline zijn. In dit geval is de beste optie om te wachten en het later opnieuw te proberen :/"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Open voor feedback"; +/* No comment provided by engineer. */ +"P" = "P"; + /* No comment provided by engineer. */ "Please try again." = "Probeer het opnieuw."; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "Dit zal slechts één keer gebeuren."; +/* No comment provided by engineer. */ +"to" = "naar"; + /* No comment provided by engineer. */ "to: %@" = "Aan: %@"; diff --git a/GeoBus/pl.lproj/Localizable.strings b/GeoBus/pl.lproj/Localizable.strings index 9e73ac6c..44e1ec44 100644 --- a/GeoBus/pl.lproj/Localizable.strings +++ b/GeoBus/pl.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Jeżeli błąd się powtarza, jedyną opcją jest poczekać i spróbować ponownie później. Niestety aplikacja nie może działać bez tego pierwszego kroku."; +/* No comment provided by engineer. */ +"in ±" = "w ±"; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Prawdopodobnie jest tak, ponieważ ich serwery są wyłączone. W tym przypadku najlepszym rozwiązaniem jest zaczekać i spróbować ponownie później :/"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Otwarci na opinie"; +/* No comment provided by engineer. */ +"P" = "P"; + /* No comment provided by engineer. */ "Please try again." = "Spróbuj ponownie."; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "Będzie to miało miejsce tylko raz."; +/* No comment provided by engineer. */ +"to" = "do"; + /* No comment provided by engineer. */ "to: %@" = "do: %@"; diff --git a/GeoBus/pt.lproj/Localizable.strings b/GeoBus/pt.lproj/Localizable.strings index 479237ab..15eb18e1 100644 --- a/GeoBus/pt.lproj/Localizable.strings +++ b/GeoBus/pt.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Se o erro persistir, a única opção é esperar e tentar novamente mais tarde. Infelizmente a app não consegue funcionar sem este primeiro passo."; +/* No comment provided by engineer. */ +"in ±" = "em ±"; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Provavelmente os servidores estão indisponíveis. Neste caso, a melhor opção é esperar e tentar novamente mais tarde :/"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Sugestões?"; +/* No comment provided by engineer. */ +"P" = "P"; + /* No comment provided by engineer. */ "Please try again." = "Por favor tente novamente."; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "Isto só acontecerá uma vez."; +/* No comment provided by engineer. */ +"to" = "para"; + /* No comment provided by engineer. */ "to: %@" = "para: %@"; diff --git a/GeoBus/tr.lproj/Localizable.strings b/GeoBus/tr.lproj/Localizable.strings index 7c6fd038..b8cadd06 100644 --- a/GeoBus/tr.lproj/Localizable.strings +++ b/GeoBus/tr.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Hata devam ederse, tek seçenek beklemek ve daha sonra tekrar denemektir. Maalesef uygulama bu ilk adım olmadan çalışamaz."; +/* No comment provided by engineer. */ +"in ±" = "içinde"; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Muhtemelen sunucuları kapalı olduğundandır. Bu durumda en iyi seçenek bekleyip daha sonra tekrar denemektir :/"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Geri Bildirime Açık"; +/* No comment provided by engineer. */ +"P" = "P"; + /* No comment provided by engineer. */ "Please try again." = "Lütfen tekrar deneyiniz."; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "Bu sadece bir kez olacak."; +/* No comment provided by engineer. */ +"to" = "➨"; + /* No comment provided by engineer. */ "to: %@" = "varış yeri: %@"; diff --git a/GeoBus/uk.lproj/Localizable.strings b/GeoBus/uk.lproj/Localizable.strings index 6c123eef..408501c2 100644 --- a/GeoBus/uk.lproj/Localizable.strings +++ b/GeoBus/uk.lproj/Localizable.strings @@ -59,7 +59,7 @@ "Favorites" = "Вподобання"; /* No comment provided by engineer. */ -"Find Routes" = "Знайти маршрути"; +"Find Routes" = "Знайдіть маршрути"; /* No comment provided by engineer. */ "GeoBus can show you where you are in the map, if you allow it." = "GeoBus може показати вам, де ви перебуваєте на мапі, якщо ви дозволяєте."; @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Якщо помилка повторюється, зачекайте будь ласка спробуйте пізніше. Нажаль, програма не зможе працювати без першого кроку."; +/* No comment provided by engineer. */ +"in ±" = "в класифікованих"; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Скоріш за все їх сервери відсутні. У такому випадку найкращий варіант - чекати та спробувати пізніше :/"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Відкрити для зворотнього зв'язку"; +/* No comment provided by engineer. */ +"P" = "Пн"; + /* No comment provided by engineer. */ "Please try again." = "Спробуйте ще раз."; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "Це станеться лише один раз."; +/* No comment provided by engineer. */ +"to" = "по"; + /* No comment provided by engineer. */ "to: %@" = "до: %@"; diff --git a/GeoBus/zh-Hans.lproj/Localizable.strings b/GeoBus/zh-Hans.lproj/Localizable.strings index 745048e4..a6afc124 100644 --- a/GeoBus/zh-Hans.lproj/Localizable.strings +++ b/GeoBus/zh-Hans.lproj/Localizable.strings @@ -82,6 +82,9 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "如果错误仍然存在,唯一的选择是请稍后再尝试。不幸的是,如果没有第一步,该应用程序将无法运行。"; +/* No comment provided by engineer. */ +"in ±" = "在 ±"; + /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "这可能是由于他们的服务器已经关闭。在这种情况下,最好的选择是等候,然后再试一次 :/"; @@ -115,6 +118,9 @@ /* No comment provided by engineer. */ "Open to Feedback" = "接受反馈"; +/* No comment provided by engineer. */ +"P" = "P"; + /* No comment provided by engineer. */ "Please try again." = "请重试。"; @@ -190,6 +196,9 @@ /* No comment provided by engineer. */ "This will only happen once." = "这只会发生一次。"; +/* No comment provided by engineer. */ +"to" = "到"; + /* No comment provided by engineer. */ "to: %@" = "到: %@"; From c8cfcdac80dfbe977d1da9f6dae9149e29fee653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 15 Oct 2022 01:29:51 +0100 Subject: [PATCH 02/63] Custom merge with production Updating this branch with the changes pushed to production in which a bunch of stuff was taken out. Better management here needed, more restraint. This is a time sink. --- GeoBus.xcodeproj/project.pbxproj | 46 +-- GeoBus/App/Animations/Pulse.swift | 61 +++ GeoBus/App/Animations/Spinner.swift | 38 ++ GeoBus/App/Components/About/CloseButton.swift | 3 +- GeoBus/App/Components/About/SyncStatus.swift | 5 +- GeoBus/App/Components/Map/MapView.swift | 4 +- .../RouteDetailsVehiclesQuantity.swift | 2 +- .../RouteDetails/RouteDetailsView.swift | 7 +- .../SelectRoute/SelectRouteView.swift | 33 +- .../StopDetails/StopEstimations.swift | 6 +- .../VehicleDetails/VehicleDetailsView.swift | 374 +----------------- GeoBus/App/Extensions/Globals.swift | 166 -------- GeoBus/App/Extensions/Helpers.swift | 15 +- GeoBus/App/Layout/RouteBadgePill.swift | 4 +- GeoBus/App/Layout/RouteBadgeSquare.swift | 4 +- GeoBus/App/Layout/TimeLeft.swift | 4 +- GeoBus/App/State/Appstate.swift | 5 +- GeoBus/App/State/CarrisAuthentication.swift | 2 +- GeoBus/App/State/MapController.swift | 12 +- GeoBus/App/State/RoutesController.swift | 2 +- GeoBus/App/State/VehiclesController.swift | 349 +--------------- GeoBus/de.lproj/Localizable.strings | 9 - GeoBus/en.lproj/Localizable.strings | 9 - GeoBus/fa.lproj/Localizable.strings | 9 - GeoBus/fr.lproj/Localizable.strings | 9 - GeoBus/it.lproj/Localizable.strings | 11 +- GeoBus/nl.lproj/Localizable.strings | 11 +- GeoBus/pl.lproj/Localizable.strings | 9 - GeoBus/pt.lproj/Localizable.strings | 9 - GeoBus/tr.lproj/Localizable.strings | 9 - GeoBus/uk.lproj/Localizable.strings | 11 +- GeoBus/zh-Hans.lproj/Localizable.strings | 9 - 32 files changed, 186 insertions(+), 1061 deletions(-) create mode 100644 GeoBus/App/Animations/Pulse.swift create mode 100644 GeoBus/App/Animations/Spinner.swift delete mode 100644 GeoBus/App/Extensions/Globals.swift diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index 3d47d7a9..01b57471 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -17,9 +17,6 @@ CF181FFD28CCB99E00248F72 /* Auth.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF181FFC28CCB99E00248F72 /* Auth.swift */; }; CF18200728CCBA3500248F72 /* RoutesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18200628CCBA3500248F72 /* RoutesController.swift */; }; CF18200928CCBA4600248F72 /* Routes.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18200828CCBA4600248F72 /* Routes.swift */; }; - CF18201928CCBB6400248F72 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18201828CCBB6400248F72 /* LoadingView.swift */; }; - CF18201B28CCBB7100248F72 /* LiveIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18201A28CCBB7100248F72 /* LiveIcon.swift */; }; - CF18201D28CCBB8000248F72 /* EstimatedIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18201C28CCBB8000248F72 /* EstimatedIcon.swift */; }; CF18202928CCBBDC00248F72 /* TapticEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18202828CCBBDC00248F72 /* TapticEngine.swift */; }; CF18202E28CCBC0100248F72 /* VehiclesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18202D28CCBC0100248F72 /* VehiclesController.swift */; }; CF18203028CCBC0B00248F72 /* EstimationsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18202F28CCBC0B00248F72 /* EstimationsController.swift */; }; @@ -62,7 +59,6 @@ CF82BB0928D7F19A007F0CDB /* LocationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF82BB0828D7F19A007F0CDB /* LocationCard.swift */; }; CF82BB0B28D7F1C6007F0CDB /* ShareCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF82BB0A28D7F1C6007F0CDB /* ShareCard.swift */; }; CF82BB0D28D7F202007F0CDB /* ContactsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF82BB0C28D7F202007F0CDB /* ContactsCard.swift */; }; - CFAF0E7628CE586300DDAD5B /* Globals.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFAF0E7528CE586300DDAD5B /* Globals.swift */; }; CFAF0E7828CE84C200DDAD5B /* Vehicles.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFAF0E7728CE84C200DDAD5B /* Vehicles.swift */; }; CFAF0E7A28CE84F400DDAD5B /* MapAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFAF0E7928CE84F400DDAD5B /* MapAnnotations.swift */; }; CFB5D45728EEFE21002368BC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = CFB5D45528EEFE21002368BC /* InfoPlist.strings */; }; @@ -83,8 +79,8 @@ CFFFAD8128F64E2000DFD5FD /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8028F64E2000DFD5FD /* Analytics.swift */; }; CFFFAD8328F6754400DFD5FD /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8228F6754400DFD5FD /* Helpers.swift */; }; CFFFAD8528F7A21100DFD5FD /* CarrisAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8428F7A21100DFD5FD /* CarrisAuthentication.swift */; }; - CFFFAD8928F8ECDE00DFD5FD /* LoadingSpinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8828F8ECDE00DFD5FD /* LoadingSpinner.swift */; }; - CFFFAD8B28F8F33200DFD5FD /* LoadingPulse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8A28F8F33200DFD5FD /* LoadingPulse.swift */; }; + CFFFAD8928F8ECDE00DFD5FD /* Spinner.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8828F8ECDE00DFD5FD /* Spinner.swift */; }; + CFFFAD8B28F8F33200DFD5FD /* Pulse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFFFAD8A28F8F33200DFD5FD /* Pulse.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -99,9 +95,6 @@ CF181FFC28CCB99E00248F72 /* Auth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Auth.swift; sourceTree = ""; }; CF18200628CCBA3500248F72 /* RoutesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutesController.swift; sourceTree = ""; }; CF18200828CCBA4600248F72 /* Routes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Routes.swift; sourceTree = ""; }; - CF18201828CCBB6400248F72 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; }; - CF18201A28CCBB7100248F72 /* LiveIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveIcon.swift; sourceTree = ""; }; - CF18201C28CCBB8000248F72 /* EstimatedIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimatedIcon.swift; sourceTree = ""; }; CF18202828CCBBDC00248F72 /* TapticEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TapticEngine.swift; sourceTree = ""; }; CF18202D28CCBC0100248F72 /* VehiclesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclesController.swift; sourceTree = ""; }; CF18202F28CCBC0B00248F72 /* EstimationsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EstimationsController.swift; sourceTree = ""; }; @@ -145,7 +138,6 @@ CF82BB0828D7F19A007F0CDB /* LocationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCard.swift; sourceTree = ""; }; CF82BB0A28D7F1C6007F0CDB /* ShareCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareCard.swift; sourceTree = ""; }; CF82BB0C28D7F202007F0CDB /* ContactsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsCard.swift; sourceTree = ""; }; - CFAF0E7528CE586300DDAD5B /* Globals.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Globals.swift; sourceTree = ""; }; CFAF0E7728CE84C200DDAD5B /* Vehicles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicles.swift; sourceTree = ""; }; CFAF0E7928CE84F400DDAD5B /* MapAnnotations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAnnotations.swift; sourceTree = ""; }; CFAF0E7B28CEC78C00DDAD5B /* GeoBus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GeoBus.entitlements; sourceTree = ""; }; @@ -158,8 +150,6 @@ CFB5D46128EEFF2C002368BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; CFB5D46228EEFF2D002368BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; CFC80FCB28D2C2FF003D059D /* DragAndDrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragAndDrop.swift; sourceTree = ""; }; - CFCED4F428EF2F8300963640 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; - CFCED4F528EF2F8600963640 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; CFCED4F628EF5CB900963640 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; CFCED4F728EF5CB900963640 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; CFCED4F828EF5CD500963640 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; @@ -171,6 +161,8 @@ CFEF85C128D34E4F00A29526 /* crowdin.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = crowdin.yml; sourceTree = ""; }; CFEF85C328D34E6300A29526 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; CFEF85C428D34E6300A29526 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + CFFCEC5F28F9F34900F8E271 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + CFFCEC6028F9F34C00F8E271 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; CFFF2D3E28D7CEE300E035E0 /* Secrets.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Secrets.xcconfig; sourceTree = ""; }; CFFFAD7828F4AD0400DFD5FD /* TimeLeft.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeLeft.swift; sourceTree = ""; }; CFFFAD7A28F4D8D000DFD5FD /* StopIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StopIcon.swift; sourceTree = ""; }; @@ -179,8 +171,8 @@ CFFFAD8028F64E2000DFD5FD /* Analytics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Analytics.swift; sourceTree = ""; }; CFFFAD8228F6754400DFD5FD /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; CFFFAD8428F7A21100DFD5FD /* CarrisAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrisAuthentication.swift; sourceTree = ""; }; - CFFFAD8828F8ECDE00DFD5FD /* LoadingSpinner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingSpinner.swift; sourceTree = ""; }; - CFFFAD8A28F8F33200DFD5FD /* LoadingPulse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingPulse.swift; sourceTree = ""; }; + CFFFAD8828F8ECDE00DFD5FD /* Spinner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spinner.swift; sourceTree = ""; }; + CFFFAD8A28F8F33200DFD5FD /* Pulse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pulse.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -335,7 +327,7 @@ CF05F61628CD08D400B4AD58 /* Components */, CF05F61C28CD1F2800B4AD58 /* Layout */, CF18200C28CCBA7300248F72 /* Extensions */, - CF18201528CCBB4900248F72 /* Lottie */, + CF18201528CCBB4900248F72 /* Animations */, ); path = App; sourceTree = ""; @@ -345,23 +337,19 @@ children = ( CF18202828CCBBDC00248F72 /* TapticEngine.swift */, CFC80FCB28D2C2FF003D059D /* DragAndDrop.swift */, - CFAF0E7528CE586300DDAD5B /* Globals.swift */, CFFFAD8228F6754400DFD5FD /* Helpers.swift */, CFDC15EE28D292FB00A4BE49 /* ViewSize.swift */, ); path = Extensions; sourceTree = ""; }; - CF18201528CCBB4900248F72 /* Lottie */ = { + CF18201528CCBB4900248F72 /* Animations */ = { isa = PBXGroup; children = ( - CF18201828CCBB6400248F72 /* LoadingView.swift */, - CF18201A28CCBB7100248F72 /* LiveIcon.swift */, - CF18201C28CCBB8000248F72 /* EstimatedIcon.swift */, - CFFFAD8A28F8F33200DFD5FD /* LoadingPulse.swift */, - CFFFAD8828F8ECDE00DFD5FD /* LoadingSpinner.swift */, + CFFFAD8A28F8F33200DFD5FD /* Pulse.swift */, + CFFFAD8828F8ECDE00DFD5FD /* Spinner.swift */, ); - path = Lottie; + path = Animations; sourceTree = ""; }; CF18205228CCBCF400248F72 /* StopDetails */ = { @@ -539,9 +527,8 @@ CF18207828CCBD2300248F72 /* RouteDetailsSheet.swift in Sources */, CF18207728CCBD2300248F72 /* RouteDetailsAddToFavorites.swift in Sources */, CF6C918428D3F2C9006C3F61 /* UserLocation.swift in Sources */, - CFFFAD8928F8ECDE00DFD5FD /* LoadingSpinner.swift in Sources */, + CFFFAD8928F8ECDE00DFD5FD /* Spinner.swift in Sources */, CF6C918E28D4DABA006C3F61 /* Stops.swift in Sources */, - CF18201928CCBB6400248F72 /* LoadingView.swift in Sources */, CF18207528CCBD2300248F72 /* VariantWarning.swift in Sources */, CF6C918C28D4B452006C3F61 /* SearchStopInput.swift in Sources */, CF18207628CCBD2300248F72 /* StopsList.swift in Sources */, @@ -566,7 +553,6 @@ CF18205728CCBD0400248F72 /* StopEstimations.swift in Sources */, CFFFAD8528F7A21100DFD5FD /* CarrisAuthentication.swift in Sources */, CF05F61A28CD09A000B4AD58 /* NavBar.swift in Sources */, - CFAF0E7628CE586300DDAD5B /* Globals.swift in Sources */, CFDD014B28D535370070FE4B /* Card.swift in Sources */, CF18202928CCBBDC00248F72 /* TapticEngine.swift in Sources */, CF18200728CCBA3500248F72 /* RoutesController.swift in Sources */, @@ -576,14 +562,12 @@ CF18204828CCBCC500248F72 /* RouteBadgeSquare.swift in Sources */, CFDD014928D5114D0070FE4B /* SyncStatus.swift in Sources */, CF18208528CCBD3A00248F72 /* FavoriteRoutes.swift in Sources */, - CF18201D28CCBB8000248F72 /* EstimatedIcon.swift in Sources */, - CFFFAD8B28F8F33200DFD5FD /* LoadingPulse.swift in Sources */, + CFFFAD8B28F8F33200DFD5FD /* Pulse.swift in Sources */, CF6C918828D3FAF9006C3F61 /* AboutGeoBus.swift in Sources */, CF6C917E28D3ED0A006C3F61 /* SquareButton.swift in Sources */, CF82BB0B28D7F1C6007F0CDB /* ShareCard.swift in Sources */, CF47994F28D33E1900B56D4B /* Disclaimer.swift in Sources */, CF47994D28D3315E00B56D4B /* Appstate.swift in Sources */, - CF18201B28CCBB7100248F72 /* LiveIcon.swift in Sources */, CFDD014D28D66D9B0070FE4B /* CloseButton.swift in Sources */, CFDC15EF28D292FB00A4BE49 /* ViewSize.swift in Sources */, CFAF0E7828CE84C200DDAD5B /* Vehicles.swift in Sources */, @@ -602,9 +586,9 @@ CFB5D45C28EEFEA6002368BC /* pt */, CFB5D46028EEFEF1002368BC /* zh-Hans */, CFB5D46228EEFF2D002368BC /* tr */, - CFCED4F428EF2F8300963640 /* en */, CFCED4F728EF5CB900963640 /* pl */, CFCED4F928EF5CD500963640 /* fa */, + CFFCEC6028F9F34C00F8E271 /* en */, ); name = InfoPlist.strings; sourceTree = ""; @@ -616,9 +600,9 @@ CFB5D45B28EEFEA3002368BC /* pt */, CFB5D45F28EEFEF1002368BC /* zh-Hans */, CFB5D46128EEFF2C002368BC /* tr */, - CFCED4F528EF2F8600963640 /* en */, CFCED4F628EF5CB900963640 /* pl */, CFCED4F828EF5CD500963640 /* fa */, + CFFCEC5F28F9F34900F8E271 /* en */, ); name = Localizable.strings; sourceTree = ""; diff --git a/GeoBus/App/Animations/Pulse.swift b/GeoBus/App/Animations/Pulse.swift new file mode 100644 index 00000000..d9863c39 --- /dev/null +++ b/GeoBus/App/Animations/Pulse.swift @@ -0,0 +1,61 @@ +// +// Pulse.swift +// GeoBus +// +// Created by João de Vasconcelos on 14/10/2022. +// + +import SwiftUI + + +struct PulseLabel: View { + + let accent: Color + let label: Text + + var body: some View { + HStack(spacing: 2) { + Pulse(size: 15, accent: self.accent) + label + .font(Font.system(size: 11, weight: .medium, design: .default) ) + .foregroundColor(self.accent) + } + } + +} + + + +struct Pulse: View { + + let speed: Double = 3 + + let size: CGFloat + let accent: Color + + @State var scale: Double = 0.0 + @State var opacity: Double = 0.8 + + + var body: some View { + ZStack { + Circle() + .scale(scale) + .fill(accent) + .opacity(opacity) + Circle() + .fill(accent) + .frame(width: size/4, height: size/4, alignment: .center) + } + .frame(width: size, height: size, alignment: .center) + .onAppear { + withAnimation(.easeOut(duration: speed).repeatForever(autoreverses: false)) { + scale = 1.0 + } + withAnimation(.easeIn(duration: speed).repeatForever(autoreverses: false)) { + opacity = 0.0 + } + } + } + +} diff --git a/GeoBus/App/Animations/Spinner.swift b/GeoBus/App/Animations/Spinner.swift new file mode 100644 index 00000000..98d72e89 --- /dev/null +++ b/GeoBus/App/Animations/Spinner.swift @@ -0,0 +1,38 @@ +// +// Spinner.swift +// GeoBus +// +// Created by João de Vasconcelos on 14/10/2022. +// + +import SwiftUI + +struct Spinner: View { + + @Environment(\.colorScheme) var colorScheme: ColorScheme + + private let timing: Double = 0.5 + private let size: CGFloat = 20.0 + + @State var trim: Double = 0.4 + @State var rotationAngle: Double = 0.0 + + var body: some View { + Circle() + .trim(from: trim, to: 1.0) + .stroke(colorScheme == .dark ? .white : .green, + style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round) + ) + .rotationEffect(.degrees(rotationAngle)) + .frame(width: size, height: size, alignment: .center) + .onAppear { + withAnimation(.linear(duration: timing * 2).repeatForever()) { + trim = 0.8 + } + withAnimation(.linear(duration: timing).repeatForever(autoreverses: false)) { + rotationAngle = 360.0 + } + } + } + +} diff --git a/GeoBus/App/Components/About/CloseButton.swift b/GeoBus/App/Components/About/CloseButton.swift index 6dec1477..45e9843c 100644 --- a/GeoBus/App/Components/About/CloseButton.swift +++ b/GeoBus/App/Components/About/CloseButton.swift @@ -9,6 +9,7 @@ import SwiftUI struct CloseButton: View { + @EnvironmentObject var appstate: Appstate @EnvironmentObject var stopsController: StopsController @EnvironmentObject var routesController: RoutesController @@ -100,7 +101,7 @@ struct CloseButton: View { var body: some View { if (stopsController.allStops.isEmpty || routesController.allRoutes.isEmpty) { - if (Appstate.shared.stops == .error || Appstate.shared.routes == .error) { + if (appstate.stops == .error || appstate.routes == .error) { syncError } else { isSyncing diff --git a/GeoBus/App/Components/About/SyncStatus.swift b/GeoBus/App/Components/About/SyncStatus.swift index 7cc018af..471a1a7d 100644 --- a/GeoBus/App/Components/About/SyncStatus.swift +++ b/GeoBus/App/Components/About/SyncStatus.swift @@ -9,6 +9,7 @@ import SwiftUI struct SyncStatus: View { + @EnvironmentObject var appstate: Appstate @EnvironmentObject var stopsController: StopsController @EnvironmentObject var routesController: RoutesController @@ -110,9 +111,9 @@ struct SyncStatus: View { var body: some View { - if (Appstate.shared.stops == .error || Appstate.shared.routes == .error) { + if (appstate.stops == .error || appstate.routes == .error) { syncError - } else if (Appstate.shared.stops == .loading || Appstate.shared.routes == .loading) { + } else if (appstate.stops == .loading || appstate.routes == .loading) { isSyncing } else { hasSynced diff --git a/GeoBus/App/Components/Map/MapView.swift b/GeoBus/App/Components/Map/MapView.swift index 27950967..93b94005 100644 --- a/GeoBus/App/Components/Map/MapView.swift +++ b/GeoBus/App/Components/Map/MapView.swift @@ -46,10 +46,10 @@ struct MapView: View { .onChange(of: routesController.selectedVariant) { newVariant in if (newVariant != nil) { self.mapController.updateAnnotations(with: newVariant!) - self.mapController.updateAnnotations(with: vehiclesController.allVehicles, for: routesController.selectedRoute?.number) + self.mapController.updateAnnotations(with: vehiclesController.vehicles, for: routesController.selectedRoute?.number) } } - .onChange(of: vehiclesController.allVehicles) { newVehiclesList in + .onChange(of: vehiclesController.vehicles) { newVehiclesList in self.mapController.updateAnnotations(with: newVehiclesList, for: routesController.selectedRoute?.number) } } diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsVehiclesQuantity.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsVehiclesQuantity.swift index 24d03fa2..0f6d57ad 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsVehiclesQuantity.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsVehiclesQuantity.swift @@ -15,7 +15,7 @@ struct RouteDetailsVehiclesQuantity: View { var body: some View { ZStack(alignment: .topLeading) { - LiveIcon() + PulseLabel(accent: .green, label: Text("Live")) VStack(alignment: .center) { Text(String(vehiclesQuantity)) diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift index 67dd85d2..61341e98 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift @@ -10,6 +10,7 @@ import SwiftUI struct RouteDetailsView: View { + @EnvironmentObject var appstate: Appstate @EnvironmentObject var routesController: RoutesController @EnvironmentObject var vehiclesController: VehiclesController @@ -70,7 +71,7 @@ struct RouteDetailsView: View { var selectedRouteDetails: some View { VStack(alignment: .leading) { HStack { - LiveIcon() + PulseLabel(accent: .green, label: Text("Live")) Text(vehiclesController.vehicles.count == 1 ? "1 active vehicle" : "\(vehiclesController.vehicles.count) active vehicles") .font(Font.system(size: 11, weight: .medium, design: .default) ) .lineLimit(1) @@ -90,9 +91,9 @@ struct RouteDetailsView: View { // The final view where screens are composed based on appstate var body: some View { VStack { - if (Appstate.shared.routes == .loading && routesController.allRoutes.count < 1) { + if (appstate.routes == .loading && routesController.allRoutes.count < 1) { updatingRoutesScreen - } else if (Appstate.shared.global == .error) { + } else if (appstate.global == .error) { connectionError } else if (routesController.selectedRoute != nil) { selectedRouteDetails diff --git a/GeoBus/App/Components/SelectRoute/SelectRouteView.swift b/GeoBus/App/Components/SelectRoute/SelectRouteView.swift index 5e328650..acd08a29 100644 --- a/GeoBus/App/Components/SelectRoute/SelectRouteView.swift +++ b/GeoBus/App/Components/SelectRoute/SelectRouteView.swift @@ -9,26 +9,33 @@ import SwiftUI struct SelectRouteView: View { - + + @Environment(\.colorScheme) var colorScheme: ColorScheme + + @EnvironmentObject var appstate: Appstate @EnvironmentObject var routesController: RoutesController - + var body: some View { - + ZStack { - - if (Appstate.shared.global == .loading) { - LoadingView() - - } else if (Appstate.shared.global == .error) { + + if (appstate.global == .loading) { + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(Color(.systemGray4)) + Spinner() + } + + } else if (appstate.global == .error) { RoundedRectangle(cornerRadius: 10) .fill(Color(.systemRed).opacity(0.5)) Image(systemName: "wifi.exclamationmark") .font(.title) .foregroundColor(Color(.white)) - + } else { - + if (routesController.selectedRoute != nil) { RouteBadgeSquare(routeNumber: routesController.selectedRoute!.number) @@ -38,11 +45,11 @@ struct SelectRouteView: View { Image(systemName: "plus") .font(.title) .foregroundColor(Color(.white)) - + } - + } - + } .aspectRatio(1, contentMode: .fit) diff --git a/GeoBus/App/Components/StopDetails/StopEstimations.swift b/GeoBus/App/Components/StopDetails/StopEstimations.swift index 3c03dfc4..f08318d7 100644 --- a/GeoBus/App/Components/StopDetails/StopEstimations.swift +++ b/GeoBus/App/Components/StopDetails/StopEstimations.swift @@ -10,6 +10,8 @@ import SwiftUI struct StopEstimations: View { + @EnvironmentObject var appstate: Appstate + let estimations: [Estimation]? @@ -20,7 +22,7 @@ struct StopEstimations: View { .textCase(.uppercase) .foregroundColor(Color(.tertiaryLabel)) Spacer() - EstimatedIcon() + PulseLabel(accent: .orange, label: Text("Estimated")) } } @@ -69,7 +71,7 @@ struct StopEstimations: View { } else { noResultsScreen } - } else if (Appstate.shared.estimations == .error) { + } else if (appstate.estimations == .error) { errorScreen } else { loadingScreen diff --git a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift index f405efb8..75be5cf9 100644 --- a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift +++ b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift @@ -5,367 +5,12 @@ // Created by João on 22/04/2020. // Copyright © 2020 João. All rights reserved. // - import SwiftUI import Combine - - -struct VehicleInfoSheet: View { - - public let busNumber: Int - - @EnvironmentObject var vehiclesController: VehiclesController - - private let refreshTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() - - - var body: some View { - ScrollView { - - // SECTION 1 - VehicleInfoSheetHeader(vehicle: vehiclesController.getVehicle(by: busNumber)) - - // SECTION 2 - VehicleInfoSheetCurrentRouteStatus(vehicle: vehiclesController.getVehicle(by: busNumber)) - .padding() - - Spacer() - - VehicleInfoSheetLastSeenTime(vehicle: vehiclesController.getVehicle(by: busNumber)) - .padding() - - } - .presentationDetents([.medium, .large]) - .presentationDragIndicator(.hidden) - .onAppear() { - self.vehiclesController.update(scope: .detail, for: self.busNumber) - self.vehiclesController.update(scope: .community, for: self.busNumber) - } - .onReceive(refreshTimer) { event in - self.vehiclesController.update(scope: .detail, for: self.busNumber) - self.vehiclesController.update(scope: .community, for: self.busNumber) - } - } - -} - - - -struct VehicleInfoSheetHeader: View { - - public let vehicle: Vehicle? - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - HStack(spacing: 10) { - VehicleDestination(routeNumber: vehicle?.routeNumber ?? "-", destination: vehicle?.lastStopOnVoyageName ?? "-") - Spacer() - VehicleIdentifier(busNumber: vehicle?.busNumber ?? 0, vehiclePlate: vehicle?.vehiclePlate ?? "-") - } - .padding() - Divider() - } - } - -} - - - -struct VehicleInfoSheetLastSeenTime: View { - - public let vehicle: Vehicle? - - private let lastSeenTimeTimer = Timer.publish(every: 1 /* seconds */, on: .main, in: .common).autoconnect() - - @State private var lastSeenTime: String = "-" - - - var body: some View { - VStack(alignment: .leading) { - HStack(alignment: .center, spacing: 5) { - Image(systemName: "antenna.radiowaves.left.and.right") - .font(.system(size: 12, weight: .bold, design: .default)) - .foregroundColor(Color(.secondaryLabel)) - Text("GPS updated \(lastSeenTime) ago") - .font(.system(size: 12, weight: .bold, design: .default)) - .foregroundColor(Color(.secondaryLabel)) - .onAppear() { - self.lastSeenTime = Globals().getTimeString(for: vehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) - } - .onReceive(lastSeenTimeTimer) { event in - self.lastSeenTime = Globals().getTimeString(for: vehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) - } - Spacer() - } - } - } - -} - - - - -struct VehicleInfoSheetCurrentRouteStatus: View { - - public let vehicle: Vehicle? - - let times = [ - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20", - "2022-10-10T23:11:20" - ] - - var body: some View { - VStack(spacing: 0) { - // ForEach(vehicle?.estimatedTimeofArrivalCorrected ?? [], id: \.self) { timeString in - ForEach(self.times, id: \.self) { timeString in - VehicleInfoSheetRouteStop(timeString: timeString) - } - } - } - -} - - - - -struct VehicleInfoSheetRouteStop: View { - - - - public let timeString: String - - var body: some View { - HStack { - VStack(alignment: .leading, spacing: 0) { - HStack(spacing: 15) { - StopIcon(orderInRoute: 3, direction: .ascending) - Text("Teste") - .fontWeight(.medium) - .foregroundColor(Color(.label)) - .multilineTextAlignment(.leading) - Spacer() -// Text("45678") -// .font(Font.system(size: 12, weight: .medium, design: .default) ) -// .foregroundColor(Color(.secondaryLabel)) -// .padding(.vertical, 2) -// .padding(.horizontal, 7) -// .cornerRadius(10) - TimeLeft(time: timeString) - } - HStack(spacing: 15) { - Rectangle() - .foregroundColor(Color("StopSelectedBackground")) - .frame(width: 5, height: 25) - .padding(.leading, 10) - VStack { - Divider() - } - } - } - Spacer() -// TimeLeft(time: timeString) - } - - } - -} - - - - - - - - -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// -// - - - - - - - - - - - -struct VehicleInfoSheetHeader2: View { - - public let vehicle: Vehicle? - - @EnvironmentObject var vehiclesController: VehiclesController - - private let lastSeenTimeTimer = Timer.publish(every: 1 /* seconds */, on: .main, in: .common).autoconnect() - - @State var lastSeenTime: String = "-" - - - var loadingScreen: some View { - HStack(spacing: 3) { - ProgressView() - .scaleEffect(0.55) - Text("Loading...") - .font(Font.system(size: 13, weight: .medium, design: .default) ) - .foregroundColor(Color(.tertiaryLabel)) - Spacer() - } - } - - var errorScreen: some View { - Text("Carris API is unavailable.") - .font(Font.system(size: 13, weight: .medium, design: .default) ) - .foregroundColor(Color(.secondaryLabel)) - } - - var vehicleDetailsHeader: some View { - HStack(spacing: 15) { - VehicleDestination(routeNumber: vehicle?.routeNumber ?? "-", destination: vehicle?.lastStopOnVoyageName ?? "-") - Spacer() - VehicleIdentifier(busNumber: vehicle?.busNumber ?? 0, vehiclePlate: vehicle?.vehiclePlate ?? "-") - } - } - - - var vehicleDetailsScreen: some View { - VStack(alignment: .leading) { - HStack(alignment: .center, spacing: 5) { - Image(systemName: "antenna.radiowaves.left.and.right") - .font(.system(size: 12, weight: .bold, design: .default)) - .foregroundColor(Color(.secondaryLabel)) - Text("GPS updated \(lastSeenTime) ago") - .font(.system(size: 12, weight: .bold, design: .default)) - .foregroundColor(Color(.secondaryLabel)) - .onAppear() { - self.lastSeenTime = Globals().getTimeString(for: vehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) - } - .onReceive(lastSeenTimeTimer) { event in - self.lastSeenTime = Globals().getTimeString(for: vehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) - } - Spacer() - } - } - } - - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - if (vehicle != nil) { - vehicleDetailsHeader - .padding() - Divider() - vehicleDetailsScreen - .padding() - } else if (Appstate.shared.vehicles == .loading) { - loadingScreen - .padding() - } else { - errorScreen - .padding() - } - } - - } - -} - - - - - - - - - - - - - - - - - - - - - - - - - - - struct VehicleDetailsView: View { - @EnvironmentObject var vehiclesController: VehiclesController - - let vehicle: VehicleSummary - - @State private var viewSize = CGSize() - - var body: some View { - VStack(alignment: .leading) { - VehicleDetailsView2( - busNumber: vehicle.busNumber, - routeNumber: vehicle.routeNumber, - lastGpsTime: vehicle.lastGpsTime - ) - .padding(.bottom, 20) - Disclaimer() - .padding(.horizontal) - .padding(.bottom, 10) - } - .readSize { size in - viewSize = size - } - .presentationDetents([.height(viewSize.height), .large]) - - } - -} - - - - - - -struct VehicleDetailsView2: View { - + @EnvironmentObject var appstate: Appstate @EnvironmentObject var vehiclesController: VehiclesController let refreshTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() @@ -405,16 +50,7 @@ struct VehicleDetailsView2: View { var vehicleDetailsHeader: some View { HStack(spacing: 15) { - HStack { - RouteBadgePill(routeNumber: routeNumber) - Text("to") - .font(.footnote) - .foregroundColor(Color(.tertiaryLabel)) - Text(vehicleDetails!.lastStopOnVoyageName) - .font(.body) - .fontWeight(.medium) - .foregroundColor(Color(.label)) - } + VehicleDestination(routeNumber: routeNumber, destination: vehicleDetails!.lastStopOnVoyageName) Spacer() VehicleIdentifier(busNumber: busNumber, vehiclePlate: vehicleDetails!.vehiclePlate) } @@ -431,10 +67,10 @@ struct VehicleDetailsView2: View { .font(.system(size: 12, weight: .bold, design: .default)) .foregroundColor(Color(.secondaryLabel)) .onAppear() { - self.lastSeenTime = Globals().getTimeString(for: lastGpsTime, in: .past, style: .full, units: [.hour, .minute, .second]) + self.lastSeenTime = Helpers.getTimeString(for: lastGpsTime, in: .past, style: .full, units: [.hour, .minute, .second]) } .onReceive(lastSeenTimeTimer) { event in - self.lastSeenTime = Globals().getTimeString(for: lastGpsTime, in: .past, style: .full, units: [.hour, .minute, .second]) + self.lastSeenTime = Helpers.getTimeString(for: lastGpsTime, in: .past, style: .full, units: [.hour, .minute, .second]) } Spacer() } @@ -450,7 +86,7 @@ struct VehicleDetailsView2: View { Divider() vehicleDetailsScreen .padding() - } else if (Appstate.shared.vehicles == .loading) { + } else if (appstate.vehicles == .loading) { loadingScreen .padding() } else { diff --git a/GeoBus/App/Extensions/Globals.swift b/GeoBus/App/Extensions/Globals.swift deleted file mode 100644 index 73cf5885..00000000 --- a/GeoBus/App/Extensions/Globals.swift +++ /dev/null @@ -1,166 +0,0 @@ -// -// Helpers.swift -// GeoBus -// -// Created by João de Vasconcelos on 11/09/2022. -// - -import Foundation -import SwiftUI - - - -//open class ExampleClass { -// -// public static let variable: ExampleClass = .init() -// -// public enum ExampleEnum { -// case primary, secondary, tertiary -// } -// -// public func set(value example: ExampleEnum) -> ExampleEnum { -// -// } -// -//} - - -open class Globals { - - - public static let variable: Globals = .init() - - /* MARK: - Get Route Kind */ - - // Discover the Route kind by analysing the route number. - - func getKind(by routeNumber: String) -> Kind { - - if (routeNumber.suffix(1) == "B") { - // Neighborhood buses end with "B" - return .neighborhood - - } else if (routeNumber.suffix(1) == "E") { - // Trams and Elevators end with "E" - if (routeNumber.prefix(1) == "5") { - // and Elevators start with "5" - return .elevator - } else { - // All other options starting with "E" are trams - return .tram - } - - } else if (routeNumber.prefix(1) == "2") { - // Night service starts with "2" - return .night - - } else { - // All other options are regular service - return .regular - - } - - } - - - /* MARK: - Get Theme Colors */ - - // Centralized functions that retrieve theme colors. - - func getBackgroundColor(for routeNumber: String) -> Color { - let routeKind = getKind(by: routeNumber) - switch routeKind { - case .tram: - return Color(red: 1.00, green: 0.85, blue: 0.00) - case .neighborhood: - return Color(red: 1.00, green: 0.55, blue: 0.40) - case .night: - return Color(red: 0.12, green: 0.35, blue: 0.70) - case .elevator: - return Color(red: 0.00, green: 0.60, blue: 0.40) - case .regular: - return Color(red: 1.00, green: 0.75, blue: 0.00) - } - } - - func getForegroundColor(for routeNumber: String) -> Color { - let routeKind = getKind(by: routeNumber) - switch routeKind { - case .tram: - return Color(.black) - case .neighborhood: - return Color(.white) - case .night: - return Color(.white) - case .elevator: - return Color(.white) - case .regular: - return Color(.black) - } - } - - - - /* MARK: - Get Time Interval */ - - // Transform an ISO Timestamp String into relative date components. - - enum TimeRelativeToNow { - case past - case future - } - - - func getTimeString(for isoDateString: String, in timeRelation: TimeRelativeToNow, style: DateComponentsFormatter.UnitsStyle, units: NSCalendar.Unit) -> String { - - // Setup Date Formatter - let dateFormatter = DateFormatter() - dateFormatter.locale = Locale(identifier: "en_US_POSIX") - dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - // Parse ISO Timestamp using the Date Formatter - let now = Date() - let dateObj = dateFormatter.date(from: isoDateString) ?? now - let seconds = now.timeIntervalSince(dateObj) // in seconds - - // Setup Date Components Formatter - let dateComponentsFormatter = DateComponentsFormatter() - dateComponentsFormatter.unitsStyle = style - dateComponentsFormatter.allowedUnits = units - dateComponentsFormatter.includesApproximationPhrase = false - dateComponentsFormatter.includesTimeRemainingPhrase = false - dateComponentsFormatter.allowsFractionalUnits = false - - // Use the configured Date Components Formatter to generate the string. - switch timeRelation { - case .past: - return dateComponentsFormatter.string(from: seconds) ?? "?" - case .future: - return dateComponentsFormatter.string(from: -seconds) ?? "?" - } - - } - - - func getLastSeenTime(since lastGpsTime: String) -> Int { - - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - - let now = Date() - let estimation = formatter.date(from: lastGpsTime) ?? now - - let seconds = now.timeIntervalSince(estimation) - - return Int(seconds) - - } - - - func getSecondsFromISO8601DateString(_ dateString: String) -> Int { - let formattedDateObject = ISO8601DateFormatter().date(from: dateString) - return Int(formattedDateObject?.timeIntervalSinceNow ?? -1) - } - -} diff --git a/GeoBus/App/Extensions/Helpers.swift b/GeoBus/App/Extensions/Helpers.swift index c368ff17..e06b4012 100644 --- a/GeoBus/App/Extensions/Helpers.swift +++ b/GeoBus/App/Extensions/Helpers.swift @@ -11,15 +11,12 @@ import SwiftUI open class Helpers { - - - public static let variable: Helpers = .init() /* MARK: - Get Route Kind */ // Discover the Route kind by analysing the route number. - func getKind(by routeNumber: String) -> Kind { + static func getKind(by routeNumber: String) -> Kind { if (routeNumber.suffix(1) == "B") { // Neighborhood buses end with "B" @@ -52,7 +49,7 @@ open class Helpers { // Centralized functions that retrieve theme colors. - func getBackgroundColor(for routeNumber: String) -> Color { + static func getBackgroundColor(for routeNumber: String) -> Color { let routeKind = getKind(by: routeNumber) switch routeKind { case .tram: @@ -68,7 +65,7 @@ open class Helpers { } } - func getForegroundColor(for routeNumber: String) -> Color { + static func getForegroundColor(for routeNumber: String) -> Color { let routeKind = getKind(by: routeNumber) switch routeKind { case .tram: @@ -96,7 +93,7 @@ open class Helpers { } - func getTimeString(for isoDateString: String, in timeRelation: TimeRelativeToNow, style: DateComponentsFormatter.UnitsStyle, units: NSCalendar.Unit) -> String { + static func getTimeString(for isoDateString: String, in timeRelation: TimeRelativeToNow, style: DateComponentsFormatter.UnitsStyle, units: NSCalendar.Unit) -> String { // Setup Date Formatter let dateFormatter = DateFormatter() @@ -127,7 +124,7 @@ open class Helpers { } - func getLastSeenTime(since lastGpsTime: String) -> Int { + static func getLastSeenTime(since lastGpsTime: String) -> Int { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") @@ -143,7 +140,7 @@ open class Helpers { } - func getSecondsFromISO8601DateString(_ dateString: String) -> Int { + static func getSecondsFromISO8601DateString(_ dateString: String) -> Int { let formattedDateObject = ISO8601DateFormatter().date(from: dateString) return Int(formattedDateObject?.timeIntervalSinceNow ?? -1) } diff --git a/GeoBus/App/Layout/RouteBadgePill.swift b/GeoBus/App/Layout/RouteBadgePill.swift index 3568e9ea..07214274 100644 --- a/GeoBus/App/Layout/RouteBadgePill.swift +++ b/GeoBus/App/Layout/RouteBadgePill.swift @@ -19,11 +19,11 @@ struct RouteBadgePill: View { .font(.footnote) .fontWeight(.heavy) .lineLimit(1) - .foregroundColor(Globals().getForegroundColor(for: routeNumber)) + .foregroundColor(Helpers.getForegroundColor(for: routeNumber)) .padding(.horizontal, 7) .padding(.vertical, 2) } - .background(Globals().getBackgroundColor(for: routeNumber)) + .background(Helpers.getBackgroundColor(for: routeNumber)) .cornerRadius(10) } diff --git a/GeoBus/App/Layout/RouteBadgeSquare.swift b/GeoBus/App/Layout/RouteBadgeSquare.swift index 937cf980..ce1789aa 100644 --- a/GeoBus/App/Layout/RouteBadgeSquare.swift +++ b/GeoBus/App/Layout/RouteBadgeSquare.swift @@ -16,10 +16,10 @@ struct RouteBadgeSquare: View { ZStack { RoundedRectangle(cornerRadius: 10) - .fill(Globals().getBackgroundColor(for: routeNumber)) + .fill(Helpers.getBackgroundColor(for: routeNumber)) Text(routeNumber) .font(Font.system(size: 22, weight: .heavy, design: .default)) - .foregroundColor(Globals().getForegroundColor(for: routeNumber)) + .foregroundColor(Helpers.getForegroundColor(for: routeNumber)) } .aspectRatio(1, contentMode: .fit) diff --git a/GeoBus/App/Layout/TimeLeft.swift b/GeoBus/App/Layout/TimeLeft.swift index eb419c84..2b839b5d 100644 --- a/GeoBus/App/Layout/TimeLeft.swift +++ b/GeoBus/App/Layout/TimeLeft.swift @@ -31,10 +31,10 @@ struct TimeLeft: View { .fontWeight(.medium) .foregroundColor(Color(.label)) .onAppear() { - self.timeLeftString = Globals().getTimeString(for: self.time ?? "", in: .future, style: .short, units: [.hour, .minute]) + self.timeLeftString = Helpers.getTimeString(for: self.time ?? "", in: .future, style: .short, units: [.hour, .minute]) } .onReceive(countdownTimer) { event in - self.timeLeftString = Globals().getTimeString(for: self.time ?? "", in: .future, style: .short, units: [.hour, .minute]) + self.timeLeftString = Helpers.getTimeString(for: self.time ?? "", in: .future, style: .short, units: [.hour, .minute]) } } } diff --git a/GeoBus/App/State/Appstate.swift b/GeoBus/App/State/Appstate.swift index 1a1d8965..8fc70210 100644 --- a/GeoBus/App/State/Appstate.swift +++ b/GeoBus/App/State/Appstate.swift @@ -31,8 +31,8 @@ class Appstate: ObservableObject { /* * */ /* MARK: - SECTION 2: MODULES */ - /* These are the modules than publish state change events. This allows the UI to provide local */ - /* loading or error messages on the relevant functionality, incresing perception of stability. */ + /* These are the modules that publish state change events. This allows the UI to provide local */ + /* loading or error messages on the relevant functionality, increasing perception of stability. */ enum Module { case auth @@ -96,7 +96,6 @@ class Appstate: ObservableObject { /* After the change, follow the set rules to also update the .global state. This might change in the future. */ func change(to newState: State, for module: Module) { - print("GB5: Module: \(module), state: \(newState)") DispatchQueue.main.async { // Change state of affected module switch module { diff --git a/GeoBus/App/State/CarrisAuthentication.swift b/GeoBus/App/State/CarrisAuthentication.swift index c1f4a743..e507ac11 100644 --- a/GeoBus/App/State/CarrisAuthentication.swift +++ b/GeoBus/App/State/CarrisAuthentication.swift @@ -1,5 +1,5 @@ // -// Authentication.swift +// CarrisAuthentication.swift // GeoBus // // Created by João on 20/04/2020. diff --git a/GeoBus/App/State/MapController.swift b/GeoBus/App/State/MapController.swift index b611f25c..e0fa7252 100644 --- a/GeoBus/App/State/MapController.swift +++ b/GeoBus/App/State/MapController.swift @@ -138,7 +138,7 @@ class MapController: ObservableObject { // ..... - func updateAnnotations(with vehiclesList: [Vehicle], for routeNumber: String?) { + func updateAnnotations(with vehiclesList: [VehicleSummary], for routeNumber: String?) { if (routeNumber != nil) { @@ -151,7 +151,7 @@ class MapController: ObservableObject { // CONDITION 2: // Vehicle was last seen no longer than 3 minutes - let isNotZombieVehicle = Globals().getLastSeenTime(since: vehicle.lastGpsTime ?? "") < 180 + let isNotZombieVehicle = Helpers.getLastSeenTime(since: vehicle.lastGpsTime) < 180 // Find index of Annotation matching this vehicle busNumber @@ -164,16 +164,16 @@ class MapController: ObservableObject { if (indexOfVisibleAnnotation != nil) { // If annotation already exists, update it's values withAnimation(.easeIn(duration: 0.5)) { - self.visibleAnnotations[indexOfVisibleAnnotation!].location.latitude = vehicle.lat ?? 0 - self.visibleAnnotations[indexOfVisibleAnnotation!].location.longitude = vehicle.lng ?? 0 + self.visibleAnnotations[indexOfVisibleAnnotation!].location.latitude = vehicle.lat + self.visibleAnnotations[indexOfVisibleAnnotation!].location.longitude = vehicle.lng self.visibleAnnotations[indexOfVisibleAnnotation!].vehicle = vehicle } } else { // If annotation does not already exist, create a new one visibleAnnotations.append( GenericMapAnnotation( - lat: vehicle.lat ?? 0, - lng: vehicle.lng ?? 0, + lat: vehicle.lat, + lng: vehicle.lng, format: .vehicle, busNumber: vehicle.busNumber, vehicle: vehicle diff --git a/GeoBus/App/State/RoutesController.swift b/GeoBus/App/State/RoutesController.swift index 01a2410b..fab4a584 100644 --- a/GeoBus/App/State/RoutesController.swift +++ b/GeoBus/App/State/RoutesController.swift @@ -307,7 +307,7 @@ class RoutesController: ObservableObject { let formattedRoute = Route( number: decodedAPIRouteDetail.routeNumber ?? "-", name: decodedAPIRouteDetail.name ?? "-", - kind: Globals().getKind(by: decodedAPIRouteDetail.routeNumber ?? "-"), + kind: Helpers.getKind(by: decodedAPIRouteDetail.routeNumber ?? "-"), variants: formattedRouteVariants ) diff --git a/GeoBus/App/State/VehiclesController.swift b/GeoBus/App/State/VehiclesController.swift index 1b6e3d1a..c0d500b4 100644 --- a/GeoBus/App/State/VehiclesController.swift +++ b/GeoBus/App/State/VehiclesController.swift @@ -16,7 +16,6 @@ class VehiclesController: ObservableObject { private var routeNumber: String? - @Published var allVehicles: [Vehicle] = [] @Published var vehicles: [VehicleSummary] = [] @@ -66,11 +65,8 @@ class VehiclesController: ObservableObject { // Check status of response if (responseAPIVehiclesList?.statusCode == 401) { - Task { - await CarrisAuthentication.shared.authenticate() - await self.fetchVehiclesFromCarrisAPI() - } - return + await CarrisAuthentication.shared.authenticate() + await self.fetchVehiclesFromCarrisAPI() } else if (responseAPIVehiclesList?.statusCode != 200) { print(responseAPIVehiclesList as Any) throw Appstate.ModuleError.carris_unavailable @@ -88,7 +84,7 @@ class VehiclesController: ObservableObject { // Discard vehicles with outdated location, // here decided to be 180 seconds (3 minutes). - if (Globals().getLastSeenTime(since: vehicleSummary.lastGpsTime ?? "") < 180) { + if (Helpers.getLastSeenTime(since: vehicleSummary.lastGpsTime ?? "") < 180) { // Format and append each vehicle // to the temporary variable. @@ -97,7 +93,7 @@ class VehiclesController: ObservableObject { busNumber: vehicleSummary.busNumber ?? -1, state: vehicleSummary.state ?? "", routeNumber: vehicleSummary.routeNumber ?? "-", - kind: Globals().getKind(by: vehicleSummary.routeNumber ?? "-"), + kind: Helpers.getKind(by: vehicleSummary.routeNumber ?? "-"), lat: vehicleSummary.lat ?? 0, lng: vehicleSummary.lng ?? 0, previousLatitude: vehicleSummary.previousLatitude ?? 0, @@ -157,11 +153,8 @@ class VehiclesController: ObservableObject { // Check status of response if (responseAPIVehicleDetail?.statusCode == 401) { - Task { - await CarrisAuthentication.shared.authenticate() - return await self.fetchVehicleDetailsFromCarrisAPI(for: busNumber) - } - return nil + await CarrisAuthentication.shared.authenticate() + return await self.fetchVehicleDetailsFromCarrisAPI(for: busNumber) } else if (responseAPIVehicleDetail?.statusCode != 200) { print(responseAPIVehicleDetail as Any) throw Appstate.ModuleError.carris_unavailable @@ -213,334 +206,4 @@ class VehiclesController: ObservableObject { } - - /* MARK: - FETCH VEHICLE FROM COMMUNITY API */ - - // This function calls the GeoBus API and receives vehicle metadata, - // including positions, for the set route number, while storing them - // in the vehicles array. It also formats VehicleAnnotations and stores - // them in the annotations array. It must have @objc flag because Timer - // is written in Objective-C. - - func fetchVehicleFromCommunityAPI(for busNumber: Int) async -> VehicleDetails? { - - Appstate.shared.change(to: .loading, for: .vehicles) - - do { - - // Request Vehicle Detail (SGO) - var requestAPIVehicleDetail = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/SGO/busNumber/\(busNumber)")!) - requestAPIVehicleDetail.addValue("application/json", forHTTPHeaderField: "Content-Type") - requestAPIVehicleDetail.addValue("application/json", forHTTPHeaderField: "Accept") - requestAPIVehicleDetail.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") - let (rawDataAPIVehicleDetail, rawResponseAPIVehicleDetail) = try await URLSession.shared.data(for: requestAPIVehicleDetail) - let responseAPIVehicleDetail = rawResponseAPIVehicleDetail as? HTTPURLResponse - - // Check status of response - if (responseAPIVehicleDetail?.statusCode == 401) { - Task { - await CarrisAuthentication.shared.authenticate() - return await self.fetchVehicleDetailsFromCarrisAPI(for: busNumber) - } - return nil - } else if (responseAPIVehicleDetail?.statusCode != 200) { - print(responseAPIVehicleDetail as Any) - throw Appstate.ModuleError.carris_unavailable - } - - let decodedAPIVehicleDetail = try JSONDecoder().decode(CarrisAPIVehicleDetail.self, from: rawDataAPIVehicleDetail) - - // Format and append each vehicle - // to the temporary variable. - let result = VehicleDetails( - busNumber: busNumber, - vehiclePlate: decodedAPIVehicleDetail.vehiclePlate ?? "", - lastStopOnVoyageName: decodedAPIVehicleDetail.lastStopOnVoyageName ?? "-" - ) - - Appstate.shared.change(to: .idle, for: .vehicles) - - return result - - } catch { - Appstate.shared.change(to: .error, for: .vehicles) - print("ERROR IN VEHICLE DETAILS: \(error)") - return nil - } - - } - - - - - // - // - // - // - // N E W V E R S I O N - // - // - // - // - - - - - - /* MARK: - UPDATE VEHICLES */ - - // This function decides whether to update available routes - - enum VehicleUpdateScope { - case summary - case detail - case community - } - - func update(scope: VehicleUpdateScope, for busNumber: Int? = nil) { - - switch scope { - - case .summary: - Task { - await fetchVehiclesListFromCarrisAPI_NEW() - } - - case .detail: - if (busNumber != nil) { - Task { - await fetchVehicleDetailsFromCarrisAPI_NEW(for: busNumber!) - } - } - - case .community: - if (busNumber != nil) { - Task { - await fetchVehicleFromCommunityAPI_NEW(for: busNumber!) - } - } - - } - - } - - - - /* MARK: - FETCH VEHICLES SUMMARY FROM CARRIS API */ - - // This function calls the GeoBus API and receives vehicle metadata, - // including positions, for the set route number, while storing them - // in the vehicles array. It also formats VehicleAnnotations and stores - // them in the annotations array. It must have @objc flag because Timer - // is written in Objective-C. - - func fetchVehiclesListFromCarrisAPI_NEW() async { - - Appstate.shared.change(to: .loading, for: .vehicles) - - do { - // Request all Vehicles from API - var requestCarrisAPIVehiclesList = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/vehicleStatuses")!) // /routeNumber/\(routeNumber!) - requestCarrisAPIVehiclesList.addValue("application/json", forHTTPHeaderField: "Content-Type") - requestCarrisAPIVehiclesList.addValue("application/json", forHTTPHeaderField: "Accept") - requestCarrisAPIVehiclesList.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") - let (rawDataCarrisAPIVehiclesList, rawResponseCarrisAPIVehiclesList) = try await URLSession.shared.data(for: requestCarrisAPIVehiclesList) - let responseCarrisAPIVehiclesList = rawResponseCarrisAPIVehiclesList as? HTTPURLResponse - - // Check status of response - if (responseCarrisAPIVehiclesList?.statusCode == 401) { - await CarrisAuthentication.shared.authenticate() - await self.fetchVehiclesListFromCarrisAPI_NEW() - return - } else if (responseCarrisAPIVehiclesList?.statusCode != 200) { - print(responseCarrisAPIVehiclesList as Any) - throw Appstate.ModuleError.carris_unavailable - } - - let decodedCarrisAPIVehiclesList = try JSONDecoder().decode([CarrisAPIVehicleSummary].self, from: rawDataCarrisAPIVehiclesList) - - - for vehicleSummary in decodedCarrisAPIVehiclesList { - - let indexOfVehicleInArray = self.allVehicles.firstIndex(where: { - $0.id == vehicleSummary.busNumber - }) - - if (indexOfVehicleInArray != nil) { - allVehicles[indexOfVehicleInArray!].routeNumber = vehicleSummary.routeNumber ?? "-" - allVehicles[indexOfVehicleInArray!].kind = Globals().getKind(by: vehicleSummary.routeNumber ?? "-") - allVehicles[indexOfVehicleInArray!].lat = vehicleSummary.lat ?? 0 - allVehicles[indexOfVehicleInArray!].lng = vehicleSummary.lng ?? 0 - allVehicles[indexOfVehicleInArray!].previousLatitude = vehicleSummary.previousLatitude ?? 0 - allVehicles[indexOfVehicleInArray!].previousLongitude = vehicleSummary.previousLongitude ?? 0 - allVehicles[indexOfVehicleInArray!].lastGpsTime = vehicleSummary.lastGpsTime ?? "" - allVehicles[indexOfVehicleInArray!].angleInRadians = self.getAngleInRadians( - prevLat: vehicleSummary.previousLatitude ?? 0, - prevLng: vehicleSummary.previousLongitude ?? 0, - currLat: vehicleSummary.lat ?? 0, - currLng: vehicleSummary.lng ?? 0 - ) - } else { - self.allVehicles.append( - Vehicle( - busNumber: vehicleSummary.busNumber ?? 0, - routeNumber: vehicleSummary.routeNumber ?? "-", - kind: Globals().getKind(by: vehicleSummary.routeNumber ?? "-"), - lat: vehicleSummary.lat ?? 0, - lng: vehicleSummary.lng ?? 0, - previousLatitude: vehicleSummary.previousLatitude ?? 0, - previousLongitude: vehicleSummary.previousLongitude ?? 0, - lastGpsTime: vehicleSummary.lastGpsTime ?? "", - angleInRadians: self.getAngleInRadians( - prevLat: vehicleSummary.previousLatitude ?? 0, - prevLng: vehicleSummary.previousLongitude ?? 0, - currLat: vehicleSummary.lat ?? 0, - currLng: vehicleSummary.lng ?? 0 - ) - ) - ) - } - - } - - Appstate.shared.change(to: .idle, for: .vehicles) - - } catch { - Appstate.shared.change(to: .error, for: .vehicles) - print("ERROR IN VEHICLES: \(error)") - return - } - - } - - - - - - - - - /* MARK: - FETCH VEHICLE DETAILS FROM CARRIS API */ - - // This function calls the GeoBus API and receives vehicle metadata, - // including positions, for the set route number, while storing them - // in the vehicles array. It also formats VehicleAnnotations and stores - // them in the annotations array. It must have @objc flag because Timer - // is written in Objective-C. - - func fetchVehicleDetailsFromCarrisAPI_NEW(for busNumber: Int) async { - - // 1. Check if Vehicle exists in array - guard let indexOfVehicleInArray = allVehicles.firstIndex(where: { $0.id == busNumber }) else { - return - } - - Appstate.shared.change(to: .loading, for: .vehicles) - - do { - - // Request Vehicle Detail (SGO) - var requestCarrisAPIVehicleDetail = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/SGO/busNumber/\(busNumber)")!) - requestCarrisAPIVehicleDetail.addValue("application/json", forHTTPHeaderField: "Content-Type") - requestCarrisAPIVehicleDetail.addValue("application/json", forHTTPHeaderField: "Accept") - requestCarrisAPIVehicleDetail.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") - let (rawDataCarrisAPIVehicleDetail, rawResponseCarrisAPIVehicleDetail) = try await URLSession.shared.data(for: requestCarrisAPIVehicleDetail) - let responseCarrisAPIVehicleDetail = rawResponseCarrisAPIVehicleDetail as? HTTPURLResponse - - // Check status of response - if (responseCarrisAPIVehicleDetail?.statusCode == 401) { - await CarrisAuthentication.shared.authenticate() - await self.fetchVehicleDetailsFromCarrisAPI_NEW(for: busNumber) - return - } else if (responseCarrisAPIVehicleDetail?.statusCode != 200) { - print(responseCarrisAPIVehicleDetail as Any) - throw Appstate.ModuleError.carris_unavailable - } - - let decodedCarrisAPIVehicleDetail = try JSONDecoder().decode(CarrisAPIVehicleDetail.self, from: rawDataCarrisAPIVehicleDetail) - - // Update details of Vehicle - allVehicles[indexOfVehicleInArray].vehiclePlate = decodedCarrisAPIVehicleDetail.vehiclePlate ?? "-" - allVehicles[indexOfVehicleInArray].lastStopOnVoyageName = decodedCarrisAPIVehicleDetail.lastStopOnVoyageName ?? "-" - - Appstate.shared.change(to: .idle, for: .vehicles) - - } catch { - Appstate.shared.change(to: .error, for: .vehicles) - print("ERROR IN VEHICLE DETAILS: \(error)") - return - } - - } - - - - /* MARK: - FETCH VEHICLE FROM COMMUNITY API */ - - // This function calls the GeoBus API and receives vehicle metadata, - // including positions, for the set route number, while storing them - // in the vehicles array. It also formats VehicleAnnotations and stores - // them in the annotations array. It must have @objc flag because Timer - // is written in Objective-C. - - func fetchVehicleFromCommunityAPI_NEW(for busNumber: Int) async { - - // 1. Check if Vehicle exists in array - guard let indexOfVehicleInArray = allVehicles.firstIndex(where: { $0.id == busNumber }) else { - return - } - - Appstate.shared.change(to: .loading, for: .vehicles) - - do { - - // Request Vehicle Detail (SGO) - var requestCommunityAPIVehicle = URLRequest(url: URL(string: "https://api.carril.workers.dev/estbus?busNumber=\(busNumber)")!) - requestCommunityAPIVehicle.addValue("application/json", forHTTPHeaderField: "Content-Type") - requestCommunityAPIVehicle.addValue("application/json", forHTTPHeaderField: "Accept") - requestCommunityAPIVehicle.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") - let (rawDataCommunityAPIVehicle, rawResponseCommunityAPIVehicle) = try await URLSession.shared.data(for: requestCommunityAPIVehicle) - let responseCommunityAPIVehicle = rawResponseCommunityAPIVehicle as? HTTPURLResponse - - // Check status of response - if (responseCommunityAPIVehicle?.statusCode != 200) { - print(responseCommunityAPIVehicle as Any) - throw Appstate.ModuleError.community_unavailable - } - - let decodedCommunityAPIVehicle = try JSONDecoder().decode([CommunityAPIVehicle].self, from: rawDataCommunityAPIVehicle) - - // Update details of Vehicle - allVehicles[indexOfVehicleInArray].estimatedTimeofArrivalCorrected = decodedCommunityAPIVehicle[0].estimatedTimeofArrivalCorrected - - Appstate.shared.change(to: .idle, for: .vehicles) - - } catch { - Appstate.shared.change(to: .error, for: .vehicles) - print("GB: ERROR IN 'fetchVehicleFromCommunityAPI_NEW': \(error)") - return - } - - } - - - - - - func getVehicle(by busNumber: Int) -> Vehicle? { - let indexInArray = self.allVehicles.firstIndex(where: { - $0.busNumber == busNumber - }) - - if (indexInArray != nil) { - return allVehicles[indexInArray!] - } else { - return nil - } - } - - - - - } diff --git a/GeoBus/de.lproj/Localizable.strings b/GeoBus/de.lproj/Localizable.strings index f6470b3f..409b771f 100644 --- a/GeoBus/de.lproj/Localizable.strings +++ b/GeoBus/de.lproj/Localizable.strings @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Wenn der Fehler weiterhin besteht, versuchen sie es später erneut. Leider funktioniert die App ohne diesen ersten Schritt nicht."; -/* No comment provided by engineer. */ -"in ±" = "in ca."; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Wahrscheinlich, weil deren Server nicht erreichbar sind. In diesem Fall ist es das Beste, wenn sie es zu einem anderen Zeitpunkt erneut versuchen. :/"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Deine Meinung zählt"; -/* No comment provided by engineer. */ -"P" = "P"; - /* No comment provided by engineer. */ "Please try again." = "Bitte versuchen sie es erneut."; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "Das wird nur einmal geschehen."; -/* No comment provided by engineer. */ -"to" = "nach"; - /* No comment provided by engineer. */ "to: %@" = "nach: %@"; diff --git a/GeoBus/en.lproj/Localizable.strings b/GeoBus/en.lproj/Localizable.strings index b250fbc5..f89111b0 100644 --- a/GeoBus/en.lproj/Localizable.strings +++ b/GeoBus/en.lproj/Localizable.strings @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step."; -/* No comment provided by engineer. */ -"in ±" = "in ±"; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "It's probably because their servers are down. In this case, the best option is to wait and try again later :/"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Open to Feedback"; -/* No comment provided by engineer. */ -"P" = "P"; - /* No comment provided by engineer. */ "Please try again." = "Please try again."; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "This will only happen once."; -/* No comment provided by engineer. */ -"to" = "to"; - /* No comment provided by engineer. */ "to: %@" = "to: %@"; diff --git a/GeoBus/fa.lproj/Localizable.strings b/GeoBus/fa.lproj/Localizable.strings index 6113940e..f449f97a 100644 --- a/GeoBus/fa.lproj/Localizable.strings +++ b/GeoBus/fa.lproj/Localizable.strings @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "اگر خطا ادامه داشت، تنها گزینه این است که صبر کنید و بعداً دوباره امتحان کنید. متأسفانه برنامه بدون این مرحله اول نمی تواند کار کند."; -/* No comment provided by engineer. */ -"in ±" = "در ±"; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "احتمالاً به این دلیل است که سرورهای آنها از کار افتاده است. در این مورد، بهترین گزینه این است که صبر کنید و بعداً دوباره امتحان کنید"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "برای بازخورد باز کنید"; -/* No comment provided by engineer. */ -"P" = "پ"; - /* No comment provided by engineer. */ "Please try again." = "لطفا دوباره امتحان کنید."; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "این فقط یک بار اتفاق می افتد."; -/* No comment provided by engineer. */ -"to" = "به"; - /* No comment provided by engineer. */ "to: %@" = "به: %@"; diff --git a/GeoBus/fr.lproj/Localizable.strings b/GeoBus/fr.lproj/Localizable.strings index 9ce82ff5..5754d647 100644 --- a/GeoBus/fr.lproj/Localizable.strings +++ b/GeoBus/fr.lproj/Localizable.strings @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Si l'erreur persiste, la seule option est d'attendre et de réessayer plus tard. Malheureusement, l'application ne peut pas fonctionner sans cette première étape."; -/* No comment provided by engineer. */ -"in ±" = "en ±"; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "C'est probablement parce que leurs serveurs sont en panne. Dans ce cas, la meilleure option est d'attendre et de réessayer plus tard :/"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Ouvert à la rétroaction"; -/* No comment provided by engineer. */ -"P" = "P"; - /* No comment provided by engineer. */ "Please try again." = "Veuillez réessayer."; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "Cela ne se fera qu'une seule fois."; -/* No comment provided by engineer. */ -"to" = "à"; - /* No comment provided by engineer. */ "to: %@" = "à: %@"; diff --git a/GeoBus/it.lproj/Localizable.strings b/GeoBus/it.lproj/Localizable.strings index 97775d71..0aa549e8 100644 --- a/GeoBus/it.lproj/Localizable.strings +++ b/GeoBus/it.lproj/Localizable.strings @@ -59,7 +59,7 @@ "Favorites" = "Preferiti"; /* No comment provided by engineer. */ -"Find Routes" = "Trova percorsi"; +"Find Routes" = "Trova Itinerari"; /* No comment provided by engineer. */ "GeoBus can show you where you are in the map, if you allow it." = "GeoBus può mostrarti dove ti trovi nella mappa, se lo consenti."; @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Se l'errore persiste, l'unica opzione è attendere e riprovare più tardi. Sfortunatamente l'app non può funzionare senza questo primo passo."; -/* No comment provided by engineer. */ -"in ±" = "in ±"; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Probabilmente perché i loro server sono giù. In questo caso, l'opzione migliore è aspettare e riprovare più tardi :/"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Apri a Feedback"; -/* No comment provided by engineer. */ -"P" = "P"; - /* No comment provided by engineer. */ "Please try again." = "Per favore riprova."; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "Questo accadrà solo una volta."; -/* No comment provided by engineer. */ -"to" = "a"; - /* No comment provided by engineer. */ "to: %@" = "a: %@"; diff --git a/GeoBus/nl.lproj/Localizable.strings b/GeoBus/nl.lproj/Localizable.strings index 46ab1194..d195c7aa 100644 --- a/GeoBus/nl.lproj/Localizable.strings +++ b/GeoBus/nl.lproj/Localizable.strings @@ -59,7 +59,7 @@ "Favorites" = "Favorieten"; /* No comment provided by engineer. */ -"Find Routes" = "Vind routes"; +"Find Routes" = "Zoek routes"; /* No comment provided by engineer. */ "GeoBus can show you where you are in the map, if you allow it." = "GeoBus kan je laten zien waar je bent in de kaart, als je het toestaat."; @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Als de fout zich blijft voordoen, is de enige optie wachten en het later opnieuw proberen. Helaas kan de app niet functioneren zonder deze eerste stap."; -/* No comment provided by engineer. */ -"in ±" = "in ±"; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Het is waarschijnlijk omdat hun servers offline zijn. In dit geval is de beste optie om te wachten en het later opnieuw te proberen :/"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Open voor feedback"; -/* No comment provided by engineer. */ -"P" = "P"; - /* No comment provided by engineer. */ "Please try again." = "Probeer het opnieuw."; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "Dit zal slechts één keer gebeuren."; -/* No comment provided by engineer. */ -"to" = "naar"; - /* No comment provided by engineer. */ "to: %@" = "Aan: %@"; diff --git a/GeoBus/pl.lproj/Localizable.strings b/GeoBus/pl.lproj/Localizable.strings index 44e1ec44..9e73ac6c 100644 --- a/GeoBus/pl.lproj/Localizable.strings +++ b/GeoBus/pl.lproj/Localizable.strings @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Jeżeli błąd się powtarza, jedyną opcją jest poczekać i spróbować ponownie później. Niestety aplikacja nie może działać bez tego pierwszego kroku."; -/* No comment provided by engineer. */ -"in ±" = "w ±"; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Prawdopodobnie jest tak, ponieważ ich serwery są wyłączone. W tym przypadku najlepszym rozwiązaniem jest zaczekać i spróbować ponownie później :/"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Otwarci na opinie"; -/* No comment provided by engineer. */ -"P" = "P"; - /* No comment provided by engineer. */ "Please try again." = "Spróbuj ponownie."; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "Będzie to miało miejsce tylko raz."; -/* No comment provided by engineer. */ -"to" = "do"; - /* No comment provided by engineer. */ "to: %@" = "do: %@"; diff --git a/GeoBus/pt.lproj/Localizable.strings b/GeoBus/pt.lproj/Localizable.strings index 15eb18e1..479237ab 100644 --- a/GeoBus/pt.lproj/Localizable.strings +++ b/GeoBus/pt.lproj/Localizable.strings @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Se o erro persistir, a única opção é esperar e tentar novamente mais tarde. Infelizmente a app não consegue funcionar sem este primeiro passo."; -/* No comment provided by engineer. */ -"in ±" = "em ±"; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Provavelmente os servidores estão indisponíveis. Neste caso, a melhor opção é esperar e tentar novamente mais tarde :/"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Sugestões?"; -/* No comment provided by engineer. */ -"P" = "P"; - /* No comment provided by engineer. */ "Please try again." = "Por favor tente novamente."; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "Isto só acontecerá uma vez."; -/* No comment provided by engineer. */ -"to" = "para"; - /* No comment provided by engineer. */ "to: %@" = "para: %@"; diff --git a/GeoBus/tr.lproj/Localizable.strings b/GeoBus/tr.lproj/Localizable.strings index b8cadd06..7c6fd038 100644 --- a/GeoBus/tr.lproj/Localizable.strings +++ b/GeoBus/tr.lproj/Localizable.strings @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Hata devam ederse, tek seçenek beklemek ve daha sonra tekrar denemektir. Maalesef uygulama bu ilk adım olmadan çalışamaz."; -/* No comment provided by engineer. */ -"in ±" = "içinde"; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Muhtemelen sunucuları kapalı olduğundandır. Bu durumda en iyi seçenek bekleyip daha sonra tekrar denemektir :/"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Geri Bildirime Açık"; -/* No comment provided by engineer. */ -"P" = "P"; - /* No comment provided by engineer. */ "Please try again." = "Lütfen tekrar deneyiniz."; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "Bu sadece bir kez olacak."; -/* No comment provided by engineer. */ -"to" = "➨"; - /* No comment provided by engineer. */ "to: %@" = "varış yeri: %@"; diff --git a/GeoBus/uk.lproj/Localizable.strings b/GeoBus/uk.lproj/Localizable.strings index 408501c2..6c123eef 100644 --- a/GeoBus/uk.lproj/Localizable.strings +++ b/GeoBus/uk.lproj/Localizable.strings @@ -59,7 +59,7 @@ "Favorites" = "Вподобання"; /* No comment provided by engineer. */ -"Find Routes" = "Знайдіть маршрути"; +"Find Routes" = "Знайти маршрути"; /* No comment provided by engineer. */ "GeoBus can show you where you are in the map, if you allow it." = "GeoBus може показати вам, де ви перебуваєте на мапі, якщо ви дозволяєте."; @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "Якщо помилка повторюється, зачекайте будь ласка спробуйте пізніше. Нажаль, програма не зможе працювати без першого кроку."; -/* No comment provided by engineer. */ -"in ±" = "в класифікованих"; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "Скоріш за все їх сервери відсутні. У такому випадку найкращий варіант - чекати та спробувати пізніше :/"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "Відкрити для зворотнього зв'язку"; -/* No comment provided by engineer. */ -"P" = "Пн"; - /* No comment provided by engineer. */ "Please try again." = "Спробуйте ще раз."; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "Це станеться лише один раз."; -/* No comment provided by engineer. */ -"to" = "по"; - /* No comment provided by engineer. */ "to: %@" = "до: %@"; diff --git a/GeoBus/zh-Hans.lproj/Localizable.strings b/GeoBus/zh-Hans.lproj/Localizable.strings index a6afc124..745048e4 100644 --- a/GeoBus/zh-Hans.lproj/Localizable.strings +++ b/GeoBus/zh-Hans.lproj/Localizable.strings @@ -82,9 +82,6 @@ /* No comment provided by engineer. */ "If the error persists, the only option is to wait and try again later. Unfortunately the app cannot function without this first step." = "如果错误仍然存在,唯一的选择是请稍后再尝试。不幸的是,如果没有第一步,该应用程序将无法运行。"; -/* No comment provided by engineer. */ -"in ±" = "在 ±"; - /* No comment provided by engineer. */ "It's probably because their servers are down. In this case, the best option is to wait and try again later :/" = "这可能是由于他们的服务器已经关闭。在这种情况下,最好的选择是等候,然后再试一次 :/"; @@ -118,9 +115,6 @@ /* No comment provided by engineer. */ "Open to Feedback" = "接受反馈"; -/* No comment provided by engineer. */ -"P" = "P"; - /* No comment provided by engineer. */ "Please try again." = "请重试。"; @@ -196,9 +190,6 @@ /* No comment provided by engineer. */ "This will only happen once." = "这只会发生一次。"; -/* No comment provided by engineer. */ -"to" = "到"; - /* No comment provided by engineer. */ "to: %@" = "到: %@"; From aecada5ee56fb3967690acc526f46a69f87c6091 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 15 Oct 2022 01:30:23 +0100 Subject: [PATCH 03/63] More merge --- GeoBus/App/Lottie/EstimatedIcon.swift | 31 --------- GeoBus/App/Lottie/LiveIcon.swift | 30 --------- GeoBus/App/Lottie/LoadingPulse.swift | 91 -------------------------- GeoBus/App/Lottie/LoadingSpinner.swift | 41 ------------ GeoBus/App/Lottie/LoadingView.swift | 29 -------- 5 files changed, 222 deletions(-) delete mode 100644 GeoBus/App/Lottie/EstimatedIcon.swift delete mode 100644 GeoBus/App/Lottie/LiveIcon.swift delete mode 100644 GeoBus/App/Lottie/LoadingPulse.swift delete mode 100644 GeoBus/App/Lottie/LoadingSpinner.swift delete mode 100644 GeoBus/App/Lottie/LoadingView.swift diff --git a/GeoBus/App/Lottie/EstimatedIcon.swift b/GeoBus/App/Lottie/EstimatedIcon.swift deleted file mode 100644 index d9df715e..00000000 --- a/GeoBus/App/Lottie/EstimatedIcon.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// LoadingView.swift -// GeoBus -// -// Created by João on 17/04/2020. -// Copyright © 2020 João de Vasconcelos. All rights reserved. -// - -import SwiftUI - -struct EstimatedIcon: View { - - @State var play: Bool = true - - var body: some View { - HStack { -// LottieView(name: "estimated-icon", loopMode: .loop, aspect: .scaleAspectFit, play: $play) -// .frame(width: 15, height: 15) -// .padding(.leading, -2) - LoadingPulse(color: .orange, size: 15) -// .frame(width: 15, height: 15) - .padding(.leading, -2) - Text("Estimated") - .font(Font.system(size: 11, weight: .medium, design: .default) ) - .foregroundColor(Color(.systemOrange)) - .padding(.leading, -5) - } - } -} - - diff --git a/GeoBus/App/Lottie/LiveIcon.swift b/GeoBus/App/Lottie/LiveIcon.swift deleted file mode 100644 index 6b37abac..00000000 --- a/GeoBus/App/Lottie/LiveIcon.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// LoadingView.swift -// GeoBus -// -// Created by João on 17/04/2020. -// Copyright © 2020 João de Vasconcelos. All rights reserved. -// - -import SwiftUI - -struct LiveIcon: View { - - @State var play: Bool = true - - var body: some View { - HStack { -// LottieView(name: "live-icon", loopMode: .loop, aspect: .scaleAspectFit, play: $play) -// .frame(width: 15, height: 15) -// .padding(.leading, -2) - LoadingPulse(color: .green, size: 15) - .padding(.leading, -2) - Text("Live") - .font(Font.system(size: 11, weight: .medium, design: .default) ) - .foregroundColor(Color(.systemGreen)) - .padding(.leading, -5) - } - } -} - - diff --git a/GeoBus/App/Lottie/LoadingPulse.swift b/GeoBus/App/Lottie/LoadingPulse.swift deleted file mode 100644 index 5396b926..00000000 --- a/GeoBus/App/Lottie/LoadingPulse.swift +++ /dev/null @@ -1,91 +0,0 @@ -// -// LoadingPulse.swift -// GeoBus -// -// Created by João de Vasconcelos on 14/10/2022. -// - -import SwiftUI - -struct LoadingPulse: View { - @State var isAnimating: Bool = false - let timing: Double - - let maxCounter: Int = 3 - - let frame: CGSize - let primaryColor: Color - - init(color: Color = .black, size: CGFloat = 50, speed: Double = 0.75) { - timing = speed * 4 - frame = CGSize(width: size, height: size) - primaryColor = color - } - - var body: some View { - ZStack { - - ForEach(0.. Date: Sat, 15 Oct 2022 01:34:32 +0100 Subject: [PATCH 04/63] More merge --- GeoBus/App/Components/Map/MapAnnotations.swift | 12 ++++-------- GeoBus/App/GeoBusApp.swift | 9 +++++++-- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index de092e23..3f51a963 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -34,10 +34,10 @@ struct GenericMapAnnotation: Identifiable { } // For Vehicles - var vehicle: Vehicle? + var vehicle: VehicleSummary? let busNumber: Int? - init(lat: Double, lng: Double, format: Format, busNumber: Int, vehicle: Vehicle) { + init(lat: Double, lng: Double, format: Format, busNumber: Int, vehicle: VehicleSummary) { self.location = CLLocationCoordinate2D(latitude: lat, longitude: lng) self.format = format self.stop = nil @@ -99,7 +99,7 @@ struct StopAnnotationView: View { struct VehicleAnnotationView: View { - let vehicle: Vehicle + let vehicle: VehicleSummary let isPresentedOnAppear: Bool @State private var isPresented: Bool = false @@ -123,16 +123,12 @@ struct VehicleAnnotationView: View { Image("RegularService-Active") case .regular: Image("RegularService-Active") - case .none: - Rectangle() - .background(Color.clear) } } } .frame(width: 40, height: 40, alignment: .center) - .rotationEffect(.radians(vehicle.angleInRadians ?? 0)) + .rotationEffect(.radians(vehicle.angleInRadians)) .sheet(isPresented: $isPresented) { - VehicleInfoSheet(busNumber: vehicle.busNumber) } } diff --git a/GeoBus/App/GeoBusApp.swift b/GeoBus/App/GeoBusApp.swift index f1632963..9c3a271f 100644 --- a/GeoBus/App/GeoBusApp.swift +++ b/GeoBus/App/GeoBusApp.swift @@ -13,14 +13,15 @@ struct GeoBusApp: App { /* MARK: - GEOBUS */ + @StateObject private var appstate = Appstate.shared + @StateObject private var mapController = MapController() + @StateObject private var stopsController = StopsController() @StateObject private var routesController = RoutesController() @StateObject private var vehiclesController = VehiclesController() @StateObject private var estimationsController = EstimationsController() - @StateObject private var mapController = MapController() @StateObject private var carrisNetworkController = CarrisNetworkController() - private let updateIntervalTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() var body: some Scene { @@ -37,6 +38,7 @@ struct GeoBusApp: App { .onAppear(perform: { // Update Carris network model // self.carrisNetworkController.start() + self.routesController.update() // Capture app open Analytics.shared.capture(event: .App_Session_Start) }) @@ -45,6 +47,9 @@ struct GeoBusApp: App { Analytics.shared.capture(event: .App_Session_Ping) // Update vehicles on timer call self.vehiclesController.update(scope: .summary) + Task { + await vehiclesController.fetchVehiclesFromCarrisAPI() + } } } } From 4432ef15434f88ff2b354ef770e567ffb76f2a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 15 Oct 2022 01:38:43 +0100 Subject: [PATCH 05/63] Update to Helpers. from Globals(). --- GeoBus/App/GeoBusApp.swift | 2 +- GeoBus/App/State/CarrisNetworkController.swift | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/GeoBus/App/GeoBusApp.swift b/GeoBus/App/GeoBusApp.swift index 9c3a271f..0d5015f2 100644 --- a/GeoBus/App/GeoBusApp.swift +++ b/GeoBus/App/GeoBusApp.swift @@ -46,7 +46,7 @@ struct GeoBusApp: App { // Capture session continuation Analytics.shared.capture(event: .App_Session_Ping) // Update vehicles on timer call - self.vehiclesController.update(scope: .summary) +// self.vehiclesController.update(scope: .summary) Task { await vehiclesController.fetchVehiclesFromCarrisAPI() } diff --git a/GeoBus/App/State/CarrisNetworkController.swift b/GeoBus/App/State/CarrisNetworkController.swift index 23777602..b97797cf 100644 --- a/GeoBus/App/State/CarrisNetworkController.swift +++ b/GeoBus/App/State/CarrisNetworkController.swift @@ -125,7 +125,7 @@ class CarrisNetworkController: ObservableObject { func start(withForcedUpdate forceUpdate: Bool = false) { // Conditions to update - let lastUpdateIsLongerThanInterval = Globals().getSecondsFromISO8601DateString(network_lastUpdated ?? "") > network_updateInterval + let lastUpdateIsLongerThanInterval = Helpers.getSecondsFromISO8601DateString(network_lastUpdated ?? "") > network_updateInterval let savedNetworkDataIsEmpty = network_allRoutes.isEmpty || network_allStops.isEmpty let updateIsForcedByCaller = forceUpdate @@ -237,7 +237,7 @@ class CarrisNetworkController: ObservableObject { let formattedRoute = Route_NEW( number: decodedAPIRouteDetail.routeNumber ?? "-", name: decodedAPIRouteDetail.name ?? "-", - kind: Globals().getKind(by: decodedAPIRouteDetail.routeNumber ?? "-"), + kind: Helpers.getKind(by: decodedAPIRouteDetail.routeNumber ?? "-"), variants: tempFormattedRouteVariants ) @@ -645,7 +645,7 @@ class CarrisNetworkController: ObservableObject { if (indexOfVehicleInArray != nil) { network_allVehicles[indexOfVehicleInArray!].routeNumber = vehicleSummary.routeNumber ?? "-" - network_allVehicles[indexOfVehicleInArray!].kind = Globals().getKind(by: vehicleSummary.routeNumber ?? "-") + network_allVehicles[indexOfVehicleInArray!].kind = Helpers.getKind(by: vehicleSummary.routeNumber ?? "-") network_allVehicles[indexOfVehicleInArray!].lat = vehicleSummary.lat ?? 0 network_allVehicles[indexOfVehicleInArray!].lng = vehicleSummary.lng ?? 0 network_allVehicles[indexOfVehicleInArray!].previousLatitude = vehicleSummary.previousLatitude ?? 0 @@ -662,7 +662,7 @@ class CarrisNetworkController: ObservableObject { Vehicle( busNumber: vehicleSummary.busNumber ?? 0, routeNumber: vehicleSummary.routeNumber ?? "-", - kind: Globals().getKind(by: vehicleSummary.routeNumber ?? "-"), + kind: Helpers.getKind(by: vehicleSummary.routeNumber ?? "-"), lat: vehicleSummary.lat ?? 0, lng: vehicleSummary.lng ?? 0, previousLatitude: vehicleSummary.previousLatitude ?? 0, From 380c68cbabc535369b13328aa88afe55268b823a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 14:35:47 +0100 Subject: [PATCH 06/63] Merge from production --- GeoBus/App/GeoBusApp.swift | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/GeoBus/App/GeoBusApp.swift b/GeoBus/App/GeoBusApp.swift index 8682ccc0..c43b1f39 100644 --- a/GeoBus/App/GeoBusApp.swift +++ b/GeoBus/App/GeoBusApp.swift @@ -10,45 +10,20 @@ struct GeoBusApp: App { @StateObject private var carrisNetworkController = CarrisNetworkController.shared // @StateObject private var tcbNetworkController = TCBNetworkController.shared - @StateObject private var carrisNetworkController = CarrisNetworkController() private let updateIntervalTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() var body: some Scene { WindowGroup { ContentView() -<<<<<<< HEAD - // OLD - .environmentObject(stopsController) - .environmentObject(routesController) - .environmentObject(vehiclesController) - .environmentObject(estimationsController) - // NEW - .environmentObject(self.mapController) -// .environmentObject(self.carrisNetworkController) - .onAppear(perform: { - // Update Carris network model -// self.carrisNetworkController.start() - self.routesController.update() - // Capture app open -======= .environmentObject(appstate) .environmentObject(mapController) .environmentObject(carrisNetworkController) .onAppear(perform: { ->>>>>>> production Analytics.shared.capture(event: .App_Session_Start) }) .onReceive(updateIntervalTimer) { event in carrisNetworkController.refresh() Analytics.shared.capture(event: .App_Session_Ping) -<<<<<<< HEAD - // Update vehicles on timer call -// self.vehiclesController.update(scope: .summary) - Task { - await vehiclesController.fetchVehiclesFromCarrisAPI() - } -======= ->>>>>>> production } } } From 8660e92aec6b3c9fcb7918c7f9cde290e08601de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 14:37:35 +0100 Subject: [PATCH 07/63] Enable DataProvidersCard --- GeoBus/App/Components/About/AboutGeoBus.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeoBus/App/Components/About/AboutGeoBus.swift b/GeoBus/App/Components/About/AboutGeoBus.swift index c764db67..1cb27137 100644 --- a/GeoBus/App/Components/About/AboutGeoBus.swift +++ b/GeoBus/App/Components/About/AboutGeoBus.swift @@ -46,7 +46,7 @@ struct AboutGeoBus: View { .padding(.top, 70) .padding(.bottom, 15) SyncStatus() -// DataProvidersCard() + DataProvidersCard() } .padding(.horizontal) From 5dcc570a9a59d8a6e5868f4fd18e76d4f5c4e45c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 14:39:27 +0100 Subject: [PATCH 08/63] Delete EstimationsController.swift --- GeoBus/App/State/EstimationsController.swift | 204 ------------------- 1 file changed, 204 deletions(-) delete mode 100644 GeoBus/App/State/EstimationsController.swift diff --git a/GeoBus/App/State/EstimationsController.swift b/GeoBus/App/State/EstimationsController.swift deleted file mode 100644 index 057d9cba..00000000 --- a/GeoBus/App/State/EstimationsController.swift +++ /dev/null @@ -1,204 +0,0 @@ -// -// AvailableRoutes.swift -// GeoBus -// -// Created by João on 20/04/2020. -// Copyright © 2020 João. All rights reserved. -// - -import Foundation -import Combine - -@MainActor -class EstimationsController: ObservableObject { - - /* MARK: - Variables */ - - private let storageKeyForEstimationsProvider: String = "estimations_estimationsProvider" - @Published var estimationsProvider: EstimationsProvider = .community - - - - /* MARK: - INITIALIZER */ - - // Retrieve data from UserDefaults on init. - - init() { - self.getProviderFromStorage() - } - - - - /* MARK: - GET ESTIMATIONS PROVIDER FROM STORAGE */ - - // Retrieve Estimations Provider from device storage. - - private func getProviderFromStorage() { - if let unwrappedEstimationsProvider = UserDefaults.standard.string(forKey: storageKeyForEstimationsProvider) { - self.estimationsProvider = EstimationsProvider(rawValue: unwrappedEstimationsProvider) ?? .carris - } - } - - - - /* MARK: - SET ESTIMATIONS PROVIDER */ - - // Set Estimations Provider for current session and save it to device storage. - - public func setProvider(selection: EstimationsProvider) { - self.estimationsProvider = selection - UserDefaults.standard.set(estimationsProvider.rawValue, forKey: storageKeyForEstimationsProvider) - print("GB: Provider is \(selection)") - } - - - - /* MARK: - GET ESTIMATIONS */ - - // This function initiates the correct API calls according to the set Estimations provider. - - public func get(for publicId: String) async -> [Estimation] { - switch estimationsProvider { - case .carris: - return await self.getCarrisEstimation(for: publicId) - case .community: - return await self.getCommunityEstimation(for: publicId) - } - } - - - - /* MARK: - GET ESTIMATIONS › CARRIS */ - - // This function calls Carris API to retrieve estimations for the given stop 'publicId'. - // It formats and returns the results to the caller. - - private func getCarrisEstimation(for publicId: String) async -> [Estimation] { - - Appstate.shared.change(to: .loading, for: .estimations) - - do { - // Request API Routes List - var requestCarrisAPIEstimations = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/Estimations/busStop/\(publicId)/top/5")!) - requestCarrisAPIEstimations.addValue("application/json", forHTTPHeaderField: "Content-Type") - requestCarrisAPIEstimations.addValue("application/json", forHTTPHeaderField: "Accept") - requestCarrisAPIEstimations.setValue("Bearer \(CarrisAuthentication.shared.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") - let (rawDataCarrisAPIEstimations, rawResponseCarrisAPIEstimations) = try await URLSession.shared.data(for: requestCarrisAPIEstimations) - let responseCarrisAPIEstimations = rawResponseCarrisAPIEstimations as? HTTPURLResponse - - // Check status of response - if (responseCarrisAPIEstimations?.statusCode == 401) { - Task { - await CarrisAuthentication.shared.authenticate() - return await self.getCarrisEstimation(for: publicId) - } - } else if (responseCarrisAPIEstimations?.statusCode != 200) { - print(responseCarrisAPIEstimations as Any) - throw Appstate.ModuleError.carris_unavailable - } - - let decodedCarrisAPIEstimations = try JSONDecoder().decode([CarrisAPIEstimation].self, from: rawDataCarrisAPIEstimations) - - // Define a temporary variable to store vehicles - // before publishing and displaying them in the map. - var tempAllEstimations: [Estimation] = [] - - // For each available vehicles in the API - for estimation in decodedCarrisAPIEstimations { - - // Format and append each estimation - // to the temporary variable. - tempAllEstimations.append( - Estimation( - routeNumber: estimation.routeNumber ?? "-", - destination: estimation.destination ?? "-", - publicId: estimation.publicId ?? "-", - busNumber: estimation.busNumber ?? "-", - eta: estimation.time ?? "" - ) - ) - - } - - Appstate.shared.change(to: .idle, for: .estimations) - - // Return the formatted estimations. - return tempAllEstimations - - } catch { - Appstate.shared.change(to: .error, for: .estimations) - print("GB: ERROR IN ESTIMATIONS: \(error)") - return [] - } - - } - - - /* MARK: - GET ESTIMATIONS › COMMUNITY */ - - // This function calls the API to retrieve estimations for the provided stop 'publicId'. - // It formats and returns the results to the caller. - - func getCommunityEstimation(for publicId: String) async -> [Estimation] { - - Appstate.shared.change(to: .loading, for: .estimations) - - do { - // Request API Routes List - var requestCommunityAPIVehicle = URLRequest(url: URL(string: "https://api.carril.workers.dev/eststop?busStop=\(publicId)")!) - requestCommunityAPIVehicle.addValue("application/json", forHTTPHeaderField: "Content-Type") - requestCommunityAPIVehicle.addValue("application/json", forHTTPHeaderField: "Accept") - let (rawDataCommunityAPIVehicle, rawResponseCommunityAPIVehicle) = try await URLSession.shared.data(for: requestCommunityAPIVehicle) - let responseCommunityAPIVehicle = rawResponseCommunityAPIVehicle as? HTTPURLResponse - - // Check status of response - if (responseCommunityAPIVehicle?.statusCode != 200) { - print(responseCommunityAPIVehicle as Any) - throw Appstate.ModuleError.community_unavailable - } - - let decodedCommunityAPIVehicle = try JSONDecoder().decode([CommunityAPIVehicle].self, from: rawDataCommunityAPIVehicle) - - // Define a temporary variable to store vehicles - // before publishing and displaying them in the map. - var tempAllEstimations: [Estimation] = [] - - // For each available vehicles in the API - for communityVehicle in decodedCommunityAPIVehicle { - - // If the vehicle is not expected to have arrived - if (!(communityVehicle.estimatedRecentlyArrived ?? false)) { - - let carrisVehicleDetails = await VehiclesController().fetchVehicleDetailsFromCarrisAPI(for: communityVehicle.busNumber ?? 0) - - // Format and append each estimation - // to the temporary variable. - tempAllEstimations.append( - Estimation( - routeNumber: communityVehicle.routeNumber ?? "-", - destination: carrisVehicleDetails?.lastStopOnVoyageName ?? "-", - publicId: publicId, - busNumber: String(communityVehicle.busNumber ?? 0), - eta: "" // communityVehicle.estimatedTimeofArrivalCorrected ?? "" - ) - ) - - } - - } - - Appstate.shared.change(to: .idle, for: .estimations) - - // Return the formatted estimations. - return tempAllEstimations - - } catch { - Appstate.shared.change(to: .error, for: .estimations) - print("ERROR IN ESTIMATIONS: \(error)") - return [] - } - - } - - -} From e3b6a5ba58314803538ec24b81e9444b3dc6d501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 14:40:38 +0100 Subject: [PATCH 09/63] Update VehicleDetailsView.swift --- .../App/Components/VehicleDetails/VehicleDetailsView.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift index 54acd7cb..978fc63f 100644 --- a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift +++ b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift @@ -21,11 +21,6 @@ struct VehicleDetailsView: View { @State var lastSeenTime: String = "-" -// init(vehicle: CarrisNetworkModel.Vehicle) { -// self.vehicle = carrisNetworkController.find(vehicle: vehicle.id) -// } - - var loadingScreen: some View { HStack(spacing: 3) { ProgressView() From 7c3233a3cf0ee8efca28949a71f468f63eb2111c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 18:11:03 +0100 Subject: [PATCH 10/63] Updated comment --- GeoBus/App/Controllers/Networks/Carris/CarrisAPI.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisAPI.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisAPI.swift index 36db1488..f66f85be 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisAPI.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisAPI.swift @@ -114,8 +114,8 @@ final class CarrisAPI { /* while loop, the same method is not repeated and the flow gets stuck in an infinite loop. This could be caused */ /* due to the way authentication in Carris API is implemented: the system returns invalid tokens for an expired key. */ /* This means that the only way to check if the tokens fetched from the current apiKey are valid is to perform the */ - /* the request and look for the response status. Keeping this centralized in one single ‹request()› functions */ - /* allows for a lot of code reuse. Also, if the response is not equal to 200 or 401, then throw an error immediately. */ + /* request and look for the response status. Keeping this centralized in one single ‹request()› function */ + /* allows for a lot of code reuse. Also, if the response is not equal to 200 or 401, throw an error immediately. */ /* If all is well, then return the raw data response to the parent caller. */ public func request(for service: String) async throws -> Data { From be7c096e35e4b437bb2ee98ea99b160288018afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 18:11:16 +0100 Subject: [PATCH 11/63] Removed unused enum --- GeoBus/App/Controllers/Appstate.swift | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/GeoBus/App/Controllers/Appstate.swift b/GeoBus/App/Controllers/Appstate.swift index 86192b89..95ad1695 100644 --- a/GeoBus/App/Controllers/Appstate.swift +++ b/GeoBus/App/Controllers/Appstate.swift @@ -44,24 +44,6 @@ final class Appstate: ObservableObject { - /* * */ - /* MARK: - SECTION 3: ERROR TYPES */ - /* Modules can publish more information on the particular error it encountered. */ - /* This functionality is planned to be expanded sometime in the future. */ - - enum ModuleError: Error { - - // For Carris API - case carris_unauthorized - case carris_unavailable - - // For Community API - case community_unavailable - - } - - - /* * */ /* MARK: - SECTION 4: SHARED INSTANCE */ /* To allow the same instance of this class to be available accross the whole app, */ From 19313c007b1c43d0780dab41a21a238a2f1ebba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 18:11:41 +0100 Subject: [PATCH 12/63] Update Appstate.swift --- GeoBus/App/Controllers/Appstate.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/GeoBus/App/Controllers/Appstate.swift b/GeoBus/App/Controllers/Appstate.swift index 95ad1695..b40d977b 100644 --- a/GeoBus/App/Controllers/Appstate.swift +++ b/GeoBus/App/Controllers/Appstate.swift @@ -45,7 +45,7 @@ final class Appstate: ObservableObject { /* * */ - /* MARK: - SECTION 4: SHARED INSTANCE */ + /* MARK: - SECTION 3: SHARED INSTANCE */ /* To allow the same instance of this class to be available accross the whole app, */ /* we create a Singleton. More info here: https://www.hackingwithswift.com/example-code/language/what-is-a-singleton */ /* Adding a private initializer is important because it stops other code from creating a new class instance. */ @@ -57,7 +57,7 @@ final class Appstate: ObservableObject { /* * */ - /* MARK: - SECTION 5: PUBLISHED VARIABLES */ + /* MARK: - SECTION 4: PUBLISHED VARIABLES */ /* Here are all the @Published variables refering to the above modules that can be consumed */ /* by the UI. It is important to keep the names of this variables short, but descriptive, */ /* to avoid clutter on the interface code. */ @@ -73,7 +73,7 @@ final class Appstate: ObservableObject { /* * */ - /* MARK: - SECTION 6: CHANGE STATE */ + /* MARK: - SECTION 5: CHANGE STATE */ /* Dispatch the change to the main queue to ensure UI updates happen smoothly and without interruptions. */ /* After the change, follow the set rules to also update the .global state. This might change in the future. */ From 40978ce6bcb8c598b6512213fb9303ab98c6a128 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 19:08:33 +0100 Subject: [PATCH 13/63] Save Community Data Provider status setting --- GeoBus.xcodeproj/project.pbxproj | 2 +- .../Components/About/DataProvidersCard.swift | 24 +++++------------ .../Carris/CarrisNetworkController.swift | 27 ++++++++++++++++--- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index 6fe8a5d3..0bb28ee2 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -378,6 +378,7 @@ children = ( CF6C918728D3FAF8006C3F61 /* AboutGeoBus.swift */, CFDD014828D5114D0070FE4B /* SyncStatus.swift */, + CF0C2569290211EF00B03052 /* DataProvidersCard.swift */, CF82BB0628D7F166007F0CDB /* LiveDataCard.swift */, CF82BB0828D7F19A007F0CDB /* LocationCard.swift */, CF82BB0A28D7F1C6007F0CDB /* ShareCard.swift */, @@ -385,7 +386,6 @@ CFDD014C28D66D9B0070FE4B /* CloseButton.swift */, CF05F61F28CD337200B4AD58 /* AppVersion.swift */, CF47994E28D33E1900B56D4B /* Disclaimer.swift */, - CF0C2569290211EF00B03052 /* DataProvidersCard.swift */, ); path = About; sourceTree = ""; diff --git a/GeoBus/App/Components/About/DataProvidersCard.swift b/GeoBus/App/Components/About/DataProvidersCard.swift index 9cea7036..0e1e7a57 100644 --- a/GeoBus/App/Components/About/DataProvidersCard.swift +++ b/GeoBus/App/Components/About/DataProvidersCard.swift @@ -9,6 +9,8 @@ import SwiftUI struct DataProvidersCard: View { + @EnvironmentObject var carrisNetworkController: CarrisNetworkController + private let cardColor: Color = Color(.systemTeal) @State var communityProviderIsOn: Bool = false @@ -27,11 +29,7 @@ struct DataProvidersCard: View { .padding(.leading, 5) } .onAppear() { -// if (estimationsController.estimationsProvider == .carris) { -// communityProviderIsOn = false -// } else { -// communityProviderIsOn = true -// } + communityProviderIsOn = carrisNetworkController.communityDataProviderStatus } } .padding() @@ -39,19 +37,11 @@ struct DataProvidersCard: View { .tint(cardColor) .background(cardColor.opacity(0.05)) .cornerRadius(10) -// .onChange(of: estimationsController.estimationsProvider) { value in -// if (value == .carris) { -// communityProviderIsOn = false -// } else { -// communityProviderIsOn = true -// } -// } + .onChange(of: carrisNetworkController.communityDataProviderStatus) { value in + communityProviderIsOn = value + } .onChange(of: communityProviderIsOn) { value in - if (value) { -// estimationsController.setProvider(selection: .community) - } else { -// estimationsController.setProvider(selection: .carris) - } + carrisNetworkController.toggleCommunityDataProviderTo(to: value) } } diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index 3bd5bf46..e7043044 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -26,6 +26,7 @@ class CarrisNetworkController: ObservableObject { private let storageKeyForFavoriteStops: String = "carris_favoriteStops" private let storageKeyForSavedRoutes: String = "carris_savedRoutes" private let storageKeyForFavoriteRoutes: String = "carris_favoriteRoutes" + private let storageKeyForCommunityDataProviderStatus: String = "carris_communityDataProviderStatus" @@ -51,6 +52,8 @@ class CarrisNetworkController: ObservableObject { @Published var favorites_routes: [CarrisNetworkModel.Route] = [] @Published var favorites_stops: [CarrisNetworkModel.Stop] = [] + @Published var communityDataProviderStatus: Bool = false + /* * */ @@ -70,6 +73,11 @@ class CarrisNetworkController: ObservableObject { private init() { + // Unwrap last timestamp from Storage + if let unwrappedLastUpdatedNetwork = UserDefaults.standard.string(forKey: storageKeyForLastUpdatedCarrisNetwork) { + self.lastUpdatedNetwork = unwrappedLastUpdatedNetwork + } + // Unwrap and Decode Stops from Storage if let unwrappedSavedNetworkStops = UserDefaults.standard.data(forKey: storageKeyForSavedStops) { if let decodedSavedNetworkStops = try? JSONDecoder().decode([CarrisNetworkModel.Stop].self, from: unwrappedSavedNetworkStops) { @@ -84,10 +92,9 @@ class CarrisNetworkController: ObservableObject { } } - // Unwrap last timestamp from Storage - if let unwrappedLastUpdatedNetwork = UserDefaults.standard.string(forKey: storageKeyForLastUpdatedCarrisNetwork) { - self.lastUpdatedNetwork = unwrappedLastUpdatedNetwork - } + // Unwrap Community Provider Status from Storage + self.communityDataProviderStatus = UserDefaults.standard.bool(forKey: storageKeyForCommunityDataProviderStatus) + print("GeoBus: Carris API: ‹toggleCommunityDataProviderTo()› Community Data switched \(communityDataProviderStatus ? "ON" : "OFF")") // Check if network needs an update self.update(reset: false) @@ -840,4 +847,16 @@ class CarrisNetworkController: ObservableObject { + /* * */ + /* MARK: - SECTION 12: TOGGLE COMMUNITY DATA PROVIDER STATUS */ + /* Call this function to switch Community Data ON or OFF. */ + /* This switches in memory for the current session, and stores the new setting in storage. */ + + public func toggleCommunityDataProviderTo(to newStatus: Bool) { + self.communityDataProviderStatus = newStatus + UserDefaults.standard.set(newStatus, forKey: storageKeyForCommunityDataProviderStatus) + print("GeoBus: Carris API: ‹toggleCommunityDataProviderTo()› Community Data switched \(newStatus ? "ON" : "OFF")") + } + + } From cee53b8967311250389832dd9a09f04e4fec6cb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 19:09:36 +0100 Subject: [PATCH 14/63] Simplified approach --- GeoBus/App/Components/About/DataProvidersCard.swift | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/GeoBus/App/Components/About/DataProvidersCard.swift b/GeoBus/App/Components/About/DataProvidersCard.swift index 0e1e7a57..3259788b 100644 --- a/GeoBus/App/Components/About/DataProvidersCard.swift +++ b/GeoBus/App/Components/About/DataProvidersCard.swift @@ -17,7 +17,7 @@ struct DataProvidersCard: View { var providerToggle: some View { - Toggle(isOn: $communityProviderIsOn) { + Toggle(isOn: $carrisNetworkController.communityDataProviderStatus) { HStack { Image(systemName: "staroflife.circle") .renderingMode(.template) @@ -28,9 +28,6 @@ struct DataProvidersCard: View { .foregroundColor(cardColor) .padding(.leading, 5) } - .onAppear() { - communityProviderIsOn = carrisNetworkController.communityDataProviderStatus - } } .padding() .frame(maxWidth: .infinity) @@ -38,9 +35,6 @@ struct DataProvidersCard: View { .background(cardColor.opacity(0.05)) .cornerRadius(10) .onChange(of: carrisNetworkController.communityDataProviderStatus) { value in - communityProviderIsOn = value - } - .onChange(of: communityProviderIsOn) { value in carrisNetworkController.toggleCommunityDataProviderTo(to: value) } } From b4b42e561a87e365e915c2902b9abfa6c477a361 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 19:20:17 +0100 Subject: [PATCH 15/63] Updated wording --- GeoBus/App/Components/About/DataProvidersCard.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GeoBus/App/Components/About/DataProvidersCard.swift b/GeoBus/App/Components/About/DataProvidersCard.swift index 3259788b..153f48e7 100644 --- a/GeoBus/App/Components/About/DataProvidersCard.swift +++ b/GeoBus/App/Components/About/DataProvidersCard.swift @@ -23,7 +23,7 @@ struct DataProvidersCard: View { .renderingMode(.template) .font(Font.system(size: 25)) .foregroundColor(cardColor) - Text("Community ETAs") + Text("Community Data") .font(Font.system(size: 18, weight: .bold)) .foregroundColor(cardColor) .padding(.leading, 5) @@ -45,16 +45,16 @@ struct DataProvidersCard: View { Image(systemName: "clock.arrow.2.circlepath") .font(Font.system(size: 30, weight: .regular)) .foregroundColor(cardColor) - Text("ETA Provider") + Text("Community Data") .font(.title) .fontWeight(.bold) .foregroundColor(cardColor) - Text("Select your prefered Data provider.") + Text("Try an experimetal feature made in partnership with people interested in improving transportation in Lisbon.") .multilineTextAlignment(.center) .font(.headline) .fontWeight(.semibold) .foregroundColor(Color(.label)) - Text("Ainda não faz nada.") + Text("This includes better arrival time estimates for all stops, more precise vehicle locations and additional route information.") .multilineTextAlignment(.center) .font(.headline) .fontWeight(.semibold) From 8f3c53c56c9b29e5dc3a870d9e3f584307568579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 19:50:23 +0100 Subject: [PATCH 16/63] Added Community API Data Model --- GeoBus.xcodeproj/project.pbxproj | 4 + .../Networks/Carris/CarrisAPIModel.swift | 8 -- .../Carris/CarrisCommunityAPIModel.swift | 135 ++++++++++++++++++ .../Carris/CarrisNetworkController.swift | 9 ++ 4 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index 0bb28ee2..ab41dd55 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ CF05F61A28CD09A000B4AD58 /* NavBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF05F61928CD09A000B4AD58 /* NavBar.swift */; }; CF05F62028CD337200B4AD58 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF05F61F28CD337200B4AD58 /* AppVersion.swift */; }; CF0C256A290211EF00B03052 /* DataProvidersCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0C2569290211EF00B03052 /* DataProvidersCard.swift */; }; + CF0C256C29031B2C00B03052 /* CarrisCommunityAPIModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0C256B29031B2C00B03052 /* CarrisCommunityAPIModel.swift */; }; CF181FE628CCB7D600248F72 /* GeoBusApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF181FE528CCB7D600248F72 /* GeoBusApp.swift */; }; CF181FE828CCB7D600248F72 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF181FE728CCB7D600248F72 /* ContentView.swift */; }; CF181FEA28CCB7D700248F72 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CF181FE928CCB7D700248F72 /* Assets.xcassets */; }; @@ -80,6 +81,7 @@ CF05F61928CD09A000B4AD58 /* NavBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavBar.swift; sourceTree = ""; }; CF05F61F28CD337200B4AD58 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; CF0C2569290211EF00B03052 /* DataProvidersCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvidersCard.swift; sourceTree = ""; }; + CF0C256B29031B2C00B03052 /* CarrisCommunityAPIModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrisCommunityAPIModel.swift; sourceTree = ""; }; CF181FE228CCB7D600248F72 /* GeoBus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GeoBus.app; sourceTree = BUILT_PRODUCTS_DIR; }; CF181FE528CCB7D600248F72 /* GeoBusApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoBusApp.swift; sourceTree = ""; }; CF181FE728CCB7D600248F72 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -352,6 +354,7 @@ children = ( CFFFAD8428F7A21100DFD5FD /* CarrisAPI.swift */, CF5094CC28FCB9E400EDD320 /* CarrisAPIModel.swift */, + CF0C256B29031B2C00B03052 /* CarrisCommunityAPIModel.swift */, CF5094CA28FC50E900EDD320 /* CarrisNetworkModel.swift */, CF5094C828FC50AC00EDD320 /* CarrisNetworkController.swift */, ); @@ -527,6 +530,7 @@ CFAF0E7A28CE84F400DDAD5B /* MapAnnotations.swift in Sources */, CF548FF328D129B000668CB6 /* VehicleDetailsView.swift in Sources */, CFFFAD7B28F4D8D000DFD5FD /* StopIcon.swift in Sources */, + CF0C256C29031B2C00B03052 /* CarrisCommunityAPIModel.swift in Sources */, CF82BB0928D7F19A007F0CDB /* LocationCard.swift in Sources */, CF18207928CCBD2300248F72 /* RouteDetailsVehiclesQuantity.swift in Sources */, CF181FE628CCB7D600248F72 /* GeoBusApp.swift in Sources */, diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift index 97d73970..d6351023 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift @@ -1,13 +1,5 @@ -// -// Routes.swift -// GeoBus -// -// Created by João de Vasconcelos on 09/09/2022. -// Copyright © 2022 João de Vasconcelos. All rights reserved. -// import Foundation - /* * */ /* MARK: - CARRIS API DATA MODEL */ /* Data model as provided by Carris API. */ diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift new file mode 100644 index 00000000..09ba0e16 --- /dev/null +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift @@ -0,0 +1,135 @@ +import Foundation + +/* * */ +/* MARK: - CARRIS COMMUNITY API DATA MODEL */ +/* Data model as provided by Community API for Carris network. */ +/* Schema is available at https://github.com/ricardojorgerm/carril */ + +struct CarrisCommunityAPIModel { + + struct Vehicle: Decodable { + let busNumber: Int? + let dataServico: String? + let direction: String? + let enrichedAvgRouteSpeed: Double? + let enrichedBusSpeed: Double? + let enrichedDbStartup: Double? + let enrichedEstRouteKm: Double? + let enrichedGeohash300m: String? + let enrichedGeohash80m: String? + let enrichedGeohashPrev300m: String? + let enrichedGeohashPrev80m: String? + let enrichedPreviousStopId: String? + let enrichedPreviousStopList: [String]? + let enrichedPreviousStopMax: Int? + let enrichedPreviousStopOrderIdx: Int? + let enrichedQueryTime: Double? + let enrichedRouteCoords: [Double]? + let enrichedRouteDirection: Double? + let enrichedRouteDoneKm: Double? + let enrichedRouteLengthKm: Double? + let enrichedSequenceNo: Int? + let enrichedStartLat: Double? + let enrichedStartLng: Double? + let enrichedStartTime: String? + let enrichedTimeHash30m: String? + let enrichedTimeHashDay30m: String? + let estimatedDebug: [String]? + let estimatedRouteItinerary: [String]? + let estimatedRouteResults: [EstimatedRouteResult]? + let lastGpsTime: String? + let lastReportTime: String? + let lat: Double? + let lng: Double? + let plateNumber: String? + let previousLatitude: Double? + let previousLongitude: Double? + let previousReportTime: String? + let routeNumber: String? + let state: String? + let timeStamp: String? + let variantNumber: Int? + let voyageNumber: Int? + } + + struct EstimatedRouteResult: Decodable { + let estimatedFeatures: [EstimatedFeature]? + let estimatedPreviouslyArrived: Bool? + let estimatedRecentlyArrived: Bool? + let estimatedRouteStopId: String? + let estimatedRouteStopPosition: Double? + let estimatedTimeofArrival: String? + let estimatedTimeofArrivalCorrected: String? + let estimatedUncertainty: String? + } + + + + struct Estimate: Decodable { + let busNumber: Int? + let dataServico: String? + let direction: String? + let enrichedAvgRouteSpeed: Double? + let enrichedBusSpeed: Double? + let enrichedDbStartup: Double? + let enrichedEstRouteKm: Double? + let enrichedGeohash300m: String? + let enrichedGeohash80m: String? + let enrichedGeohashPrev300m: String? + let enrichedGeohashPrev80m: String? + let enrichedPreviousStopId: String? + let enrichedPreviousStopList: [String]? + let enrichedPreviousStopMax: Int? + let enrichedPreviousStopOrderIdx: Int? + let enrichedQueryTime: Double? + let enrichedRouteCoords: [Double]? + let enrichedRouteDirection: Double? + let enrichedRouteDoneKm: Double? + let enrichedRouteLengthKm: Double? + let enrichedSequenceNo: Int? + let enrichedStartLat: Double? + let enrichedStartLng: Double? + let enrichedStartTime: String? + let enrichedTimeHash30m: String? + let enrichedTimeHashDay30m: String? + let estimatedDebug: [String]? + let estimatedFeatures: [EstimatedFeature]? + let estimatedPreviouslyArrived: Bool? + let estimatedRecentlyArrived: Bool? + let estimatedRouteStopId: String? + let estimatedRouteStopPosition: Double? + let estimatedTimeofArrival: String? + let estimatedTimeofArrivalCorrected: String? + let estimatedUncertainty: String? + let lastGpsTime: String? + let lastReportTime: String? + let lat: Double? + let lng: Double? + let plateNumber: String? + let previousLatitude: Double? + let previousLongitude: Double? + let previousReportTime: String? + let routeNumber: String? + let state: String? + let timeStamp: String? + let variantNumber: Int? + let voyageNumber: Int? + } + + + + struct EstimatedFeature: Decodable { + let avgHistorDeltaDistanceKm: Double? + let avgHistorDeltaSeconds: Double? + let avgHistorDeltaSeqNo: Double? + let avgHistorInstSpeedAtPositionKmh: Double? + let avgHistorLongtermSpeedAtPositionKmh: Double? + let correctionFactorLongTerm: Double? + let correctionFactorShortTerm: Double? + let maxHistorDelta: Int? + let minHistorDelta: Int? + let noHistorSamples: Int? + let stdHistorDeltaSeconds: Double? + } + +} diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index e7043044..2aedff75 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -859,4 +859,13 @@ class CarrisNetworkController: ObservableObject { } + + /* * */ + /* MARK: - SECTION 12: TOGGLE COMMUNITY DATA PROVIDER STATUS */ + /* Call this function to switch Community Data ON or OFF. */ + /* This switches in memory for the current session, and stores the new setting in storage. */ + + // + + } From adc589404f69ecfc744f6c1d64c8862a48a06c64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 20:49:55 +0100 Subject: [PATCH 17/63] Added community estimates --- GeoBus.xcodeproj/project.pbxproj | 4 + .../Components/About/DataProvidersCard.swift | 2 - .../StopDetails/StopDetailsView.swift | 39 ++++++++ .../Networks/Carris/CarrisCommunityAPI.swift | 92 ++++++++++++++++++ .../Carris/CarrisCommunityAPIModel.swift | 10 +- .../Carris/CarrisNetworkController.swift | 97 ++++++++++++++++--- 6 files changed, 226 insertions(+), 18 deletions(-) create mode 100644 GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPI.swift diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index ab41dd55..aefdf21b 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ CF05F62028CD337200B4AD58 /* AppVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF05F61F28CD337200B4AD58 /* AppVersion.swift */; }; CF0C256A290211EF00B03052 /* DataProvidersCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0C2569290211EF00B03052 /* DataProvidersCard.swift */; }; CF0C256C29031B2C00B03052 /* CarrisCommunityAPIModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0C256B29031B2C00B03052 /* CarrisCommunityAPIModel.swift */; }; + CF0C256E290324A600B03052 /* CarrisCommunityAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF0C256D290324A600B03052 /* CarrisCommunityAPI.swift */; }; CF181FE628CCB7D600248F72 /* GeoBusApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF181FE528CCB7D600248F72 /* GeoBusApp.swift */; }; CF181FE828CCB7D600248F72 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF181FE728CCB7D600248F72 /* ContentView.swift */; }; CF181FEA28CCB7D700248F72 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CF181FE928CCB7D700248F72 /* Assets.xcassets */; }; @@ -82,6 +83,7 @@ CF05F61F28CD337200B4AD58 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; CF0C2569290211EF00B03052 /* DataProvidersCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataProvidersCard.swift; sourceTree = ""; }; CF0C256B29031B2C00B03052 /* CarrisCommunityAPIModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrisCommunityAPIModel.swift; sourceTree = ""; }; + CF0C256D290324A600B03052 /* CarrisCommunityAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrisCommunityAPI.swift; sourceTree = ""; }; CF181FE228CCB7D600248F72 /* GeoBus.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = GeoBus.app; sourceTree = BUILT_PRODUCTS_DIR; }; CF181FE528CCB7D600248F72 /* GeoBusApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoBusApp.swift; sourceTree = ""; }; CF181FE728CCB7D600248F72 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -354,6 +356,7 @@ children = ( CFFFAD8428F7A21100DFD5FD /* CarrisAPI.swift */, CF5094CC28FCB9E400EDD320 /* CarrisAPIModel.swift */, + CF0C256D290324A600B03052 /* CarrisCommunityAPI.swift */, CF0C256B29031B2C00B03052 /* CarrisCommunityAPIModel.swift */, CF5094CA28FC50E900EDD320 /* CarrisNetworkModel.swift */, CF5094C828FC50AC00EDD320 /* CarrisNetworkController.swift */, @@ -546,6 +549,7 @@ CF548FF628D14BA400668CB6 /* VehicleIdentifier.swift in Sources */, CF5094CB28FC50E900EDD320 /* CarrisNetworkModel.swift in Sources */, CF18208728CCBD3A00248F72 /* SelectRouteInput.swift in Sources */, + CF0C256E290324A600B03052 /* CarrisCommunityAPI.swift in Sources */, CF18204728CCBCC500248F72 /* RouteBadgePill.swift in Sources */, CF0C256A290211EF00B03052 /* DataProvidersCard.swift in Sources */, CF18204828CCBCC500248F72 /* RouteBadgeSquare.swift in Sources */, diff --git a/GeoBus/App/Components/About/DataProvidersCard.swift b/GeoBus/App/Components/About/DataProvidersCard.swift index 153f48e7..53a676e4 100644 --- a/GeoBus/App/Components/About/DataProvidersCard.swift +++ b/GeoBus/App/Components/About/DataProvidersCard.swift @@ -13,8 +13,6 @@ struct DataProvidersCard: View { private let cardColor: Color = Color(.systemTeal) - @State var communityProviderIsOn: Bool = false - var providerToggle: some View { Toggle(isOn: $carrisNetworkController.communityDataProviderStatus) { diff --git a/GeoBus/App/Components/StopDetails/StopDetailsView.swift b/GeoBus/App/Components/StopDetails/StopDetailsView.swift index 4c65c494..e0793d4b 100644 --- a/GeoBus/App/Components/StopDetails/StopDetailsView.swift +++ b/GeoBus/App/Components/StopDetails/StopDetailsView.swift @@ -125,6 +125,43 @@ struct ConnectionDetailsView2: View { } } + // DEBUG!!! + var providerToggle: some View { + VStack { + Toggle(isOn: $carrisNetworkController.communityDataProviderStatus) { + HStack { + Image(systemName: "staroflife.circle") + .renderingMode(.template) + .font(Font.system(size: 25)) + .foregroundColor(.teal) + Text("Community Data") + .font(Font.system(size: 18, weight: .bold)) + .foregroundColor(.teal) + .padding(.leading, 5) + } + } + .padding() + .frame(maxWidth: .infinity) + .tint(.teal) + .background(.teal.opacity(0.05)) + .cornerRadius(10) + .onChange(of: carrisNetworkController.communityDataProviderStatus) { value in + carrisNetworkController.toggleCommunityDataProviderTo(to: value) + self.getEstimationsFromController() + } + + Button(action: getEstimationsFromController, label: { + Text("Reload Estimate") + .font(Font.system(size: 15, weight: .bold, design: .default) ) + .foregroundColor(Color(.white)) + .padding(5) + .frame(maxWidth: .infinity) + .background(Color(.systemBlue)) + .cornerRadius(10) + }) + } + } + var body: some View { VStack(spacing: 0) { @@ -138,6 +175,8 @@ struct ConnectionDetailsView2: View { .padding([.horizontal, .bottom]) .padding(.top, 7) } + providerToggle + .padding() } .background( canToggle diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPI.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPI.swift new file mode 100644 index 00000000..8fbc4da9 --- /dev/null +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPI.swift @@ -0,0 +1,92 @@ +import Foundation + + +/* * */ +/* MARK: - CARRIS API WRAPPER */ +/* A series of self contained steps to request and authenticate with Carris API. */ + + +final class CarrisCommunityAPI { + + /* * */ + /* MARK: - SECTION 1: SETTINGS */ + /* In this section private constants for update intervals and storage keys are defined. */ + /* ‹totalAuthenticationAttemptsBeforeFailing› should be more than 3 to make sure any error in the auth server */ + /* is not passed on to the user. */ + + private let apiEndpoint = "https://api.carril.workers.dev/" + + + + /* * */ + /* MARK: - SECTION 4: CARRIS API AUTHENTICATION MODELS */ + /* Data models as provided by the authentication APIs. */ + /* Example request for ‹CarrisAPICredential› is available at https://joao.earth/api/geobus/carris_auth */ + /* Schema for ‹CarrisAPIAuthorization› is available at https://joaodcp.github.io/Carris-API */ + + private enum CarrisCommunityAPIError: Error { + case unavailable + } + + + + /* * */ + /* MARK: - SECTION 5: SHARED INSTANCE */ + /* To allow the same instance of this class to be available accross the whole app, */ + /* we create a Singleton. More info here: https://www.hackingwithswift.com/example-code/language/what-is-a-singleton */ + + static let shared = CarrisCommunityAPI() + + private init() { } + + + + /* * */ + /* MARK: - SECTION 7: REQUEST */ + /* This function makes GET requests to Carris API and agregates all the steps required for authentication. */ + + public func request(for service: String) async throws -> Data { + + let (data, response) = try await makeGETRequest(url: self.apiEndpoint + service) + + if (response.statusCode != 200) { + print("GeoBus: Carris API: Routes: Unknown error. More info: \(response as Any)") + print("********************************************************") + throw CarrisCommunityAPIError.unavailable + } else { + return data + } + + } + + + + /* * */ + /* MARK: - SECTION 10: MAKE REQUEST TO URL */ + /* Convenience function to format, perform and return a GET or POST request to an URL. */ + /* Provide the option to authenticate the request, and for POST the body. */ + + private func makeGETRequest(url requestURL: String) async throws -> (Data, HTTPURLResponse) { + + // Format the request + var carrisAPIGETRequest = URLRequest(url: URL(string: requestURL)!) + + carrisAPIGETRequest.httpMethod = "GET" + carrisAPIGETRequest.addValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") + carrisAPIGETRequest.addValue("application/json", forHTTPHeaderField: "Accept") + + // Perform the request + let (carrisAPIGETRequestRawData, carrisAPIGETRequestRawResponse) = try await URLSession.shared.data(for: carrisAPIGETRequest) + + // If cast to HTTPResponse is valid return, else throw an error + if let carrisAPIGETRequestHTTPResponse = carrisAPIGETRequestRawResponse as? HTTPURLResponse { + return (carrisAPIGETRequestRawData, carrisAPIGETRequestHTTPResponse) + } else { + throw CarrisCommunityAPIError.unavailable + } + + } + + + +} diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift index 09ba0e16..6bf15052 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift @@ -22,7 +22,7 @@ struct CarrisCommunityAPIModel { let enrichedPreviousStopId: String? let enrichedPreviousStopList: [String]? let enrichedPreviousStopMax: Int? - let enrichedPreviousStopOrderIdx: Int? + let enrichedPreviousStopOrderIdx: Double? let enrichedQueryTime: Double? let enrichedRouteCoords: [Double]? let enrichedRouteDirection: Double? @@ -65,7 +65,7 @@ struct CarrisCommunityAPIModel { - struct Estimate: Decodable { + struct Estimation: Decodable { let busNumber: Int? let dataServico: String? let direction: String? @@ -77,10 +77,10 @@ struct CarrisCommunityAPIModel { let enrichedGeohash80m: String? let enrichedGeohashPrev300m: String? let enrichedGeohashPrev80m: String? - let enrichedPreviousStopId: String? +// let enrichedPreviousStopId: String? ———› ERROR: typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 1", intValue: 1), CodingKeys(stringValue: "enrichedPreviousStopId", intValue: nil)], debugDescription: "Expected to decode String but found a number instead.", underlyingError: nil)) let enrichedPreviousStopList: [String]? let enrichedPreviousStopMax: Int? - let enrichedPreviousStopOrderIdx: Int? + let enrichedPreviousStopOrderIdx: Double? let enrichedQueryTime: Double? let enrichedRouteCoords: [Double]? let enrichedRouteDirection: Double? @@ -93,7 +93,7 @@ struct CarrisCommunityAPIModel { let enrichedTimeHash30m: String? let enrichedTimeHashDay30m: String? let estimatedDebug: [String]? - let estimatedFeatures: [EstimatedFeature]? + let estimatedFeatures: EstimatedFeature? let estimatedPreviouslyArrived: Bool? let estimatedRecentlyArrived: Bool? let estimatedRouteStopId: String? diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index 2aedff75..9999fe34 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -548,6 +548,14 @@ class CarrisNetworkController: ObservableObject { /* These functions search for the provided object identifier in the storage arrays */ /* and return it if found or nil if not found. */ + private func find(vehicle vehicleId: Int) -> CarrisNetworkModel.Vehicle? { + if let requestedVehicleObject = self.allVehicles[withId: vehicleId] { + return requestedVehicleObject + } else { + return nil + } + } + private func find(route routeNumber: String) -> CarrisNetworkModel.Route? { if let requestedRouteObject = self.allRoutes[withId: routeNumber] { return requestedRouteObject @@ -564,12 +572,24 @@ class CarrisNetworkController: ObservableObject { } } - private func find(vehicle vehicleId: Int) -> CarrisNetworkModel.Vehicle? { - if let requestedVehicleObject = self.allVehicles[withId: vehicleId] { - return requestedVehicleObject - } else { + private func find(route routeNumber: String, variant: Int, direction: String) -> CarrisNetworkModel.Stop? { + guard let requestedRouteObject = self.find(route: routeNumber) else { return nil } + + let requestedVariantObject = requestedRouteObject.variants[variant] + + switch direction { + case "ASC": + return requestedVariantObject.ascendingItinerary?.last?.stop + case "DESC": + return requestedVariantObject.descendingItinerary?.last?.stop + case "CIRC": + return requestedVariantObject.circularItinerary?.last?.stop + default: + return nil + } + } @@ -794,15 +814,24 @@ class CarrisNetworkController: ObservableObject { - /* MARK: - Get Estimations */ + /* MARK: - GET ESTIMATION */ // This function calls the API to retrieve estimations for the provided stop 'publicId'. // It formats and returns the results to the caller. public func getEstimation(for stopId: Int) async -> [CarrisNetworkModel.Estimation] { + if (!communityDataProviderStatus) { return await self.fetchEstimationsFromCarrisAPI(for: stopId) -// self.populateActiveVehicles() + } else { + return await self.fetchEstimationsFromCommunityAPI(for: stopId) + } } + + + /* MARK: - GET CARRIS ESTIMATIONS */ + // This function calls the API to retrieve estimations for the provided stop 'publicId'. + // It formats and returns the results to the caller. + public func fetchEstimationsFromCarrisAPI(for stopId: Int) async -> [CarrisNetworkModel.Estimation] { Appstate.shared.change(to: .loading, for: .estimations) @@ -860,12 +889,58 @@ class CarrisNetworkController: ObservableObject { - /* * */ - /* MARK: - SECTION 12: TOGGLE COMMUNITY DATA PROVIDER STATUS */ - /* Call this function to switch Community Data ON or OFF. */ - /* This switches in memory for the current session, and stores the new setting in storage. */ + /* MARK: - GET COMMUNITY ESTIMATIONS */ + // This function calls the API to retrieve estimations for the provided stop 'publicId'. + // It formats and returns the results to the caller. - // + public func fetchEstimationsFromCommunityAPI(for stopId: Int) async -> [CarrisNetworkModel.Estimation] { + + Appstate.shared.change(to: .loading, for: .estimations) + + print("GeoBus: Carris API: Estimations: Starting update...") + + do { + // Request API Estimations List + let rawDataCarrisCommunityAPIEstimations = try await CarrisCommunityAPI.shared.request(for: "eststop?busStop=\(stopId)") + let decodedCarrisCommunityAPIEstimations = try JSONDecoder().decode([CarrisCommunityAPIModel.Estimation].self, from: rawDataCarrisCommunityAPIEstimations) + + + var tempFormattedEstimations: [CarrisNetworkModel.Estimation] = [] + + + // For each available vehicles in the API + for apiEstimation in decodedCarrisCommunityAPIEstimations { + + let destinationStop = find( + route: apiEstimation.routeNumber ?? "-", + variant: apiEstimation.variantNumber ?? -1, + direction: apiEstimation.direction ?? "-" + ) + + tempFormattedEstimations.append( + CarrisNetworkModel.Estimation( + stopId: stopId, + routeNumber: apiEstimation.routeNumber ?? "-", + destination: destinationStop?.name ?? "-", + eta: apiEstimation.estimatedTimeofArrivalCorrected ?? "", + busNumber: apiEstimation.busNumber ?? -1 + ) + ) + } + + print("GeoBus: Carris API: Estimations: Update complete!") + + Appstate.shared.change(to: .idle, for: .estimations) + + return tempFormattedEstimations + + } catch { + Appstate.shared.change(to: .error, for: .estimations) + print("GeoBus: Carris API: Estimations: Error found while updating. More info: \(error)") + return [] + } + + } } From ed41d72f832a41955ad88e5a148605d037eca81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 23:39:04 +0100 Subject: [PATCH 18/63] Moved function --- .../Carris/CarrisNetworkController.swift | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index 9999fe34..bfe61589 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -147,6 +147,19 @@ class CarrisNetworkController: ObservableObject { + /* * */ + /* MARK: - SECTION 12: TOGGLE COMMUNITY DATA PROVIDER STATUS */ + /* Call this function to switch Community Data ON or OFF. */ + /* This switches in memory for the current session, and stores the new setting in storage. */ + + public func toggleCommunityDataProviderTo(to newStatus: Bool) { + self.communityDataProviderStatus = newStatus + UserDefaults.standard.set(newStatus, forKey: storageKeyForCommunityDataProviderStatus) + print("GeoBus: Carris API: ‹toggleCommunityDataProviderTo()› Community Data switched \(newStatus ? "ON" : "OFF")") + } + + + /* * */ /* MARK: - SECTION 5.2: REFRESH DATA */ /* This function initiates vehicles refresh from Carris API, updates the ‹activeVehicles› array */ @@ -876,19 +889,6 @@ class CarrisNetworkController: ObservableObject { - /* * */ - /* MARK: - SECTION 12: TOGGLE COMMUNITY DATA PROVIDER STATUS */ - /* Call this function to switch Community Data ON or OFF. */ - /* This switches in memory for the current session, and stores the new setting in storage. */ - - public func toggleCommunityDataProviderTo(to newStatus: Bool) { - self.communityDataProviderStatus = newStatus - UserDefaults.standard.set(newStatus, forKey: storageKeyForCommunityDataProviderStatus) - print("GeoBus: Carris API: ‹toggleCommunityDataProviderTo()› Community Data switched \(newStatus ? "ON" : "OFF")") - } - - - /* MARK: - GET COMMUNITY ESTIMATIONS */ // This function calls the API to retrieve estimations for the provided stop 'publicId'. // It formats and returns the results to the caller. From 2e49013f8e9cf2c015f9925c05e4121b984cc65d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 23:39:16 +0100 Subject: [PATCH 19/63] Only receive what's needed --- .../Carris/CarrisCommunityAPIModel.swift | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift index 6bf15052..257f84b2 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift @@ -67,69 +67,69 @@ struct CarrisCommunityAPIModel { struct Estimation: Decodable { let busNumber: Int? - let dataServico: String? + // let dataServico: String? let direction: String? - let enrichedAvgRouteSpeed: Double? - let enrichedBusSpeed: Double? - let enrichedDbStartup: Double? - let enrichedEstRouteKm: Double? - let enrichedGeohash300m: String? - let enrichedGeohash80m: String? - let enrichedGeohashPrev300m: String? - let enrichedGeohashPrev80m: String? -// let enrichedPreviousStopId: String? ———› ERROR: typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 1", intValue: 1), CodingKeys(stringValue: "enrichedPreviousStopId", intValue: nil)], debugDescription: "Expected to decode String but found a number instead.", underlyingError: nil)) - let enrichedPreviousStopList: [String]? - let enrichedPreviousStopMax: Int? - let enrichedPreviousStopOrderIdx: Double? - let enrichedQueryTime: Double? - let enrichedRouteCoords: [Double]? - let enrichedRouteDirection: Double? - let enrichedRouteDoneKm: Double? - let enrichedRouteLengthKm: Double? - let enrichedSequenceNo: Int? - let enrichedStartLat: Double? - let enrichedStartLng: Double? - let enrichedStartTime: String? - let enrichedTimeHash30m: String? - let enrichedTimeHashDay30m: String? - let estimatedDebug: [String]? - let estimatedFeatures: EstimatedFeature? - let estimatedPreviouslyArrived: Bool? - let estimatedRecentlyArrived: Bool? - let estimatedRouteStopId: String? - let estimatedRouteStopPosition: Double? - let estimatedTimeofArrival: String? + // let enrichedAvgRouteSpeed: Double? + // let enrichedBusSpeed: Double? + // let enrichedDbStartup: Double? + // let enrichedEstRouteKm: Double? + // let enrichedGeohash300m: String? + // let enrichedGeohash80m: String? + // let enrichedGeohashPrev300m: String? + // let enrichedGeohashPrev80m: String? + // let enrichedPreviousStopId: String? + // let enrichedPreviousStopList: [String]? + // let enrichedPreviousStopMax: Int? + // let enrichedPreviousStopOrderIdx: Double? + // let enrichedQueryTime: Double? + // let enrichedRouteCoords: [Double]? + // let enrichedRouteDirection: Double? + // let enrichedRouteDoneKm: Double? + // let enrichedRouteLengthKm: Double? + // let enrichedSequenceNo: Int? + // let enrichedStartLat: Double? + // let enrichedStartLng: Double? + // let enrichedStartTime: String? + // let enrichedTimeHash30m: String? + // let enrichedTimeHashDay30m: String? + // let estimatedDebug: [String]? + // let estimatedFeatures: EstimatedFeature? + // let estimatedPreviouslyArrived: Bool? + // let estimatedRecentlyArrived: Bool? + // let estimatedRouteStopId: String? + // let estimatedRouteStopPosition: Double? + // let estimatedTimeofArrival: String? let estimatedTimeofArrivalCorrected: String? - let estimatedUncertainty: String? - let lastGpsTime: String? - let lastReportTime: String? - let lat: Double? - let lng: Double? - let plateNumber: String? - let previousLatitude: Double? - let previousLongitude: Double? - let previousReportTime: String? + // let estimatedUncertainty: String? + // let lastGpsTime: String? + // let lastReportTime: String? + // let lat: Double? + // let lng: Double? + // let plateNumber: String? + // let previousLatitude: Double? + // let previousLongitude: Double? + // let previousReportTime: String? let routeNumber: String? - let state: String? - let timeStamp: String? + // let state: String? + // let timeStamp: String? let variantNumber: Int? - let voyageNumber: Int? + // let voyageNumber: Int? } struct EstimatedFeature: Decodable { - let avgHistorDeltaDistanceKm: Double? - let avgHistorDeltaSeconds: Double? - let avgHistorDeltaSeqNo: Double? - let avgHistorInstSpeedAtPositionKmh: Double? - let avgHistorLongtermSpeedAtPositionKmh: Double? - let correctionFactorLongTerm: Double? - let correctionFactorShortTerm: Double? - let maxHistorDelta: Int? - let minHistorDelta: Int? - let noHistorSamples: Int? - let stdHistorDeltaSeconds: Double? + // let avgHistorDeltaDistanceKm: Double? + // let avgHistorDeltaSeconds: Double? + // let avgHistorDeltaSeqNo: Double? + // let avgHistorInstSpeedAtPositionKmh: Double? + // let avgHistorLongtermSpeedAtPositionKmh: Double? + // let correctionFactorLongTerm: Double? + // let correctionFactorShortTerm: Double? + // let maxHistorDelta: Int? + // let minHistorDelta: Int? + // let noHistorSamples: Int? + // let stdHistorDeltaSeconds: Double? } } From 5ccffdf8ba44247920489788a062dd290663acd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 23:39:22 +0100 Subject: [PATCH 20/63] Reduced spacing --- GeoBus/App/Components/ContentView.swift | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/GeoBus/App/Components/ContentView.swift b/GeoBus/App/Components/ContentView.swift index 5c9dd42e..e96d3598 100644 --- a/GeoBus/App/Components/ContentView.swift +++ b/GeoBus/App/Components/ContentView.swift @@ -11,14 +11,11 @@ import SwiftUI struct ContentView: View { var body: some View { - - VStack(alignment: .trailing, spacing: 0) { - + VStack(spacing: 0) { + ZStack(alignment: .topTrailing) { - MapView() .edgesIgnoringSafeArea(.vertical) - VStack(spacing: 15) { AboutGeoBus() Spacer() @@ -26,14 +23,12 @@ struct ContentView: View { UserLocation() } .padding() - } - + NavBar() .edgesIgnoringSafeArea(.vertical) - } - + } } - + } From 34961f667f9af665e95749220a25351b82ff8662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 21 Oct 2022 23:40:30 +0100 Subject: [PATCH 21/63] . --- GeoBus/App/Components/ContentView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeoBus/App/Components/ContentView.swift b/GeoBus/App/Components/ContentView.swift index e96d3598..34d410e1 100644 --- a/GeoBus/App/Components/ContentView.swift +++ b/GeoBus/App/Components/ContentView.swift @@ -28,7 +28,7 @@ struct ContentView: View { NavBar() .edgesIgnoringSafeArea(.vertical) - } + } } } From 260d2067ff477ab0c19fe2fbca574a5b01d3518e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 22 Oct 2022 22:20:41 +0100 Subject: [PATCH 22/63] Annotations have an UUID() Currently they are not animatable anyways, so why bother? This allows for vehicle to be a class updatable anywhere. --- GeoBus/App/Controllers/MapController.swift | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index a63cf570..72257b26 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -9,7 +9,7 @@ import Foundation import MapKit import SwiftUI -@MainActor +//@MainActor class MapController: ObservableObject { /* * */ @@ -41,7 +41,7 @@ class MapController: ObservableObject { /* To allow the same instance of this class to be available accross the whole app, */ /* we create a Singleton. More info here: https://www.hackingwithswift.com/example-code/language/what-is-a-singleton */ - static let shared = MapController() + public static let shared = MapController() @@ -51,7 +51,11 @@ class MapController: ObservableObject { /* Setup the initial map region on init. */ private init() { - self.region = MKCoordinateRegion(center: initialMapRegion, latitudinalMeters: initialMapZoom, longitudinalMeters: initialMapZoom) + self.region = MKCoordinateRegion( + center: self.initialMapRegion, + latitudinalMeters: self.initialMapZoom, + longitudinalMeters: self.initialMapZoom + ) } @@ -147,7 +151,7 @@ class MapController: ObservableObject { visibleAnnotations.append( GenericMapAnnotation( - id: activeStop.id, + id: UUID(), location: CLLocationCoordinate2D(latitude: activeStop.lat, longitude: activeStop.lng), item: .carris_stop(activeStop) ) @@ -178,7 +182,7 @@ class MapController: ObservableObject { for connection in activeVariant.circularItinerary! { visibleAnnotations.append( GenericMapAnnotation( - id: connection.stop.id, + id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), item: .carris_connection(connection) ) @@ -190,7 +194,7 @@ class MapController: ObservableObject { for connection in activeVariant.ascendingItinerary! { visibleAnnotations.append( GenericMapAnnotation( - id: connection.stop.id, + id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), item: .carris_connection(connection) ) @@ -202,7 +206,7 @@ class MapController: ObservableObject { for connection in activeVariant.descendingItinerary! { visibleAnnotations.append( GenericMapAnnotation( - id: connection.stop.id, + id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), item: .carris_connection(connection) ) @@ -237,7 +241,7 @@ class MapController: ObservableObject { for vehicle in activeVehiclesList { visibleAnnotations.append( GenericMapAnnotation( - id: vehicle.id, + id: UUID(), location: vehicle.coordinate, item: .carris_vehicle(vehicle) ) From 079fba240b97d048fc8bbcacdf5fffff56e21138 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 22 Oct 2022 22:21:49 +0100 Subject: [PATCH 23/63] Centralized place to display sheet contents Appstate is responsible for managing the view shown in the single sheet. It either is presented or not. --- GeoBus.xcodeproj/project.pbxproj | 4 +++ .../App/Components/PresentedSheetView.swift | 32 +++++++++++++++++++ GeoBus/App/Controllers/Appstate.swift | 28 +++++++++++++++- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 GeoBus/App/Components/PresentedSheetView.swift diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index aefdf21b..61ea42bf 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -40,6 +40,7 @@ CF18209B28CCBD5000248F72 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18209A28CCBD5000248F72 /* MapView.swift */; }; CF47994D28D3315E00B56D4B /* Appstate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF47994C28D3315E00B56D4B /* Appstate.swift */; }; CF47994F28D33E1900B56D4B /* Disclaimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF47994E28D33E1900B56D4B /* Disclaimer.swift */; }; + CF47FCB529035D8300AE33B0 /* PresentedSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF47FCB429035D8300AE33B0 /* PresentedSheetView.swift */; }; CF5094C528FC279A00EDD320 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF5094C428FC279A00EDD320 /* Array.swift */; }; CF5094C928FC50AC00EDD320 /* CarrisNetworkController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF5094C828FC50AC00EDD320 /* CarrisNetworkController.swift */; }; CF5094CB28FC50E900EDD320 /* CarrisNetworkModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF5094CA28FC50E900EDD320 /* CarrisNetworkModel.swift */; }; @@ -112,6 +113,7 @@ CF18209A28CCBD5000248F72 /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; CF47994C28D3315E00B56D4B /* Appstate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Appstate.swift; sourceTree = ""; }; CF47994E28D33E1900B56D4B /* Disclaimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Disclaimer.swift; sourceTree = ""; }; + CF47FCB429035D8300AE33B0 /* PresentedSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PresentedSheetView.swift; sourceTree = ""; }; CF5094C428FC279A00EDD320 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; CF5094C828FC50AC00EDD320 /* CarrisNetworkController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrisNetworkController.swift; sourceTree = ""; }; CF5094CA28FC50E900EDD320 /* CarrisNetworkModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarrisNetworkModel.swift; sourceTree = ""; }; @@ -179,6 +181,7 @@ isa = PBXGroup; children = ( CF181FE728CCB7D600248F72 /* ContentView.swift */, + CF47FCB429035D8300AE33B0 /* PresentedSheetView.swift */, CF05F61928CD09A000B4AD58 /* NavBar.swift */, CF18208828CCBD4600248F72 /* Map */, CF05F62528CD60BD00B4AD58 /* SelectRoute */, @@ -558,6 +561,7 @@ CFFFAD8B28F8F33200DFD5FD /* Pulse.swift in Sources */, CF6C918828D3FAF9006C3F61 /* AboutGeoBus.swift in Sources */, CF6C917E28D3ED0A006C3F61 /* SquareButton.swift in Sources */, + CF47FCB529035D8300AE33B0 /* PresentedSheetView.swift in Sources */, CF82BB0B28D7F1C6007F0CDB /* ShareCard.swift in Sources */, CF47994F28D33E1900B56D4B /* Disclaimer.swift in Sources */, CF47994D28D3315E00B56D4B /* Appstate.swift in Sources */, diff --git a/GeoBus/App/Components/PresentedSheetView.swift b/GeoBus/App/Components/PresentedSheetView.swift new file mode 100644 index 00000000..75689a23 --- /dev/null +++ b/GeoBus/App/Components/PresentedSheetView.swift @@ -0,0 +1,32 @@ +// +// PresentedSheetView.swift +// GeoBus +// +// Created by João de Vasconcelos on 22/10/2022. +// + +import SwiftUI + +struct PresentedSheetView: View { + + @EnvironmentObject var appstate: Appstate + + var body: some View { + switch appstate.currentlyPresentedSheetView { + case .carris_RouteSelector: + SelectRouteSheet() + case .carris_RouteDetails: + RouteDetailsSheet() + case .carris_stopSelector: + StopSearchView() + case .carris_vehicleDetails: + VehicleDetailsView() + case .carris_connectionDetails: +// ConnectionDetailsView() + EmptyView() + case .none: + EmptyView() + } + } + +} diff --git a/GeoBus/App/Controllers/Appstate.swift b/GeoBus/App/Controllers/Appstate.swift index b40d977b..23c567a8 100644 --- a/GeoBus/App/Controllers/Appstate.swift +++ b/GeoBus/App/Controllers/Appstate.swift @@ -6,7 +6,7 @@ // import Foundation - +import SwiftUI /* * */ /* MARK: - APPSTATE */ @@ -44,6 +44,29 @@ final class Appstate: ObservableObject { + /* * */ + /* MARK: - SECTION 2: MODULES */ + /* These are the modules that publish state change events. This allows the UI to provide local */ + /* loading or error messages on the relevant functionality, increasing perception of stability. */ + + enum PresentableSheetView { + case carris_RouteSelector + case carris_RouteDetails + case carris_stopSelector + case carris_vehicleDetails + case carris_connectionDetails + } + + public func present(sheet: PresentableSheetView) { + self.currentlyPresentedSheetView = sheet + self.sheetIsPresented = true + } + + public func unpresent() { + self.sheetIsPresented = false + } + + /* * */ /* MARK: - SECTION 3: SHARED INSTANCE */ /* To allow the same instance of this class to be available accross the whole app, */ @@ -70,6 +93,9 @@ final class Appstate: ObservableObject { @Published var vehicles: State = .idle @Published var estimations: State = .idle + @Published var sheetIsPresented: Bool = false + @Published var currentlyPresentedSheetView: PresentableSheetView? = nil + /* * */ From 08931b1b72c114a7f3794d1025b0cf1465634c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 22 Oct 2022 22:23:29 +0100 Subject: [PATCH 24/63] Sheet close is now managed by Appstate --- GeoBus/App/Layout/SheetHeader.swift | 37 +++++++++++++++-------------- 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/GeoBus/App/Layout/SheetHeader.swift b/GeoBus/App/Layout/SheetHeader.swift index 256769aa..37fbebac 100644 --- a/GeoBus/App/Layout/SheetHeader.swift +++ b/GeoBus/App/Layout/SheetHeader.swift @@ -9,24 +9,25 @@ import SwiftUI struct SheetHeader: View { - - let title: Text - @Binding var toggle: Bool - - var body: some View { - VStack { - HStack { - Spacer() - Button(action: { self.toggle = false }) { - Text("Close") + + @EnvironmentObject var appstate: Appstate + + let title: Text + + var body: some View { + VStack { + HStack { + Spacer() + Button(action: { appstate.unpresent() }) { + Text("Close") + .fontWeight(.bold) + } + .padding(25) + } + title + .font(.largeTitle) .fontWeight(.bold) - } - .padding(25) } - title - .font(.largeTitle) - .fontWeight(.bold) - } - .padding(.bottom, 20) - } + .padding(.bottom, 20) + } } From 67f185eba2ed62e504a21a99fb6cd8d40b630e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 22 Oct 2022 22:23:49 +0100 Subject: [PATCH 25/63] Sheet lives in ContentView --- GeoBus/App/Components/ContentView.swift | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/GeoBus/App/Components/ContentView.swift b/GeoBus/App/Components/ContentView.swift index 34d410e1..15359abd 100644 --- a/GeoBus/App/Components/ContentView.swift +++ b/GeoBus/App/Components/ContentView.swift @@ -10,9 +10,10 @@ import SwiftUI struct ContentView: View { + @EnvironmentObject var appstate: Appstate + var body: some View { VStack(spacing: 0) { - ZStack(alignment: .topTrailing) { MapView() .edgesIgnoringSafeArea(.vertical) @@ -24,10 +25,11 @@ struct ContentView: View { } .padding() } - NavBar() .edgesIgnoringSafeArea(.vertical) - + } + .sheet(isPresented: $appstate.sheetIsPresented) { + PresentedSheetView() } } From 296fe17a7719e81798dfb003fa340e6b8a33e1fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 22 Oct 2022 22:26:25 +0100 Subject: [PATCH 26/63] Vehicle is now a class This means references are passed instead of copies of structs --- .../Networks/Carris/CarrisNetworkModel.swift | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift index 7dd0d85a..67acc0a3 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift @@ -141,13 +141,28 @@ struct CarrisNetworkModel { /* MARK: - CARRIS VEHICLE */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia. */ - struct Vehicle: Identifiable, Equatable { + class Vehicle: Identifiable, Equatable { + + static func == (lhs: CarrisNetworkModel.Vehicle, rhs: CarrisNetworkModel.Vehicle) -> Bool { + return false + } + // IDENTIFIER // The unique identifier for this model. let id: Int // Bus Number + init(id: Int, routeNumber: String, lat: Double, lng: Double, previousLatitude: Double, previousLongitude: Double, lastGpsTime: String) { + self.id = id + self.routeNumber = routeNumber + self.lat = lat + self.lng = lng + self.previousLatitude = previousLatitude + self.previousLongitude = previousLongitude + self.lastGpsTime = lastGpsTime + } + /* * */ From e6cc701123ec09e2faf09ff8d9d47ce810b6eabe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 22 Oct 2022 22:26:51 +0100 Subject: [PATCH 27/63] Use appstate sheet system --- GeoBus/App/Components/NavBar.swift | 27 ++++++++----------- .../RouteDetails/RouteDetailsSheet.swift | 5 ++-- .../SelectRoute/FavoriteRoutes.swift | 5 ++-- .../SelectRoute/SelectRouteInput.swift | 5 ++-- .../SelectRoute/SelectRouteSheet.swift | 18 ++++++------- .../Components/SelectRoute/SetOfRoutes.swift | 8 +++--- 6 files changed, 29 insertions(+), 39 deletions(-) diff --git a/GeoBus/App/Components/NavBar.swift b/GeoBus/App/Components/NavBar.swift index a65e9f55..ca47b780 100644 --- a/GeoBus/App/Components/NavBar.swift +++ b/GeoBus/App/Components/NavBar.swift @@ -9,44 +9,39 @@ import SwiftUI import Combine struct NavBar: View { - + + @EnvironmentObject var appstate: Appstate @EnvironmentObject var carrisNetworkController: CarrisNetworkController - + @State var showSelectRouteSheet: Bool = false @State var showRouteDetailsSheet: Bool = false - - + + // This is the route button, the left side of the NavBar. // Depending on the state, the button conveys different information. var routeSelector: some View { Button(action: { - self.showSelectRouteSheet = true + appstate.present(sheet: .carris_RouteSelector) }) { SelectRouteView() } - .sheet(isPresented: $showSelectRouteSheet) { - SelectRouteSheet(isPresentingSheet: $showSelectRouteSheet) - } } - + // This is the route details panel, the right side of the NavBar. // If a route is selected, it's details appear here. If no route is selected, // then it acts as button to choose a route. var routeDetails: some View { Button(action: { if (carrisNetworkController.activeRoute != nil) { - showRouteDetailsSheet = true + appstate.present(sheet: .carris_RouteDetails) } else { - showSelectRouteSheet = true + appstate.present(sheet: .carris_RouteSelector) } }) { RouteDetailsView() } - .sheet(isPresented: $showRouteDetailsSheet) { - RouteDetailsSheet(showRouteDetailsSheet: $showRouteDetailsSheet) - } } - + // The composed final view for the NavBar. // It encompasses the route selector and the route details panel, // as well as the current app version. @@ -64,5 +59,5 @@ struct NavBar: View { .frame(height: 120) .background(Color("BackgroundSecondary")) } - + } diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift index 3289efdf..7c05d3d1 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift @@ -10,10 +10,9 @@ import SwiftUI struct RouteDetailsSheet: View { + @EnvironmentObject var appstate: Appstate @EnvironmentObject var carrisNetworkController: CarrisNetworkController - @Binding var showRouteDetailsSheet: Bool - @State var routeDirection: Int = 0 @State var routeDirectionPicker: Int = 0 @@ -22,7 +21,7 @@ struct RouteDetailsSheet: View { VStack(spacing: 15) { - SheetHeader(title: Text("Route Details"), toggle: $showRouteDetailsSheet) + SheetHeader(title: Text("Route Details")) HStack(spacing: 25) { RouteBadgeSquare(routeNumber: carrisNetworkController.activeRoute!.number) diff --git a/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift b/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift index a44a7a51..2b8113b1 100644 --- a/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift +++ b/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift @@ -12,10 +12,9 @@ struct FavoriteRoutes: View { @Environment(\.colorScheme) var colorScheme: ColorScheme + @EnvironmentObject var appstate: Appstate @EnvironmentObject var carrisNetworkController: CarrisNetworkController - @Binding var showSelectRouteSheet: Bool - @State var routes: [CarrisNetworkModel.Route] = [] @@ -38,7 +37,7 @@ struct FavoriteRoutes: View { Button(action: { _ = self.carrisNetworkController.select(route: route.number) Analytics.shared.capture(event: .Routes_Select_FromFavorites, properties: ["routeNumber": route.number]) - self.showSelectRouteSheet = false + appstate.unpresent() }){ RouteBadgeSquare(routeNumber: route.number) } diff --git a/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift b/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift index 168a3a6e..d69c1230 100644 --- a/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift +++ b/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift @@ -10,10 +10,9 @@ import SwiftUI struct SelectRouteInput: View { + @EnvironmentObject var appstate: Appstate @EnvironmentObject var carrisNetworkController: CarrisNetworkController - @Binding var showSheet: Bool - @State var showErrorLabel: Bool = false @State var routeNumber = "" @@ -33,7 +32,7 @@ struct SelectRouteInput: View { let success = carrisNetworkController.select(route: self.routeNumber.uppercased()) if success { Analytics.shared.capture(event: .Routes_Select_FromTextInput, properties: ["routeNumber": self.routeNumber.uppercased()]) - self.showSheet = false + appstate.unpresent() } else { self.showErrorLabel = true } diff --git a/GeoBus/App/Components/SelectRoute/SelectRouteSheet.swift b/GeoBus/App/Components/SelectRoute/SelectRouteSheet.swift index 3880eefb..4c00ea3d 100644 --- a/GeoBus/App/Components/SelectRoute/SelectRouteSheet.swift +++ b/GeoBus/App/Components/SelectRoute/SelectRouteSheet.swift @@ -10,28 +10,26 @@ import SwiftUI struct SelectRouteSheet: View { - @Binding var isPresentingSheet: Bool - - + var body: some View { ScrollView(.vertical, showsIndicators: true) { VStack(spacing: 30) { - SheetHeader(title: Text("Find Routes"), toggle: $isPresentingSheet) + SheetHeader(title: Text("Find Routes")) - SelectRouteInput(showSheet: $isPresentingSheet) + SelectRouteInput() .padding(.horizontal) Divider() VStack(spacing: 30) { - FavoriteRoutes(showSelectRouteSheet: $isPresentingSheet) - SetOfRoutes(title: Text("Trams"), kind: .tram, showSheet: $isPresentingSheet) - SetOfRoutes(title: Text("Neighborhood Buses"), kind: .neighborhood, showSheet: $isPresentingSheet) - SetOfRoutes(title: Text("Night Buses"), kind: .night, showSheet: $isPresentingSheet) - SetOfRoutes(title: Text("Regular Service"), kind: .regular, showSheet: $isPresentingSheet) + FavoriteRoutes() + SetOfRoutes(title: Text("Trams"), kind: .tram) + SetOfRoutes(title: Text("Neighborhood Buses"), kind: .neighborhood) + SetOfRoutes(title: Text("Night Buses"), kind: .night) + SetOfRoutes(title: Text("Regular Service"), kind: .regular) } .padding(.horizontal) diff --git a/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift b/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift index 60c7a4a9..903b46bf 100644 --- a/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift +++ b/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift @@ -10,12 +10,12 @@ import SwiftUI struct SetOfRoutes: View { + @EnvironmentObject var appstate: Appstate @EnvironmentObject var carrisNetworkController: CarrisNetworkController - var title: Text - var kind: CarrisNetworkModel.Kind + let title: Text + let kind: CarrisNetworkModel.Kind - @Binding var showSheet: Bool @State private var routes: [CarrisNetworkModel.Route] = [] var body: some View { @@ -35,7 +35,7 @@ struct SetOfRoutes: View { Button(action: { _ = self.carrisNetworkController.select(route: route.number) Analytics.shared.capture(event: .Routes_Select_FromList, properties: ["routeNumber": route.number]) - self.showSheet = false + appstate.unpresent() }){ RouteBadgeSquare(routeNumber: route.number) } From 06f660aec286b9a6e4dbec5cdb76307524591aed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 22 Oct 2022 22:27:26 +0100 Subject: [PATCH 28/63] Use appstate sheet system --- .../App/Components/Map/MapAnnotations.swift | 21 +++----- .../StopDetails/SearchStopInput.swift | 4 +- .../Components/StopDetails/StopSearch.swift | 45 +++++++++-------- .../VehicleDetails/VehicleDetailsView.swift | 50 +++++++++---------- 4 files changed, 58 insertions(+), 62 deletions(-) diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index c948fe05..fe74cdc5 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -13,7 +13,7 @@ import SwiftUI struct GenericMapAnnotation: Identifiable { - let id: Int + let id: UUID var location: CLLocationCoordinate2D var item: AnnotationItem @@ -101,13 +101,17 @@ struct CarrisVehicleAnnotationView: View { let vehicle: CarrisNetworkModel.Vehicle + @EnvironmentObject var appstate: Appstate + @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @State private var isPresented: Bool = false @State private var viewSize = CGSize() var body: some View { Button(action: { - self.isPresented = true + carrisNetworkController.select(vehicle: vehicle.id) + appstate.present(sheet: .carris_vehicleDetails) TapticEngine.impact.feedback(.light) }) { ZStack(alignment: .init(horizontal: .leading, vertical: .center)) { @@ -127,19 +131,6 @@ struct CarrisVehicleAnnotationView: View { .frame(width: 40, height: 40, alignment: .center) .rotationEffect(.radians(vehicle.angleInRadians ?? 0)) .animation(.default, value: vehicle.angleInRadians) - .sheet(isPresented: $isPresented) { - VStack(alignment: .leading) { - VehicleDetailsView(vehicle: self.vehicle) - .padding(.bottom, 20) - Disclaimer() - .padding(.horizontal) - .padding(.bottom, 10) - } - .readSize { size in - viewSize = size - } - .presentationDetents([.height(viewSize.height)]) - } } } diff --git a/GeoBus/App/Components/StopDetails/SearchStopInput.swift b/GeoBus/App/Components/StopDetails/SearchStopInput.swift index db57c1e7..1c302dc3 100644 --- a/GeoBus/App/Components/StopDetails/SearchStopInput.swift +++ b/GeoBus/App/Components/StopDetails/SearchStopInput.swift @@ -10,9 +10,9 @@ import SwiftUI struct SearchStopInput: View { + @EnvironmentObject var appstate: Appstate @EnvironmentObject var carrisNetworkController: CarrisNetworkController - @Binding var showSheet: Bool @FocusState private var stopIdInputIsFocused: Bool @State private var showErrorLabel: Bool = false @@ -42,8 +42,8 @@ struct SearchStopInput: View { Button(action: { let success = self.carrisNetworkController.select(stop: Int(self.stopPublicId.uppercased()) ?? -1) if success { - self.showSheet = false Analytics.shared.capture(event: .Stops_Select_FromTextInput, properties: ["stopPublicId": self.stopPublicId.uppercased()]) + appstate.unpresent() } else { self.showErrorLabel = true } diff --git a/GeoBus/App/Components/StopDetails/StopSearch.swift b/GeoBus/App/Components/StopDetails/StopSearch.swift index 923ecc3d..f0aa54ac 100644 --- a/GeoBus/App/Components/StopDetails/StopSearch.swift +++ b/GeoBus/App/Components/StopDetails/StopSearch.swift @@ -9,32 +9,37 @@ import SwiftUI struct StopSearch: View { - @State var showSearchStopSheet: Bool = false - @State private var viewSize = CGSize() - + @EnvironmentObject var appstate: Appstate var body: some View { SquareButton(icon: "mail.and.text.magnifyingglass", size: 26) .onTapGesture() { TapticEngine.impact.feedback(.medium) - self.showSearchStopSheet = true + appstate.present(sheet: .carris_stopSelector) + } + } +} + + +struct StopSearchView: View { + + @State private var viewSize = CGSize() + + var body: some View { + ScrollView() { + VStack { + Text("Search Stop") + .font(.largeTitle) + .fontWeight(.bold) + .padding(.vertical, 30) + SearchStopInput() } - .sheet(isPresented: self.$showSearchStopSheet) { - ScrollView() { - VStack { - Text("Search Stop") - .font(.largeTitle) - .fontWeight(.bold) - .padding(.vertical, 30) - SearchStopInput(showSheet: $showSearchStopSheet) - } - .padding() - .readSize { size in - viewSize = size - } - } - .background(Color("BackgroundPrimary")) - .presentationDetents([.height(viewSize.height)]) + .padding() + .readSize { size in + viewSize = size } + } + .background(Color("BackgroundPrimary")) + .presentationDetents([.height(viewSize.height)]) } } diff --git a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift index 978fc63f..37b1af40 100644 --- a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift +++ b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift @@ -10,15 +10,13 @@ import Combine struct VehicleDetailsView: View { - let vehicle: CarrisNetworkModel.Vehicle - @EnvironmentObject var appstate: Appstate @EnvironmentObject var carrisNetworkController: CarrisNetworkController - let refreshTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() let lastSeenTimeTimer = Timer.publish(every: 1 /* seconds */, on: .main, in: .common).autoconnect() @State var lastSeenTime: String = "-" + @State private var viewSize = CGSize() var loadingScreen: some View { @@ -40,9 +38,9 @@ struct VehicleDetailsView: View { var vehicleDetailsHeader: some View { HStack(spacing: 15) { - VehicleDestination(routeNumber: vehicle.routeNumber ?? "-", destination: vehicle.lastStopOnVoyageName ?? "-") + VehicleDestination(routeNumber: carrisNetworkController.activeVehicle?.routeNumber ?? "-", destination: carrisNetworkController.activeVehicle?.lastStopOnVoyageName ?? "-") Spacer() - VehicleIdentifier(busNumber: vehicle.id, vehiclePlate: vehicle.vehiclePlate) + VehicleIdentifier(busNumber: carrisNetworkController.activeVehicle?.id ?? -1, vehiclePlate: carrisNetworkController.activeVehicle?.vehiclePlate) } } @@ -57,10 +55,10 @@ struct VehicleDetailsView: View { .font(.system(size: 12, weight: .bold, design: .default)) .foregroundColor(Color(.secondaryLabel)) .onAppear() { - self.lastSeenTime = Helpers.getTimeString(for: vehicle.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) + self.lastSeenTime = Helpers.getTimeString(for: carrisNetworkController.activeVehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) } .onReceive(lastSeenTimeTimer) { event in - self.lastSeenTime = Helpers.getTimeString(for: vehicle.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) + self.lastSeenTime = Helpers.getTimeString(for: carrisNetworkController.activeVehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) } Spacer() } @@ -69,27 +67,29 @@ struct VehicleDetailsView: View { var body: some View { - VStack(alignment: .leading, spacing: 0) { - if (appstate.vehicles == .loading) { - loadingScreen - .padding() - } else if (appstate.vehicles == .error) { - errorScreen - .padding() - } else { - vehicleDetailsHeader - .padding() - Divider() - vehicleDetailsScreen - .padding() + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 0) { + if (appstate.vehicles == .loading) { + loadingScreen + .padding() + } else if (appstate.vehicles == .error) { + errorScreen + .padding() + } else { + vehicleDetailsHeader + .padding() + Divider() + vehicleDetailsScreen + .padding() + } } +// Disclaimer() +// Spacer() } - .onAppear() { - carrisNetworkController.getAdditionalDetailsFor(vehicle: self.vehicle.id) - } - .onReceive(refreshTimer) { event in - carrisNetworkController.getAdditionalDetailsFor(vehicle: self.vehicle.id) + .readSize { size in + viewSize = size } + .presentationDetents([.height(viewSize.height)]) } From 9b9cd0a1018b6c06dba4c079e40a3f9336203471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 22 Oct 2022 22:28:30 +0100 Subject: [PATCH 29/63] Fix for debug buttons --- GeoBus/App/Components/StopDetails/StopDetailsView.swift | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/GeoBus/App/Components/StopDetails/StopDetailsView.swift b/GeoBus/App/Components/StopDetails/StopDetailsView.swift index e0793d4b..2a5795be 100644 --- a/GeoBus/App/Components/StopDetails/StopDetailsView.swift +++ b/GeoBus/App/Components/StopDetails/StopDetailsView.swift @@ -11,6 +11,9 @@ struct ConnectionDetailsView: View { let connection: CarrisNetworkModel.Connection + @EnvironmentObject var appstate: Appstate + @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @State private var viewSize = CGSize() var body: some View { @@ -174,9 +177,9 @@ struct ConnectionDetailsView2: View { content .padding([.horizontal, .bottom]) .padding(.top, 7) + providerToggle + .padding() } - providerToggle - .padding() } .background( canToggle From e559482989df7f3d1e16758aadb0cf3fed112f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 22 Oct 2022 22:29:37 +0100 Subject: [PATCH 30/63] Ability to select vehicles --- .../Carris/CarrisNetworkController.swift | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index bfe61589..6100f961 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -48,6 +48,7 @@ class CarrisNetworkController: ObservableObject { @Published var activeConnection: CarrisNetworkModel.Connection? = nil @Published var activeStop: CarrisNetworkModel.Stop? = nil @Published var activeVehicles: [CarrisNetworkModel.Vehicle] = [] + @Published var activeVehicle: CarrisNetworkModel.Vehicle? = nil @Published var favorites_routes: [CarrisNetworkModel.Route] = [] @Published var favorites_stops: [CarrisNetworkModel.Stop] = [] @@ -61,7 +62,7 @@ class CarrisNetworkController: ObservableObject { /* To allow the same instance of this class to be available accross the whole app, */ /* we create a Singleton. More info here: https://www.hackingwithswift.com/example-code/language/what-is-a-singleton */ - static let shared = CarrisNetworkController() + public static let shared = CarrisNetworkController() @@ -665,6 +666,18 @@ class CarrisNetworkController: ObservableObject { } + public func select(vehicle vehicleId: Int?) { + if (vehicleId != nil) { + if let foundVehicle = self.find(vehicle: vehicleId!) { + Task { + await self.fetchVehicleDetailsFromCarrisAPI(for: foundVehicle.id) + self.activeVehicle = foundVehicle + } + } + } + } + + /* * */ /* MARK: - SECTION 11: SET ACTIVE VEHICLES */ @@ -698,6 +711,8 @@ class CarrisNetworkController: ObservableObject { } +// self.select(vehicle: activeVehicle?.id) + } @@ -780,12 +795,15 @@ class CarrisNetworkController: ObservableObject { /* function to allow the UI to request this information only when necessary. After retrieving the new details */ /* fromt the API, re-populate the activeVehicles array to trigger an update in the UI. */ - public func getAdditionalDetailsFor(vehicle vehicleId: Int) { - Task { - await self.fetchVehicleDetailsFromCarrisAPI(for: vehicleId) - self.populateActiveVehicles() - } - } +// public func getAdditionalDetailsForActiveVehicle() { +// Task { +// if (activeVehicle != nil) { +// await self.fetchVehicleDetailsFromCarrisAPI(for: self.activeVehicle!.id) +// self.populateActiveVehicles() +// self.select(vehicle: activeVehicle!.id) +// } +// } +// } private func fetchVehicleDetailsFromCarrisAPI(for vehicleId: Int) async { From 1c004288433aaf363c1090cffd88e1853da518ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Fri, 28 Oct 2022 12:03:43 +0100 Subject: [PATCH 31/63] Can click estimates to select the respective bus --- GeoBus/App/Components/ContentView.swift | 2 +- .../App/Components/Map/MapAnnotations.swift | 46 ++--- GeoBus/App/Components/Map/MapView.swift | 9 +- .../App/Components/PresentedSheetView.swift | 37 +++- .../RouteDetails/ConnectionsList.swift | 4 +- .../RouteDetails/RouteDetailsSheet.swift | 2 +- .../SelectRoute/SelectRouteSheet.swift | 2 +- .../StopDetails/SearchStopInput.swift | 2 +- .../StopDetails/StopDetailsView.swift | 188 +++++------------- .../StopDetails/StopEstimations.swift | 150 ++++++++++++-- .../Components/StopDetails/StopSearch.swift | 9 +- .../VehicleDetails/VehicleDetailsView.swift | 9 +- GeoBus/App/Controllers/Appstate.swift | 13 +- GeoBus/App/Controllers/MapController.swift | 110 +++++++++- .../Carris/CarrisNetworkController.swift | 55 +++-- GeoBus/App/Layout/StopIcon.swift | 31 +-- 16 files changed, 416 insertions(+), 253 deletions(-) diff --git a/GeoBus/App/Components/ContentView.swift b/GeoBus/App/Components/ContentView.swift index 15359abd..776a4f43 100644 --- a/GeoBus/App/Components/ContentView.swift +++ b/GeoBus/App/Components/ContentView.swift @@ -10,7 +10,7 @@ import SwiftUI struct ContentView: View { - @EnvironmentObject var appstate: Appstate + @ObservedObject var appstate = Appstate.shared var body: some View { VStack(spacing: 0) { diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index fe74cdc5..026a2350 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -33,33 +33,19 @@ struct CarrisStopAnnotationView: View { public let stop: CarrisNetworkModel.Stop - @State private var isAnnotationSelected: Bool = false + @ObservedObject var appstate = Appstate.shared + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared var body: some View { Button(action: { - self.isAnnotationSelected = true TapticEngine.impact.feedback(.light) + carrisNetworkController.select(stop: self.stop) + appstate.present(sheet: .carris_stopDetails) }) { - StopIcon( - orderInRoute: 0, - direction: .circular, - isSelected: self.isAnnotationSelected - ) + StopIcon(isSelected: carrisNetworkController.activeStop?.id == self.stop.id) } .frame(width: 40, height: 40, alignment: .center) - .onAppear() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.isAnnotationSelected = true - } - } - .sheet(isPresented: $isAnnotationSelected, onDismiss: { - withAnimation(.easeInOut(duration: 0.1)) { - self.isAnnotationSelected = false - } - }) { - StopDetailsView(stop: self.stop) - } } } @@ -69,26 +55,23 @@ struct CarrisConnectionAnnotationView: View { public let connection: CarrisNetworkModel.Connection - @State private var isAnnotationSelected: Bool = false + @ObservedObject var appstate = Appstate.shared + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared var body: some View { Button(action: { - self.isAnnotationSelected = true TapticEngine.impact.feedback(.light) + carrisNetworkController.select(connection: self.connection) + appstate.present(sheet: .carris_connectionDetails) }) { StopIcon( orderInRoute: self.connection.orderInRoute, direction: self.connection.direction, - isSelected: self.isAnnotationSelected + isSelected: carrisNetworkController.activeConnection == self.connection ) } .frame(width: 40, height: 40, alignment: .center) - .sheet(isPresented: $isAnnotationSelected, onDismiss: { - self.isAnnotationSelected = false - }) { - ConnectionDetailsView(connection: self.connection) - } } } @@ -101,18 +84,15 @@ struct CarrisVehicleAnnotationView: View { let vehicle: CarrisNetworkModel.Vehicle - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController - - @State private var isPresented: Bool = false - @State private var viewSize = CGSize() + @ObservedObject var appstate = Appstate.shared + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared var body: some View { Button(action: { + TapticEngine.impact.feedback(.light) carrisNetworkController.select(vehicle: vehicle.id) appstate.present(sheet: .carris_vehicleDetails) - TapticEngine.impact.feedback(.light) }) { ZStack(alignment: .init(horizontal: .leading, vertical: .center)) { switch (vehicle.kind) { diff --git a/GeoBus/App/Components/Map/MapView.swift b/GeoBus/App/Components/Map/MapView.swift index bbbeab31..e4b430f3 100644 --- a/GeoBus/App/Components/Map/MapView.swift +++ b/GeoBus/App/Components/Map/MapView.swift @@ -12,8 +12,8 @@ import MapKit struct MapView: View { - @EnvironmentObject var mapController: MapController - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject var mapController = MapController.shared + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared var body: some View { @@ -46,6 +46,11 @@ struct MapView: View { self.mapController.updateAnnotations(with: newVariant!) } } +// .onChange(of: carrisNetworkController.activeVehicle) { newVehicle in +// if (newVehicle != nil) { +// self.mapController.updateAnnotations(with: newVehicle!) +// } +// } .onChange(of: carrisNetworkController.activeVehicles) { newVehiclesList in self.mapController.updateAnnotations(with: newVehiclesList) } diff --git a/GeoBus/App/Components/PresentedSheetView.swift b/GeoBus/App/Components/PresentedSheetView.swift index 75689a23..3fe111f4 100644 --- a/GeoBus/App/Components/PresentedSheetView.swift +++ b/GeoBus/App/Components/PresentedSheetView.swift @@ -9,23 +9,54 @@ import SwiftUI struct PresentedSheetView: View { - @EnvironmentObject var appstate: Appstate + @ObservedObject var appstate = Appstate.shared + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared var body: some View { switch appstate.currentlyPresentedSheetView { + case .carris_RouteSelector: SelectRouteSheet() + .presentationDetents([.large]) + .presentationDragIndicator(.hidden) + case .carris_RouteDetails: RouteDetailsSheet() + .presentationDetents([.large]) + .presentationDragIndicator(.hidden) + case .carris_stopSelector: StopSearchView() + .presentationDetents([.medium]) + .presentationDragIndicator(.hidden) + case .carris_vehicleDetails: VehicleDetailsView() + .presentationDetents([.medium]) + .presentationDragIndicator(.hidden) + .onDisappear() { + carrisNetworkController.deselect([.vehicle]) + } + case .carris_connectionDetails: -// ConnectionDetailsView() - EmptyView() + ConnectionSheetView() + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.hidden) + .onDisappear() { + carrisNetworkController.deselect([.connection]) + } + + case .carris_stopDetails: + StopSheetView() + .presentationDetents([.medium, .large]) + .presentationDragIndicator(.hidden) + .onDisappear() { + carrisNetworkController.deselect([.stop]) + } + case .none: EmptyView() + } } diff --git a/GeoBus/App/Components/RouteDetails/ConnectionsList.swift b/GeoBus/App/Components/RouteDetails/ConnectionsList.swift index cccc6997..23e097dd 100644 --- a/GeoBus/App/Components/RouteDetails/ConnectionsList.swift +++ b/GeoBus/App/Components/RouteDetails/ConnectionsList.swift @@ -16,9 +16,9 @@ struct ConnectionsList: View { var body: some View { VStack(alignment: .leading, spacing: 15) { ForEach(connections) { connection in - ConnectionDetailsView2( + StopDetailsView( canToggle: true, - publicId: connection.stop.id, + stopId: connection.stop.id, name: connection.stop.name, orderInRoute: connection.orderInRoute, direction: connection.direction diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift index 7c05d3d1..ce8ac52e 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift @@ -85,7 +85,7 @@ struct RouteDetailsSheet: View { var body: some View { - ScrollView(.vertical, showsIndicators: true) { + ScrollView(showsIndicators: true) { VStack(spacing: 5) { liveInfo .padding() diff --git a/GeoBus/App/Components/SelectRoute/SelectRouteSheet.swift b/GeoBus/App/Components/SelectRoute/SelectRouteSheet.swift index 4c00ea3d..c12d3339 100644 --- a/GeoBus/App/Components/SelectRoute/SelectRouteSheet.swift +++ b/GeoBus/App/Components/SelectRoute/SelectRouteSheet.swift @@ -13,7 +13,7 @@ struct SelectRouteSheet: View { var body: some View { - ScrollView(.vertical, showsIndicators: true) { + ScrollView(showsIndicators: true) { VStack(spacing: 30) { diff --git a/GeoBus/App/Components/StopDetails/SearchStopInput.swift b/GeoBus/App/Components/StopDetails/SearchStopInput.swift index 1c302dc3..8c1ea83c 100644 --- a/GeoBus/App/Components/StopDetails/SearchStopInput.swift +++ b/GeoBus/App/Components/StopDetails/SearchStopInput.swift @@ -43,7 +43,7 @@ struct SearchStopInput: View { let success = self.carrisNetworkController.select(stop: Int(self.stopPublicId.uppercased()) ?? -1) if success { Analytics.shared.capture(event: .Stops_Select_FromTextInput, properties: ["stopPublicId": self.stopPublicId.uppercased()]) - appstate.unpresent() + appstate.present(sheet: .carris_stopDetails) } else { self.showErrorLabel = true } diff --git a/GeoBus/App/Components/StopDetails/StopDetailsView.swift b/GeoBus/App/Components/StopDetails/StopDetailsView.swift index 2a5795be..362b5087 100644 --- a/GeoBus/App/Components/StopDetails/StopDetailsView.swift +++ b/GeoBus/App/Components/StopDetails/StopDetailsView.swift @@ -7,177 +7,105 @@ import SwiftUI -struct ConnectionDetailsView: View { - - let connection: CarrisNetworkModel.Connection - - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + +struct ConnectionSheetView: View { - @State private var viewSize = CGSize() + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared - var body: some View { - VStack(alignment: .leading) { - ConnectionDetailsView2( + var body: some View { + ScrollView { + StopDetailsView( canToggle: false, - publicId: connection.stop.id, - name: connection.stop.name, - orderInRoute: connection.orderInRoute, - direction: connection.direction - ) - .padding(.bottom, 20) - Disclaimer() - .padding(.horizontal) - .padding(.bottom, 10) - } - .readSize { size in - viewSize = size - } - .presentationDetents([.height(viewSize.height)]) - } + stopId: carrisNetworkController.activeConnection?.stop.id ?? 0, + name: carrisNetworkController.activeConnection?.stop.name ?? "-", + orderInRoute: carrisNetworkController.activeConnection?.orderInRoute, + direction: carrisNetworkController.activeConnection?.direction + ) + } + } + } -struct StopDetailsView: View { +struct StopSheetView: View { - let stop: CarrisNetworkModel.Stop - - @State private var viewSize = CGSize() + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared var body: some View { - VStack(alignment: .leading) { - ConnectionDetailsView2( + ScrollView { + StopDetailsView( canToggle: false, - publicId: stop.id, - name: stop.name, - orderInRoute: 0, - direction: .circular + stopId: carrisNetworkController.activeStop?.id ?? 0, + name: carrisNetworkController.activeStop?.name ?? "-", + orderInRoute: nil, + direction: nil ) - .padding(.bottom, 20) - Disclaimer() - .padding(.horizontal) - .padding(.bottom, 10) - } - .readSize { size in - viewSize = size } - .presentationDetents([.height(viewSize.height)]) } + } - - - -struct ConnectionDetailsView2: View { - - @Environment(\.colorScheme) var colorScheme: ColorScheme - - @EnvironmentObject var carrisNetworkController: CarrisNetworkController +struct StopDetailsHeader: View { - let refreshTimer = Timer.publish(every: 60 /* seconds */, on: .main, in: .common).autoconnect() - - let canToggle: Bool - let publicId: Int + let stopId: Int let name: String let orderInRoute: Int? let direction: CarrisNetworkModel.Direction? - @State private var isOpen = false - @State private var estimations: [CarrisNetworkModel.Estimation]? = nil - - - func getEstimationsFromController() { - Task { - self.estimations = await carrisNetworkController.getEstimation(for: self.publicId) - } - } - - - var fixedHeader: some View { + var body: some View { HStack(spacing: 15) { - StopIcon(orderInRoute: self.orderInRoute ?? 0, direction: self.direction ?? .circular) + StopIcon(orderInRoute: self.orderInRoute, direction: self.direction) Text(name) .fontWeight(.medium) .foregroundColor(Color(.label)) .multilineTextAlignment(.leading) Spacer() - Text(String(publicId)) + Text(String(self.stopId)) .font(Font.system(size: 12, weight: .medium, design: .default) ) .foregroundColor(Color(.secondaryLabel)) .padding(.vertical, 2) .padding(.horizontal, 7) - .background(colorScheme == .dark ? Color(.secondarySystemFill) : Color(.secondarySystemBackground)) + .background(Color(.secondarySystemFill)) .cornerRadius(10) } + .padding() } +} + + + + +struct StopDetailsView: View { + let canToggle: Bool - var content: some View { - StopEstimations(estimations: self.estimations) - .onAppear() { - // Get estimations when view appears - self.getEstimationsFromController() - } - .onReceive(refreshTimer) { event in - // Update estimations on timer call - self.getEstimationsFromController() - } - } + let stopId: Int + let name: String + let orderInRoute: Int? + let direction: CarrisNetworkModel.Direction? - // DEBUG!!! - var providerToggle: some View { - VStack { - Toggle(isOn: $carrisNetworkController.communityDataProviderStatus) { - HStack { - Image(systemName: "staroflife.circle") - .renderingMode(.template) - .font(Font.system(size: 25)) - .foregroundColor(.teal) - Text("Community Data") - .font(Font.system(size: 18, weight: .bold)) - .foregroundColor(.teal) - .padding(.leading, 5) - } - } - .padding() - .frame(maxWidth: .infinity) - .tint(.teal) - .background(.teal.opacity(0.05)) - .cornerRadius(10) - .onChange(of: carrisNetworkController.communityDataProviderStatus) { value in - carrisNetworkController.toggleCommunityDataProviderTo(to: value) - self.getEstimationsFromController() - } - - Button(action: getEstimationsFromController, label: { - Text("Reload Estimate") - .font(Font.system(size: 15, weight: .bold, design: .default) ) - .foregroundColor(Color(.white)) - .padding(5) - .frame(maxWidth: .infinity) - .background(Color(.systemBlue)) - .cornerRadius(10) - }) - } - } + @State private var isOpen = false + + @Environment(\.colorScheme) var colorScheme: ColorScheme var body: some View { - VStack(spacing: 0) { - // The header of the view is always visible - fixedHeader - .padding() - // Estimations are visible only when the view is opened + VStack(alignment: .leading, spacing: 0) { + Button(action: { + if (canToggle) { + self.isOpen = !self.isOpen + TapticEngine.impact.feedback(.medium) + } + }, label: { + StopDetailsHeader(stopId: self.stopId, name: self.name, orderInRoute: self.orderInRoute, direction: self.direction) + }) if (isOpen || !canToggle) { Divider() - content - .padding([.horizontal, .bottom]) - .padding(.top, 7) - providerToggle + EstimationsContainer(stopId: self.stopId) .padding() } } @@ -189,13 +117,7 @@ struct ConnectionDetailsView2: View { : Color.clear ) .cornerRadius(10) - .onTapGesture { - if (canToggle) { - // If the view can be opened and closed - self.isOpen = !self.isOpen - TapticEngine.impact.feedback(.medium) - } - } } } + diff --git a/GeoBus/App/Components/StopDetails/StopEstimations.swift b/GeoBus/App/Components/StopDetails/StopEstimations.swift index 0a14922d..512ca302 100644 --- a/GeoBus/App/Components/StopDetails/StopEstimations.swift +++ b/GeoBus/App/Components/StopDetails/StopEstimations.swift @@ -8,14 +8,91 @@ import SwiftUI -struct StopEstimations: View { +struct EstimationsContainer: View { - @EnvironmentObject var appstate: Appstate + let stopId: Int - let estimations: [CarrisNetworkModel.Estimation]? + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + + @State var estimations: [CarrisNetworkModel.Estimation]? + + let refreshTimer = Timer.publish(every: 60 /* seconds */, on: .main, in: .common).autoconnect() + + + func getEstimationsFromController() { + Task { + self.estimations = await carrisNetworkController.getEstimation(for: self.stopId) + } + } + + + var body: some View { + VStack(alignment: .leading, spacing: 10) { + EstimationsHeader() + EstimationsList(estimations: self.estimations) + .onAppear(perform: self.getEstimationsFromController) + .onReceive(refreshTimer) { event in + self.getEstimationsFromController() + } + debug_providerToggle + .padding(.vertical) + Disclaimer() + .padding(.vertical) + } + } - var fixedInfo: some View { + + // ! DEBUG + var debug_providerToggle: some View { + VStack { + Toggle(isOn: $carrisNetworkController.communityDataProviderStatus) { + HStack { + Image(systemName: "staroflife.circle") + .renderingMode(.template) + .font(Font.system(size: 25)) + .foregroundColor(.teal) + Text("Community Data") + .font(Font.system(size: 18, weight: .bold)) + .foregroundColor(.teal) + .padding(.leading, 5) + } + } + .padding() + .frame(maxWidth: .infinity) + .tint(.teal) + .background(.teal.opacity(0.05)) + .cornerRadius(10) + .onChange(of: carrisNetworkController.communityDataProviderStatus) { value in + carrisNetworkController.toggleCommunityDataProviderTo(to: value) + self.estimations = nil + self.getEstimationsFromController() + } + Button(action: { + self.estimations = nil + self.getEstimationsFromController() + }, label: { + Text("Reload Estimate") + .font(Font.system(size: 15, weight: .bold, design: .default) ) + .foregroundColor(Color(.white)) + .padding(5) + .frame(maxWidth: .infinity) + .background(Color(.systemBlue)) + .cornerRadius(10) + }) + } + } + + + +} + + + + +struct EstimationsHeader: View { + + var body: some View { HStack { Text("Next on this stop:") .font(Font.system(size: 10, weight: .bold, design: .default) ) @@ -26,6 +103,19 @@ struct StopEstimations: View { } } +} + + + + + + + +struct EstimationsList: View { + + let estimations: [CarrisNetworkModel.Estimation]? + + var loadingScreen: some View { HStack(spacing: 3) { ProgressView() @@ -37,17 +127,6 @@ struct StopEstimations: View { } } - var estimationsList: some View { - VStack(spacing: 12) { - ForEach(estimations!) { estimation in - HStack(spacing: 5) { - VehicleDestination(routeNumber: estimation.routeNumber, destination: estimation.destination) - Spacer() - TimeLeft(time: estimation.eta) - } - } - } - } var noResultsScreen: some View { Text("No estimations available for this stop.") @@ -55,24 +134,24 @@ struct StopEstimations: View { .foregroundColor(Color(.secondaryLabel)) } - var errorScreen: some View { - Text("Carris API is unavailable.") - .font(Font.system(size: 13, weight: .medium, design: .default) ) - .foregroundColor(Color(.secondaryLabel)) + + var estimationsList: some View { + VStack(spacing: 12) { + ForEach(estimations!) { estimation in + EstimationContainer(estimation: estimation) + } + } } var body: some View { VStack(alignment: .leading, spacing: 10) { - fixedInfo if (estimations != nil) { if (estimations!.count > 0) { estimationsList } else { noResultsScreen } - } else if (appstate.estimations == .error) { - errorScreen } else { loadingScreen } @@ -82,3 +161,30 @@ struct StopEstimations: View { } + + + + +struct EstimationContainer: View { + + let estimation: CarrisNetworkModel.Estimation + + @ObservedObject var appstate = Appstate.shared + @ObservedObject var mapController = MapController.shared + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + + var body: some View { + Button(action: { + carrisNetworkController.select(vehicle: estimation.busNumber) +// mapController.moveMap(to:) + appstate.present(sheet: .carris_vehicleDetails) + }, label: { + HStack(spacing: 5) { + VehicleDestination(routeNumber: estimation.routeNumber, destination: estimation.destination) + Spacer() + TimeLeft(time: estimation.eta) + } + }) + } +} + diff --git a/GeoBus/App/Components/StopDetails/StopSearch.swift b/GeoBus/App/Components/StopDetails/StopSearch.swift index f0aa54ac..19cfd275 100644 --- a/GeoBus/App/Components/StopDetails/StopSearch.swift +++ b/GeoBus/App/Components/StopDetails/StopSearch.swift @@ -9,7 +9,7 @@ import SwiftUI struct StopSearch: View { - @EnvironmentObject var appstate: Appstate + @ObservedObject var appstate = Appstate.shared var body: some View { SquareButton(icon: "mail.and.text.magnifyingglass", size: 26) @@ -23,8 +23,6 @@ struct StopSearch: View { struct StopSearchView: View { - @State private var viewSize = CGSize() - var body: some View { ScrollView() { VStack { @@ -35,11 +33,8 @@ struct StopSearchView: View { SearchStopInput() } .padding() - .readSize { size in - viewSize = size - } } .background(Color("BackgroundPrimary")) - .presentationDetents([.height(viewSize.height)]) } + } diff --git a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift index 37b1af40..f05d9d0a 100644 --- a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift +++ b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift @@ -16,7 +16,6 @@ struct VehicleDetailsView: View { let lastSeenTimeTimer = Timer.publish(every: 1 /* seconds */, on: .main, in: .common).autoconnect() @State var lastSeenTime: String = "-" - @State private var viewSize = CGSize() var loadingScreen: some View { @@ -83,13 +82,9 @@ struct VehicleDetailsView: View { .padding() } } -// Disclaimer() -// Spacer() - } - .readSize { size in - viewSize = size + Disclaimer() + Spacer() } - .presentationDetents([.height(viewSize.height)]) } diff --git a/GeoBus/App/Controllers/Appstate.swift b/GeoBus/App/Controllers/Appstate.swift index 23c567a8..8325f298 100644 --- a/GeoBus/App/Controllers/Appstate.swift +++ b/GeoBus/App/Controllers/Appstate.swift @@ -55,11 +55,20 @@ final class Appstate: ObservableObject { case carris_stopSelector case carris_vehicleDetails case carris_connectionDetails + case carris_stopDetails } public func present(sheet: PresentableSheetView) { - self.currentlyPresentedSheetView = sheet - self.sheetIsPresented = true + if (sheetIsPresented) { + self.sheetIsPresented = false + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.currentlyPresentedSheetView = sheet + self.sheetIsPresented = true + } + } else { + self.currentlyPresentedSheetView = sheet + self.sheetIsPresented = true + } } public func unpresent() { diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index 72257b26..e3aacb5f 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -149,7 +149,9 @@ class MapController: ObservableObject { } }) - visibleAnnotations.append( + var tempNewAnnotations: [GenericMapAnnotation] = [] + + tempNewAnnotations.append( GenericMapAnnotation( id: UUID(), location: CLLocationCoordinate2D(latitude: activeStop.lat, longitude: activeStop.lng), @@ -157,7 +159,7 @@ class MapController: ObservableObject { ) ) - zoomToFitMapAnnotations(annotations: visibleAnnotations) + self.addAnnotations(tempNewAnnotations, zoom: true) } @@ -178,9 +180,13 @@ class MapController: ObservableObject { } }) + + var tempNewAnnotations: [GenericMapAnnotation] = [] + + if (activeVariant.circularItinerary != nil) { for connection in activeVariant.circularItinerary! { - visibleAnnotations.append( + tempNewAnnotations.append( GenericMapAnnotation( id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), @@ -192,7 +198,7 @@ class MapController: ObservableObject { if (activeVariant.ascendingItinerary != nil) { for connection in activeVariant.ascendingItinerary! { - visibleAnnotations.append( + tempNewAnnotations.append( GenericMapAnnotation( id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), @@ -204,7 +210,7 @@ class MapController: ObservableObject { if (activeVariant.descendingItinerary != nil) { for connection in activeVariant.descendingItinerary! { - visibleAnnotations.append( + tempNewAnnotations.append( GenericMapAnnotation( id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), @@ -214,17 +220,14 @@ class MapController: ObservableObject { } } - // Remove annotations with duplicate IDs (same stop on different itineraries) - visibleAnnotations.uniqueInPlace(for: \.id) - - zoomToFitMapAnnotations(annotations: visibleAnnotations) + self.addAnnotations(tempNewAnnotations, zoom: true) } /* * */ - /* MARK: - SECTION 10: UPDATE ANNOTATIONS WITH ACTIVE CARRIS VEHICLES */ + /* MARK: - SECTION 10: UPDATE ANNOTATIONS WITH LIST OF ACTIVE CARRIS VEHICLES */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ func updateAnnotations(with activeVehiclesList: [CarrisNetworkModel.Vehicle]) { @@ -238,8 +241,11 @@ class MapController: ObservableObject { } }) + + var tempNewAnnotations: [GenericMapAnnotation] = [] + for vehicle in activeVehiclesList { - visibleAnnotations.append( + tempNewAnnotations.append( GenericMapAnnotation( id: UUID(), location: vehicle.coordinate, @@ -248,7 +254,89 @@ class MapController: ObservableObject { ) } + self.addAnnotations(tempNewAnnotations) + + } + + + + /* * */ + /* MARK: - SECTION 10: UPDATE ANNOTATIONS WITH SINGLE ACTIVE CARRIS VEHICLE */ + /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ + + func updateAnnotations(with activeVehicle: CarrisNetworkModel.Vehicle) { + +// let indexOfVehicleInArray = allVehicles.firstIndex(where: { +// $0.id == vehicleId +// }) + + + if let activeVehicleAnnotation = visibleAnnotations.first(where: { + switch $0.item { + case .carris_vehicle(let item): + if (item.id == activeVehicle.id) { + return true + } else { + return false + } + case .carris_connection(_), .carris_stop(_): + return false + } + }) { + + self.zoomToFitMapAnnotations(annotations: [activeVehicleAnnotation]) + + } else { + + var tempNewAnnotations: [GenericMapAnnotation] = [] + + tempNewAnnotations.append( + GenericMapAnnotation( + id: UUID(), + location: activeVehicle.coordinate, + item: .carris_vehicle(activeVehicle) + ) + ) + + self.addAnnotations(tempNewAnnotations, zoom: true) + + } + + } + + + + + + + + + + + private func addAnnotations(_ newAnnotations: [GenericMapAnnotation], zoom: Bool = false) { + DispatchQueue.main.async { + // Add the annotations to the map + self.visibleAnnotations.append(contentsOf: newAnnotations) + // Remove annotations with duplicate IDs (ex: same stop on different itineraries) + self.visibleAnnotations.uniqueInPlace(for: \.id) + // Adjust map region to annotations + if (zoom) { + self.zoomToFitMapAnnotations(annotations: newAnnotations) + } + } } +// private func removeAnnotations(ofType annotationTypes: [GenericMapAnnotation.AnnotationItem]) { +// visibleAnnotations.removeAll(where: { +// for type in annotationTypes { +// if ($0.item == type) { +// return true +// } +// } +// return false +// }) +// } + + } diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index 6100f961..ed7eb48c 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -95,7 +95,6 @@ class CarrisNetworkController: ObservableObject { // Unwrap Community Provider Status from Storage self.communityDataProviderStatus = UserDefaults.standard.bool(forKey: storageKeyForCommunityDataProviderStatus) - print("GeoBus: Carris API: ‹toggleCommunityDataProviderTo()› Community Data switched \(communityDataProviderStatus ? "ON" : "OFF")") // Check if network needs an update self.update(reset: false) @@ -140,6 +139,9 @@ class CarrisNetworkController: ObservableObject { } + // Get favorites from KVS + self.retrieveFavoritesFromKVS() + // Always update vehicles and favorites self.refresh() @@ -170,7 +172,6 @@ class CarrisNetworkController: ObservableObject { Task { await self.fetchVehiclesListFromCarrisAPI() self.populateActiveVehicles() - self.retrieveFavoritesFromKVS() } } @@ -613,17 +614,39 @@ class CarrisNetworkController: ObservableObject { /* These functions select and deselect the currently active objects. */ /* Provide public functions to more easily select object by their identifier. */ - private func deselect() { - self.activeRoute = nil - self.activeVariant = nil - self.activeConnection = nil - self.activeStop = nil - self.activeVehicles = [] + + enum SelectableObject { + case route + case variant + case connection + case vehicle + case stop + case all + } + + + public func deselect(_ objectType: [SelectableObject]) { + for type in objectType { + switch type { + case .route: + self.activeRoute = nil + case .variant: + self.activeVariant = nil + case .connection: + self.activeConnection = nil + case .vehicle: + self.activeVehicle = nil + case .stop: + self.activeStop = nil + case .all: + self.deselect([.route, .variant, .connection, .vehicle, .stop]) + } + } } private func select(route: CarrisNetworkModel.Route) { - self.deselect() + self.deselect([.all]) self.activeRoute = route self.select(variant: route.variants[0]) self.populateActiveVehicles() @@ -644,21 +667,25 @@ class CarrisNetworkController: ObservableObject { } - private func select(connection: CarrisNetworkModel.Connection) { - self.deselect() + public func deselect(connection: CarrisNetworkModel.Connection) { self.activeConnection = connection } + public func select(connection: CarrisNetworkModel.Connection) { + self.activeConnection = connection + } - private func select(stop: CarrisNetworkModel.Stop) { - self.deselect() + public func select(stop: CarrisNetworkModel.Stop) { + self.deselect([.all]) self.activeStop = stop } public func select(stop stopId: Int) -> Bool { let stop = self.find(stop: stopId) if (stop != nil) { - self.select(stop: stop!) + self.deselect([.all]) + self.activeStop = stop +// self.select(stop: stop!) return true } else { return false diff --git a/GeoBus/App/Layout/StopIcon.swift b/GeoBus/App/Layout/StopIcon.swift index ccc674b1..ee11baa8 100644 --- a/GeoBus/App/Layout/StopIcon.swift +++ b/GeoBus/App/Layout/StopIcon.swift @@ -9,17 +9,11 @@ import SwiftUI struct StopIcon: View { - public let orderInRoute: Int - public let direction: CarrisNetworkModel.Direction + public let orderInRoute: Int? + public let direction: CarrisNetworkModel.Direction? public let isSelected: Bool - init(orderInRoute: Int, direction: CarrisNetworkModel.Direction) { - self.orderInRoute = orderInRoute - self.direction = direction - self.isSelected = false - } - - init(orderInRoute: Int, direction: CarrisNetworkModel.Direction, isSelected: Bool) { + init(orderInRoute: Int? = nil, direction: CarrisNetworkModel.Direction? = nil, isSelected: Bool = false) { self.orderInRoute = orderInRoute self.direction = direction self.isSelected = isSelected @@ -59,6 +53,8 @@ struct StopIcon: View { return Color("StopDescendingBorder") case .circular: return Color("StopCircularBorder") + case .none: + return Color("StopCircularBorder") } } } @@ -74,6 +70,8 @@ struct StopIcon: View { return Color("StopDescendingBackground") case .circular: return Color("StopCircularBackground") + case .none: + return Color("StopCircularBackground") } } } @@ -91,10 +89,17 @@ struct StopIcon: View { .frame(width: self.borderWidth, height: self.borderWidth) .animation(.default, value: self.backgroundColor) .animation(.default, value: self.borderWidth) - Text(String(self.orderInRoute)) - .font(.system(size: self.textSize, weight: .bold)) - .foregroundColor(.white) - .animation(.default, value: self.textSize) + if (self.orderInRoute != nil) { + Text(String(self.orderInRoute!)) + .font(.system(size: self.textSize, weight: .bold)) + .foregroundColor(.white) + .animation(.default, value: self.textSize) + } else { + Image(systemName: "mappin") + .font(.system(size: self.textSize, weight: .bold)) + .foregroundColor(.white) + .animation(.default, value: self.textSize) + } } } From 9e635d27655e2b1c0d30b0308a10c647ba51001f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 29 Oct 2022 03:57:34 +0100 Subject: [PATCH 32/63] Starting to build sheet for vehicles --- GeoBus.xcodeproj/project.pbxproj | 4 + .../App/Components/PresentedSheetView.swift | 4 +- .../VehicleDetails/VehicleDetailsView.swift | 108 +++++++++++++++--- .../Carris/CarrisCommunityAPIModel.swift | 92 +++++++-------- .../Carris/CarrisNetworkController.swift | 88 +++++++++++--- .../Networks/Carris/CarrisNetworkModel.swift | 8 +- GeoBus/App/Layout/Animations/Spinner.swift | 6 +- GeoBus/App/Layout/LoadingSheet.swift | 15 +++ 8 files changed, 242 insertions(+), 83 deletions(-) create mode 100644 GeoBus/App/Layout/LoadingSheet.swift diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index 61ea42bf..0b54cff0 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ CFAF0E7A28CE84F400DDAD5B /* MapAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFAF0E7928CE84F400DDAD5B /* MapAnnotations.swift */; }; CFB5D45728EEFE21002368BC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = CFB5D45528EEFE21002368BC /* InfoPlist.strings */; }; CFB5D45A28EEFE6B002368BC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CFB5D45828EEFE6B002368BC /* Localizable.strings */; }; + CFB71F82290CAFB500B37E69 /* LoadingSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFB71F81290CAFB500B37E69 /* LoadingSheet.swift */; }; CFDC15EF28D292FB00A4BE49 /* ViewSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDC15EE28D292FB00A4BE49 /* ViewSize.swift */; }; CFDD014928D5114D0070FE4B /* SyncStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDD014828D5114D0070FE4B /* SyncStatus.swift */; }; CFDD014B28D535370070FE4B /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDD014A28D535370070FE4B /* Card.swift */; }; @@ -142,6 +143,7 @@ CFB5D46028EEFEF1002368BC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; CFB5D46128EEFF2C002368BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; CFB5D46228EEFF2D002368BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; + CFB71F81290CAFB500B37E69 /* LoadingSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingSheet.swift; sourceTree = ""; }; CFCED4F628EF5CB900963640 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; CFCED4F728EF5CB900963640 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; CFCED4F828EF5CD500963640 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; @@ -218,6 +220,7 @@ CF18204228CCBCC500248F72 /* RouteBadgeSquare.swift */, CFFFAD7828F4AD0400DFD5FD /* TimeLeft.swift */, CFFFAD7A28F4D8D000DFD5FD /* StopIcon.swift */, + CFB71F81290CAFB500B37E69 /* LoadingSheet.swift */, CF548FF528D14BA400668CB6 /* VehicleIdentifier.swift */, CF03D6AD28F3B00F0077299B /* VehicleDestination.swift */, CFDD014A28D535370070FE4B /* Card.swift */, @@ -536,6 +539,7 @@ CFAF0E7A28CE84F400DDAD5B /* MapAnnotations.swift in Sources */, CF548FF328D129B000668CB6 /* VehicleDetailsView.swift in Sources */, CFFFAD7B28F4D8D000DFD5FD /* StopIcon.swift in Sources */, + CFB71F82290CAFB500B37E69 /* LoadingSheet.swift in Sources */, CF0C256C29031B2C00B03052 /* CarrisCommunityAPIModel.swift in Sources */, CF82BB0928D7F19A007F0CDB /* LocationCard.swift in Sources */, CF18207928CCBD2300248F72 /* RouteDetailsVehiclesQuantity.swift in Sources */, diff --git a/GeoBus/App/Components/PresentedSheetView.swift b/GeoBus/App/Components/PresentedSheetView.swift index 3fe111f4..e0cb6a40 100644 --- a/GeoBus/App/Components/PresentedSheetView.swift +++ b/GeoBus/App/Components/PresentedSheetView.swift @@ -31,8 +31,8 @@ struct PresentedSheetView: View { .presentationDragIndicator(.hidden) case .carris_vehicleDetails: - VehicleDetailsView() - .presentationDetents([.medium]) + CarrisVehicleSheetView() + .presentationDetents([.medium, .large]) .presentationDragIndicator(.hidden) .onDisappear() { carrisNetworkController.deselect([.vehicle]) diff --git a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift index f05d9d0a..52566672 100644 --- a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift +++ b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift @@ -8,6 +8,28 @@ import SwiftUI import Combine + +struct CarrisVehicleSheetView: View { + + @ObservedObject var appstate = Appstate.shared + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + + var body: some View { + if (carrisNetworkController.activeVehicle != nil && carrisNetworkController.activeVehicle!.hasLoadedCarrisDetails) { + Text("Details") + } else if (appstate.vehicles == .loading) { + Spinner(size: 30) + } else { + Text("Error") + } + } + +} + + + + + struct VehicleDetailsView: View { @EnvironmentObject var appstate: Appstate @@ -61,31 +83,83 @@ struct VehicleDetailsView: View { } Spacer() } + VehicleRouteContainer(vehicle: carrisNetworkController.activeVehicle) } } var body: some View { - VStack(alignment: .leading) { - VStack(alignment: .leading, spacing: 0) { - if (appstate.vehicles == .loading) { - loadingScreen - .padding() - } else if (appstate.vehicles == .error) { - errorScreen - .padding() - } else { - vehicleDetailsHeader - .padding() - Divider() - vehicleDetailsScreen - .padding() + ScrollView { + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 0) { + if (appstate.vehicles == .loading) { + loadingScreen + .padding() + } else if (appstate.vehicles == .error) { + errorScreen + .padding() + } else { + vehicleDetailsHeader + .padding() + Divider() + vehicleDetailsScreen + .padding() + } } + Disclaimer() + Spacer() + } + } + } + +} + + + + + +struct VehicleRouteContainer: View { + + let vehicle: CarrisNetworkModel.Vehicle? + + @ObservedObject var appstate = Appstate.shared + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + if (vehicle?.routeEstimates != nil) { + ForEach(Array(vehicle!.routeEstimates!.enumerated()), id: \.offset) { index, element in + VStack(alignment: .leading, spacing: 0) { + if (index > 0) { + Rectangle() + .frame(width: 5, height: 25) + .foregroundColor(element.hasArrived ?? false ? .green : .blue) + .padding(.horizontal, 10) + } + Button(action: { + _ = carrisNetworkController.select(stop: element.stopId) + appstate.present(sheet: .carris_stopDetails) + }, label: { + HStack(alignment: .center, spacing: 10) { + StopIcon(orderInRoute: index) + Text(String(element.stopId)) + Spacer() + if (element.hasArrived ?? false) { + Text("já passou") + } else { + TimeLeft(time: element.eta) + } + } + }) + } + } + } else { + Text("Is nil, loading?") } - Disclaimer() - Spacer() } - } + + } diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift index 257f84b2..361f40dc 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift @@ -8,59 +8,59 @@ import Foundation struct CarrisCommunityAPIModel { struct Vehicle: Decodable { - let busNumber: Int? - let dataServico: String? - let direction: String? - let enrichedAvgRouteSpeed: Double? - let enrichedBusSpeed: Double? - let enrichedDbStartup: Double? - let enrichedEstRouteKm: Double? - let enrichedGeohash300m: String? - let enrichedGeohash80m: String? - let enrichedGeohashPrev300m: String? - let enrichedGeohashPrev80m: String? - let enrichedPreviousStopId: String? - let enrichedPreviousStopList: [String]? - let enrichedPreviousStopMax: Int? - let enrichedPreviousStopOrderIdx: Double? - let enrichedQueryTime: Double? - let enrichedRouteCoords: [Double]? - let enrichedRouteDirection: Double? - let enrichedRouteDoneKm: Double? - let enrichedRouteLengthKm: Double? - let enrichedSequenceNo: Int? - let enrichedStartLat: Double? - let enrichedStartLng: Double? - let enrichedStartTime: String? - let enrichedTimeHash30m: String? - let enrichedTimeHashDay30m: String? - let estimatedDebug: [String]? - let estimatedRouteItinerary: [String]? + // let busNumber: Int? + // let dataServico: String? + // let direction: String? + // let enrichedAvgRouteSpeed: Double? + // let enrichedBusSpeed: Double? + // let enrichedDbStartup: Double? + // let enrichedEstRouteKm: Double? + // let enrichedGeohash300m: String? + // let enrichedGeohash80m: String? + // let enrichedGeohashPrev300m: String? + // let enrichedGeohashPrev80m: String? + // let enrichedPreviousStopId: String? + // let enrichedPreviousStopList: [String]? + // let enrichedPreviousStopMax: Int? + // let enrichedPreviousStopOrderIdx: Double? + // let enrichedQueryTime: Double? + // let enrichedRouteCoords: [Double]? + // let enrichedRouteDirection: Double? + // let enrichedRouteDoneKm: Double? + // let enrichedRouteLengthKm: Double? + // let enrichedSequenceNo: Int? + // let enrichedStartLat: Double? + // let enrichedStartLng: Double? + // let enrichedStartTime: String? + // let enrichedTimeHash30m: String? + // let enrichedTimeHashDay30m: String? + // let estimatedDebug: [String]? + // let estimatedRouteItinerary: [String]? let estimatedRouteResults: [EstimatedRouteResult]? - let lastGpsTime: String? - let lastReportTime: String? - let lat: Double? - let lng: Double? - let plateNumber: String? - let previousLatitude: Double? - let previousLongitude: Double? - let previousReportTime: String? - let routeNumber: String? - let state: String? - let timeStamp: String? - let variantNumber: Int? - let voyageNumber: Int? + // let lastGpsTime: String? + // let lastReportTime: String? + // let lat: Double? + // let lng: Double? + // let plateNumber: String? + // let previousLatitude: Double? + // let previousLongitude: Double? + // let previousReportTime: String? + // let routeNumber: String? + // let state: String? + // let timeStamp: String? + // let variantNumber: Int? + // let voyageNumber: Int? } struct EstimatedRouteResult: Decodable { - let estimatedFeatures: [EstimatedFeature]? + // let estimatedFeatures: [EstimatedFeature]? let estimatedPreviouslyArrived: Bool? - let estimatedRecentlyArrived: Bool? + // let estimatedRecentlyArrived: Bool? let estimatedRouteStopId: String? - let estimatedRouteStopPosition: Double? - let estimatedTimeofArrival: String? + // let estimatedRouteStopPosition: Double? + // let estimatedTimeofArrival: String? let estimatedTimeofArrivalCorrected: String? - let estimatedUncertainty: String? + // let estimatedUncertainty: String? } diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index ed7eb48c..c3f48de1 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -170,7 +170,23 @@ class CarrisNetworkController: ObservableObject { public func refresh() { Task { + // Update all vehicles from Carris API await self.fetchVehiclesListFromCarrisAPI() + + // DEBUG ! + self.select(vehicle: self.allVehicles[0].id) + Appstate.shared.present(sheet: .carris_vehicleDetails) + // ! DEBUG + + // If there is an active vehicle, also refresh it's details + if (self.activeVehicle != nil) { + await self.fetchVehicleDetailsFromCarrisAPI(for: self.activeVehicle!.id) + // If Community provider is also enabled, then also refresh those details + if (self.communityDataProviderStatus) { + await self.fetchVehicleDetailsFromCarrisAPI(for: self.activeVehicle!.id) + } + } + // Update the list of active vehicles (the current selected route) self.populateActiveVehicles() } } @@ -698,6 +714,9 @@ class CarrisNetworkController: ObservableObject { if let foundVehicle = self.find(vehicle: vehicleId!) { Task { await self.fetchVehicleDetailsFromCarrisAPI(for: foundVehicle.id) + if (self.communityDataProviderStatus) { + await self.fetchVehicleDetailsFromCommunityAPI(for: foundVehicle.id) + } self.activeVehicle = foundVehicle } } @@ -707,7 +726,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 11: SET ACTIVE VEHICLES */ + /* MARK: - 11. VEHICLES: SET ACTIVE VEHICLES */ /* This function compares the currently active route number with all vehicles */ /* appending the ones that match to the ‹activeVehicles› array. It also checks */ /* if vehicles have an up-to-date location. */ @@ -738,8 +757,6 @@ class CarrisNetworkController: ObservableObject { } -// self.select(vehicle: activeVehicle?.id) - } @@ -822,16 +839,6 @@ class CarrisNetworkController: ObservableObject { /* function to allow the UI to request this information only when necessary. After retrieving the new details */ /* fromt the API, re-populate the activeVehicles array to trigger an update in the UI. */ -// public func getAdditionalDetailsForActiveVehicle() { -// Task { -// if (activeVehicle != nil) { -// await self.fetchVehicleDetailsFromCarrisAPI(for: self.activeVehicle!.id) -// self.populateActiveVehicles() -// self.select(vehicle: activeVehicle!.id) -// } -// } -// } - private func fetchVehicleDetailsFromCarrisAPI(for vehicleId: Int) async { Appstate.shared.change(to: .loading, for: .vehicles) @@ -856,6 +863,7 @@ class CarrisNetworkController: ObservableObject { allVehicles[indexOfVehicleInArray!].vehiclePlate = decodedCarrisAPIVehicleDetail.vehiclePlate allVehicles[indexOfVehicleInArray!].lastStopOnVoyageId = decodedCarrisAPIVehicleDetail.lastStopOnVoyageId allVehicles[indexOfVehicleInArray!].lastStopOnVoyageName = decodedCarrisAPIVehicleDetail.lastStopOnVoyageName + allVehicles[indexOfVehicleInArray!].hasLoadedCarrisDetails = true } print("GeoBus: Carris API: Vehicle Details: Update complete!") @@ -871,6 +879,60 @@ class CarrisNetworkController: ObservableObject { } + private func fetchVehicleDetailsFromCommunityAPI(for vehicleId: Int) async { + + Appstate.shared.change(to: .loading, for: .vehicles) + + print("GeoBus: Community API: Vehicle Details: Starting update...") + + do { + + // Request Vehicle Detail (SGO) + let rawDataCarrisCommunityAPIVehicleDetail = try await CarrisCommunityAPI.shared.request(for: "estbus?busNumber=\(vehicleId)") + + let decodedCarrisCommunityAPIVehicleDetail = try JSONDecoder().decode([CarrisCommunityAPIModel.Vehicle].self, from: rawDataCarrisCommunityAPIVehicleDetail) + + + if (decodedCarrisCommunityAPIVehicleDetail[0].estimatedRouteResults != nil) { + + var tempRouteEstimates: [CarrisNetworkModel.Estimation] = [] + + for routeResult in decodedCarrisCommunityAPIVehicleDetail[0].estimatedRouteResults! { + tempRouteEstimates.append( + CarrisNetworkModel.Estimation( + stopId: Int(routeResult.estimatedRouteStopId ?? "-1") ?? -1, + routeNumber: "", + destination: "", + eta: routeResult.estimatedTimeofArrivalCorrected ?? "", + hasArrived: routeResult.estimatedPreviouslyArrived + ) + ) + } + + let indexOfVehicleInArray = allVehicles.firstIndex(where: { + $0.id == vehicleId + }) + + if (indexOfVehicleInArray != nil) { + allVehicles[indexOfVehicleInArray!].routeEstimates = tempRouteEstimates + allVehicles[indexOfVehicleInArray!].hasLoadedCommunityDetails = true + } + + } + + print("GeoBus: Community API: Vehicle Details: Update complete!") + + Appstate.shared.change(to: .idle, for: .vehicles) + + } catch { + Appstate.shared.change(to: .error, for: .vehicles) + print("GeoBus: Community API: Vehicles Details: Error found while updating. More info: \(error)") + return + } + + } + + /* MARK: - GET ESTIMATION */ // This function calls the API to retrieve estimations for the provided stop 'publicId'. diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift index 67acc0a3..7ec2ad28 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift @@ -123,15 +123,17 @@ struct CarrisNetworkModel { let routeNumber: String let destination: String let eta: String + let hasArrived: Bool? let busNumber: Int? - init(stopId: Int, routeNumber: String, destination: String, eta: String, busNumber: Int? = nil) { + init(stopId: Int, routeNumber: String, destination: String, eta: String, busNumber: Int? = nil, hasArrived: Bool? = nil) { self.id = UUID() self.stopId = stopId self.routeNumber = routeNumber self.destination = destination self.busNumber = busNumber self.eta = eta + self.hasArrived = hasArrived } } @@ -182,9 +184,11 @@ struct CarrisNetworkModel { var vehiclePlate: String? var lastStopOnVoyageId: Int? var lastStopOnVoyageName: String? + var hasLoadedCarrisDetails: Bool = false // Community API - var estimatedTimeofArrivalCorrected: [String]? + var routeEstimates: [Estimation]? + var hasLoadedCommunityDetails: Bool = false diff --git a/GeoBus/App/Layout/Animations/Spinner.swift b/GeoBus/App/Layout/Animations/Spinner.swift index 98d72e89..b177af7f 100644 --- a/GeoBus/App/Layout/Animations/Spinner.swift +++ b/GeoBus/App/Layout/Animations/Spinner.swift @@ -9,10 +9,10 @@ import SwiftUI struct Spinner: View { - @Environment(\.colorScheme) var colorScheme: ColorScheme + public var size: CGFloat = 20.0 + public var timing: Double = 0.5 - private let timing: Double = 0.5 - private let size: CGFloat = 20.0 + @Environment(\.colorScheme) var colorScheme: ColorScheme @State var trim: Double = 0.4 @State var rotationAngle: Double = 0.0 diff --git a/GeoBus/App/Layout/LoadingSheet.swift b/GeoBus/App/Layout/LoadingSheet.swift new file mode 100644 index 00000000..af64c58c --- /dev/null +++ b/GeoBus/App/Layout/LoadingSheet.swift @@ -0,0 +1,15 @@ +// +// LoadingSheet.swift +// GeoBus +// +// Created by João de Vasconcelos on 29/10/2022. +// + +import SwiftUI + +struct LoadingSheet: View { + var body: some View { + ProgressView() + .scaleEffect(5) + } +} From b73e9767f709b5a49a6eb32bd50d40cd5990cf50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sat, 29 Oct 2022 04:18:41 +0100 Subject: [PATCH 33/63] Some navigational structure in the file --- .../Carris/CarrisNetworkController.swift | 200 +++++++++++++++--- 1 file changed, 165 insertions(+), 35 deletions(-) diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index c3f48de1..0d1f0214 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -12,7 +12,7 @@ import Combine class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 1: SETTINGS */ + /* MARK: - 1. SETTINGS */ /* In this section one can find private constants for update intervals, */ /* storage keys and more. Change these values with caution because they can */ /* trigger updates on the users devices, which can take a long time or fail. */ @@ -31,7 +31,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 2: PUBLISHED VARIABLES */ + /* MARK: - 2. PUBLISHED VARIABLES */ /* Here are all the @Published variables that can be consumed by the app views. */ /* It is important to keep the names of this variables short, but descriptive, */ /* to avoid clutter on the interface code. */ @@ -57,8 +57,24 @@ class CarrisNetworkController: ObservableObject { + + + + + + + + + + + /* * */ + /* MARK: - 3. INITIALIZER */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut ornare ipsum. */ + /* Nunc neque nulla, pretium ac lectus id, scelerisque facilisis est. */ + + /* * */ - /* MARK: - SECTION 3: SHARED INSTANCE */ + /* MARK: - 3.1. SHARED INSTANCE */ /* To allow the same instance of this class to be available accross the whole app, */ /* we create a Singleton. More info here: https://www.hackingwithswift.com/example-code/language/what-is-a-singleton */ @@ -67,7 +83,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 4: INITIALIZER */ + /* MARK: - 3.2. INITIALIZE CLASS */ /* When this class is initialized, data stored on the users device must be retrieved */ /* from UserDefaults to avoid requesting a new update to the APIs. After this, check if */ /* this stored data needs an update or not. */ @@ -103,8 +119,24 @@ class CarrisNetworkController: ObservableObject { + + + + + + + + + + + /* * */ + /* MARK: - 4. UPDATE DATA */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut ornare ipsum. */ + /* Nunc neque nulla, pretium ac lectus id, scelerisque facilisis est. */ + + /* * */ - /* MARK: - SECTION 5.1: UPDATE NETWORK FROM CARRIS API */ + /* MARK: - 4.1. UPDATE NETWORK MODEL */ /* This function decides whether to update the complete network model */ /* if it is considered outdated or is inexistent on device storage. */ /* Provide a convenience method to allow user-requested updates from the UI. */ @@ -151,20 +183,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 12: TOGGLE COMMUNITY DATA PROVIDER STATUS */ - /* Call this function to switch Community Data ON or OFF. */ - /* This switches in memory for the current session, and stores the new setting in storage. */ - - public func toggleCommunityDataProviderTo(to newStatus: Bool) { - self.communityDataProviderStatus = newStatus - UserDefaults.standard.set(newStatus, forKey: storageKeyForCommunityDataProviderStatus) - print("GeoBus: Carris API: ‹toggleCommunityDataProviderTo()› Community Data switched \(newStatus ? "ON" : "OFF")") - } - - - - /* * */ - /* MARK: - SECTION 5.2: REFRESH DATA */ + /* MARK: - 4.2. REFRESH DATA */ /* This function initiates vehicles refresh from Carris API, updates the ‹activeVehicles› array */ /* and syncronizes favorites with iCloud, to ensure changes are always up to date. */ @@ -194,7 +213,36 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 6: FETCH & FORMAT STOPS FROM CARRIS API */ + /* MARK: - 4.3. COMMUNITY PROVIDER */ + /* Call this function to switch Community Data ON or OFF. */ + /* This switches in memory for the current session, and stores the new setting in storage. */ + + public func toggleCommunityDataProviderTo(to newStatus: Bool) { + self.communityDataProviderStatus = newStatus + UserDefaults.standard.set(newStatus, forKey: storageKeyForCommunityDataProviderStatus) + print("GeoBus: Carris API: ‹toggleCommunityDataProviderTo()› Community Data switched \(newStatus ? "ON" : "OFF")") + } + + + + + + + + + + + + + + /* * */ + /* MARK: - 5. FORMAT NETWORK */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut ornare ipsum. */ + /* Nunc neque nulla, pretium ac lectus id, scelerisque facilisis est. */ + + + /* * */ + /* MARK: - 5.1. STOPS: FETCH & FORMAT FROM CARRIS API */ /* Call Carris API and retrieve all stops. Format them to the app model. */ private func fetchStopsFromCarrisAPI() async { @@ -254,7 +302,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 7: FETCH & FORMAT ROUTES FROM CARRIS API */ + /* MARK: - 5.2. ROUTES: FETCH & FORMAT FROM CARRIS API */ /* This function first fetches the Routes List from Carris API, */ /* which is an object that contains all the available routes. */ /* The information for each Route is very short, so it is necessary to retrieve */ @@ -351,7 +399,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 7.1: FORMAT CARRIS ROUTE VARIANTS */ + /* MARK: - 5.3. VARIANTS: FORMAT CARRIS ROUTE VARIANTS */ /* Parse and simplify the data model for variants. Variants contain */ /* one or more itineraries, each with a direction. Each itinerary is composed */ /* of a series of connections, in which each contains a stop. */ @@ -406,7 +454,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 7.2: FORMAT CARRIS CONNECTIONS */ + /* MARK: - 5.4. CONNECTIONS: FORMAT CARRIS CONNECTIONS */ /* Each itinerary is composed of a series of connections, in which each */ /* has a single stop. Connections contain the property ‹orderInRoute›. */ @@ -438,8 +486,20 @@ class CarrisNetworkController: ObservableObject { + + + + + + + + /* * */ + /* MARK: - 6. FAVORITES */ + /* This section holds the logic to deal with favorites from iCloud Key-Value-Storage. */ + + /* * */ - /* MARK: - SECTION 8.1: RETRIEVE FAVORITES FROM ICLOUD KVS */ + /* MARK: - 6.1. RETRIEVE FAVORITES FROM ICLOUD KVS */ /* This function retrieves favorites from iCloud Key-Value-Storage. */ private func retrieveFavoritesFromKVS() { @@ -473,7 +533,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 8.2: SAVE FAVORITES TO ICLOUD KVS */ + /* MARK: - 6.2. SAVE FAVORITES TO ICLOUD KVS */ /* This function saves a representation of the objects stored in the favorites arrays */ /* to iCloud Key-Value-Store. This function should be called whenever a change */ /* in favorites occurs, to ensure consistency across devices. */ @@ -503,7 +563,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 8.3: TOGGLE ROUTES AND STOPS AS FAVORITES */ + /* MARK: - 6.3. TOGGLE ROUTES AND STOPS AS FAVORITES */ /* These functions mark an object as favorite if it is not, and remove it from favorites if it is. */ public func toggleFavorite(route: CarrisNetworkModel.Route) { @@ -544,7 +604,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 8.4: IS FAVORITE CHECKER */ + /* MARK: - 6.4: IS FAVORITE CHECKER */ /* These functions check if an object is marked as favorite. */ public func isFavourite(route: CarrisNetworkModel.Route?) -> Bool { @@ -574,8 +634,24 @@ class CarrisNetworkController: ObservableObject { + + + + + + + + + + + /* * */ + /* MARK: - 7. FINDERS */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut ornare ipsum. */ + /* Nunc neque nulla, pretium ac lectus id, scelerisque facilisis est. */ + + /* * */ - /* MARK: - SECTION 9: FIND OBJECTS BY IDENTIFIER */ + /* MARK: - 7.1. FIND OBJECTS BY IDENTIFIER */ /* These functions search for the provided object identifier in the storage arrays */ /* and return it if found or nil if not found. */ @@ -625,8 +701,24 @@ class CarrisNetworkController: ObservableObject { + + + + + + + + + + /* * */ - /* MARK: - SECTION 10: OBJECT SELECTORS */ + /* MARK: - 8. SELECTORS */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut ornare ipsum. */ + /* Nunc neque nulla, pretium ac lectus id, scelerisque facilisis est. */ + + + /* * */ + /* MARK: - 8.1: OBJECT SELECTORS */ /* These functions select and deselect the currently active objects. */ /* Provide public functions to more easily select object by their identifier. */ @@ -725,8 +817,24 @@ class CarrisNetworkController: ObservableObject { + + + + + + + + + + + /* * */ + /* MARK: - 9. VEHICLES */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut ornare ipsum. */ + /* Nunc neque nulla, pretium ac lectus id, scelerisque facilisis est. */ + + /* * */ - /* MARK: - 11. VEHICLES: SET ACTIVE VEHICLES */ + /* MARK: - 9.1. VEHICLES: SET ACTIVE VEHICLES */ /* This function compares the currently active route number with all vehicles */ /* appending the ones that match to the ‹activeVehicles› array. It also checks */ /* if vehicles have an up-to-date location. */ @@ -762,7 +870,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 12: FETCH ALL VEHICLES FROM CARRIS API */ + /* MARK: - 9.2. FETCH ALL VEHICLES FROM CARRIS API */ /* This function calls the Carris API and receives vehicle metadata, */ /* including positions, for all currently active vehicles, */ /* and stores them in the ‹allVehicles› array. */ @@ -833,7 +941,7 @@ class CarrisNetworkController: ObservableObject { /* * */ - /* MARK: - SECTION 12: FETCH VEHICLE DETAILS FROM CARRIS API */ + /* MARK: - 9.3. FETCH VEHICLE DETAILS FROM CARRIS API */ /* This function calls the Carris API SGO endpoint to retrieve additional vehicle metadata, */ /* such as location, license plate number and last stop on the current trip. Provide a convenience */ /* function to allow the UI to request this information only when necessary. After retrieving the new details */ @@ -879,6 +987,12 @@ class CarrisNetworkController: ObservableObject { } + + /* * */ + /* MARK: - 9.4. FETCH VEHICLE DETAILS FROM COMMUNITY API */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut ornare ipsum. */ + /* Nunc neque nulla, pretium ac lectus id, scelerisque facilisis est. */ + private func fetchVehicleDetailsFromCommunityAPI(for vehicleId: Int) async { Appstate.shared.change(to: .loading, for: .vehicles) @@ -934,7 +1048,23 @@ class CarrisNetworkController: ObservableObject { - /* MARK: - GET ESTIMATION */ + + + + + + + + + + + /* * */ + /* MARK: - 10. ESTIMATES */ + /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi ut ornare ipsum. */ + /* Nunc neque nulla, pretium ac lectus id, scelerisque facilisis est. */ + + + /* MARK: - 10.1. GET ESTIMATION */ // This function calls the API to retrieve estimations for the provided stop 'publicId'. // It formats and returns the results to the caller. @@ -948,7 +1078,7 @@ class CarrisNetworkController: ObservableObject { - /* MARK: - GET CARRIS ESTIMATIONS */ + /* MARK: - 10.2. GET CARRIS ESTIMATIONS */ // This function calls the API to retrieve estimations for the provided stop 'publicId'. // It formats and returns the results to the caller. @@ -996,7 +1126,7 @@ class CarrisNetworkController: ObservableObject { - /* MARK: - GET COMMUNITY ESTIMATIONS */ + /* MARK: - 10.3. GET COMMUNITY ESTIMATIONS */ // This function calls the API to retrieve estimations for the provided stop 'publicId'. // It formats and returns the results to the caller. From 1231f7c62509cb321f9a94e46d15a3fa11bcbc25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Mon, 31 Oct 2022 01:24:11 +0000 Subject: [PATCH 34/63] Added more StopIcon colors --- .../Colors/Placeholder/Contents.json | 6 +++ .../PlaceholderShape.colorset/Contents.json | 34 +++++++++++++++++ .../PlaceholderText.colorset/Contents.json | 34 +++++++++++++++++ .../StopAscendingText.colorset/Contents.json | 38 +++++++++++++++++++ .../StopCircularText.colorset/Contents.json | 38 +++++++++++++++++++ .../StopDescendingText.colorset/Contents.json | 38 +++++++++++++++++++ .../Colors/Stops/Muted/Contents.json | 6 +++ .../Contents.json | 38 +++++++++++++++++++ .../StopMutedBorder.colorset/Contents.json | 38 +++++++++++++++++++ .../StopMutedText.colorset/Contents.json | 38 +++++++++++++++++++ .../StopSelectedText.colorset/Contents.json | 38 +++++++++++++++++++ 11 files changed, 346 insertions(+) create mode 100644 GeoBus/Assets.xcassets/Colors/Placeholder/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Placeholder/PlaceholderShape.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Placeholder/PlaceholderText.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Ascending/StopAscendingText.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Circular/StopCircularText.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Descending/StopDescendingText.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Muted/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedBackground.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedBorder.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedText.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Selected/StopSelectedText.colorset/Contents.json diff --git a/GeoBus/Assets.xcassets/Colors/Placeholder/Contents.json b/GeoBus/Assets.xcassets/Colors/Placeholder/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Placeholder/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Placeholder/PlaceholderShape.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Placeholder/PlaceholderShape.colorset/Contents.json new file mode 100644 index 00000000..f33a61e7 --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Placeholder/PlaceholderShape.colorset/Contents.json @@ -0,0 +1,34 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "extended-gray", + "components" : { + "alpha" : "1.000", + "white" : "235" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "extended-gray", + "components" : { + "alpha" : "1.000", + "white" : "64" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Placeholder/PlaceholderText.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Placeholder/PlaceholderText.colorset/Contents.json new file mode 100644 index 00000000..f2912710 --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Placeholder/PlaceholderText.colorset/Contents.json @@ -0,0 +1,34 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "extended-gray", + "components" : { + "alpha" : "1.000", + "white" : "220" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "extended-gray", + "components" : { + "alpha" : "1.000", + "white" : "0.350" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Ascending/StopAscendingText.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Ascending/StopAscendingText.colorset/Contents.json new file mode 100644 index 00000000..c60cea5b --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Ascending/StopAscendingText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Circular/StopCircularText.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Circular/StopCircularText.colorset/Contents.json new file mode 100644 index 00000000..22c4bb0a --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Circular/StopCircularText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Descending/StopDescendingText.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Descending/StopDescendingText.colorset/Contents.json new file mode 100644 index 00000000..22c4bb0a --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Descending/StopDescendingText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Muted/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Muted/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Muted/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedBackground.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedBackground.colorset/Contents.json new file mode 100644 index 00000000..085f942e --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "230", + "green" : "235", + "red" : "235" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "120", + "green" : "115", + "red" : "110" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedBorder.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedBorder.colorset/Contents.json new file mode 100644 index 00000000..cf4d4a3e --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedBorder.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "215", + "green" : "215", + "red" : "215" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "160", + "green" : "155", + "red" : "150" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedText.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedText.colorset/Contents.json new file mode 100644 index 00000000..19bc36fa --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Muted/StopMutedText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "180", + "green" : "180", + "red" : "180" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Selected/StopSelectedText.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Selected/StopSelectedText.colorset/Contents.json new file mode 100644 index 00000000..22c4bb0a --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Selected/StopSelectedText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From dea022e4f98b2c3ff416f80ee615e00e0d82b5f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Mon, 31 Oct 2022 01:25:01 +0000 Subject: [PATCH 35/63] Create new Chip layout component --- .../RouteDetails/CircularVariantInfo.swift | 28 -------- .../RouteDetails/VariantWarning.swift | 30 +++----- GeoBus/App/Layout/Chip.swift | 70 +++++++++++++++++++ 3 files changed, 81 insertions(+), 47 deletions(-) delete mode 100644 GeoBus/App/Components/RouteDetails/CircularVariantInfo.swift create mode 100644 GeoBus/App/Layout/Chip.swift diff --git a/GeoBus/App/Components/RouteDetails/CircularVariantInfo.swift b/GeoBus/App/Components/RouteDetails/CircularVariantInfo.swift deleted file mode 100644 index 0df56475..00000000 --- a/GeoBus/App/Components/RouteDetails/CircularVariantInfo.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// RouteVariantWarning.swift -// GeoBus -// -// Created by João on 23/04/2020. -// Copyright © 2020 João. All rights reserved. -// - -import SwiftUI - -struct RouteCircularVariantInfo: View { - var body: some View { - VStack { - HStack { - Image(systemName: "repeat") - .font(.callout) - Text("This is a circular route.") - .font(.callout) - .fixedSize(horizontal: true, vertical: true) - Spacer() - } - } - .padding() - .foregroundColor(Color(.systemBlue)) - .background(Color(.systemBlue).opacity(0.2)) - .cornerRadius(10) - } -} diff --git a/GeoBus/App/Components/RouteDetails/VariantWarning.swift b/GeoBus/App/Components/RouteDetails/VariantWarning.swift index 4fc98f71..f51d6469 100644 --- a/GeoBus/App/Components/RouteDetails/VariantWarning.swift +++ b/GeoBus/App/Components/RouteDetails/VariantWarning.swift @@ -9,23 +9,15 @@ import SwiftUI struct VariantWarning: View { - - var qty: Int - - var body: some View { - VStack { - HStack { - Image(systemName: "info.circle.fill") - .font(.callout) - Text("This route may have \(qty) alternative paths.") - .font(.callout) - .fixedSize(horizontal: true, vertical: true) - } - } - .padding() - .foregroundColor(Color(.systemOrange)) - .background(Color(.systemOrange).opacity(0.2)) - .cornerRadius(10) - - } + + var qty: Int + + var body: some View { + Chip( + icon: Image(systemName: "info.circle.fill"), + text: Text("This route may have \(qty) alternative paths."), + color: Color(.systemOrange) + ) + } + } diff --git a/GeoBus/App/Layout/Chip.swift b/GeoBus/App/Layout/Chip.swift new file mode 100644 index 00000000..f1c51c81 --- /dev/null +++ b/GeoBus/App/Layout/Chip.swift @@ -0,0 +1,70 @@ +// +// Chip.swift +// GeoBus +// +// Created by João de Vasconcelos on 30/10/2022. +// + +import SwiftUI + +struct Chip: View { + + let icon: Image + let text: Text + let color: Color + let showContent: Bool + + @State private var placeholderOpacity: Double = 1 + + + init(icon: Image, text: Text, color: Color, showContent: Bool = true) { + self.icon = icon + self.text = text + self.color = color + self.showContent = showContent + } + + + var actualContent: some View { + HStack(alignment: .center) { + icon + text + Spacer() + } + .font(Font.system(size: 15, weight: .medium)) + .padding() + .foregroundColor(color) + .frame(maxWidth: .infinity) + .background(color.opacity(0.1)) + .cornerRadius(10) + } + + + var placeholder: some View { + HStack(alignment: .center) { + Circle() + .frame(width: 15, height: 15) + Rectangle() + .frame(width: 90, height: 12) + Spacer() + } + .font(Font.system(size: 15, weight: .medium)) + .padding() + .foregroundColor(Color("PlaceholderShape")) + .frame(maxWidth: .infinity) + .background(Color("PlaceholderShape").opacity(0.3)) + .cornerRadius(10) + .opacity(placeholderOpacity) + .animatePlaceholder(binding: $placeholderOpacity) + } + + + var body: some View { + if (showContent) { + actualContent + } else { + placeholder + } + } + +} From cf578643757a9fad23dc4e14753a1dc8b85726f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Mon, 31 Oct 2022 01:26:37 +0000 Subject: [PATCH 36/63] Chip --- GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift index ce8ac52e..48c0ecdf 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift @@ -54,7 +54,7 @@ struct RouteDetailsSheet: View { VStack(spacing: 15) { if (carrisNetworkController.activeVariant?.circularItinerary != nil) { - RouteCircularVariantInfo() + Chip(icon: Image(systemName: "repeat"), text: Text("This is a circular route."), color: Color(.systemBlue)) ConnectionsList(connections: carrisNetworkController.activeVariant!.circularItinerary!) } else if (carrisNetworkController.activeVariant?.ascendingItinerary != nil && carrisNetworkController.activeVariant?.descendingItinerary != nil) { From f3c4502e9e9d0c5a8eb6c13bb6e44bd12fcc309b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Mon, 31 Oct 2022 01:28:34 +0000 Subject: [PATCH 37/63] Embracing the nil Take the nil until the last view to create loading screens. This produces cleaner code, and improves consistency in handling data (instead of deciding on different fallback values every time the property is used). --- GeoBus.xcodeproj/project.pbxproj | 16 +- .../App/Components/Map/MapAnnotations.swift | 28 +- .../StopDetails/StopEstimations.swift | 7 +- .../VehicleDetails/VehicleDetailsView.swift | 242 +++++++++++++----- .../Carris/CarrisNetworkController.swift | 16 +- .../Networks/Carris/CarrisNetworkModel.swift | 6 +- GeoBus/App/Extensions/Helpers.swift | 15 +- .../Animations/PlaceholderAnimation.swift | 24 ++ GeoBus/App/Layout/RouteBadgePill.swift | 62 +++-- GeoBus/App/Layout/SheetErrorScreen.swift | 42 +++ GeoBus/App/Layout/StopIcon.swift | 87 ++++--- GeoBus/App/Layout/TimeLeft.swift | 49 +++- GeoBus/App/Layout/VehicleDestination.swift | 41 ++- GeoBus/App/Layout/VehicleIdentifier.swift | 51 ++-- 14 files changed, 504 insertions(+), 182 deletions(-) create mode 100644 GeoBus/App/Layout/Animations/PlaceholderAnimation.swift create mode 100644 GeoBus/App/Layout/SheetErrorScreen.swift diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index 0b54cff0..53be940f 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -26,7 +26,6 @@ CF18207028CCBD2300248F72 /* RouteDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18206128CCBD2300248F72 /* RouteDetailsView.swift */; }; CF18207228CCBD2300248F72 /* VariantPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18206528CCBD2300248F72 /* VariantPicker.swift */; }; CF18207328CCBD2300248F72 /* VariantButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18206628CCBD2300248F72 /* VariantButton.swift */; }; - CF18207428CCBD2300248F72 /* CircularVariantInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18206728CCBD2300248F72 /* CircularVariantInfo.swift */; }; CF18207528CCBD2300248F72 /* VariantWarning.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18206828CCBD2300248F72 /* VariantWarning.swift */; }; CF18207628CCBD2300248F72 /* ConnectionsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18206928CCBD2300248F72 /* ConnectionsList.swift */; }; CF18207728CCBD2300248F72 /* RouteDetailsAddToFavorites.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF18206A28CCBD2300248F72 /* RouteDetailsAddToFavorites.swift */; }; @@ -62,6 +61,9 @@ CFB5D45728EEFE21002368BC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = CFB5D45528EEFE21002368BC /* InfoPlist.strings */; }; CFB5D45A28EEFE6B002368BC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CFB5D45828EEFE6B002368BC /* Localizable.strings */; }; CFB71F82290CAFB500B37E69 /* LoadingSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFB71F81290CAFB500B37E69 /* LoadingSheet.swift */; }; + CFB71F84290E8ECF00B37E69 /* PlaceholderAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFB71F83290E8ECF00B37E69 /* PlaceholderAnimation.swift */; }; + CFB71F86290E913F00B37E69 /* Chip.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFB71F85290E913F00B37E69 /* Chip.swift */; }; + CFB71F88290F1AB100B37E69 /* SheetErrorScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFB71F87290F1AB100B37E69 /* SheetErrorScreen.swift */; }; CFDC15EF28D292FB00A4BE49 /* ViewSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDC15EE28D292FB00A4BE49 /* ViewSize.swift */; }; CFDD014928D5114D0070FE4B /* SyncStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDD014828D5114D0070FE4B /* SyncStatus.swift */; }; CFDD014B28D535370070FE4B /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDD014A28D535370070FE4B /* Card.swift */; }; @@ -100,7 +102,6 @@ CF18206128CCBD2300248F72 /* RouteDetailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouteDetailsView.swift; sourceTree = ""; }; CF18206528CCBD2300248F72 /* VariantPicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariantPicker.swift; sourceTree = ""; }; CF18206628CCBD2300248F72 /* VariantButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariantButton.swift; sourceTree = ""; }; - CF18206728CCBD2300248F72 /* CircularVariantInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularVariantInfo.swift; sourceTree = ""; }; CF18206828CCBD2300248F72 /* VariantWarning.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VariantWarning.swift; sourceTree = ""; }; CF18206928CCBD2300248F72 /* ConnectionsList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectionsList.swift; sourceTree = ""; }; CF18206A28CCBD2300248F72 /* RouteDetailsAddToFavorites.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RouteDetailsAddToFavorites.swift; sourceTree = ""; }; @@ -144,6 +145,9 @@ CFB5D46128EEFF2C002368BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; CFB5D46228EEFF2D002368BC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; CFB71F81290CAFB500B37E69 /* LoadingSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingSheet.swift; sourceTree = ""; }; + CFB71F83290E8ECF00B37E69 /* PlaceholderAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderAnimation.swift; sourceTree = ""; }; + CFB71F85290E913F00B37E69 /* Chip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chip.swift; sourceTree = ""; }; + CFB71F87290F1AB100B37E69 /* SheetErrorScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetErrorScreen.swift; sourceTree = ""; }; CFCED4F628EF5CB900963640 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; CFCED4F728EF5CB900963640 /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; CFCED4F828EF5CD500963640 /* fa */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fa; path = fa.lproj/Localizable.strings; sourceTree = ""; }; @@ -205,7 +209,6 @@ CF18206528CCBD2300248F72 /* VariantPicker.swift */, CF18206828CCBD2300248F72 /* VariantWarning.swift */, CF18206628CCBD2300248F72 /* VariantButton.swift */, - CF18206728CCBD2300248F72 /* CircularVariantInfo.swift */, CF18206928CCBD2300248F72 /* ConnectionsList.swift */, ); path = RouteDetails; @@ -216,9 +219,11 @@ children = ( CF18204328CCBCC500248F72 /* SheetHeader.swift */, CF6C917D28D3ED0A006C3F61 /* SquareButton.swift */, + CFB71F87290F1AB100B37E69 /* SheetErrorScreen.swift */, CF18204128CCBCC500248F72 /* RouteBadgePill.swift */, CF18204228CCBCC500248F72 /* RouteBadgeSquare.swift */, CFFFAD7828F4AD0400DFD5FD /* TimeLeft.swift */, + CFB71F85290E913F00B37E69 /* Chip.swift */, CFFFAD7A28F4D8D000DFD5FD /* StopIcon.swift */, CFB71F81290CAFB500B37E69 /* LoadingSheet.swift */, CF548FF528D14BA400668CB6 /* VehicleIdentifier.swift */, @@ -323,6 +328,7 @@ children = ( CFFFAD8A28F8F33200DFD5FD /* Pulse.swift */, CFFFAD8828F8ECDE00DFD5FD /* Spinner.swift */, + CFB71F83290E8ECF00B37E69 /* PlaceholderAnimation.swift */, ); path = Animations; sourceTree = ""; @@ -521,7 +527,6 @@ CF03D6AE28F3B00F0077299B /* VehicleDestination.swift in Sources */, CF18207328CCBD2300248F72 /* VariantButton.swift in Sources */, CF18209B28CCBD5000248F72 /* MapView.swift in Sources */, - CF18207428CCBD2300248F72 /* CircularVariantInfo.swift in Sources */, CF18208228CCBD3A00248F72 /* SelectRouteView.swift in Sources */, CF18208328CCBD3A00248F72 /* SelectRouteSheet.swift in Sources */, CF5094C528FC279A00EDD320 /* Array.swift in Sources */, @@ -532,6 +537,7 @@ CF18207528CCBD2300248F72 /* VariantWarning.swift in Sources */, CF6C918C28D4B452006C3F61 /* SearchStopInput.swift in Sources */, CF18207628CCBD2300248F72 /* ConnectionsList.swift in Sources */, + CFB71F88290F1AB100B37E69 /* SheetErrorScreen.swift in Sources */, CF181FE828CCB7D600248F72 /* ContentView.swift in Sources */, CF05F62028CD337200B4AD58 /* AppVersion.swift in Sources */, CF6C918628D3F8C8006C3F61 /* StopSearch.swift in Sources */, @@ -547,10 +553,12 @@ CF18205028CCBCE900248F72 /* OpenExternalLinkButton.swift in Sources */, CF18204928CCBCC500248F72 /* SheetHeader.swift in Sources */, CF18205728CCBD0400248F72 /* StopEstimations.swift in Sources */, + CFB71F84290E8ECF00B37E69 /* PlaceholderAnimation.swift in Sources */, CF5094CD28FCB9E400EDD320 /* CarrisAPIModel.swift in Sources */, CF5094C928FC50AC00EDD320 /* CarrisNetworkController.swift in Sources */, CFFFAD8528F7A21100DFD5FD /* CarrisAPI.swift in Sources */, CF05F61A28CD09A000B4AD58 /* NavBar.swift in Sources */, + CFB71F86290E913F00B37E69 /* Chip.swift in Sources */, CFDD014B28D535370070FE4B /* Card.swift in Sources */, CF18202928CCBBDC00248F72 /* TapticEngine.swift in Sources */, CF548FF628D14BA400668CB6 /* VehicleIdentifier.swift in Sources */, diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index 026a2350..9a081005 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -60,20 +60,24 @@ struct CarrisConnectionAnnotationView: View { var body: some View { - Button(action: { - TapticEngine.impact.feedback(.light) - carrisNetworkController.select(connection: self.connection) - appstate.present(sheet: .carris_connectionDetails) - }) { - StopIcon( - orderInRoute: self.connection.orderInRoute, - direction: self.connection.direction, - isSelected: carrisNetworkController.activeConnection == self.connection - ) - } - .frame(width: 40, height: 40, alignment: .center) + EmptyView() } +// var body: some View { +// Button(action: { +// TapticEngine.impact.feedback(.light) +// carrisNetworkController.select(connection: self.connection) +// appstate.present(sheet: .carris_connectionDetails) +// }) { +// StopIcon( +// orderInRoute: self.connection.orderInRoute, +// direction: self.connection.direction, +// isSelected: carrisNetworkController.activeConnection == self.connection +// ) +// } +// .frame(width: 40, height: 40, alignment: .center) +// } + } diff --git a/GeoBus/App/Components/StopDetails/StopEstimations.swift b/GeoBus/App/Components/StopDetails/StopEstimations.swift index 512ca302..89f8f60b 100644 --- a/GeoBus/App/Components/StopDetails/StopEstimations.swift +++ b/GeoBus/App/Components/StopDetails/StopEstimations.swift @@ -179,9 +179,10 @@ struct EstimationContainer: View { // mapController.moveMap(to:) appstate.present(sheet: .carris_vehicleDetails) }, label: { - HStack(spacing: 5) { - VehicleDestination(routeNumber: estimation.routeNumber, destination: estimation.destination) - Spacer() + HStack(spacing: 4) { + RouteBadgePill(routeNumber: estimation.routeNumber) + DestinationText(destination: estimation.destination) + Spacer(minLength: 5) TimeLeft(time: estimation.eta) } }) diff --git a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift index 52566672..6bfd0d41 100644 --- a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift +++ b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift @@ -14,13 +14,23 @@ struct CarrisVehicleSheetView: View { @ObservedObject var appstate = Appstate.shared @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + var body: some View { - if (carrisNetworkController.activeVehicle != nil && carrisNetworkController.activeVehicle!.hasLoadedCarrisDetails) { - Text("Details") - } else if (appstate.vehicles == .loading) { - Spinner(size: 30) + if (appstate.vehicles == .error) { + SheetErrorScreen() } else { - Text("Error") + VStack(spacing: 0) { + CarrisVehicleSheetHeader(vehicle: carrisNetworkController.activeVehicle) + ScrollView { + VStack(alignment: .leading, spacing: 15) { + CarrisVehicleLastSeenTime(vehicle: carrisNetworkController.activeVehicle) + CarrisVehicleNextStop(vehicle: carrisNetworkController.activeVehicle) + CarrisVehicleRouteOverview(vehicle: carrisNetworkController.activeVehicle) + Disclaimer() + } + .padding() + } + } } } @@ -30,95 +40,190 @@ struct CarrisVehicleSheetView: View { -struct VehicleDetailsView: View { +struct CarrisVehicleSheetHeader: View { - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + public let vehicle: CarrisNetworkModel.Vehicle? - let lastSeenTimeTimer = Timer.publish(every: 1 /* seconds */, on: .main, in: .common).autoconnect() + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 4) { + RouteBadgePill(routeNumber: vehicle?.routeNumber) + DestinationText(destination: vehicle?.lastStopOnVoyageName) + Spacer(minLength: 15) + VehicleIdentifier(busNumber: vehicle?.id, vehiclePlate: vehicle?.vehiclePlate) + } + .padding() + Divider() + } + } - @State var lastSeenTime: String = "-" +} + + + + + + +struct CarrisVehicleLastSeenTime: View { + + public let vehicle: CarrisNetworkModel.Vehicle? + private let lastSeenTimeTimer = Timer.publish(every: 1 /* seconds */, on: .main, in: .common).autoconnect() - var loadingScreen: some View { - HStack(spacing: 3) { - ProgressView() - .scaleEffect(0.55) - Text("Loading...") - .font(Font.system(size: 13, weight: .medium, design: .default) ) - .foregroundColor(Color(.tertiaryLabel)) - Spacer() + @State private var lastSeenTime: String? = nil + + + func updateLastSeenTime(_ value: Any) { + if (vehicle?.lastGpsTime != nil) { + self.lastSeenTime = Helpers.getTimeString(for: vehicle!.lastGpsTime!, in: .past, style: .full, units: [.hour, .minute, .second]) } } - var errorScreen: some View { - Text("Carris API is unavailable.") - .font(Font.system(size: 13, weight: .medium, design: .default) ) - .foregroundColor(Color(.secondaryLabel)) + + var body: some View { + Chip( + icon: Image(systemName: "antenna.radiowaves.left.and.right"), + text: Text("GPS updated \(lastSeenTime ?? "-") ago."), + color: Color(.secondaryLabel), + showContent: lastSeenTime != nil + ) + .onChange(of: vehicle, perform: updateLastSeenTime(_:)) + .onReceive(lastSeenTimeTimer, perform: updateLastSeenTime(_:)) } - var vehicleDetailsHeader: some View { - HStack(spacing: 15) { - VehicleDestination(routeNumber: carrisNetworkController.activeVehicle?.routeNumber ?? "-", destination: carrisNetworkController.activeVehicle?.lastStopOnVoyageName ?? "-") - Spacer() - VehicleIdentifier(busNumber: carrisNetworkController.activeVehicle?.id ?? -1, vehiclePlate: carrisNetworkController.activeVehicle?.vehiclePlate) +} + + + + + + + +struct CarrisVehicleNextStop: View { + + let vehicle: CarrisNetworkModel.Vehicle? + + @ObservedObject var appstate = Appstate.shared + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + + + @State var nextStopIndex: Int = 0 + + + func setNextStop(_ value: Any) { + if (vehicle?.routeEstimates != nil) { + if let lastStop = vehicle!.routeEstimates!.lastIndex(where: { + $0.hasArrived ?? false + }) { + nextStopIndex = lastStop + 1 + } else { + nextStopIndex = 10 + } } } - var vehicleDetailsScreen: some View { - VStack(alignment: .leading) { - HStack(alignment: .center, spacing: 5) { - Image(systemName: "antenna.radiowaves.left.and.right") - .font(.system(size: 12, weight: .bold, design: .default)) - .foregroundColor(Color(.secondaryLabel)) - Text("GPS updated \(lastSeenTime) ago") - .font(.system(size: 12, weight: .bold, design: .default)) + + var actualContent: some View { + VStack(spacing: 0) { + HStack { + Text("Next stop:") + .font(Font.system(size: 10, weight: .bold)) + .textCase(.uppercase) .foregroundColor(Color(.secondaryLabel)) - .onAppear() { - self.lastSeenTime = Helpers.getTimeString(for: carrisNetworkController.activeVehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) - } - .onReceive(lastSeenTimeTimer) { event in - self.lastSeenTime = Helpers.getTimeString(for: carrisNetworkController.activeVehicle?.lastGpsTime ?? "", in: .past, style: .full, units: [.hour, .minute, .second]) - } Spacer() + PulseLabel(accent: .blue, label: Text("Community")) } - VehicleRouteContainer(vehicle: carrisNetworkController.activeVehicle) + .padding(.vertical, 10) + .padding(.horizontal) + Divider() + Button(action: { + _ = carrisNetworkController.select(stop: vehicle!.routeEstimates![nextStopIndex].stopId) + appstate.present(sheet: .carris_stopDetails) + }, label: { + HStack(alignment: .center, spacing: 10) { + StopIcon(orderInRoute: nextStopIndex + 1, style: .standard) + Text(carrisNetworkController.find(stop: (vehicle!.routeEstimates![nextStopIndex].stopId))?.name ?? "") + .font(Font.system(size: 17, weight: .medium)) + .lineLimit(1) + .foregroundColor(Color(.label)) + Spacer(minLength: 5) + TimeLeft(time: vehicle?.routeEstimates![nextStopIndex].eta) + } + .onChange(of: vehicle, perform: setNextStop(_:)) + .padding() + }) } + .frame(maxWidth: .infinity) + .background(Color(.secondaryLabel).opacity(0.1)) + .cornerRadius(10) } var body: some View { - ScrollView { - VStack(alignment: .leading) { - VStack(alignment: .leading, spacing: 0) { - if (appstate.vehicles == .loading) { - loadingScreen - .padding() - } else if (appstate.vehicles == .error) { - errorScreen - .padding() - } else { - vehicleDetailsHeader - .padding() - Divider() - vehicleDetailsScreen - .padding() - } - } - Disclaimer() - Spacer() - } + if (vehicle?.routeEstimates != nil && nextStopIndex < vehicle!.routeEstimates!.count && vehicle?.routeEstimates?[nextStopIndex] != nil) { + actualContent + } else { + EmptyView() } } + } -struct VehicleRouteContainer: View { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +struct CarrisVehicleRouteOverview: View { let vehicle: CarrisNetworkModel.Vehicle? @@ -134,7 +239,7 @@ struct VehicleRouteContainer: View { if (index > 0) { Rectangle() .frame(width: 5, height: 25) - .foregroundColor(element.hasArrived ?? false ? .green : .blue) + .foregroundColor(element.hasArrived ?? false ? Color("StopMutedBackground") : .blue) .padding(.horizontal, 10) } Button(action: { @@ -142,9 +247,12 @@ struct VehicleRouteContainer: View { appstate.present(sheet: .carris_stopDetails) }, label: { HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: index) - Text(String(element.stopId)) - Spacer() + StopIcon(orderInRoute: index+1, style: element.hasArrived ?? false ? .muted : .standard) + Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") + .font(Font.system(size: 17, weight: .medium)) + .lineLimit(1) + .foregroundColor(element.hasArrived ?? false ? Color("StopMutedText") : .black) + Spacer(minLength: 5) if (element.hasArrived ?? false) { Text("já passou") } else { diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index 0d1f0214..ab7a8095 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -193,8 +193,10 @@ class CarrisNetworkController: ObservableObject { await self.fetchVehiclesListFromCarrisAPI() // DEBUG ! - self.select(vehicle: self.allVehicles[0].id) - Appstate.shared.present(sheet: .carris_vehicleDetails) + if (self.activeVehicle == nil) { + self.select(vehicle: self.allVehicles[0].id) + Appstate.shared.present(sheet: .carris_vehicleDetails) + } // ! DEBUG // If there is an active vehicle, also refresh it's details @@ -202,7 +204,7 @@ class CarrisNetworkController: ObservableObject { await self.fetchVehicleDetailsFromCarrisAPI(for: self.activeVehicle!.id) // If Community provider is also enabled, then also refresh those details if (self.communityDataProviderStatus) { - await self.fetchVehicleDetailsFromCarrisAPI(for: self.activeVehicle!.id) + await self.fetchVehicleDetailsFromCommunityAPI(for: self.activeVehicle!.id) } } // Update the list of active vehicles (the current selected route) @@ -269,7 +271,7 @@ class CarrisNetworkController: ObservableObject { // Save the formatted route object in the allRoutes temporary variable tempAllStops.append( CarrisNetworkModel.Stop( - id: availableStop.id ?? -1, + id: Int(availableStop.publicId ?? "-1") ?? -1, name: availableStop.name ?? "-", lat: availableStop.lat ?? 0, lng: availableStop.lng ?? 0 @@ -671,7 +673,7 @@ class CarrisNetworkController: ObservableObject { } } - private func find(stop stopId: Int) -> CarrisNetworkModel.Stop? { + public func find(stop stopId: Int) -> CarrisNetworkModel.Stop? { if let requestedStopObject = self.allStops[withId: stopId] { return requestedStopObject } else { @@ -1102,8 +1104,8 @@ class CarrisNetworkController: ObservableObject { tempFormattedEstimations.append( CarrisNetworkModel.Estimation( stopId: Int(apiEstimation.publicId ?? "-1") ?? -1, - routeNumber: apiEstimation.routeNumber ?? "-", - destination: apiEstimation.destination ?? "-", + routeNumber: apiEstimation.routeNumber, + destination: apiEstimation.destination, eta: apiEstimation.time ?? "", busNumber: Int(apiEstimation.busNumber ?? "-1") ) diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift index 7ec2ad28..aa991710 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift @@ -120,13 +120,13 @@ struct CarrisNetworkModel { struct Estimation: Codable, Identifiable, Equatable { let id: UUID let stopId: Int - let routeNumber: String - let destination: String + let routeNumber: String? + let destination: String? let eta: String let hasArrived: Bool? let busNumber: Int? - init(stopId: Int, routeNumber: String, destination: String, eta: String, busNumber: Int? = nil, hasArrived: Bool? = nil) { + init(stopId: Int, routeNumber: String?, destination: String?, eta: String, busNumber: Int? = nil, hasArrived: Bool? = nil) { self.id = UUID() self.stopId = stopId self.routeNumber = routeNumber diff --git a/GeoBus/App/Extensions/Helpers.swift b/GeoBus/App/Extensions/Helpers.swift index 7b7160eb..8dae8984 100644 --- a/GeoBus/App/Extensions/Helpers.swift +++ b/GeoBus/App/Extensions/Helpers.swift @@ -124,16 +124,17 @@ open class Helpers { } - static func getLastSeenTime(since lastGpsTime: String) -> Int { + static func getLastSeenTime(since isoDateString: String) -> Int { - let formatter = DateFormatter() - formatter.locale = Locale(identifier: "en_US_POSIX") - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + // Setup Date Formatter + let dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + // Parse ISO Timestamp using the Date Formatter let now = Date() - let estimation = formatter.date(from: lastGpsTime) ?? now - - let seconds = now.timeIntervalSince(estimation) + let dateObj = dateFormatter.date(from: isoDateString) ?? now + let seconds = now.timeIntervalSince(dateObj) // in seconds return Int(seconds) diff --git a/GeoBus/App/Layout/Animations/PlaceholderAnimation.swift b/GeoBus/App/Layout/Animations/PlaceholderAnimation.swift new file mode 100644 index 00000000..5840d210 --- /dev/null +++ b/GeoBus/App/Layout/Animations/PlaceholderAnimation.swift @@ -0,0 +1,24 @@ +// +// PlaceholderAnimation.swift +// GeoBus +// +// Created by João de Vasconcelos on 30/10/2022. +// + +import SwiftUI + +// Create an immediate, looping animation +extension View { + func animatePlaceholder(binding: Binding) -> some View { + + let minOpacity: Double = 0.5 + let animationSpeed: Double = 1 + + return onAppear { + withAnimation(.easeInOut(duration: animationSpeed).repeatForever(autoreverses: true)) { + binding.wrappedValue = minOpacity + } + } + + } +} diff --git a/GeoBus/App/Layout/RouteBadgePill.swift b/GeoBus/App/Layout/RouteBadgePill.swift index 07214274..999bfdfe 100644 --- a/GeoBus/App/Layout/RouteBadgePill.swift +++ b/GeoBus/App/Layout/RouteBadgePill.swift @@ -9,23 +9,55 @@ import SwiftUI struct RouteBadgePill: View { - - let routeNumber: String - - var body: some View { - + + public let routeNumber: String? + + private let fontSize: CGFloat = 13 + private let fontWeight: Font.Weight = .heavy + private let cornerRadius: CGFloat = 10 + private let paddingHorizontal: CGFloat = 7 + private let paddingVertical: CGFloat = 2 + + @State private var placeholderOpacity: Double = 1 + private let placeholderColor: Color = Color("PlaceholderShape") + + + var placeholder: some View { VStack { - Text(routeNumber) - .font(.footnote) - .fontWeight(.heavy) + Text("000") + .font(Font.system(size: fontSize, weight: fontWeight)) .lineLimit(1) - .foregroundColor(Helpers.getForegroundColor(for: routeNumber)) - .padding(.horizontal, 7) - .padding(.vertical, 2) + .foregroundColor(.clear) + .padding(.horizontal, paddingHorizontal) + .padding(.vertical, paddingVertical) } - .background(Helpers.getBackgroundColor(for: routeNumber)) - .cornerRadius(10) - + .background(placeholderColor) + .cornerRadius(cornerRadius) + .opacity(placeholderOpacity) + .animatePlaceholder(binding: $placeholderOpacity) } - + + + var actualContent: some View { + VStack { + Text(routeNumber!) + .font(Font.system(size: fontSize, weight: fontWeight)) + .lineLimit(1) + .foregroundColor(Helpers.getForegroundColor(for: routeNumber!)) + .padding(.horizontal, paddingHorizontal) + .padding(.vertical, paddingVertical) + } + .background(Helpers.getBackgroundColor(for: routeNumber!)) + .cornerRadius(cornerRadius) + } + + + var body: some View { + if (routeNumber != nil) { + actualContent + } else { + placeholder + } + } + } diff --git a/GeoBus/App/Layout/SheetErrorScreen.swift b/GeoBus/App/Layout/SheetErrorScreen.swift new file mode 100644 index 00000000..252e537a --- /dev/null +++ b/GeoBus/App/Layout/SheetErrorScreen.swift @@ -0,0 +1,42 @@ +// +// SheetErrorScreen.swift +// GeoBus +// +// Created by João de Vasconcelos on 30/10/2022. +// + +import SwiftUI + +struct SheetErrorScreen: View { + + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + + var body: some View { + VStack(alignment: .center, spacing: 15) { + Image(systemName: "exclamationmark.octagon.fill") + .foregroundColor(Color(.systemRed)) + .font(Font.system(size: 50, weight: .bold)) + Text("Connection Error") + .font(Font.system(size: 24, weight: .bold, design: .default) ) + .foregroundColor(Color(.label)) + Text("A connection to the API was not possible.") + .font(Font.system(size: 18, weight: .medium, design: .default) ) + .foregroundColor(Color(.secondaryLabel)) + Button(action: { + carrisNetworkController.refresh() + }, label: { + Text("Try Again") + .font(Font.system(size: 18, weight: .bold)) + .foregroundColor(.white) + .padding(.vertical, 10) + .padding(.horizontal, 20) + .frame(maxWidth: .infinity) + .background(Color(.systemBlue)) + .cornerRadius(10) + .padding(.top, 30) + }) + } + .padding() + .frame(maxWidth: .infinity) + } +} diff --git a/GeoBus/App/Layout/StopIcon.swift b/GeoBus/App/Layout/StopIcon.swift index ee11baa8..39496bb7 100644 --- a/GeoBus/App/Layout/StopIcon.swift +++ b/GeoBus/App/Layout/StopIcon.swift @@ -11,15 +11,27 @@ struct StopIcon: View { public let orderInRoute: Int? public let direction: CarrisNetworkModel.Direction? + public let style: Style public let isSelected: Bool - init(orderInRoute: Int? = nil, direction: CarrisNetworkModel.Direction? = nil, isSelected: Bool = false) { + init(orderInRoute: Int? = nil, direction: CarrisNetworkModel.Direction? = nil, style: Style = .standard, isSelected: Bool = false) { self.orderInRoute = orderInRoute self.direction = direction + self.style = style self.isSelected = isSelected } + enum Style { + case standard + case circular + case ascending + case descending + case selected + case muted + } + + // Properties: // The defaults for the icon private let size: CGFloat = 25 @@ -43,36 +55,53 @@ struct StopIcon: View { } private var borderColor: Color { - if (self.isSelected) { - return Color("StopSelectedBorder") - } else { - switch direction { - case .ascending: - return Color("StopAscendingBorder") - case .descending: - return Color("StopDescendingBorder") - case .circular: - return Color("StopCircularBorder") - case .none: - return Color("StopCircularBorder") - } + switch style { + case .standard: + return Color("StopCircularBorder") + case .ascending: + return Color("StopAscendingBorder") + case .descending: + return Color("StopDescendingBorder") + case .circular: + return Color("StopCircularBorder") + case .selected: + return Color("StopSelectedBorder") + case .muted: + return Color("StopMutedBorder") } } private var backgroundColor: Color { - if (self.isSelected) { - return Color("StopSelectedBackground") - } else { - switch direction { - case .ascending: - return Color("StopAscendingBackground") - case .descending: - return Color("StopDescendingBackground") - case .circular: - return Color("StopCircularBackground") - case .none: - return Color("StopCircularBackground") - } + switch style { + case .standard: + return Color("StopCircularBackground") + case .ascending: + return Color("StopAscendingBackground") + case .descending: + return Color("StopDescendingBackground") + case .circular: + return Color("StopCircularBackground") + case .selected: + return Color("StopSelectedBackground") + case .muted: + return Color("StopMutedBackground") + } + } + + private var textColor: Color { + switch style { + case .standard: + return Color("StopCircularText") + case .ascending: + return Color("StopAscendingText") + case .descending: + return Color("StopDescendingText") + case .circular: + return Color("StopCircularText") + case .selected: + return Color("StopSelectedText") + case .muted: + return Color("StopMutedText") } } @@ -92,12 +121,12 @@ struct StopIcon: View { if (self.orderInRoute != nil) { Text(String(self.orderInRoute!)) .font(.system(size: self.textSize, weight: .bold)) - .foregroundColor(.white) + .foregroundColor(textColor) .animation(.default, value: self.textSize) } else { Image(systemName: "mappin") .font(.system(size: self.textSize, weight: .bold)) - .foregroundColor(.white) + .foregroundColor(textColor) .animation(.default, value: self.textSize) } } diff --git a/GeoBus/App/Layout/TimeLeft.swift b/GeoBus/App/Layout/TimeLeft.swift index 2b839b5d..accd8b88 100644 --- a/GeoBus/App/Layout/TimeLeft.swift +++ b/GeoBus/App/Layout/TimeLeft.swift @@ -9,10 +9,29 @@ import SwiftUI struct TimeLeft: View { - public let time: String? + public let timeString: String? + + private let countdownUnits: NSCalendar.Unit private let countdownTimer = Timer.publish(every: 1 /* seconds */, on: .main, in: .common).autoconnect() - @State var timeLeftString: String = "" + @State private var countdownValue: Int = 0 + @State private var countdownString: String = "" + + + init(time: String?, units: NSCalendar.Unit = [.hour, .minute]) { + self.timeString = time + self.countdownUnits = units + } + + + func setCountdownString(_ value: Any) { + if (timeString != nil) { + self.countdownValue = Helpers.getLastSeenTime(since: self.timeString!) + print("countdownValue: \(countdownValue)") + self.countdownString = Helpers.getTimeString(for: self.timeString!, in: .future, style: .short, units: self.countdownUnits) + } + } + var loading: some View { HStack(spacing: 3) { @@ -26,22 +45,30 @@ struct TimeLeft: View { Image(systemName: "plusminus") .font(.footnote) .foregroundColor(Color(.tertiaryLabel)) - Text(self.timeLeftString) + Text(self.countdownString) .font(.body) .fontWeight(.medium) .foregroundColor(Color(.label)) - .onAppear() { - self.timeLeftString = Helpers.getTimeString(for: self.time ?? "", in: .future, style: .short, units: [.hour, .minute]) - } - .onReceive(countdownTimer) { event in - self.timeLeftString = Helpers.getTimeString(for: self.time ?? "", in: .future, style: .short, units: [.hour, .minute]) - } + .onChange(of: timeString, perform: setCountdownString) + .onReceive(countdownTimer, perform: setCountdownString) } } + var icon: some View { + Image(systemName: "figure.walk.arrival") + .font(.body) + .fontWeight(.medium) + .foregroundColor(Color(.systemBlue)) + } + + var body: some View { - if (time != nil) { - content + if (timeString != nil) { + if (countdownValue < 0) { + icon + } else { + content + } } else { loading } diff --git a/GeoBus/App/Layout/VehicleDestination.swift b/GeoBus/App/Layout/VehicleDestination.swift index d6a7ddf5..ee6b5379 100644 --- a/GeoBus/App/Layout/VehicleDestination.swift +++ b/GeoBus/App/Layout/VehicleDestination.swift @@ -7,18 +7,37 @@ import SwiftUI -struct VehicleDestination: View { + + + + +struct DestinationText: View { - public let routeNumber: String - public let destination: String + let destination: String? - var body: some View { + @State private var placeholderOpacity: Double = 1 + + + var placeholder: some View { + HStack(alignment: .center, spacing: 4) { + Image(systemName: "arrow.forward") + .font(.system(size: 8, weight: .bold, design: .default)) + .foregroundColor(Color("PlaceholderText")) + Rectangle() + .frame(width: 80, height: 12) + .foregroundColor(Color("PlaceholderShape")) + } + .opacity(self.placeholderOpacity) + .animatePlaceholder(binding: self.$placeholderOpacity) + } + + + var actualContent: some View { HStack(spacing: 4) { - RouteBadgePill(routeNumber: self.routeNumber) Image(systemName: "arrow.forward") .font(.system(size: 8, weight: .bold, design: .default)) .foregroundColor(Color(.tertiaryLabel)) - Text(self.destination) + Text(self.destination!) .font(.body) .fontWeight(.medium) .foregroundColor(Color(.label)) @@ -26,5 +45,13 @@ struct VehicleDestination: View { } } + + var body: some View { + if (self.destination != nil) { + actualContent + } else { + placeholder + } + } + } - diff --git a/GeoBus/App/Layout/VehicleIdentifier.swift b/GeoBus/App/Layout/VehicleIdentifier.swift index 2de59b76..5c791d02 100644 --- a/GeoBus/App/Layout/VehicleIdentifier.swift +++ b/GeoBus/App/Layout/VehicleIdentifier.swift @@ -10,14 +10,29 @@ import SwiftUI struct VehicleIdentifier: View { - let busNumber: Int + let busNumber: Int? let vehiclePlate: String? @State var toggleIdentifier: Bool = false + @State var placeholderOpacity: Double = 1 + + var placeholder: some View { + Text("00000") + .font(Font.system(size: 12, weight: .bold, design: .monospaced) ) + .foregroundColor(.clear) + .padding(.vertical, 2) + .padding(.horizontal, 7) + .background(Color("PlaceholderShape")) + .cornerRadius(5) + .opacity(placeholderOpacity) + .animatePlaceholder(binding: $placeholderOpacity) + } + + var busNumberView: some View { - Text(String(busNumber)) + Text(String(busNumber!)) .font(Font.system(size: 12, weight: .bold, design: .monospaced) ) .foregroundColor(.primary) .padding(.vertical, 2) @@ -51,24 +66,26 @@ struct VehicleIdentifier: View { var body: some View { - // If vehiclePlate is not available, - // then show only the busNumber - if (vehiclePlate != nil) { - // If both are available, allow them to be toggled - VStack { - if (toggleIdentifier) { - busNumberView - } else { - licensePlateView + if (busNumber != nil) { + + if (vehiclePlate != nil) { + VStack { + if (toggleIdentifier) { + busNumberView + } else { + licensePlateView + } } + .onTapGesture { + TapticEngine.impact.feedback(.light) + self.toggleIdentifier = !toggleIdentifier + } + } else { + busNumberView } - .onTapGesture { - TapticEngine.impact.feedback(.light) - self.toggleIdentifier = !toggleIdentifier - } - + } else { - busNumberView + placeholder } } From 48bf831f18cb000ab1ee8fece9859fed72af4e2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 1 Nov 2022 19:31:22 +0000 Subject: [PATCH 38/63] Good next version Missing: center routeOverview on current stop. --- GeoBus/App/Components/About/AboutGeoBus.swift | 2 +- GeoBus/App/Components/About/CloseButton.swift | 4 +- .../Components/About/DataProvidersCard.swift | 70 +++---- GeoBus/App/Components/About/SyncStatus.swift | 4 +- GeoBus/App/Components/ContentView.swift | 2 +- .../App/Components/Map/MapAnnotations.swift | 49 +++-- GeoBus/App/Components/Map/MapView.swift | 4 +- GeoBus/App/Components/Map/UserLocation.swift | 2 +- GeoBus/App/Components/NavBar.swift | 4 +- .../App/Components/PresentedSheetView.swift | 4 +- .../RouteDetailsAddToFavorites.swift | 2 +- .../RouteDetails/RouteDetailsSheet.swift | 4 +- .../RouteDetails/RouteDetailsView.swift | 4 +- .../RouteDetails/VariantPicker.swift | 2 +- .../SelectRoute/FavoriteRoutes.swift | 4 +- .../SelectRoute/SelectRouteInput.swift | 4 +- .../SelectRoute/SelectRouteView.swift | 4 +- .../Components/SelectRoute/SetOfRoutes.swift | 4 +- .../StopDetails/SearchStopInput.swift | 4 +- .../StopDetails/StopDetailsView.swift | 4 +- .../StopDetails/StopEstimations.swift | 105 ++++------ .../Components/StopDetails/StopSearch.swift | 2 +- .../VehicleDetails/VehicleDetailsView.swift | 179 ++++++++++++++---- .../Carris/CarrisNetworkController.swift | 23 ++- .../Networks/Carris/CarrisNetworkModel.swift | 6 +- GeoBus/App/Extensions/Helpers.swift | 45 ++++- GeoBus/App/GeoBusApp.swift | 11 +- GeoBus/App/Layout/SheetErrorScreen.swift | 2 +- GeoBus/App/Layout/SheetHeader.swift | 2 +- GeoBus/App/Layout/TimeLeft.swift | 60 +++--- .../RegularService.imageset/Contents.json | 12 +- .../regular-service-active-dark.png | Bin 0 -> 1777 bytes .../regular-service-active-dark@2x.png | Bin 0 -> 4489 bytes .../regular-service-active-dark@3x.png | Bin 0 -> 7868 bytes .../regular-service-active-light.png | Bin 0 -> 1228 bytes .../regular-service-active-light@2x.png | Bin 0 -> 2615 bytes .../regular-service-active-light@3x.png | Bin 0 -> 4118 bytes .../regular-service-dark.png | Bin 1444 -> 0 bytes .../regular-service-dark@2x.png | Bin 3364 -> 0 bytes .../regular-service-dark@3x.png | Bin 6191 -> 0 bytes .../regular-service-light.png | Bin 828 -> 0 bytes .../regular-service-light@2x.png | Bin 1482 -> 0 bytes .../regular-service-light@3x.png | Bin 2207 -> 0 bytes 43 files changed, 369 insertions(+), 259 deletions(-) create mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-active-dark.png create mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-active-dark@2x.png create mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-active-dark@3x.png create mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-active-light.png create mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-active-light@2x.png create mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-active-light@3x.png delete mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-dark.png delete mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-dark@2x.png delete mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-dark@3x.png delete mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-light.png delete mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-light@2x.png delete mode 100644 GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-light@3x.png diff --git a/GeoBus/App/Components/About/AboutGeoBus.swift b/GeoBus/App/Components/About/AboutGeoBus.swift index 1cb27137..ac90be34 100644 --- a/GeoBus/App/Components/About/AboutGeoBus.swift +++ b/GeoBus/App/Components/About/AboutGeoBus.swift @@ -9,7 +9,7 @@ import SwiftUI struct AboutGeoBus: View { - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @State private var showInfoSheet: Bool = false diff --git a/GeoBus/App/Components/About/CloseButton.swift b/GeoBus/App/Components/About/CloseButton.swift index 977a4ac2..5a4bda5b 100644 --- a/GeoBus/App/Components/About/CloseButton.swift +++ b/GeoBus/App/Components/About/CloseButton.swift @@ -9,8 +9,8 @@ import SwiftUI struct CloseButton: View { - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @Binding var isPresenting: Bool diff --git a/GeoBus/App/Components/About/DataProvidersCard.swift b/GeoBus/App/Components/About/DataProvidersCard.swift index 53a676e4..08361239 100644 --- a/GeoBus/App/Components/About/DataProvidersCard.swift +++ b/GeoBus/App/Components/About/DataProvidersCard.swift @@ -9,45 +9,18 @@ import SwiftUI struct DataProvidersCard: View { - @EnvironmentObject var carrisNetworkController: CarrisNetworkController - - private let cardColor: Color = Color(.systemTeal) - - - var providerToggle: some View { - Toggle(isOn: $carrisNetworkController.communityDataProviderStatus) { - HStack { - Image(systemName: "staroflife.circle") - .renderingMode(.template) - .font(Font.system(size: 25)) - .foregroundColor(cardColor) - Text("Community Data") - .font(Font.system(size: 18, weight: .bold)) - .foregroundColor(cardColor) - .padding(.leading, 5) - } - } - .padding() - .frame(maxWidth: .infinity) - .tint(cardColor) - .background(cardColor.opacity(0.05)) - .cornerRadius(10) - .onChange(of: carrisNetworkController.communityDataProviderStatus) { value in - carrisNetworkController.toggleCommunityDataProviderTo(to: value) - } - } - + private let accentColor: Color = Color(.systemTeal) var body: some View { Card { Image(systemName: "clock.arrow.2.circlepath") .font(Font.system(size: 30, weight: .regular)) - .foregroundColor(cardColor) + .foregroundColor(accentColor) Text("Community Data") .font(.title) .fontWeight(.bold) - .foregroundColor(cardColor) - Text("Try an experimetal feature made in partnership with people interested in improving transportation in Lisbon.") + .foregroundColor(accentColor) + Text("Try an experimental feature made in partnership with people interested in improving transportation in Lisbon.") .multilineTextAlignment(.center) .font(.headline) .fontWeight(.semibold) @@ -57,7 +30,40 @@ struct DataProvidersCard: View { .font(.headline) .fontWeight(.semibold) .foregroundColor(Color(.secondaryLabel)) - providerToggle + CommunityProviderToggle() + } + } + +} + + + +struct CommunityProviderToggle: View { + + private let accentColor: Color = Color(.systemTeal) + + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + + var body: some View { + Toggle(isOn: $carrisNetworkController.communityDataProviderStatus) { + HStack { + Image(systemName: "staroflife.circle") + .renderingMode(.template) + .font(Font.system(size: 25)) + .foregroundColor(accentColor) + Text("Community Data") + .font(Font.system(size: 18, weight: .bold)) + .foregroundColor(accentColor) + .padding(.leading, 5) + } + } + .padding() + .frame(maxWidth: .infinity) + .tint(accentColor) + .background(accentColor.opacity(0.05)) + .cornerRadius(10) + .onChange(of: carrisNetworkController.communityDataProviderStatus) { value in + carrisNetworkController.toggleCommunityDataProviderStatus(to: value) } } diff --git a/GeoBus/App/Components/About/SyncStatus.swift b/GeoBus/App/Components/About/SyncStatus.swift index 77d4b8e2..68393881 100644 --- a/GeoBus/App/Components/About/SyncStatus.swift +++ b/GeoBus/App/Components/About/SyncStatus.swift @@ -9,8 +9,8 @@ import SwiftUI struct SyncStatus: View { - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var syncError: some View { diff --git a/GeoBus/App/Components/ContentView.swift b/GeoBus/App/Components/ContentView.swift index 776a4f43..f983bd08 100644 --- a/GeoBus/App/Components/ContentView.swift +++ b/GeoBus/App/Components/ContentView.swift @@ -10,7 +10,7 @@ import SwiftUI struct ContentView: View { - @ObservedObject var appstate = Appstate.shared + @ObservedObject private var appstate = Appstate.shared var body: some View { VStack(spacing: 0) { diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index 9a081005..79607220 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -33,8 +33,8 @@ struct CarrisStopAnnotationView: View { public let stop: CarrisNetworkModel.Stop - @ObservedObject var appstate = Appstate.shared - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { @@ -55,29 +55,29 @@ struct CarrisConnectionAnnotationView: View { public let connection: CarrisNetworkModel.Connection - @ObservedObject var appstate = Appstate.shared - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + // var body: some View { + // EmptyView() + // } + var body: some View { - EmptyView() + Button(action: { + TapticEngine.impact.feedback(.light) + carrisNetworkController.select(connection: self.connection) + appstate.present(sheet: .carris_connectionDetails) + }) { + StopIcon( + orderInRoute: self.connection.orderInRoute, + direction: self.connection.direction, + isSelected: carrisNetworkController.activeConnection == self.connection + ) + } + .frame(width: 40, height: 40, alignment: .center) } -// var body: some View { -// Button(action: { -// TapticEngine.impact.feedback(.light) -// carrisNetworkController.select(connection: self.connection) -// appstate.present(sheet: .carris_connectionDetails) -// }) { -// StopIcon( -// orderInRoute: self.connection.orderInRoute, -// direction: self.connection.direction, -// isSelected: carrisNetworkController.activeConnection == self.connection -// ) -// } -// .frame(width: 40, height: 40, alignment: .center) -// } - } @@ -88,8 +88,8 @@ struct CarrisVehicleAnnotationView: View { let vehicle: CarrisNetworkModel.Vehicle - @ObservedObject var appstate = Appstate.shared - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { @@ -104,11 +104,6 @@ struct CarrisVehicleAnnotationView: View { Image("Tram") case .neighborhood, .night, .regular, .none: Image("RegularService") - Text(verbatim: String(vehicle.id)) - .font(Font.system(size: 10, weight: .bold, design: .monospaced)) - .tracking(1) - .foregroundColor(.white) - .padding(.leading, 12) } } } diff --git a/GeoBus/App/Components/Map/MapView.swift b/GeoBus/App/Components/Map/MapView.swift index e4b430f3..00ab1352 100644 --- a/GeoBus/App/Components/Map/MapView.swift +++ b/GeoBus/App/Components/Map/MapView.swift @@ -12,8 +12,8 @@ import MapKit struct MapView: View { - @ObservedObject var mapController = MapController.shared - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var mapController = MapController.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { diff --git a/GeoBus/App/Components/Map/UserLocation.swift b/GeoBus/App/Components/Map/UserLocation.swift index e09ac2d9..bce0d456 100644 --- a/GeoBus/App/Components/Map/UserLocation.swift +++ b/GeoBus/App/Components/Map/UserLocation.swift @@ -9,7 +9,7 @@ import SwiftUI struct UserLocation: View { - @EnvironmentObject var mapController: MapController + @ObservedObject private var mapController = MapController.shared var body: some View { SquareButton(icon: "location.fill", size: 22) diff --git a/GeoBus/App/Components/NavBar.swift b/GeoBus/App/Components/NavBar.swift index ca47b780..047aa6c6 100644 --- a/GeoBus/App/Components/NavBar.swift +++ b/GeoBus/App/Components/NavBar.swift @@ -10,8 +10,8 @@ import Combine struct NavBar: View { - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @State var showSelectRouteSheet: Bool = false @State var showRouteDetailsSheet: Bool = false diff --git a/GeoBus/App/Components/PresentedSheetView.swift b/GeoBus/App/Components/PresentedSheetView.swift index e0cb6a40..18c08fd5 100644 --- a/GeoBus/App/Components/PresentedSheetView.swift +++ b/GeoBus/App/Components/PresentedSheetView.swift @@ -9,8 +9,8 @@ import SwiftUI struct PresentedSheetView: View { - @ObservedObject var appstate = Appstate.shared - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { switch appstate.currentlyPresentedSheetView { diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsAddToFavorites.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsAddToFavorites.swift index 275cb520..76bb4fed 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsAddToFavorites.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsAddToFavorites.swift @@ -10,7 +10,7 @@ import SwiftUI struct RouteDetailsAddToFavorites: View { - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift index 48c0ecdf..2c2a024d 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsSheet.swift @@ -10,8 +10,8 @@ import SwiftUI struct RouteDetailsSheet: View { - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @State var routeDirection: Int = 0 @State var routeDirectionPicker: Int = 0 diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift index 15d9ee54..671ed612 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift @@ -10,8 +10,8 @@ import SwiftUI struct RouteDetailsView: View { - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared // Initial screen simply explaining how to select a route, diff --git a/GeoBus/App/Components/RouteDetails/VariantPicker.swift b/GeoBus/App/Components/RouteDetails/VariantPicker.swift index db3264f1..c427e7c6 100644 --- a/GeoBus/App/Components/RouteDetails/VariantPicker.swift +++ b/GeoBus/App/Components/RouteDetails/VariantPicker.swift @@ -10,7 +10,7 @@ import SwiftUI struct VariantPicker: View { - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { diff --git a/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift b/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift index 2b8113b1..1d5a6a35 100644 --- a/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift +++ b/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift @@ -12,8 +12,8 @@ struct FavoriteRoutes: View { @Environment(\.colorScheme) var colorScheme: ColorScheme - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @State var routes: [CarrisNetworkModel.Route] = [] diff --git a/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift b/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift index d69c1230..fd822ad7 100644 --- a/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift +++ b/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift @@ -10,8 +10,8 @@ import SwiftUI struct SelectRouteInput: View { - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @State var showErrorLabel: Bool = false diff --git a/GeoBus/App/Components/SelectRoute/SelectRouteView.swift b/GeoBus/App/Components/SelectRoute/SelectRouteView.swift index 203eecc6..e840fb42 100644 --- a/GeoBus/App/Components/SelectRoute/SelectRouteView.swift +++ b/GeoBus/App/Components/SelectRoute/SelectRouteView.swift @@ -12,8 +12,8 @@ struct SelectRouteView: View { @Environment(\.colorScheme) var colorScheme: ColorScheme - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { diff --git a/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift b/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift index 903b46bf..e347d53e 100644 --- a/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift +++ b/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift @@ -10,8 +10,8 @@ import SwiftUI struct SetOfRoutes: View { - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared let title: Text let kind: CarrisNetworkModel.Kind diff --git a/GeoBus/App/Components/StopDetails/SearchStopInput.swift b/GeoBus/App/Components/StopDetails/SearchStopInput.swift index 8c1ea83c..a388ce29 100644 --- a/GeoBus/App/Components/StopDetails/SearchStopInput.swift +++ b/GeoBus/App/Components/StopDetails/SearchStopInput.swift @@ -10,8 +10,8 @@ import SwiftUI struct SearchStopInput: View { - @EnvironmentObject var appstate: Appstate - @EnvironmentObject var carrisNetworkController: CarrisNetworkController + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @FocusState private var stopIdInputIsFocused: Bool diff --git a/GeoBus/App/Components/StopDetails/StopDetailsView.swift b/GeoBus/App/Components/StopDetails/StopDetailsView.swift index 362b5087..5b642341 100644 --- a/GeoBus/App/Components/StopDetails/StopDetailsView.swift +++ b/GeoBus/App/Components/StopDetails/StopDetailsView.swift @@ -10,7 +10,7 @@ import SwiftUI struct ConnectionSheetView: View { - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { ScrollView { @@ -29,7 +29,7 @@ struct ConnectionSheetView: View { struct StopSheetView: View { - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { ScrollView { diff --git a/GeoBus/App/Components/StopDetails/StopEstimations.swift b/GeoBus/App/Components/StopDetails/StopEstimations.swift index 89f8f60b..5a85f393 100644 --- a/GeoBus/App/Components/StopDetails/StopEstimations.swift +++ b/GeoBus/App/Components/StopDetails/StopEstimations.swift @@ -12,14 +12,14 @@ struct EstimationsContainer: View { let stopId: Int - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @State var estimations: [CarrisNetworkModel.Estimation]? let refreshTimer = Timer.publish(every: 60 /* seconds */, on: .main, in: .common).autoconnect() - func getEstimationsFromController() { + func getEstimationsFromController(_ value: Any?) { Task { self.estimations = await carrisNetworkController.getEstimation(for: self.stopId) } @@ -30,11 +30,13 @@ struct EstimationsContainer: View { VStack(alignment: .leading, spacing: 10) { EstimationsHeader() EstimationsList(estimations: self.estimations) - .onAppear(perform: self.getEstimationsFromController) - .onReceive(refreshTimer) { event in - self.getEstimationsFromController() + .onAppear() { self.getEstimationsFromController(nil) } + .onReceive(refreshTimer, perform: self.getEstimationsFromController(_:)) + .onChange(of: carrisNetworkController.communityDataProviderStatus) { value in + self.estimations = nil + self.getEstimationsFromController(nil) } - debug_providerToggle + CommunityProviderToggle() .padding(.vertical) Disclaimer() .padding(.vertical) @@ -43,48 +45,6 @@ struct EstimationsContainer: View { - // ! DEBUG - var debug_providerToggle: some View { - VStack { - Toggle(isOn: $carrisNetworkController.communityDataProviderStatus) { - HStack { - Image(systemName: "staroflife.circle") - .renderingMode(.template) - .font(Font.system(size: 25)) - .foregroundColor(.teal) - Text("Community Data") - .font(Font.system(size: 18, weight: .bold)) - .foregroundColor(.teal) - .padding(.leading, 5) - } - } - .padding() - .frame(maxWidth: .infinity) - .tint(.teal) - .background(.teal.opacity(0.05)) - .cornerRadius(10) - .onChange(of: carrisNetworkController.communityDataProviderStatus) { value in - carrisNetworkController.toggleCommunityDataProviderTo(to: value) - self.estimations = nil - self.getEstimationsFromController() - } - Button(action: { - self.estimations = nil - self.getEstimationsFromController() - }, label: { - Text("Reload Estimate") - .font(Font.system(size: 15, weight: .bold, design: .default) ) - .foregroundColor(Color(.white)) - .padding(5) - .frame(maxWidth: .infinity) - .background(Color(.systemBlue)) - .cornerRadius(10) - }) - } - } - - - } @@ -92,6 +52,8 @@ struct EstimationsContainer: View { struct EstimationsHeader: View { + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + var body: some View { HStack { Text("Next on this stop:") @@ -99,7 +61,11 @@ struct EstimationsHeader: View { .textCase(.uppercase) .foregroundColor(Color(.tertiaryLabel)) Spacer() - PulseLabel(accent: .orange, label: Text("Estimated")) + if (carrisNetworkController.communityDataProviderStatus) { + PulseLabel(accent: Color(.systemTeal), label: Text("Community")) + } else { + PulseLabel(accent: Color(.systemOrange), label: Text("Estimated")) + } } } @@ -169,23 +135,34 @@ struct EstimationContainer: View { let estimation: CarrisNetworkModel.Estimation - @ObservedObject var appstate = Appstate.shared - @ObservedObject var mapController = MapController.shared - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var mapController = MapController.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + + + var estimationLine: some View { + HStack(spacing: 4) { + RouteBadgePill(routeNumber: estimation.routeNumber) + DestinationText(destination: estimation.destination) + Spacer(minLength: 5) + TimeLeft(time: estimation.eta) + } + } + var body: some View { - Button(action: { - carrisNetworkController.select(vehicle: estimation.busNumber) -// mapController.moveMap(to:) - appstate.present(sheet: .carris_vehicleDetails) - }, label: { - HStack(spacing: 4) { - RouteBadgePill(routeNumber: estimation.routeNumber) - DestinationText(destination: estimation.destination) - Spacer(minLength: 5) - TimeLeft(time: estimation.eta) - } - }) + + if (estimation.busNumber != nil) { + Button(action: { + carrisNetworkController.select(vehicle: estimation.busNumber) + // mapController.moveMap(to:) + appstate.present(sheet: .carris_vehicleDetails) + }, label: { + estimationLine + }) + } else { + estimationLine + } } } diff --git a/GeoBus/App/Components/StopDetails/StopSearch.swift b/GeoBus/App/Components/StopDetails/StopSearch.swift index 19cfd275..39be3813 100644 --- a/GeoBus/App/Components/StopDetails/StopSearch.swift +++ b/GeoBus/App/Components/StopDetails/StopSearch.swift @@ -9,7 +9,7 @@ import SwiftUI struct StopSearch: View { - @ObservedObject var appstate = Appstate.shared + @ObservedObject private var appstate = Appstate.shared var body: some View { SquareButton(icon: "mail.and.text.magnifyingglass", size: 26) diff --git a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift index 6bfd0d41..14a60d38 100644 --- a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift +++ b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift @@ -11,22 +11,26 @@ import Combine struct CarrisVehicleSheetView: View { - @ObservedObject var appstate = Appstate.shared - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { - if (appstate.vehicles == .error) { - SheetErrorScreen() - } else { - VStack(spacing: 0) { + VStack(spacing: 0) { + if (appstate.vehicles == .error) { + SheetErrorScreen() + } else { CarrisVehicleSheetHeader(vehicle: carrisNetworkController.activeVehicle) ScrollView { VStack(alignment: .leading, spacing: 15) { CarrisVehicleLastSeenTime(vehicle: carrisNetworkController.activeVehicle) - CarrisVehicleNextStop(vehicle: carrisNetworkController.activeVehicle) - CarrisVehicleRouteOverview(vehicle: carrisNetworkController.activeVehicle) - Disclaimer() + if (carrisNetworkController.communityDataProviderStatus) { + // CarrisVehicleNextStop(vehicle: carrisNetworkController.activeVehicle) + CarrisVehicleRouteOverview(vehicle: carrisNetworkController.activeVehicle) + Disclaimer() + } else { + DataProvidersCard() + } } .padding() } @@ -103,19 +107,19 @@ struct CarrisVehicleNextStop: View { let vehicle: CarrisNetworkModel.Vehicle? - @ObservedObject var appstate = Appstate.shared - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @State var nextStopIndex: Int = 0 func setNextStop(_ value: Any) { - if (vehicle?.routeEstimates != nil) { - if let lastStop = vehicle!.routeEstimates!.lastIndex(where: { + if (vehicle?.routeOverview != nil) { + if let previousStop = vehicle!.routeOverview!.lastIndex(where: { $0.hasArrived ?? false }) { - nextStopIndex = lastStop + 1 + nextStopIndex = previousStop + 1 } else { nextStopIndex = 10 } @@ -123,6 +127,11 @@ struct CarrisVehicleNextStop: View { } + var placeholder: some View { + EmptyView() + } + + var actualContent: some View { VStack(spacing: 0) { @@ -138,33 +147,39 @@ struct CarrisVehicleNextStop: View { .padding(.horizontal) Divider() Button(action: { - _ = carrisNetworkController.select(stop: vehicle!.routeEstimates![nextStopIndex].stopId) + _ = carrisNetworkController.select(stop: vehicle!.routeOverview![nextStopIndex].stopId) appstate.present(sheet: .carris_stopDetails) }, label: { HStack(alignment: .center, spacing: 10) { StopIcon(orderInRoute: nextStopIndex + 1, style: .standard) - Text(carrisNetworkController.find(stop: (vehicle!.routeEstimates![nextStopIndex].stopId))?.name ?? "") + Text(carrisNetworkController.find(stop: (vehicle!.routeOverview![nextStopIndex].stopId))?.name ?? "") .font(Font.system(size: 17, weight: .medium)) .lineLimit(1) .foregroundColor(Color(.label)) Spacer(minLength: 5) - TimeLeft(time: vehicle?.routeEstimates![nextStopIndex].eta) + TimeLeft(time: vehicle?.routeOverview![nextStopIndex].eta) } .onChange(of: vehicle, perform: setNextStop(_:)) .padding() }) } .frame(maxWidth: .infinity) - .background(Color(.secondaryLabel).opacity(0.1)) + .background(Color(.systemBlue).opacity(0.05)) .cornerRadius(10) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(Color(.systemBlue).opacity(1), lineWidth: 2) + ) } var body: some View { - if (vehicle?.routeEstimates != nil && nextStopIndex < vehicle!.routeEstimates!.count && vehicle?.routeEstimates?[nextStopIndex] != nil) { - actualContent + if (vehicle?.routeOverview != nil) { + if (nextStopIndex < vehicle!.routeOverview!.count && vehicle?.routeOverview?[nextStopIndex] != nil) { + actualContent + } } else { - EmptyView() + placeholder } } @@ -231,43 +246,125 @@ struct CarrisVehicleRouteOverview: View { @ObservedObject var carrisNetworkController = CarrisNetworkController.shared - var body: some View { + func findNextStop() -> Int? { + if (vehicle?.routeOverview != nil) { + + if let previousStop = vehicle!.routeOverview!.lastIndex(where: { + $0.hasArrived ?? false + }) { + if (previousStop + 1 < vehicle!.routeOverview!.count) { + let nextStop = vehicle!.routeOverview![previousStop + 1] + return nextStop.stopId + } + } + + } + return nil + } + + + + + + var content: some View { VStack(alignment: .leading, spacing: 0) { - if (vehicle?.routeEstimates != nil) { - ForEach(Array(vehicle!.routeEstimates!.enumerated()), id: \.offset) { index, element in + if (vehicle?.routeOverview != nil) { + + ForEach(Array(vehicle!.routeOverview!.enumerated()), id: \.offset) { index, element in VStack(alignment: .leading, spacing: 0) { - if (index > 0) { - Rectangle() - .frame(width: 5, height: 25) - .foregroundColor(element.hasArrived ?? false ? Color("StopMutedBackground") : .blue) - .padding(.horizontal, 10) - } - Button(action: { - _ = carrisNetworkController.select(stop: element.stopId) - appstate.present(sheet: .carris_stopDetails) - }, label: { + + if (element.hasArrived ?? false) { + + if (index > 0) { + Rectangle() + .frame(width: 3, height: 30) + .foregroundColor(Color("StopMutedBackground")) + .padding(.horizontal, 11) + } + HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: index+1, style: element.hasArrived ?? false ? .muted : .standard) + StopIcon(orderInRoute: index+1, style: .muted) Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") .font(Font.system(size: 17, weight: .medium)) .lineLimit(1) .foregroundColor(element.hasArrived ?? false ? Color("StopMutedText") : .black) Spacer(minLength: 5) - if (element.hasArrived ?? false) { - Text("já passou") - } else { - TimeLeft(time: element.eta) - } + Image(systemName: "checkmark.circle") + .font(Font.system(size: 15, weight: .medium)) + .foregroundColor(Color("StopMutedText")) + } + + } else if (findNextStop() == element.stopId) { + + VStack(spacing: 0) { + Rectangle() + .frame(width: 3, height: 30) + .foregroundColor(Color("StopMutedBackground")) + .padding(.horizontal, 11) + Image(systemName: "arrowtriangle.down.circle.fill") + .font(Font.system(size: 15, weight: .medium)) + .foregroundColor(Color(.systemBlue)) + .padding(.vertical, -2) + Rectangle() + .frame(width: 5, height: 30) + .foregroundColor(Color(.systemBlue)) + .padding(.horizontal, 10) + } + + HStack(alignment: .center, spacing: 10) { + StopIcon(orderInRoute: index+1, style: .standard) + Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") + .font(Font.system(size: 17, weight: .medium)) + .lineLimit(1) + .foregroundColor(Color(.label)) + Spacer(minLength: 5) + TimeLeft(time: element.eta) + } + + } else { + + if (index > 0) { + Rectangle() + .frame(width: 5, height: 30) + .foregroundColor(Color(.systemBlue)) + .padding(.horizontal, 10) + } + + HStack(alignment: .center, spacing: 10) { + StopIcon(orderInRoute: index+1, style: .standard) + Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") + .font(Font.system(size: 17, weight: .medium)) + .lineLimit(1) + .foregroundColor(Color(.label)) + Spacer(minLength: 5) + TimeLeft(time: element.eta) } - }) + + } + + + } } + + } else { Text("Is nil, loading?") } } + .padding() + .frame(maxWidth: .infinity) + .background(Color(.secondaryLabel).opacity(0.1)) + .cornerRadius(10) } + var body: some View { + content + } + + } + + diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index ab7a8095..71f0553c 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -193,10 +193,10 @@ class CarrisNetworkController: ObservableObject { await self.fetchVehiclesListFromCarrisAPI() // DEBUG ! - if (self.activeVehicle == nil) { - self.select(vehicle: self.allVehicles[0].id) - Appstate.shared.present(sheet: .carris_vehicleDetails) - } +// if (self.activeVehicle == nil) { +// self.select(vehicle: self.allVehicles[0].id) +// Appstate.shared.present(sheet: .carris_vehicleDetails) +// } // ! DEBUG // If there is an active vehicle, also refresh it's details @@ -219,10 +219,11 @@ class CarrisNetworkController: ObservableObject { /* Call this function to switch Community Data ON or OFF. */ /* This switches in memory for the current session, and stores the new setting in storage. */ - public func toggleCommunityDataProviderTo(to newStatus: Bool) { + public func toggleCommunityDataProviderStatus(to newStatus: Bool) { self.communityDataProviderStatus = newStatus UserDefaults.standard.set(newStatus, forKey: storageKeyForCommunityDataProviderStatus) print("GeoBus: Carris API: ‹toggleCommunityDataProviderTo()› Community Data switched \(newStatus ? "ON" : "OFF")") + self.refresh() } @@ -686,6 +687,10 @@ class CarrisNetworkController: ObservableObject { return nil } + if (variant >= requestedRouteObject.variants.count) { + return nil + } + let requestedVariantObject = requestedRouteObject.variants[variant] switch direction { @@ -1011,15 +1016,15 @@ class CarrisNetworkController: ObservableObject { if (decodedCarrisCommunityAPIVehicleDetail[0].estimatedRouteResults != nil) { - var tempRouteEstimates: [CarrisNetworkModel.Estimation] = [] + var tempRouteOverview: [CarrisNetworkModel.Estimation] = [] for routeResult in decodedCarrisCommunityAPIVehicleDetail[0].estimatedRouteResults! { - tempRouteEstimates.append( + tempRouteOverview.append( CarrisNetworkModel.Estimation( stopId: Int(routeResult.estimatedRouteStopId ?? "-1") ?? -1, routeNumber: "", destination: "", - eta: routeResult.estimatedTimeofArrivalCorrected ?? "", + eta: routeResult.estimatedTimeofArrivalCorrected, hasArrived: routeResult.estimatedPreviouslyArrived ) ) @@ -1030,7 +1035,7 @@ class CarrisNetworkController: ObservableObject { }) if (indexOfVehicleInArray != nil) { - allVehicles[indexOfVehicleInArray!].routeEstimates = tempRouteEstimates + allVehicles[indexOfVehicleInArray!].routeOverview = tempRouteOverview allVehicles[indexOfVehicleInArray!].hasLoadedCommunityDetails = true } diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift index aa991710..775862bc 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift @@ -122,11 +122,11 @@ struct CarrisNetworkModel { let stopId: Int let routeNumber: String? let destination: String? - let eta: String + let eta: String? let hasArrived: Bool? let busNumber: Int? - init(stopId: Int, routeNumber: String?, destination: String?, eta: String, busNumber: Int? = nil, hasArrived: Bool? = nil) { + init(stopId: Int, routeNumber: String?, destination: String?, eta: String?, busNumber: Int? = nil, hasArrived: Bool? = nil) { self.id = UUID() self.stopId = stopId self.routeNumber = routeNumber @@ -187,7 +187,7 @@ struct CarrisNetworkModel { var hasLoadedCarrisDetails: Bool = false // Community API - var routeEstimates: [Estimation]? + var routeOverview: [Estimation]? var hasLoadedCommunityDetails: Bool = false diff --git a/GeoBus/App/Extensions/Helpers.swift b/GeoBus/App/Extensions/Helpers.swift index 8dae8984..b55083c7 100644 --- a/GeoBus/App/Extensions/Helpers.swift +++ b/GeoBus/App/Extensions/Helpers.swift @@ -93,18 +93,37 @@ open class Helpers { } - static func getTimeString(for isoDateString: String, in timeRelation: TimeRelativeToNow, style: DateComponentsFormatter.UnitsStyle, units: NSCalendar.Unit) -> String { - + static func getTimeInterval(for isoDateString: String, in timeRelation: TimeRelativeToNow) -> Double { + // Setup Date Formatter let dateFormatter = DateFormatter() dateFormatter.locale = Locale(identifier: "en_US_POSIX") dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" - + // Parse ISO Timestamp using the Date Formatter let now = Date() let dateObj = dateFormatter.date(from: isoDateString) ?? now let seconds = now.timeIntervalSince(dateObj) // in seconds - + + // Use the configured Date Components Formatter to generate the string. + switch timeRelation { + case .past: + return seconds + case .future: + return -seconds + } + + } + + + static func getTimeString(for isoDateString: String, in timeRelation: TimeRelativeToNow, style: DateComponentsFormatter.UnitsStyle, units: NSCalendar.Unit, alwaysPositive: Bool = false) -> String { + + var seconds = self.getTimeInterval(for: isoDateString, in: timeRelation) + + if (alwaysPositive && seconds < 60) { + seconds = 60.1 // Do not let it be 0 + } + // Setup Date Components Formatter let dateComponentsFormatter = DateComponentsFormatter() dateComponentsFormatter.unitsStyle = style @@ -114,16 +133,22 @@ open class Helpers { dateComponentsFormatter.allowsFractionalUnits = false // Use the configured Date Components Formatter to generate the string. - switch timeRelation { - case .past: - return dateComponentsFormatter.string(from: seconds) ?? "?" - case .future: - return dateComponentsFormatter.string(from: -seconds) ?? "?" - } + return dateComponentsFormatter.string(from: seconds) ?? "?" } + + + + + + + + + + + static func getLastSeenTime(since isoDateString: String) -> Int { // Setup Date Formatter diff --git a/GeoBus/App/GeoBusApp.swift b/GeoBus/App/GeoBusApp.swift index c43b1f39..cd72805f 100644 --- a/GeoBus/App/GeoBusApp.swift +++ b/GeoBus/App/GeoBusApp.swift @@ -5,19 +5,16 @@ import SwiftUI @main struct GeoBusApp: App { - @StateObject private var appstate = Appstate.shared - @StateObject private var mapController = MapController.shared - @StateObject private var carrisNetworkController = CarrisNetworkController.shared - // @StateObject private var tcbNetworkController = TCBNetworkController.shared + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var mapController = MapController.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + // @ObservedObject private var tcbNetworkController = TCBNetworkController.shared private let updateIntervalTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() var body: some Scene { WindowGroup { ContentView() - .environmentObject(appstate) - .environmentObject(mapController) - .environmentObject(carrisNetworkController) .onAppear(perform: { Analytics.shared.capture(event: .App_Session_Start) }) diff --git a/GeoBus/App/Layout/SheetErrorScreen.swift b/GeoBus/App/Layout/SheetErrorScreen.swift index 252e537a..4d1a8d30 100644 --- a/GeoBus/App/Layout/SheetErrorScreen.swift +++ b/GeoBus/App/Layout/SheetErrorScreen.swift @@ -9,7 +9,7 @@ import SwiftUI struct SheetErrorScreen: View { - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { VStack(alignment: .center, spacing: 15) { diff --git a/GeoBus/App/Layout/SheetHeader.swift b/GeoBus/App/Layout/SheetHeader.swift index 37fbebac..fc93e762 100644 --- a/GeoBus/App/Layout/SheetHeader.swift +++ b/GeoBus/App/Layout/SheetHeader.swift @@ -10,7 +10,7 @@ import SwiftUI struct SheetHeader: View { - @EnvironmentObject var appstate: Appstate + @ObservedObject private var appstate = Appstate.shared let title: Text diff --git a/GeoBus/App/Layout/TimeLeft.swift b/GeoBus/App/Layout/TimeLeft.swift index accd8b88..b026d161 100644 --- a/GeoBus/App/Layout/TimeLeft.swift +++ b/GeoBus/App/Layout/TimeLeft.swift @@ -14,8 +14,8 @@ struct TimeLeft: View { private let countdownUnits: NSCalendar.Unit private let countdownTimer = Timer.publish(every: 1 /* seconds */, on: .main, in: .common).autoconnect() - @State private var countdownValue: Int = 0 - @State private var countdownString: String = "" + @State private var countdownValue: Double = 0 + @State private var countdownString: String? init(time: String?, units: NSCalendar.Unit = [.hour, .minute]) { @@ -24,54 +24,62 @@ struct TimeLeft: View { } - func setCountdownString(_ value: Any) { + func setCountdownString(_ value: Any?) { if (timeString != nil) { - self.countdownValue = Helpers.getLastSeenTime(since: self.timeString!) - print("countdownValue: \(countdownValue)") - self.countdownString = Helpers.getTimeString(for: self.timeString!, in: .future, style: .short, units: self.countdownUnits) + self.countdownValue = Helpers.getTimeInterval(for: self.timeString!, in: .future) + self.countdownString = Helpers.getTimeString(for: self.timeString!, in: .future, style: .short, units: self.countdownUnits, alwaysPositive: true) } } - var loading: some View { - HStack(spacing: 3) { - ProgressView() - .scaleEffect(0.55) + var positiveTime: some View { + HStack(spacing: 5) { + Image(systemName: "plusminus") + .font(.footnote) + .foregroundColor(Color(.tertiaryLabel)) + Text(self.countdownString!) + .font(.body) + .fontWeight(.medium) + .foregroundColor(Color(.label)) } } - var content: some View { + + var negativeTime: some View { HStack(spacing: 5) { - Image(systemName: "plusminus") + Image(systemName: "lessthan") .font(.footnote) .foregroundColor(Color(.tertiaryLabel)) - Text(self.countdownString) + Text(self.countdownString!) .font(.body) .fontWeight(.medium) .foregroundColor(Color(.label)) - .onChange(of: timeString, perform: setCountdownString) - .onReceive(countdownTimer, perform: setCountdownString) } } - var icon: some View { - Image(systemName: "figure.walk.arrival") - .font(.body) - .fontWeight(.medium) - .foregroundColor(Color(.systemBlue)) + + var invalidValue: some View { + Image(systemName: "circle.dashed") + .font(Font.system(size: 15, weight: .medium)) + .foregroundColor(Color(.secondaryLabel)) } var body: some View { - if (timeString != nil) { - if (countdownValue < 0) { - icon + VStack { + if (countdownString != nil) { + if (countdownValue > 0) { + positiveTime + } else { + negativeTime + } } else { - content + invalidValue } - } else { - loading } + .onAppear() { setCountdownString(nil) } + .onChange(of: timeString, perform: setCountdownString) + .onReceive(countdownTimer, perform: setCountdownString) } } diff --git a/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/Contents.json b/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/Contents.json index 27c242f0..28937d23 100644 --- a/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/Contents.json +++ b/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "regular-service-light.png", + "filename" : "regular-service-active-light.png", "idiom" : "universal", "scale" : "1x" }, @@ -12,12 +12,12 @@ "value" : "dark" } ], - "filename" : "regular-service-dark.png", + "filename" : "regular-service-active-dark.png", "idiom" : "universal", "scale" : "1x" }, { - "filename" : "regular-service-light@2x.png", + "filename" : "regular-service-active-light@2x.png", "idiom" : "universal", "scale" : "2x" }, @@ -28,12 +28,12 @@ "value" : "dark" } ], - "filename" : "regular-service-dark@2x.png", + "filename" : "regular-service-active-dark@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "regular-service-light@3x.png", + "filename" : "regular-service-active-light@3x.png", "idiom" : "universal", "scale" : "3x" }, @@ -44,7 +44,7 @@ "value" : "dark" } ], - "filename" : "regular-service-dark@3x.png", + "filename" : "regular-service-active-dark@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-active-dark.png b/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-active-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..5746aed16ecee4cf5ca3b1e5fcab71c8e9253d27 GIT binary patch literal 1777 zcmV&!6h8OOOxu~Z(^A`l6=-7@ z#3~IIZGx*B*&vAv31|XaS*!m63JVj%0^`mE5=jg$j0-nP(h(DX1WTw<&;_IfhCn+7 z2l`{C(|Pm!zT0=oooT7B3+#B3({t~+=iYbk_r81XyHg`8#lS_4LA_96He4#0fgftH z;j;-R5U-ucDUpV8Xz>*}d1G4k%@w7&HfAI0iMTXW0`X0vRZJ|Ex^jJbwovj3>f_`l z8ZJYBf_@Gy20_5}k{lGi1ilbkH^!_r7UNeH9gL$T5YaLwvgxB26tuqtbx?d`rhGgi#C-T>geAL-ZWipw=BO@bBO%zQ750|R)0Ja1r zD-}put*b4&fW|}Sbs=yw$}RU}a-_GTe!;^$rE+gf;+-$c`$vZ4=46QiqRL7tEC=%`}}3X>8P?#lj= zK`|#Xf(1pI?da&}00K$b-=I&0oivRREapuDNo|J`O#0YHA#jYhEGCeIDiu0mY^ze- z*6a08C4W`AI@@Jr=;_E)%x1Icd_Lc_w9FOVn8tdcZBQmJs+QSCvQdeTTFbS0IA)zeD>7>-F19*txYmGq^OWn?E=r`wrz#w6SnEMyO=z{Q)_7pIjl-9M6ORwKHTrN&^wTj}HZ@7O zlovnyjRYmGClnuR@C(wjd6Rk~+4JYm^AI@$C9`xu*OG|@Oix|4^zk66`mDk1?MP(d zBu1H^j@h?CceMZ<6Kljmkq%-=BT(a^u4T4ag`Ym##`wmix{YHw?Bj6^x9*e)VvWSM zzbOkB-U92mjwJ~IU~5ZKtC4Ijm%{|)=bB(0)CG$n%RsuHE^c27ARTxe+~@h-4RYUh zbEC9uPdmQ2Pz^05Oae=l>c3SnYs6LeT2V7Cy<(cTNiaL{s0}P^YG`>>k5DR?%RC&o zZ8;eNM&T{Qg7`R~Y}6`$s`g3-AJjeNf4jOV2(VS3523%xSNlW--1WOYx9rB?QN<~- zdCzr&)rBI{QFMvN<9{QXs1ryYCR*l++lEh*R%jBMfO=H;A1juDSUQjC)QrfMzD4Pp zsJ-m4d&S2qTeRGh057bvAc5vP4;`psUH!)T;}TelN7!7Kz}83EiD&+^u|~(Ko-=cw zf8$F$Q$NR+)?;ExnVDt!jxOnRng>Ro(?jzgcOZpKT;94Y14Q*L)G}1JFh8WbG5zlD zl5AdaTSTMHJ$_l3cUBYSp z62vBesVA^~o4m?d_MtI_ky5*75HK>2LL|86 zPAJ)V6Y7eBsetRjLokjYJD=0*Eq6p4kOMj8sS%ZU5jb#6qDQ{-+lXIs$`m9{& z?~(Qo4%Xi<(!BX32Hurl#wX;}cg|rwk%T}TMB={$%bSQRx*c6hpjcPXs(wUuJ1+yM z&XXd^*`2a0yIc%D@9c`5hx^_6j(2(Deb#s#k>1cO(!Co2B{H`_U^ Td`HqB00000NkvXXu0mjf001ut1^@s6WGTTQ00004XF*Lt006O% z3;baP0000uWmrjOO-%qQ0000800D<-00aO40096102%-Q00002paK8{000010001> zpaTE|000010000l00000w~`$2000pDNklBL z`hrNcfVSOk+huol#`C-L{O{a(c4l^Vb_N2Slbk*0o_p@O_qq2!=RVJ!XEt#Qr^K4T zkYrZ(l}G8Ha(dlJ@liSz^Ivuhp!vK7yqr10lqKhDdoe z00p-ED(4FCJlCBz(C0pLXxg1ITXF*}^>mat_2*sxvCG_ex#VVEnsj^mmMkkQ9X@{9 z)O2wS6yCD+xu<7m%ZmZzD;DNY#`}$T8b6%sCSHHk6o58MeSfn|_lRY>)y}Q>+fvu} z;M?4fe)pLBz>(?K-^IQ`M?7QK^>gEI8$YXSESJmuruV5FNiwo1{vgD5GB@OHNs`2V zm6{IytqVZwjlt9D@CydM36swZxFz2`+1+>lGj5E&*I6n&XN_|sgOqyIYzC60<1QIK z#k)+sGVOkL%XU|^t)s&PXntV)W#f_oaLA-qryhW^1BlDgYk+9+R7T=s*i7VW#f4KL z+av`a10OnK9oS9YGLwC`zTI7Y|08Z}c!_h@e9F0xotJIdD>s~hU{)X8|*i8{6peCw|({I;ICcKUr=3DgF0R=VlK2mLn^M+}AJP*hZ9=n_P0)WtV-# zyw_#;Oe=4QY?R+>e0s{OO~S_+gM$^uJY>RTQ|UYlyIe`?r$qp zD~)cu?Y1kYrlx+g#K`<7>$D*g3K+_YcDEHWB(}ILeah@)LNXq#Y=?|(DkHfkA=UP%;`r~CkF8V7Yvqo7y>Uy7#qw(k?Ql>mIbVO8PEgJ z8s+U2$Y(^;d$huDR2J{t{OXE6k>-}ke21@&$FetFz&W+l`$VV?+ z+kUWlT24<-=XdPbamd_Ormg~=0H+99JKzGe0vR%9+5x=S#)An&Y(OW@VoZR?>kQkdN-6i?zn@s1x$XyO=_*R?v%x*)({|z2|p-nuUIRf z6PU5dAJB=g1*{33RLYr~Og=R*qn4dwk2){|Lqd{^eBX zFgpetfP)1z)v4_J=1I6yLL^UamE=ug=%e;Z~zh%XEkha3_5g3cKIU#pFiU>DS!!p^e6DefSl1W43Ewb zjvACC*)R8R=f3l=6Wo>dnHcw;y+4bd0e=)zD%$K;rvJioPwXlHw`|$60j-(C#ZH&Sq;N zr0YxDB$;x+I`!E)Oj%pD+$?VVSxWu9P>crk>XRo_>2dS<^jS*;{)}}#N2|B*@5}Zu zlJt*w=aW~}yFF_mPKz!NJ@gP?I&U+MCjmf-VgO=`%jf_O5|{)uCNs9lo|SWe=cCyl zWk5y-2?&LnofYnsZY|_HEn&IzNkOqR>xI;9F^E}>Z;RHe;!IRt~BwCD?alRSK51TeV^D` zwDnH`IJ&&iLuK5ceSPQ7opkWDaadc^S!)0U35;x~kk%U@K>`%I2U8@msXms{d1p)g z;9EnsdR4N~O`E5j%l5G;*qzCY_1Ibl^x1=Wx=zVz9}P8sHfCLiCPAq*smV&2y6TXv zeOia~x-MsK&vp>X}|~er{Iz`0K{k`;)ZgcEk&O_Iu&*s z&*c1#Ce_U;ET-Jz#gBFBb?vEd0F2sZWB|9~#O z+SSk;0IJq{_CUPUz7L*hbqkIbS8=LR(+vj5Jn=$Ty}aguk*N=0Ar5t_m!zfX-mlsh zR&NXKy(RjSY42;)UNUlTMa?g9$>>{MdGFT7-fY1sEg$YK0PPrt4&vJ@E*juvFHm3v zTq5ZnaEk$(0>+YKmZtP817azu`D8Tqj}{q_&Aq-#(@N&iwIl0+fadbCdLNJW*|>3g z(}N$rDb2AdF7@^Fvj33y*0i0|K8@Q%BnHB!#Qkzgv!(d@W$mc+Tcbs!BORLnC~mD_ z>SZVKc)P9Bv~e+I-gfTzKpSR28-%;jw&8n>?L5Zr+*=?PDqT+-tTo7vH(qRf3~lJ7pS#VW>d#;M)aA`l8AhK*&HSP1lk;_UNIId>jKcCb7(nek4=R&_Pe8N%l@mJ z`GeP6s2ZCecQsA`UDZ_8VqepJ9Bi?@>RknJWRn{DQQHd0=H3cq!JREDU~@UU`({@@ z-26Siq)-Lm{)5%+xOwFEY72z|0Hnj(38>!l$u2wJGWWIv(FFE`y@8m#fNy4EQyezi$u;WLXHfNG=gdR%Y1kAn^7`r~?vX<7YI zj&*bKZM+}tX+j&tL1M4enNraeqhe_Ow_J3^9lvnJr?)isqc+Nb`wny$fI~w=oa2oe z>zF0JNP&n^Nf!WBM#i>SR~Y>`v0oSB*Bx75wM{%otSKEcY;x@7F z+jE{tzfi1KER``yPo*6!mnNRIspq;|pcsG?-38#ZK1H`dC)g4zQ8@Se zKfln4unl8ee);8ZN-GbfQUFsMz@@Aw9SP8|qsMk^Z0iCUz(cpMk-`>CS%8$^oZo~f z1JY3dp6P%Q#`vKX2YUcyE;>cY;E5sB{)1EQAG>+8sXMfC<;wDg4I9>5bY7j(dILaS zua8LpQ|6-#H3A^<@F6P-Y~Uz>Ez-$?9PdyMC1ga~mP-Y*aPP-{z@Tc#480#E)yzm| z3NZkm-L=PVo3$>Ti62MVym>R3iZz}v)*9FAkYgH{-+llraSTY>iEVTA;1oc0zo>eE z2&qrT0I6)3UFO?-PimAFEBAo_&d#bzJ@1{+gI6`6TB@2lH2vUQ} zs%JNp4Qz;%HK|iQbs{%`2{>zDJ9gfuwJ_O9nU* zsn9(SKPR;-@|<(d;r^cOve`N+0mYJR2oPc6ff$stXNsx^Ptr{dI-tz<@3F@pDrh7J z|633%_oxdqkc_+~MBP7e*xkByS15N{S-*aLX>gFk4ELh3)>!Kum$AjnP}qXC{ZfXs z-BOp0qbFp=!5^JWrwoV%@qc>&Pb-fu#fmd;Ra^sEr*4-Y)$ibd9@ zJ9NbrS8)Evy%)e?T}9FXLQL2Ny7(S224hxNSx>x@G4Rl{?E3|P`Xpdr0NFud@My)M zNA1r*GH{HG@BePR<%#x#wb1h7i!Wx~UNWAurfG8lG0tpM352m-dR)f?4lK|irPDW0 zzDU_>B!|=U{wT>>>?#(Ve|NmmRagf-YGDSFk!tpP@#P73*F(>>(5gL`fq{XE@$vD! z)_S+GeuxZsVj_D%vMZa(Mp-ubAhGGGSD*rD4Z)Vy@jz5_7_BWBY{j1jEBgI6XD3uQ zP8@HK=P$Z7ySS6+QQI?+8+}`7d*{|CyL$X`!37sw-{0T=eCV*g0+65>T$BYm>6FQI zPz*S9l_l{J5M>MC%zh>30cgmJf0V>iupf}M${SCeI^+Kl_C=5X)AG5UCattL6neC< z8Az6&>5?TJo!Rdm>_-mYy#C>Z1z4E(lQ-Xd^NrTzPKdYag~R ziBGlv_rR%8kDAUvGQ7r>`&ZdVeAb?NOYW*4-q+NB5tTcw?fZ<8>{Ir$o~9%q$w@#G zP*qmEB0yL8i0jNd*@s9?+r|p@B3<7Ern9K+JiyI$yOUK75Z_SvkN9hcluxh~mt87x-KUp}kV#RQIWMp~y(BYZ< z)RA2It_OB-Hntc7Jb*6?qQ%5~&DypKKn+kp6d18DG=^fmktG>LSw;FA`FeZT)|-LX b=M4NGS2h6h0lKPN00000NkvXXu0mjfct^0l literal 0 HcmV?d00001 diff --git a/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-active-dark@3x.png b/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-active-dark@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..003c3fc93a40ddb64af3923719dc5e8f5ee8ec7d GIT binary patch literal 7868 zcmV;t9z)@YP)PqeN2sB;$UUue69MlmQTBME%q;M~jUiu<6Ri#yT0djcMXM$!XI30OI<>yp z47Hc0n#cd;tGgavKu_kIPxIC9vNm3@a=^;IOg2_gw-K0cKs(0S>ALY5tNEG%_UaJE zc9{OO$*X@p{N~Q*!tZZ?GMs&6qS`ul9K6cd%LqJU&i7jRtd%cVIc}vy&1RE@g93G% z)9f-lNf#%MT}V!~T1|DiZP0{n2V3f=9SJKeG_*g2(Wh*{eq?3F0Q~r+Vbj-Y;q%{o zB)qESQoU9qu;?Se%KQl{w_7Oy*#OL?;FuLpDfs+cF90S?OMc!|at5g2K z3$pc|Guw!jVJpqU2HqofFgBWFdkoY+yewSycQ1x7*vdQV14nE~H>|ZlhPI-Y-v|t? zvUi@hy-oWL91RDLOtuaEksh;V-(%%lYTf`$M^{UqhYNtITMAg!d%I)-8jj8D#Lb>A zV9yCJ>lv#qK7cnCQ%3A9Y)lz|C)S1)pPC4t@!);6t-*i%Ya#r?RtwOYZNU}2yhfll zIu?d5d%vBk+voV__ZQ>m2a06LcjM&?^ z(t5V~KevRBfA7imneYAAg>c_TLb&dyD)8EPy{xG@xi8Ef{6XOuHmqM0K5)yG1zVpK z7L&iW^1E64%wy&&1t9w(J2Boanbliy_@Gm~zhpSRijkk{sYCWf?F_Tkw*p_ZrLBE> zwN@d(Ys`0y&H&(G03JDJgYcpq#O|@M;^vX?^%oAcZXUe#@(}L+5MC86l@X|oZwy11 z{!Za=_B?waT>SRG470OZfC|;8TVeHkmjU97#xIRM;v0D6U0t~8$&)`OMXUK4H+c*m{wuik4O6)mX| zXukAZXdbd#m5$Ci`;7438}0gNFbMGc?QOT+w#E2aSvkkJFWAw~&5G)CkrA1Eb{pyP>AIWf5V2W%p2P?Cc8pY}sL|n5I)TnNkvO;1Zt};2tyaYr*K@4KK@*|;wD{w z7OMK9tXj3|-ou9v|5KO7ZCV$IIqz3w>ZQX~pV~=6y?nKDvZ7Mdj&wNWb!76x@rRQ~ zx{0|R+q^f!Kz=04#_Fpxjm{eAT7g?<2?Mz~t~hu+oY{&DVJqjfik9REG>_~G&53=X zHnu)LzDqCM7_PbUoN(XvJ-Mx)!jU6KuD#=qJ07|9)?1%iKtBXPx!SrENgqu~mmhZI z6wuh@LresmaaLBzaBu||Z~-hb`QWmP%Qx29Y8-*y?FJa2B$HpxS#7Zk#BglG*7C@j z5Pr$t%HNoNv32ch_OP~k){0?RvNXH@!O+-rUHkZKYJKq6uNoKt)3Hx}@{`vYfH|X_ zXXU@Gykw;l38(-=0Gx}f4v-=%UOG0m+YmBEt14iG;ItP(naTJF+|*Oh@sXXF=w~>F zm#&i|fT^$oNX1ReS7QNs*s`v$?B`oqYo14Os%Ytt!0i6*p|SBTHmx*WLioiuT^`n~ z`bs!tkK7FcJ@n8+=N>(Jbav&+mFx#!Yh`!VF8~2Cx2L1jdI4yV2PC>c=w%Ml)~>t` zpA@tn(A1*?I@%XNkty^=c7ZhlH1nk%5~$&EWj7!fw$%hftg;YXE3Ra%#sGQ50%8oU zSF}tSnb93Kw1zVkH$GXQp1ymt59cdI|`{k|+Mz{H)Tnc&>4qHqI?qMKl^ zG?`+LAw`ZI9RRDyS9&K-JRiXVJTRk@>Oct_I7_)=JTKhj;`3fAAsrm(M~0J>24bBF zLv|{N{w8CuXqk*aGxeK-` z3Xx0+1zxU$Ws`q&u3nb-2+T}|*~5<)4svv)5#DvfWd&P56`RoE{`>FObuQc7PNaFA zasf(q=`2`a0w7aZebeIYsUNyX*g;o2-bPz$7i1c0w)5RrQKMi-@c_*tTq;0f-qB))NmUs78{J{oTpy=im_D-bsREI9W z17`3eFsd)!SF*T#d|u*t=%e8QY~N4Ffh%Wh$dEd$Er9H;+Uj-5k3jRtlci&P-D@_N zZ2dIcb=O^MY+^HSd9sT6odQ5}JME_Io&3qi-D(QkV5rml$0nR8kMtOn#jy?6mRaqp zUUAY@-^m`g(7q$cbP{Je4N0a>+ml#Py)3U0XimIfv*JkMpf{{r6)wG?+itbsdRi3w zC~4@xfdgFo0$y%LbpwF9- zbVMY1%CaEGK$A&Y(J~u>*>n}Q>G#ezZyu;ouk*$LkXSGF6O9U_(Dq4Oc z(0u8c(n0_9)=ec_KMn3W7yxV13!vh%r(Fd=fR+MCI~9QJfr+fuR$TSc1x&T;0V)}u z*84iiF&8(nbiof;xljkJ;#{zECV{Z?P$nxjGDJfaEwd45zVv+Qh%ec^p=9f);Tzxh z1`lWeKt4+1rjdYQVr%6>Fk;>VXu!sN7U)dOWo(e@JjV_|v1!$nEh>ck?H}9I3T%F@RW{?&;UwT}##2}`b}{g%8HiR2u#~eEPJ#v z4DoPLi`J|h4(D$=BkbB^Uo;&6vb)f=ZQHgz^0v3V&36dk39Ra+2Pigl<~v+Kl`dbu z(wbZJC9h5h0V05WITxD+UI@^yi*??wK_J??KK;^Ho5TU7+>=r7pFr9`0oUmKYCA z=^lj0oEW^H;?+OZVaHbZ@`C9F$^ncu1F^1gY3QgGw$$;u7~rzcwj(348Z@z2vefa% z((IDDMxm)TuXV6RAMEt9D_5R6+l;brA&x$`+w7D-_M>Zir0>w5!m+gbTF%w3%kvc4 zQhl+X@wj5U*g3{-RDOG!JPJ9nH_Yt&PU)96x@GBPZ%!Sumd`F!ZQjJ?=HSo+4?Mt} z{jrsFkQf*NkPgo$Q%@ldP>~OiL2${`dtI{L2z2E@Rv!8Jdgd?syi@=%-j;PjEvC_C zAF?Shb$FXd5nWzN0LCu^FFbBS8pEXzjK6vmTXFY!n-06@2kp@zLL>KUgkzEr|bzYYZE<17P z%IAH&-<(@yi=UG^0+K>tQ-ov;Fj}-N2f)6__8lQ%C1)plhW-Auh0#0yMtj znS2 zTNtm6pB;v-_=7O@;2$n5Kwh)yWu#3Ae%Wwnhu+)3S91Fb;MkoJI5z)3kkhJ^HkD_E|GpkLmQuQ(>}_PH8oc(#2)Z zn(i8}T#7Rt-^4#&Q|9lPa%V9Dykf-jK4yI=u`cnspX*a@%LLjaKV6CK%U4`|Oao6$ zZ*00g%X9XY)paxW22P8iq<7N5FPrQ>o z)-5Jr)k*O=0W2pw;t1s5Hg4b@wy$|`g{=#?t52S*E^WUzy*B>J-U6DM$L+8o`=8Q5BKwIQ&M2H|)G>jLZk7+AZSw<+ zFS=|tbxJoQ@aY@r;QH2q)~aaD1CjzsFN|NoBPw~E{N*BDT=q`f0|7bH^W2?cybb>j z?R1Toui7=9`l1(X^7ZX5j@;>{)SQ{!uX4z5@_2b$t-5}20~!w>>5XB)HRyDQd9NZL zI7OK7^~jczpQBp%kFHzHDW?Umz;^VFkF=gE9o=o$!jHEP^yE6J)lj@J&ZO43su^@g zd7NzieZpq`@&OP@cq>XVR7xf^a~3bf%NHKs<5oH~qB!w9x7u;>>?(X5F?k$m<(u^> zYiTi9^T79eOH(UPo5{(+0`Sn#5CG<*B(1g@!1P#EFT3~ib^Ku&J~__${#ay*>6;*(IuAP8x%UG}myfu&%f@P*%ULXbr5|V~@rh7J09y;p5Ks=AhCzD1FLRZ?SQrt6p}vHVr1yP30C>oB4a8SmnyN{8LWaNQx7m zCthJCZt-fv*A;Bat+u>l?{d=~shs-$N9}|ChkFkmDnGQbXYUC-(bap)3yv5c9|ypk zX*6IOKyqM{9bF(pmfibM&$dH;o|6~d1021ZJ?1xHa zN+vh`V~P0A+Gc_}x~;k{zSPIu1>c69HkF@^33<@*&2q>5_0WuuKDO)S7u&Ts`NtvT zhENul_KV}uW0|evaea@+YWlq7eC+dEeimo;g$L}j-%p*`W0V{@f3TC#Q_uGHG*=S7 z=;|exT*7bvY_y^Q(8ThOP|~i~JAG~(0cL*U%7ZQ&Tv!5_=(H<4oH);!jM$<*Td8|~ zDoc%8o|uWGal33B@baB8cwCLmc?=NSbR|v$V`7h_T}3fnh?|OQ$0DSGW*Rlei>H(VSDUb=3&2s<>0Y-{|iVSgfSDoYt(#(zz_1;E~ z=xadE?F(6z3jnqx0xx#J8-Z&aeZ*Sf)4pN5u;4$EXtbhbHv+bU7_kc}M@+dNK4}lx z4iH^`{q@XG0PN?ffT*6L0~omc^zesn44^9xp31}nY5`B66^L0BA=TrHje6RYzv^Y@ z-xpYsb;2;$GUWiaXp*A)ZL@T~7Yuku&0znE6aV;F2VBltS(fYw)JE+yh~zHxPYzCm z=lAh5QG-Et2YTiWZ+HXS1TNMA1|;CMwDuS91z7pW&vP%!AAolPH-Hm($%#xKu!9+j zz-nT&dq7K$KsFHPdZ!gR0@$)~7SL8%0M_cORe9K2=IZC_bs^j|axD7Ofr^&V2-L?=_p$^?nJ-Nj9Y8d)!&QS zxGX;!D?fJm=&te)-Su-|-u>=(Gtn=!!maVAtT2ZGgNPr!W3EHmq6&=Ikkc)+2d8{l z+$i#w-AwTgxMKY3n*!PE#EAr8ff_%^aNPcjQ304vKyU&9E*zZ$g|h;$v@9_Zl@%?& z5vZ@UrvMWA&W>jiOMg|nfyIszvgeL+;0my@!o3Is#0cm-R;HDexepK_0TRwfR9?p) zU)fE}2|wBwSb>iK2|$slPwnU`1%H8f$f8sMXrZ{;W*j+!*Ej&K{$A7uV)>HYz*>1; z;qbYE_3!sQS+Mm}S-*aLu);Ty_&F#&4^5WZ>m<9qpIzeXL~br{Rrnd?Rz3Im`;& zi3FImPV-sysgAz~aT+h3IK^U1b@|bbifs4-Q1tVpl6b}io+ICH5n1z}S%bxKRQ z?8H{I)JMR+z))L#LE$JT>{r4+{necXTR)X8TegJNt5>rS@u4(ZY38ivvgUTm&1Fu) zpE=LN`SgF(06dMh`sAiw&i01yybp6yrJQ-9_@AOsf_0TG*IcnZ`M zN9Gbqc!L8*0UFtZQ#$n&_0PmMX0?T;lbpH4aYkkfO-EG61v!Z;T4p0qTYFjQh`+S$ zk&>;ShMR7>iHQ#JWQl+h4fX;sp@rC4r~oA-pbEUw#RcNDo>sDnk|LHoV4xp9bY(suOb$* zES_SIPlk&wx`@wl0f~PwyBl!TIP+WEZLOr#34CzK>s|(&5C9cWeLXUL5G9TY^`#^hconimi6*vUor9TMs@K_U`8!dxJr@ z+;Yo7`-jA})*BuR156@70SU+eN+j^f23IU~-i}NmpJdO?T(W!M3Y_>!7BIbz>HcU|(MNKWm@)`;-;^BLe^va5YWQ1zZygAVpW5R#g1S4yd4r~kp*O`;#S&jVbtv5(r-T(!j923;dZmfFNXePa8AVZ zyP`!Lf!gT0($iOa_a6=a_Vt~M*viSJ8W|aR=6&yb-xna#40>u#u+orT8qsy zN8$(N=TiB-CwQ%BDULvW{m+yR>$6{eFwD$oewM0FyTLv_`uNz`m>yZwoMjF}(gix% zrQ?GvKMz#ZrCbLu@KG;W`K7}l?A|6na;XlEjvxKWuXRoCNI8JBTE7DG94VxR;^Qc` zeXd*fnXNP7zpU={INbYa2-|muaOL@E6)l+&u*W9rXU4ZUKctDtneeeYzLV<%Abftu zjvb%2eq53DML^8wt?Q!)Eq($kF1}8(2P!_~q~rk0WMervBk-K=>(V%K`8HziZmTVc zQYrx0TIP;)3Sw5`Sgei}c*c~mYyLNU@uF9T-+O#-7@o0i-S+n(-1lKyrVAO<<|_T= zGy?T?SCwvFeR{D(i;eR#Cq{tR+zJUk{$sD2taufU}OVSqE52%sID8w&(}$o zT{@hiy!5B=9beNIOgzX@AV6#e286=3FD1&>(oqXIYS+R?%_<(GMAIW0rkk_mp?jo>F`uWR}STq-?Xi?QPCR>iKl%Aamj&_RR(HquOR$%V% z4coPfw}){3YwZV#&a?ocD|*?DK>f@s z1OFlHY1_Ad6mIWtS$UQY*w^ZKMp-}9r8%t?4wsZzq(t)9%8c&oC5sb> z?ETPHuXf2-@atu3Y_SU>b-t@}24d;~@sMqvMh(E+)f%%?VB@h3Va=Uu!`JWK8OlG^ zKvYH52rS+Ru;Trxm5*3qWs770W^suhb!H~dG{*%j)d_IY;R2jwIB|;gF{w>;C$*x| zU@xSDTTqPkm@W5+?Urlyu<59MVqV{E<*D&7y7TgI=YM=Zy!q&4>6&9fxhtn?1m+k4 zZm4pj>Z4YkNc{}J>=Z{+d!h?)n)B$g;VQdUVr(AJvga|UVnwCVUPztpB_{gXMyl~O z1FxM18=Gx4-f06+yCz(E?0ERtqtAq&-St9P@lt+{>^XfEu)IfrO`ct)g$JzMW94&J zy8i5?0hoEr;sOa^3!U3qIb-l3fHHPufiDX^$ZqL~*WfXL6wZe5+FLFUXU)#UpP=RT z_Q|$oDJxp65wQP9Z=Sz#cyz^z;V{$)vwQb9jvhN+pIW(Mym{7!^^N1xGxa0Krnqbh zhYvMo?!NEQ)A6_tZBVq@(R@X7jKE@mN+9-JXG7%|pQ9^PRUbw`+ekPo5kbf;>v z*AZAuU?~^e%f0rg@;|*Jpe?6>EB*BLqH67gMnFJ2q2PIfPG#?yo)>rYBC&Y=4k-a>+0SNPfR6KX%W;?^qCYdiTj9e6HgOY zoWx4AllO2KPlm^_^YTF;&|p_DY?~SN&{~e**}yQa-FznN%qRhAHh3f3K^A8ZAlu%I z<_|H9zP%1K`en_`FHT>32$>aOgpYM&a3n4}_&5PK6aSFyAcGUzHJ{koDO_w_wRnRk zoXt%3!_akHIUUaTyNhl_IzoCx@M7ws`P|1@wZqNSdP8nrqUprCXq<2kHum^oA|$)OE!Q!5iuve zf7d#t0QC0uYP5~FDRpuII|A6}#{(DqqEYe07l3o@*xIb*Q&(5lEOm$}b#eha&Vdj8 z7=jqWPK=-$F_kP^T5YRj8lAmOoF`k(;pyP!(nnyZpJh|jneDlz;z&pCfv0}vyh_i| zu{w!0!4)}~L~USTpn>w{HwmZ|guIs|3J%k~G|?Kmqtu$rG-_LQp)bO=_R)lm{B~Yl1kpx%%TdfcI!>!D_jEZ0uwj|chAi8ZPnu;-(exxRIS6eTS{^z6 z_Xc#iSruShaJf;6*4VQ3VD+|qN2{>5h90OIsV&(uw$%b?8nc2xr5R9bgsi+kX6dZ5 z*pVY&mS4B}%$0n49A@6j7t^XvPSaLaqX{**J6e7VXVT#KW3E4Nx`JkAz^#0zQO*;b zZCi3?$_;R{q?KakIuE5ds^{TF{xh-6vQzk6Mea+F+@G+!tg55K0A-v?g{jQnTA z5x~Y2e4~Z_&kZ4RPkI(2?-V0zT^7?-MMlAc;X@>tcv|=tfLUmyr=PG8s ziiTL-8q<3J>n}>9^z`(!&^fSTpGX`5RPts4!K`krIePE&h>{nfQ0O}yV}2=hase;d z0BXxah)w>)|^ z>=YkZ?@10L1+$+U*zOJ3HW&2aZlM32^jr8h`L&67 ztdjmYR^Zc*c;DWH$xF9@DBYaINApF+tQLIb4)pa8ssvAb&OcG<;FWq1#+g0Xx^D|c z{62_(4q}fiRTgiUJH#R4s*rsPi$R1;tW0r6H2i*eDa{nak$QfJZi8IYkcr1J+uGVX qYZwL}8X78>{KTc%?Wa>koc{~)4S=5y957b^0000001ut1^@s6WGTTQ00004XF*Lt006O% z3;baP0000uWmrjOO-%qQ0000800D<-00aO40096102%-Q00002paK8{000010001> zpaTE|000010000l00000w~`$2000T9NklW6y-~6(+GUUdItJ zD1bw<;0+PSiX&29B7s0af`x*R5RniP5>J3o5Pn_6A$VXx5XcMKmyOv6#Bv5mh!6x4 zBMah?5Fii-h;ba-@yvAd|J_~HHSO_K&rFxyw)>aL^>_VO9iQuRw;7aA3{5Bz@!ZQ> z!bSSSblOchPm8oG)XkOwq|)ZVi=iSVdgi&xb}`90=nQOh9uwspl#8h!e6`gt06Ey= z=rw-pRd7*V0vU>37Buce$s6D?PT1^A8r^et!B3WOPO1IQcPUSL$!rgm* z_%5F}s{!OWbn`K$5lr7>+VAj~EleWt5%6j=8K}SGYUoeZilv4Bwnw?U+|G7iOyStP zjA<37c}IR`Kw4(w2)<-jGYsuJ49lsNm$U)=y%V5m2|Yzrss~uA6TJTNW!@lkoV9`}|t_)uUDFVN4fbx(8DhQ|O6iHx zO>X8Oe{zo~zW>f^A-$wlJO|9&h5lX-*rzskDge?9$QqKc?d&;V?MngfzE)dZ7PMpI z_b+iyI%Bo5sqs8`?7kkI?IjT2tTRARzLfy{6reRW25Ttb+RmWDE-w>U&k+S8AYqd{a-|t_ki)o*TQ(kg&DYJEuQ4` z(PiD;-Or-?&-8_;-m?;b7{bp8dmxDbsB`#>`Ob}jIrWE-+_c`lpcVxIGrM-}BDCLt z%P`#&hK?ct&cTqFXMYk=*R5_BybfmP&YgsCx&haE?^9{rBLG&~b5+=x<1PSKt!#r> zOy4jJ4-a=`v)KWxZYcMGep)_JMPEmzMS8#-|5=pMUqsejvP?YdU=AHRG>Crn2C1;( zN&w>gy=|}q=r<^$fO^2XiZOD++l^OUD0m%APfyP$jIoDw_^#Gxjmp6ZJhPR5XYaW> zWS-}Ot3|I2YgAHSs&P?0+H)82DavM$b6<2q<-8P1$>#AjJ^X8jFT$4vO+1XS+9pfA zqpi`GS*1#N6|<^RTitd_0G3q&h7&5o!xknMOE^4+h+$%sldJ}1^lbC4%r0Z3M>1hU z&yGx#IkuKhTh1Kc1-6!-gcOhby7ekv58L-NnE>z%Jz`|R7(EQR!>@pG@}n7)q~aXz zY#abKH>4zi7Ivi;uK_q!F2QBR5HI@j8HndTypXN1Cw-4?!K~zwi8tP**^Bfo8=CQo z6JAZAfcqj|ekL0(BVIuW>{!#KP}}n_h>ekMv5i9@;KteI) z!IycGc?MSihGa!ZfRp=??KzJ**W)?GA>MkHU8zBT9`xc~wO377Mnd?0V>xv?zI zR3_{_vAlq@4ICwpAFt_5&+#O%=I)uKb~!D<4mG4B0P?=5E|&(maCs|qYtKAP)+U2aQ?vOV%bk2}G9TAA0L>+*6FO`JMW&UvopBWDQ0l0^Guhw_FKix&i+ z`#c>5A6Vtl?FE&lAGtJ8Hpsjk%Lka8q09mp>Bzw0JkLp2wv&015lLmjr!BF?lH4lr z){6?M%&7}W31CQSO69qsDd)jz)OIg z-bh&&dCL!#$>ow&DPKayZEZkK-wJqv>V1~tm09QAbmp|N(vT$c)@B9SRPD+ zK3By7mIqnz3`~g^K)^DDresHbCO^ymX5b~iZfsXq#44zpE4OWMVX*vAe3_?bBGmU1 zUMw48HyYsOmPogquO zHzPwoYM-F!oxo6%au4_mzDu@z6wVK2?dNHwSPDPgl}^F$eeS-Tprc9s{r#P|?Qs#W zBS?ill>p2DY+p8Fee7Ieh{(*_18j-=^1;ZNN%;2I?_QO_o->(@v3c|6N73Qxz?=r? zND06IV@0l<9*_Yzbo2ynoAqrB5PUyj*ilUQVRHCL!yYhvjSO3jWB`6OsCW6@vSkb1 z0e*ppuTWk_M>F6oqmv~XfYrHwCw_PT4_a%mVZ#P`5%pcIL8@>b0kDeBtHhKwS>O0u zrX!={(7)$sC2oA#bai!|OQ+KW!2kO)m;HKu@GnsqMJ@ekg*5kWm4Us%#_@@BuLgj{}^d8>NWK?g7?tF|Z_F z>myBN^U(M5{<3|PVK9KY|1Lv+&Qw(Zq*d2^b1%Hr{Wln!z#YYdui@tm^X5Mg&so1t z3mk8`PL#d<(Lp$N;!p7$FhlQw-HA!x=_bGmt4_fLK!RIt{VM1kItl;9f7J^&JO%LR zEdW=p1Za%@Nx5=3Sh^F;+BW~?vzER@G? z0SlrvEduf=6>DhJ5DnEd5;ev)wtqC5#((@n?LYieS`DVwCPv%PXrnZQSRoW!Y3+j& z`cMjWX$ys29$WUoKJOjBZ+5=fGjsOtGP^VPoO}12m>504fTfi4BwozNsLqIr+XHa` zm_bXajZrLfK5n~>ym&moz@;=R#CW6|DT#Z405+K*f;NSM2*9aT^Ppz&Y`Dd-U@=xy zOh$p>P0j-g8sUZ#8*Iyh?DVl$krE_eUU#ea8`AHFO# zeu(CWvpBByVBChW6r%|-d4R!7POaE_8!TMg3?F0O3{c~Pq{cuyj<26!G(bita2W5B zhUvtVco@JX-?|N&-)x0fPM(J{k*E~!t{dyn+i~1wi-Ju)KOQKl#v7s_RxWn*z?JU) z{OHfETPlIT{k1_66ZHQ|9Q6UyM`>tC8Pab#&R&poq z+YIo>`vI2B!kc57&N88JJ&?HyR>k#Tm0$1op}MjRy1P2z$nj77Iz<^8w_nA01>>uV zY|ujO$lvR=nM3ph&64IhP<^Qj+AnmvW!Sh3VDCfMCd5z`h6k*QnNYIihXG^Hoa=yv zcRdE#EIoBn9_W7i9*oZ@w?vTv6FE=1AiixO{D8q*fwxM(*`cThMr~pqaC*;!(|Ix= zf9|Ylu;XsLKhh)6So|%lK;80Ry-!sgQ1ckXm9nY5TZkZG6aiXQm( zYMr`RgT_3elR^2kB`Sc)HW$P_ebC5;E}hFclNOr?obFa|`Yr~#wPe9eSiM54gMJ4Z zsMWtpz+nj%q5|0J0r(Ey{d|CHW&Cq)omp#@;2}~kI(zwO&(HAlv!AbQ!&B z`FyP|qr1DiZ}08xeFN>U$4Fo<`d|QD^oKa64rclx+x0;}SXpTb?%C2Ds8d|9_U+sE zsb>StOc#AH051AN9A5{Wu2Yh*Eo&A^bt)@%@7~Q%N2bJmumP~j(NZ9koP<}4RHoB+0bsB@(7VPN)v#oKL!eG&0oQ2P zw6(Rpg*Fg)i+*v30kF!^R)D&*;j=K#^=@6;q*YJ$?%mtr8C3N5!3Mxu4L$vhaeC!f z2|u@VwpNBe{`lk9831c_NQ^PMc);mxlT2CE>TSii0`G(IUAf{7 zxH2+Yz{B_K4BQ86sMOtPEE=<+E_e@&2H+d#G=Oc}y1XMyKE9!|v-95C+S>D$WepU( z)5qvQMNXMlFH35J#?zU>UwD=g7vbkT>%m(cNe1g9?D&C>Ay!=gybR!356 zlscD&>ggq@oGHt(k(}}&+5jK*WJKp^@?AT^GW;@$AH|+PUqTzpO1X=8+H{G2n`iJEcaGa)q1t`$mJWEX2;MZ!j? z0Wc488}}x&Xn!&S9D#|-oN_)`_UQD&aFos2qhKzYpSuad|@aFnuiiWCQn& zQqGf3F;8VLxcVKynN+L@^*6njGuJ;KsLv`HF z^-Rb0Udr~mWK6>;^D+;Yqo-EIEGSw2)98#@d1AMUb~e;2RO@9|%`UfC7LvOOLKJo9ks+vwRKB{}$gI>9Et&dZQa*6XHB z%PHF!W}H|#HTzDmr!9}h>;!k`Am%dtKEYuC9POw^q-HSiVD;H~fbxLl2Cm04@r=t) zd;$;UiI1ZzHhg)?Iz2wt;nL!dWGmA%0jD|mx!F&NKO}>6vR+PIdQYa$8!r0|ku!F~ z?a_F$@)N}JvUD^V$x|BuC+cGcLtv1K8hL^NMZJ_~U0%JU zi}^UcMt=VfMdTCnA$@L<+u5dY8rB`XOkZG#6dZX|_Y(Y0sb(SYtuD=Mi_IMMUT1J{ z>UT{XsGRp3M>0NNn1;)ojkk4P!C`}8*(ZEfd;NPc*Q(5h03GxGfe;x;=vUG z#d$$?PJX7PRJ7BxiTOD7ZKHPL5%^sifse{QACmgC9t~B+6r7IZ(Ii=;0JvW}08ilg zSTr~2K&>9YIYGtl;X%#=m&;ze&(H15$0^b?O#l$wE~X*b+?10)taBWG>5pg(e$wgc zWtl`vk;`n{mg7bfOCQ=JgC`GJaE1C zG5ML+;|a47{WRcEb+~&%)Axul{uIvD93h7X{8)<1H$)PgQda|jE`W$!Z<48Nx zxq6Y0>D`Z8(1ei37q*LJ@v_We8wK0leqnjsAkg6RTiO;Ud*M~cv_3PjWt6DTDwS7n zeRNJcynHyWYl?tC4S@ME1c6ToQO_+==>=6LI~fm*vi!33tabo?e==Px6ka-Im|#2* z!2@^&Q5t-8kS?13bDFnfjvy*o(v!)G*F(AiupY;uV63pp@WuVmEcB%-eb9DMen(hg zr|9!4A1lz^r+Yx2ip-nYMv*EHSmpBmS0A2fS7pwqdN*v?a1P(# zdJH3OAXD5SGJtVAowa!5ye^fYSrikdoCnI~WtS&f<&ExC`enzC9lfakM;JG#v?Cw7 z4G8n$mvDD{*_FD#Zz`s^C>AO<+C$ z(rIlvR9{~Yjg5_S(V!3TRZ)EmOUOe8zzI3F7{yu@c>kLUes1V&TPJ*cjz5H;zSmo} zY@rn;^zi#1>a9uM0t4XWeOxpetm-DIFUQ`~YrVv_ZQF33)eQ!~($O*%Q`!S|%|faF z`(HjK)v2slw{9IR6T@YDa7d*c`O`Yd=_v}loo zUt^#h=HI~kUwy9Y173@_U;rGiKZ{nHU4#3i)0%G~(B*SyH$Ab@BjDw8t7pjpJuFJz z3IpKeeOwe8th#2YANvmfSE^H4v3>h?eg}9;-2alV4S-dSxI)owmx5gz{Ln!^UOqqd zw+Xz8m{6Znsg%>w($a+D>BCJ&5>lg`SOZ|~4vBsS+1D+_$48|B!;Tz11?`vlgcoR1 zQdqx!{oDA3M4G_*C*J=?+o3>-#qlVO{5oMD&h8=VS7y`j88!i*<(p8%9YHG@WprnhH3}J$&$dBCp!yws&X z?>`EeOmH&>i73AnKSOm>4!q>zr~r1si+s*?6|jc{P z|7<`;Uw;N3dFr)5o#Mh@QU9M5+n`K+Q~;Bq1mf(IFT$P$jo?leWN_Wf_n!dJZ*Om9 zek}^c_Ka21S^FJNORV z#6s#S0Uo*w;LckB=1xa~sMBOD$fQa=U{7BG^bKJ#9RBx7xch-;#VS>K`WN{I#&fC+ zC}h1NfXmmkz^v6vTu1u%$JjZOH$F&W4B%5t_$9{RL+L_BlOo#*#I!Irjj?>%G^qRf zt#Gia%zXHs`);uE;F z^#WA(PMP&E>=Kj4)6iI-8W_sh-B$sFa?&+>2R(2)x`titqIOC2X29MvC-Hu zh6myV7R#bk%#WeoG-==g0x<;<%)>S2S9=dI(2_7TN!dx*JC$WJh^Mj-3bOD22i+#g UxP8ppRw#5MxRdT(h+q+Pr)DLBA}KBm8Zne+p=~gdNoFP= z6O&Bl&GY}i^UnAtnM|>MkeLG~=YE`b^X_lXz31L%)bb&jz#1Wn}fSVfP*X z;E;PGcyX)GBC!l{Zi=LrM222_SFWeBa#j0!$?nxeez@21jk8}b zdLO)bdiL!2`0KaeQ4hQbu>*c zf$pJBfVBWMBB2{LpEl6neCm~7L@6Ak^DT!kPBFbUN`n44@T9jy2V;%_hgJ1I-Y{15G!#DjC* zU+mgmgxlB{RM-Dt&`V38%-$3mUxD=zbn*wZ3T=vMIciJ4S_TajyO>o**#?wRr^}gj z`W!&%rxB>ui=em09!L?vZ)(HxBf|2@W>f$D_5FtLd0qv}Z#{@aLega$hNpYsF8T?K z$_=(<@x0vw(I#t91I)#>OY#j4Q7^HvAI0}Qzg#YVm`EfTafCz(TecGtmu%Rw%tX?5 ztg(4Kh{DwhjxpYl4P*HAsbIM+EG&GN&*ygmQ71__6osbs7C=T@)202d1;{2q!C%18!F0KIIQ>gW^OiWCSMWa#fXB6H4xB@AJ z+FH6ShoJ5shN%(AgLDBmkwWI4Jc@GP3XaicbaeEJ>$=RYgvX$yJ*>>kOei*Sz!GX4 zFl{CVNcIV@ayI%fv6q4ZgfHTl6RBs|ET2Yv~#5M5{rq3nJfh+q;-rKegp8iuj#U-BbRG<2{Q_{jbQZ#65bDOLg7{J0N{8Z zUPEk};strHAo4LIuCcWwCOwAM2mh*g{zM2aYF*#s?4Vz(R;#ySvDi8iiTVzC)Ho~_F5SXwUoTX%%oOH%%=``b=Fo|-L{N?#TV z1>Mthsf8^;v98eUx4%v!^J|P1z}vt&UinPq?)(?>%I>~=0;swNs9Vq4ks+zS_>B4d z{N=Zx}Q3*001ut1^@s6WGTTQ00004XF*Lt006O% z3;baP0000uWmrjOO-%qQ0000800D<-00aO40096102%-Q00002paK8{000010001> zpaTE|000010000l00000w~`$2000b^NklA6O702&;Gh=%^{z{z00TLXw z3WPLGA&{yOLQqtRen3T`-LPsUKw<-8(?vl@An_sbu|Q(MrmPUHSg>G0QL0ufs8rNS zRYMS^Z9*wc<0ernZ3fs+Pi@ED3$reJ^hC0X+7*aOnJWs4lmgNfH{>ayVV>3r{}& zYoNmpCqsJlx#N!?e&FJfOP4Oy8;wTY29l2-9w6xvJoC+$1s?lD zUBkgixw%>(I{@_XSz`m7Kv){6d>{;s+!VfL#ZE~K&z_Nxl#0{9&WTyyQmHgy4RSPA zFE6~F4gdAY$G&&lkB3zmFUmJk*`^vx1+}YRH z_jH=3a~1`g2I6#UJOG8L{NCkaEK8p;TN|jBPMPF^q#Pk^<>(~?GQznOh!H?*h1IrZ z2>|2rOWEREYK5EA>9F6*PSTK$e!PIs&B$g}&s}H^SF6=S2Elo^L;{%Za0oU)luqu; z(g70iB3PCSP5~$gRVF7gvivWIh>CzTNRq^X*?pJVNC;nTycC8TpI4RTmBEmd;k1x$ zn1M!8E!S$b`wW7=*?4uF74^um2}p9mCm(O~x}sx*%(l}#V!=ll@DNx_TM?0S#W;?# z{j&73>?#7bbE6O)9}9*~7CYzTLa9FKoCU&bumLglTAM4*c2h7-5_iVKtCWw!05D+pR6%m zz_*f#y8b>o&H_Yb<+2laNau1xmY<+hwd_Dj8$>&7vJ?5F`_;4okyNme984|b2Au(p zvyQcnyKLln$%kI~LFe@gAS$0*t=YpWyRQ*4wvXXw}3IN`t#-Tc&S?@YCA8;fqG#hc3Gozuh(i zjoHcOg$oy+wtIn7TzT5T*D)4=%7M!3D#M3>qh7aN^1ul;Z})nHQ1CI2@f^@bLlop0 zAstD0hEkff`-HuH!LmZSbq2C#qd7A(bIcyc@jMS81eA8kLx;_m1srz4MD8P8-mZG( z3&5i>q%%ASI6t+XGf_-zZlOJ=}U z-R#tPH9Q3% zeUIy{XZ*>hD3=umeDAl~-vP9B)F@hU_+sy^z{(m&3hQE~HCmz?RZv z=gyrY+=%A2eA^8H#~ukfuM0rck$aohRmTTPTFRqsCFhH60Q2x!hXY1kVkFk=mHgsS z9EYb%B~6pmz6ahrJUm>tTQI<*`+nLZf8!aO%G?(eY~+Fs(4@;wnWQCNU(7|nrC@9W zXd5C#zV=5+2H>E5@77bAE0*9mJ6-ydSlF{~g7>og8VCSE1oRL}2RsshNjEj>_yDA~ zk6^XR(mjlTh@Hf%_|Yd1Vg%4e!#ST`M2*WYWow}}7Q!9bvGBog&@6>?P}1SPq2 z;Nt-Y^$6dD#ETaHQ4)jd5HkQ_$+q%uw08Iug|3 zy06puqUwPUQJ-AkA}Sk$S>hw0A5=UKG*6=9@*gF|c8i}bm;u4jhM+ZOm9+~TciB7_ zikj4^o^7N9q=J?AVcDe}L1}6{H*DXAQ3F&E^?aw;3c0aoV6o%v|K18_Evf>?Qj0L!&qfLf(#El_aU4U)5%X^R=G=j!3;tJBF_(sN*`9uV=BI*@SXODpp;KZln`?+<^k)WU)J&9jhAz82HGt2iX%7UFBA)6O15&=!@mCI1R$D8| zI{;)0>yd#H0&V3D@7W^}J|HN+_{Z>k|A)gj$M%OG+E;zs@hN2U&On-k?Eb^07jM1$ z(MRkhg!<6X(44)B1z27LmTZE^+htSUaS@>n*P1T?Pz?#h7Ysx@IZWTMrT7m&3s2nh za5(zlk?@^rZ@ABV>;$lo%`^kG-jLn0JDfc@o__!A;1_;-=FC)mVq&7f{}VDbH5Eoi z>=onrrBy=*OaXSq?|8MrdgaE@vLYXHZcOvogOomSd+6OiUM$Hr7aJ=7`fsStH9DT7 zF52JpY?yuOrT9mKfQcwZLKj^52xwK;R|MB`qmUnYK51L|s9faNZU#J9==sD*yw9~8 u*)lz?6`Hkfx6E*g>UErfO$l6n|Nak||Mu{DTHg!+0000>6`mdN*f242KqL$eN!G=Z=5j9m|JI!U(RcQo(OfLeoJ#A1HQ!p_`qpKCvwvspz0W>- z*V5&YTBVz6xl&zMDL%&|-V%(J_B@MXYSis6@b^qGR6kv(4qJ zQX91OY@FL#kEs*#*5qBcp8iXl5`jxaz~G`gO4hV7yw=#RN@=Wfs|=8a-K%t*MAc?^(bT!U{j%O{$X8r}HG2+?CzQsu;&%5a{|njFaB^C<-S?+` zcYiJYeSJI~luwZoflZ0PbFXx7KJxw#fAp2+)aS-O_9t&Q8jaS(#6)L$db)G=?AbIk zGvj#~K$oqnzz(z9$3SnmX^WEBP1B-zT8GRNn$cF(Z@cS{)9;Tr z)4i%IQX()C5%|%Y-R;l+^EV$~SXgM1I-O3lUazw+vj|NNNgQ2LPaQs#^ATsyX1g`M zZeu_9?QtkOdCYM^-B3d3(^2_+Xj!rX1Fw3G6JsI(oECZ$*GJIg!c>~t_R;hQ*jE-l zjC=~QvUl;g$|I0>1utZme_Q|M`7;lFrF-GqV`F1;TJ_oj`NHJnq;~Bk;?R3cNt;PZ5A_Xa+=KLaBMv!|7g~2wx}r zaH-W(ZQH?Ao6=eH_>7|9I1G0$)@vsMJ5L?c#y)?=Hu&i2RR8faPdxVVKYi*ML40m} zeB2N*CSW2u8|=%lylpz@%;I*|fJdH0|F)C&c(%KY5AAFRYJ)ANyV$euY7e&u09IqK zyoNV}D|>_0S4;D>X25`xTW(GH+Pv~hWo$By?fGn~H8}b%QX;VN5qRyj*FLKYSwGa_ z^G-qBX}8;5<(UX8iQ%LSR4$`kU$x^)y?woM8an(Dmotc!GPXXjy*m0~Oxjb2<{Ucr zvl7UCfYpjaXUeR)eQ=cr<_V3^&_~g@MwpUgv(`*nT8GQn&bte|!$VW78x(<4XXkba z;9n86ZD0r>7(mZtwwIajX_xJXojhRKhIUdm(Z=8!P?zC~JdmOzu-I(3wr{RuS*Bf> zzls+n11t{)HMnT(hS;m}xO$w>-5C#EpVC*)znqSxTP}9g&uP`IHTU(4B~v1>84*}$ zT)*|Y>#lorZf@?`-p&sQh(WCaNnHfgW!r$1yxY5sjSE!>d6Ey4fe}G7UWT%l$H#C} zPoj*E?X<~$;uyW;JlQKSj(}^eY=1R2(BtZrE77-TgzaPV=>*PIsf};1no7AuV8bIY zR&Ug`0`JxZtQ!TeS1_N?G9N%gB9N2~Vax;Twor#p71{_i^^}1QAPr~)*mlF30qyx3 zEkljYD|_W=B15XWyxLkKG2BCx>`Facp$fMFlD zIB!?O?L%e*UIo*3bWCs)%Yfjr$3z=k+YG1e1{&=-;p=(kqiP>gH)J)4Y!C@YYvA>U za|q3+K=zjt_BBcO!h5Ov!CM-E51S&_Y6SK-ul$F6*g4ssNy~{{4K({a*L{Eo01aXl zFRLa|6T7g3znI%t=g7ytgGe?yZgqUyc1=Fv3%ZazP$3fj2}k_`GS^LV2p;8PMHA7 z6IXUo)JyEh=AugkMk)e5`QbN1P2L1qfss3W0@8`uz5 zP{Zg|mOAqE7vYTc!|V}A#*St3wxOMmcKcfoT*Ly_xB>$#7qFN-vY;OM0*DoO5$3W2 z=fIRdBOd`kBLM{xL1w=F^I3Y8Q#84J_~vT;Bz8-qP;im&tC}VCgo3wz-UFFGk>mq;lhQNb*1(L zZe5b_+h8UM0EqW*07N3FE=L>rxbhm@st;w-a^OZlnTtWT9h>v$YB?ER)%DMF$yX1T zeb`I~=k=$d-r66e^vbD}-oD{L`bGhmuoW4_2rSILzo2Jf{Rhv)()>t>VO=Z@hnD>v z0GC^t<4Q|E_|mUG5Dc!zHGufqhPK@{m(k9S1T%|wfo2#jO|I;}QO;y9w;0k`y6osNE12n2{hTP$6K8W0iJ8+O~Hj(G!1+YDMy zyNoR+J{Y2(+XF4)I_w$bVc=#@{pkaYp?4_lm6w6eDRD^69aszMv8Bqcwv5NLdq7ne zDG?a02(f=S-} z*e}*|m`r&^82}h>^*ZYjW9#1R^6IPe;I3}^@;P&KEBO+Ek%&O2z0f{=`t%XOy8y)c zX(6j{oZE8X0;z@FzPIaVQv}a`u8%~-j2?L$GhbaT45SEb_M<8kE{?XAV`uVrY^tomk3NXEBEBJoc=fnH}uy$4(sS8+LK|Ec~f!7@C(qJPa z&`n((LNDmK&RYN>Gdk}dbExhZuykYfC3$2y;!;k zE@eX(^Y(Y0^)P#`yPUv!lGc|5*iJo=JBJ0aP8`N>72A)NLuiMm=n;$nU;G|9a%67n z)~)k;KT>P^_U%>B;;gANfBPfsu)T_)TxT@kU;K&zw2aI(P0|e%V=Xf(9|p*H8}Awhi1w5gTt-eoX*x)+Ecg zB?4=YKrQLQRZUN&?CSgAu8yGl5UMY3366)-0PV6@V0;kQaqn?{=h0@Yw2M<&`ghd; zKdp`accmRV4}O;)D7SSM?3*r^_FF6^0-G8E-8EUe$-?FAw8y7jT%gAgXg(ocOA9?cvX)6)ZvScc22!*S5d+Z0>WtB?;teNns zh0nwuUA)qQJE@CUjT?AtA@^bxDG?aa2xtMW>EhMYwr$($`YyPo*Q@yZT+i6BRyFkz zI@i0s%l5I3cH80|VA$}<>2j^qLjl$Zth(I9k>QmF-fIbpP4)Lwxj;M6Z?&;4tm#-DudYrQ}( z0a!g6UMAd6nhgL?>VdgU{lcNCJ2eZy3Q&>Jj6hv)X3)9s9-aHP1#z2qB=u$*Z=u8# z%j7d)(h*YpT#og&q0JKGP&UBWDH~R6I3u6S0vew+-<9P-)i(E3g_j3h12JzVY{+>^ zg-PjgUBnt_OZy@v0;3XvF%dg6GjkJ%&su@oTH%;l1QbIoS3$OaRr`Y=1E)%ZN*Q5c zbDi^s);{QHqmS$x&Y@QYa}~n=z0c|pczqbnqj&Nf3N^}vo^;*R6dABciNHukpe};- zHPTJM%j-pZho})!=52t{K!o^bgz;yEZilYs0Wls98(KrmqKFt+e6dmA2RGE)KE|r7 zdHVxHE=#2kuzIIc%KgLUP~M`E8ggLuZOWls#9A85Vi}{Xllp*{I3&;Vz6mCboW4zMJ{Onnt-!<|7E#Qkz_J809F46u6RQzIWL_q6lW zdsWV_7u74;?$hfiU)os`XX!%5pdh{}7F#ZICOYX;w-8+O{}_Z&+Pykay1j8O?_t;th| z=1@B4)!Vo-pHezhKa<|Xxk!n?C`O=GpX9sWoAisM|Iy0TR00NzUUQxIb;>?OHH_HE z#}(ROq7nPDxRLB{J3fBd88*f+lvR*pnRXWY8*2OzPLq8~kio>rBoH(W0T)dfPC5T& z1>T}wa!2~Qn))Io0wWoLu(+_Fl7eYq$}CO@N1-fM__mb1vKmmC`YS9@MnDZNzP96U zp`P&#D`OZ)03}a-)lS)Iz~2y8UoR^E{>saSk(d-f54>JfWp}7-krIKCj{qPHlVM=9 z-?K{dWR@)oc5^aHpSLrgi&$KzG)*&m4g=>woNL|d2u z9oS}_uRgE&^`h>z=n{bqh(Or$*{>rM)zO1%u8O|w>k(v_h%2o7j%|GCO}`}k5qtn0 z0YKM#A#^#OH{(k^brF8bc}xJNfLJq0#FHa$c>Avx#aN_7U{oUzh+@C)@9@3+uxY!A zIdBd(Fpgm6JJh}fJ*S3#|EB$q7a4GoM2ntX)QW$HYV#u^|1cWffdIo4kI6d(#&sS|xoWe$>bK-Z4mQB6to6X^tDbq(za?}$q~pjEItB<#fN>?Oaqj~@Y#k2M1;C%Cr_el=cAT+i9Lc# zyUdQX^OQ-kOg^Sq&ux@mK;?Gv5Sly?18rVyrM)4%A1S}&Oua71=|!!=``g>{PYhL6 zk@+ioXjB=k6iWn#MxfiCZRufluj+a3Kj+><39A;q{4uzcfe^8FombTA%8VUf`$dS! z10Rr~BYfl$+YGn;u*YP3)Z_;we~qLW^6+-V(2_UERFUWNY)tFx3qMckC%ZnH{z%u< z@4#_DIqiF6rBEWUkr8O7GiUW+_wVX?Sm%LPV^?>61B;!!L1mf-G1{xZ z+7BPEiXI1T8~qzh=V@!3wd*L8cR6c)_@I|a|9jvb#6B0mnL}wd3OOgkglJ;Su7<28xVn=GtyxgcJvj;$bB!j+R{-~1_0K` zywI!)G3@4~kr<3gOdkE$^yBNlklv|Jr2W-SUG82ul9Cd$DpDfg2%LG(xhtbTzOOd- zi3k4dX}ytwSBrM^v%-$FcS8f?!P%cJ!}4zDy5%%}vCethT}G4KFY_7rjjzh9mH~ii z6(IHim~&pvoC_~#6>iC|e&WB=-|e|4eG7*oB?6-ofd}v3{gfV~bH4X1ES`yFc$i|` zuRY~h@4Q!1{GF()+UX|UI%Sy{2!(<7-I03QLoJQiD7#~*PmW=$Qu+=*k zZoKkjdS>Qt)2Ev|(ic@bsW9FX~jx1rxgP3Qz+o=3~-UBRvI zeLH>Q^M94LOf=JPyIs*G0-F(m-@Lu{-lre>y$3b+Ii2&ibOpMt!{;P`nU2Bo)C`=< zXoKlIns#m*+f;3?Tk*mZon9`{(eR}Oh{eiw9K6~^?I`KDu zo*q5*etKLz@ttvz5`hhkz?R9>edIIs@85dg7eARM4;())5wsUho;*3Hw-V0k?F?M3 za^eCNiEBO1yUapmKkCV&Idyd2ZRWM2&M7VQ5|6GW8EiGV^1|}IR>^H1(RaeX)L}$Z z=fZdEXI4K^`FFpbc0F)cdTh%?dT6|!Zqt_Vh>{{D0s|3v_NA1*^u&N^&4uZS)cwfr zbpE!R(hJ)f=`Vh8>|?L$A$G0V+1b|a-Mc%745)gYw=OSUIkU$`1GxOO zR#&qgfa%%o&?Hv!YO4L^BWY&e)Y9J+%eTli9D$?nr^f3i@{=ys6@|HWI`>a6q+h5p zfC8XXzM|W+dnL4%`RWscxSlPGZ6nCeTd$|@(xyaUr3e6KOhD|i+g{c1vXK!&Z}eJq z>?1-O?Q89`_?HN*CISZZngiFtx!jOl?*2T_qDur;ihyBW$?vMF|33hj`AFi(*OveQ N002ovPDHLkV1oMj6`TM7 diff --git a/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-light.png b/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-light.png deleted file mode 100644 index 9e40efa628bbf0ac9062adeb3375c3763eeb40ec..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 828 zcmeAS@N?(olHy`uVBq!ia0vp^0YEIy!3HGf_?)N&QY`6?zK#qG8~eHcB(ehe7O4@Q zX}-P;T0k}j17mw80}GJF2*grA%)r33fC(-Vuz(rP76(cG`29);sARpTi(^Ox=i5l% z3}Hu+e`)5=maAmWo)R>@>sGV?zZ{R87FU&sP2`D%0*-q)_f}tOuIMpe*eJJz{i9OT z@>)j8Sl?3m3aNJtr{o8}A%M>$TKYsb@i;mFG zhN$zvy-^=)L@zlfUUJPjjci7w)8!st>bnUc9@EGi=3!wv0JK zuAU;dSY67*glj~sA{e4REm?m=`Kr@IQLDEd{-JB@>-H7sO!YeZ_QuLyHqQeeg#0e( zaocWDowqfHW%H3+9~k+LMz8m|oTM|+Lq;(8rZl5!Z-wxleGAf#Pd#x}noVhL?em}M z#lh>h3b}b6c&}Wsu;u1zl{-!=L$p>E`3r5R;andwv3g>A^XC$)y(>-tscs@5NiE zPq|do=CQ8mvdQanYu`qzGNy0ZQ70#{xMOn`cf!TPN(=AbTz>1H$u{?h8r$hxayP{C zx}2TgmZe?3@;_46ZKQzyMqav3Y`*L|}_8%rA_W3>2+Uxv5N!ZiX&t;ucLK6Uv CcVR35 diff --git a/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-light@2x.png b/GeoBus/Assets.xcassets/VehicleMarkers/RegularService.imageset/regular-service-light@2x.png deleted file mode 100644 index fe4b77592c6adaf82ecb901ad7390033490e5b49..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1482 zcmV;*1vUDKP)001ut1^@s6WGTTQ00004XF*Lt006O% z3;baP0000uWmrjOO-%qQ0000800D<-00aO40096102%-Q00002paK8{000010001> zpaTE|000010000l00000w~`$2000F&NklX#q)lQ8DvGv9 z{dh=fBKQ#nTSTY@AAAu^K~RxCilPC<;0OH&d=RvtFU9(1K`LUSjSp67saikMs%bG$ zYizX*$+nx_8PC~v=$+k|JG+^gy}LOF?#{h;?w#|yzxi?R-DKvhh45x6O*!wE{iAYP zvQ1>bBj23Zs*jgz4f;%l#NvyLkG7Jy?4JvHDQOln11vgMNxlf#t(xa@(=ChueSxv? z97?Hoo*jY!`&0uPwh*T%9L1; z#)*$Gg_^zmSufSshxCzg?Cj~pt2CNUzJOT#4BYu5@cUo&`5D=SKd$S=wmp+LPCZ$O zz6gyHBF}2enZLw3EWQ)FuyHYQ(ONX7 zOgAh?wt+R=L|EZCXXt9Yiq>z3r?GZ)1aEwB5HC^h))e6AgLr_zuL%r4-3`0c}(e4BrB$Ivmf6WtpG#aJv+-Uze`)LLE1wY1|r(7JW42S?#HYA@o10ul0DHn$-10q0` z4aq0YfCw;g%Eh6|fCx}!L-L6;AOcLBa&f3KAOcj`kbL3{Xaz_w1U}p~fencx6Niy8 zU^^LWdU~2)P&dxB0$fKABXsm(Nk0~c5DW_B(EuP2W;2^R~nGNHtQvC(VlmS6_Vz)Rnp#B&tit9`$YHOipj z{T>~}zTF4#<(2!mvZ*wNvFZfKE*W%r>+AUC)LD$W9_swTfEZBACAAFL7QF47acJ+? zc!C;pd|&>(kd_l51Fi^=2eIjSeDeNR7+$!9cd7C;)rlF#z?v+);SHGGF@l{RAI3uj zZ;_Ib;$!N|nUZ@W=`DT%m<)sD{~S1KDa4}-R#~!oD7@>-^ZUKu-w*$D{^$J9IluGi{LU3O7dsIlIUxW5AmU(e?auQY z9&DikJiFB|yUTOnP%E4j0MPO$>{lQ^FE$RwJA~kH04*Me0)W|J05FdMd644)007ZI z|7JKezJIyxZ!xTvYr|_pI9OYGCIUZK2IP2p$qzhD_%bre=&WLgux}4llg{4nNz!!2 z(V-wE#}ZBGE*j1ihf`(XCF17qPI*4DmMbIbTDdFNXdWu76GNS=$<^Em)h7EaURqqd z{$>ZK_&D!A`zD)ZFcTk zL*K3r$5$@ZjsM!?wv_A#b>0t#uEp2CQcX%VoXDC$BHh$;i!oP@CVz0{XEaiR>?aRwZ}xYJLFaH$$+#k9SIOit)(4w720rihVS+?n1nMYN0iw4zmd;F{_giR; z7*?P&Tn~~&k_Yjx45XOG+HPu{xgTijJN)>~n&@UfqAJxEVH}5eX)tURAdoey>!D*43}AT`xa@6vt*Z5IkD#!#C<2>}u^1_;oz%Zm>RN^4$3Eb<4c|kR ze>5(~N_iEp_Q<{!)R+U3wi-PCGuv=9IJgz+{K?}_`qyNV)sOGbQ^Vk8_cRs*>Z}-q zMD>T`a(|SJRwJ=8WC8ax)#|#|S{4#6A!dE1TAGBMtCnv8NX(n}ZArnxth6e*R+V@h z7%LhMEvV?9o-lL4r2t$yf*%YsG^m4u!op>nqRmZngOG@EKn>RT&tb752s5+Ttn>6F`K0B=P@9J zs5=H6aClB$JkC{oxU?z@!<(+(>Fp(Ow$84Wo9=_KfqiY0I$SbYs52v)WPLMZ(@YU0v7(Jv$; z?SIOMzm9Wci9x7B%+lrgE$Jy|HE}x-`r4S-4=%UyYSqQST~ZgHE8o5hgdxAtv!ka# z*G52#yWLhsqLY(D>qJwJ{CTWAPR>`{>53FUL934O%N4v!h`5^_vyL@5}o7 z={fR8mZ<2h%n%0(7$i=k@L2{)BXVH8-E|DCEbNs5JO(g~SD`-QiL3e&+nKzwy4#dW zz)}lLu&}!vyrDt#Nd=S+<*f_r76Amy;d=X#Y6jn)%+?z&oK><3Tb6N?`|7_OI0@ap z{a0$0MOdI6;^H0{`z5p=Jrf_GMjc;!d5ZR#MdU)-f>N09gpYOHk`P-ge{RGB3vVUu-Jlo62xa>td zF*iw)yS*$g-|)K0FlVw4?WNmN1m3XeRxl%Xw6$@~jR-TQeMviTu5k@$R1?CcA**Nu7(%hV;gM&QHGQk+7G%5U1RTJ6)dtNBefHsHoPUk@1`d_ z5A(KwESbp`Bqj0>G`=n=eSvn9SIje8OA$p)D<3LtM7FHYVMqgI*q*VQ0lDT)nJ(lJ zYmkw|s|6A48UNVW)yoDNL7Zq^6HhyCExT|GYcp8X=wuZ|A2wyGo;I7v7Gt1$C{anE zZzf;s8Lp*VQAWZ_#fkPIfHOaKuZ`m5e{m(dNBl{n;ZQ8%yd+iQ%RoTDL55d&3XAQF zAq1wNL1jLV{_=U+TXD^fQzaAh`v8`wi@Ds?AyT)jqAK=GqtT03<03qOKLcLAlWO{B zFG|wHXZi6yFr{vEW}w$u%`E_=eJ0UK=~-ET*qwWw5b5bP->%p2gN_-pcZrpv$WNsd zL78SZ%8wVWuZ%9pJ-I$x09%*biV~8THrnTANDZqkC?`#8M= zM4aiZMGoqO4-%6=xR7;keW$OE9e$W>arn@%3*mZY!Zj6(L)kUH!mYR%==QBsyBygE z0{Q>M!1*>tH%m5jK{K1=;ejeE_-nSd^X1@hHJVL> z6P$TsR-J)t2CqxgMPEPIe#6O~4Y(Ly{Mmtq1o_ggNHkrRXdmopuiYa#facJ&z7$rx cxBr2dXnpKyn1<7#-(P}*jf-{DNn*x-0lARfZvX%Q From 0a6b60e7c929646fc462885d37bf7fc27155aa62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Thu, 3 Nov 2022 23:40:06 +0000 Subject: [PATCH 39/63] Blip on MapKit experiment Its not gonna fly. It works bad. --- GeoBus/App/Components/NewMap/NewMapView.swift | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 GeoBus/App/Components/NewMap/NewMapView.swift diff --git a/GeoBus/App/Components/NewMap/NewMapView.swift b/GeoBus/App/Components/NewMap/NewMapView.swift new file mode 100644 index 00000000..f103192e --- /dev/null +++ b/GeoBus/App/Components/NewMap/NewMapView.swift @@ -0,0 +1,222 @@ +// +// MapView.swift +// GeoBus +// +// Created by João de Vasconcelos on 14/04/2020. +// Copyright © 2020 João de Vasconcelos. All rights reserved. +// + + + +import SwiftUI +import MapKit + +struct NewMapView: UIViewRepresentable { + + private let mapView = MKMapView() + + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + + + func makeUIView(context: UIViewRepresentableContext) -> MKMapView { + + mapView.delegate = context.coordinator + mapView.mapType = MKMapType.standard + mapView.showsUserLocation = true + mapView.showsTraffic = true + mapView.isRotateEnabled = false + mapView.isPitchEnabled = false + + mapView.register(NewConnectionAnnotationView.self, forAnnotationViewWithReuseIdentifier: "stop") + + // Set initial location in Lisbon + let lisbon = CLLocation(latitude: 38.721917, longitude: -9.137732) + let lisbonArea = MKCoordinateRegion(center: lisbon.coordinate, latitudinalMeters: 15000, longitudinalMeters: 15000) + mapView.setRegion(lisbonArea, animated: true) + + return mapView + } + + + func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext) { + + var annotationsToAdd: [MKAnnotation] = [] + + print("MAPTEST: is called updateUIView") +// print("MAPTEST: carrisNetworkController.activeRoute: \(carrisNetworkController.activeRoute)") + + if (carrisNetworkController.activeRoute != nil) { + for connection in carrisNetworkController.activeRoute?.variants[0].ascendingItinerary ?? [] { + annotationsToAdd.append( + NewConnectionAnnotation( + name: connection.stop.name, + publicId: connection.stop.id, + latitude: connection.stop.lat, + longitude: connection.stop.lng, + connection: connection + ) + ) + } + } + + + // Update whatever was set to update +// mapView.removeAnnotations([]) + uiView.addAnnotations(annotationsToAdd) + + uiView.showAnnotations(annotationsToAdd, animated: true) + + } + + + func makeCoordinator() -> NewMapView.Coordinator { + Coordinator(control: self) + } + + + + // MARK: - MKMapViewDelegate + + final class Coordinator: NSObject, MKMapViewDelegate { + + private let control: NewMapView + + private let appstate = Appstate.shared + private let carrisNetworkController = CarrisNetworkController.shared + + init(control: NewMapView) { + self.control = control + } + + + + @MainActor func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + print("MAPTEST: mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)") + if view.isKind(of: NewConnectionAnnotationView.self) { + + let selectedStopAnnotationView = view as! NewConnectionAnnotationView + let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation + +// selectedStopAnnotationView.marker.image = UIImage(named: "GreenInfo") + + TapticEngine.impact.feedback(.light) + _ = carrisNetworkController.select(connection: stopAnnotation.connection) + appstate.present(sheet: .carris_connectionDetails) + + } + } + + + + @MainActor func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { + print("MAPTEST: mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView)") + if view.isKind(of: NewConnectionAnnotationView.self) { + +// let selectedStopAnnotationView = view as! NewConnectionAnnotationView +// let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation + +// selectedStopAnnotationView.marker.image = stopAnnotation.markerSymbol + + carrisNetworkController.deselect([.connection]) + + } + } + + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + print("MAPTEST: mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?") + if annotation.isKind(of: NewConnectionAnnotation.self) { + +// return MKAnnotationView(annotation: annotation, reuseIdentifier: "stop") + + let identifier = "stop" + var view: MKAnnotationView + + if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) { + dequeuedView.annotation = annotation + view = dequeuedView + } else { + view = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier) + } + + return view + + } else { + + return nil + + } + + } + + + } + +} + + + + + + + + + + + +class NewConnectionAnnotationView: MKAnnotationView { + + override var annotation: MKAnnotation? { + + willSet { + guard let annotation = newValue as? NewConnectionAnnotation else { + return + } + + canShowCallout = false + +// marker.image = annotation.markerSymbol +// marker.frame = CGRect(x: 0, y: 0, width: 35, height: 35) +// frame = marker.frame + let swiftUIView = CarrisConnectionAnnotationView(connection: annotation.connection) // swiftUIView is View + let viewCtrl = UIHostingController(rootView: swiftUIView) + addSubview(viewCtrl.view) + + } + + } + + + +} + + + +class NewConnectionAnnotation: NSObject, MKAnnotation { + + let name: String + let publicId: Int + + let coordinate: CLLocationCoordinate2D + + let connection: CarrisNetworkModel.Connection + + + init(name: String?, publicId: Int?, latitude: Double, longitude: Double, connection: CarrisNetworkModel.Connection) { + self.name = name ?? "-" + self.publicId = publicId ?? -1 + self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + self.connection = connection + super.init() + } + + +// var title: String? = nil +// +// var subtitle: String? = nil + + + var markerSymbol = UIImage(named: "PinkArrowUp") + +} + From 5e71d44777277923e7c8eacfd94e4da8693a8404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Thu, 3 Nov 2022 23:40:59 +0000 Subject: [PATCH 40/63] Delete NewMapView.swift --- GeoBus/App/Components/NewMap/NewMapView.swift | 222 ------------------ 1 file changed, 222 deletions(-) delete mode 100644 GeoBus/App/Components/NewMap/NewMapView.swift diff --git a/GeoBus/App/Components/NewMap/NewMapView.swift b/GeoBus/App/Components/NewMap/NewMapView.swift deleted file mode 100644 index f103192e..00000000 --- a/GeoBus/App/Components/NewMap/NewMapView.swift +++ /dev/null @@ -1,222 +0,0 @@ -// -// MapView.swift -// GeoBus -// -// Created by João de Vasconcelos on 14/04/2020. -// Copyright © 2020 João de Vasconcelos. All rights reserved. -// - - - -import SwiftUI -import MapKit - -struct NewMapView: UIViewRepresentable { - - private let mapView = MKMapView() - - @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared - - - func makeUIView(context: UIViewRepresentableContext) -> MKMapView { - - mapView.delegate = context.coordinator - mapView.mapType = MKMapType.standard - mapView.showsUserLocation = true - mapView.showsTraffic = true - mapView.isRotateEnabled = false - mapView.isPitchEnabled = false - - mapView.register(NewConnectionAnnotationView.self, forAnnotationViewWithReuseIdentifier: "stop") - - // Set initial location in Lisbon - let lisbon = CLLocation(latitude: 38.721917, longitude: -9.137732) - let lisbonArea = MKCoordinateRegion(center: lisbon.coordinate, latitudinalMeters: 15000, longitudinalMeters: 15000) - mapView.setRegion(lisbonArea, animated: true) - - return mapView - } - - - func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext) { - - var annotationsToAdd: [MKAnnotation] = [] - - print("MAPTEST: is called updateUIView") -// print("MAPTEST: carrisNetworkController.activeRoute: \(carrisNetworkController.activeRoute)") - - if (carrisNetworkController.activeRoute != nil) { - for connection in carrisNetworkController.activeRoute?.variants[0].ascendingItinerary ?? [] { - annotationsToAdd.append( - NewConnectionAnnotation( - name: connection.stop.name, - publicId: connection.stop.id, - latitude: connection.stop.lat, - longitude: connection.stop.lng, - connection: connection - ) - ) - } - } - - - // Update whatever was set to update -// mapView.removeAnnotations([]) - uiView.addAnnotations(annotationsToAdd) - - uiView.showAnnotations(annotationsToAdd, animated: true) - - } - - - func makeCoordinator() -> NewMapView.Coordinator { - Coordinator(control: self) - } - - - - // MARK: - MKMapViewDelegate - - final class Coordinator: NSObject, MKMapViewDelegate { - - private let control: NewMapView - - private let appstate = Appstate.shared - private let carrisNetworkController = CarrisNetworkController.shared - - init(control: NewMapView) { - self.control = control - } - - - - @MainActor func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { - print("MAPTEST: mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)") - if view.isKind(of: NewConnectionAnnotationView.self) { - - let selectedStopAnnotationView = view as! NewConnectionAnnotationView - let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation - -// selectedStopAnnotationView.marker.image = UIImage(named: "GreenInfo") - - TapticEngine.impact.feedback(.light) - _ = carrisNetworkController.select(connection: stopAnnotation.connection) - appstate.present(sheet: .carris_connectionDetails) - - } - } - - - - @MainActor func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { - print("MAPTEST: mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView)") - if view.isKind(of: NewConnectionAnnotationView.self) { - -// let selectedStopAnnotationView = view as! NewConnectionAnnotationView -// let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation - -// selectedStopAnnotationView.marker.image = stopAnnotation.markerSymbol - - carrisNetworkController.deselect([.connection]) - - } - } - - - func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { - print("MAPTEST: mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?") - if annotation.isKind(of: NewConnectionAnnotation.self) { - -// return MKAnnotationView(annotation: annotation, reuseIdentifier: "stop") - - let identifier = "stop" - var view: MKAnnotationView - - if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) { - dequeuedView.annotation = annotation - view = dequeuedView - } else { - view = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier) - } - - return view - - } else { - - return nil - - } - - } - - - } - -} - - - - - - - - - - - -class NewConnectionAnnotationView: MKAnnotationView { - - override var annotation: MKAnnotation? { - - willSet { - guard let annotation = newValue as? NewConnectionAnnotation else { - return - } - - canShowCallout = false - -// marker.image = annotation.markerSymbol -// marker.frame = CGRect(x: 0, y: 0, width: 35, height: 35) -// frame = marker.frame - let swiftUIView = CarrisConnectionAnnotationView(connection: annotation.connection) // swiftUIView is View - let viewCtrl = UIHostingController(rootView: swiftUIView) - addSubview(viewCtrl.view) - - } - - } - - - -} - - - -class NewConnectionAnnotation: NSObject, MKAnnotation { - - let name: String - let publicId: Int - - let coordinate: CLLocationCoordinate2D - - let connection: CarrisNetworkModel.Connection - - - init(name: String?, publicId: Int?, latitude: Double, longitude: Double, connection: CarrisNetworkModel.Connection) { - self.name = name ?? "-" - self.publicId = publicId ?? -1 - self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - self.connection = connection - super.init() - } - - -// var title: String? = nil -// -// var subtitle: String? = nil - - - var markerSymbol = UIImage(named: "PinkArrowUp") - -} - From b5c0dd81482c8541c4c7b8fb2b9efe6fb5d84c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Thu, 3 Nov 2022 23:41:33 +0000 Subject: [PATCH 41/63] Making Chip receive a second view --- GeoBus.xcodeproj/project.pbxproj | 2 +- GeoBus/App/Layout/Chip.swift | 24 +++++++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index 53be940f..b7f1ca3c 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -187,8 +187,8 @@ isa = PBXGroup; children = ( CF181FE728CCB7D600248F72 /* ContentView.swift */, - CF47FCB429035D8300AE33B0 /* PresentedSheetView.swift */, CF05F61928CD09A000B4AD58 /* NavBar.swift */, + CF47FCB429035D8300AE33B0 /* PresentedSheetView.swift */, CF18208828CCBD4600248F72 /* Map */, CF05F62528CD60BD00B4AD58 /* SelectRoute */, CF05F61828CD097700B4AD58 /* RouteDetails */, diff --git a/GeoBus/App/Layout/Chip.swift b/GeoBus/App/Layout/Chip.swift index f1c51c81..e46376e5 100644 --- a/GeoBus/App/Layout/Chip.swift +++ b/GeoBus/App/Layout/Chip.swift @@ -24,16 +24,26 @@ struct Chip: View { self.showContent = showContent } - var actualContent: some View { - HStack(alignment: .center) { - icon - text - Spacer() + VStack(alignment: .leading, spacing: 0) { + + HStack(alignment: .center) { + icon + text + Spacer() + } + .font(Font.system(size: 15, weight: .medium)) + .padding() + .foregroundColor(color) + +// if (customContent != nil) { +// Divider() +// customContent!() +// .padding() +// } + } - .font(Font.system(size: 15, weight: .medium)) .padding() - .foregroundColor(color) .frame(maxWidth: .infinity) .background(color.opacity(0.1)) .cornerRadius(10) From 8b63239ed2290f60ab36b04d61bdb3da771f8389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Mon, 7 Nov 2022 22:00:12 +0000 Subject: [PATCH 42/63] Changes changes --- .../App/Components/Map/MapAnnotations.swift | 52 +- GeoBus/App/Components/Map/MapView.swift | 14 + .../StopDetails/StopEstimations.swift | 2 +- .../VehicleDetails/VehicleDetailsView.swift | 580 +++++++++++++----- GeoBus/App/Controllers/Appstate.swift | 5 + GeoBus/App/Controllers/MapController.swift | 113 +++- .../Carris/CarrisNetworkController.swift | 41 +- .../Networks/Carris/CarrisNetworkModel.swift | 1 + GeoBus/App/GeoBusApp.swift | 2 - GeoBus/App/Layout/Chip.swift | 20 +- GeoBus/App/Layout/SheetErrorScreen.swift | 12 + GeoBus/App/Layout/StopIcon.swift | 15 +- 12 files changed, 668 insertions(+), 189 deletions(-) diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index 79607220..c9614434 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -21,6 +21,7 @@ struct GenericMapAnnotation: Identifiable { case carris_stop(CarrisNetworkModel.Stop) case carris_connection(CarrisNetworkModel.Connection) case carris_vehicle(CarrisNetworkModel.Vehicle) + case ministop(CarrisNetworkModel.Stop) } } @@ -59,9 +60,9 @@ struct CarrisConnectionAnnotationView: View { @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared - // var body: some View { - // EmptyView() - // } + var body2: some View { + EmptyView() + } var body: some View { Button(action: { @@ -92,6 +93,11 @@ struct CarrisVehicleAnnotationView: View { @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + var body2: some View { + EmptyView() + } + + var body: some View { Button(action: { TapticEngine.impact.feedback(.light) @@ -113,3 +119,43 @@ struct CarrisVehicleAnnotationView: View { } } + + + + + +struct CarrisMiniStopAnnotationView: View { + + public let stop: CarrisNetworkModel.Stop + + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var mapController = MapController.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + + + var icon: some View { + Button(action: { + TapticEngine.impact.feedback(.light) + carrisNetworkController.select(stop: self.stop) + appstate.present(sheet: .carris_stopDetails) + }) { + Circle() + .foregroundColor(.blue) + .background(Color(.blue)) + } + .frame(width: 15, height: 15, alignment: .center) + } + + + var body: some View { + if (mapController.region.span.latitudeDelta < 0.0025 || mapController.region.span.longitudeDelta < 0.0025) { + icon + } else { + Circle() + .foregroundColor(.blue) + .background(Color(.blue)) + .frame(width: 5, height: 5) + } + } + +} diff --git a/GeoBus/App/Components/Map/MapView.swift b/GeoBus/App/Components/Map/MapView.swift index 00ab1352..5a48cd6b 100644 --- a/GeoBus/App/Components/Map/MapView.swift +++ b/GeoBus/App/Components/Map/MapView.swift @@ -32,6 +32,8 @@ struct MapView: View { CarrisConnectionAnnotationView(connection: item) case .carris_vehicle(let item): CarrisVehicleAnnotationView(vehicle: item) + case .ministop(let item): + CarrisMiniStopAnnotationView(stop: item) } } @@ -54,6 +56,18 @@ struct MapView: View { .onChange(of: carrisNetworkController.activeVehicles) { newVehiclesList in self.mapController.updateAnnotations(with: newVehiclesList) } +// .onAppear() { +// self.mapController.updateAnnotations(ministop: carrisNetworkController.allStops) +// } +// .onChange(of: carrisNetworkController.allStops) { allStops in +// self.mapController.updateAnnotations(ministop: allStops) +// } + .onChange(of: [mapController.region.center.latitude, mapController.region.center.longitude]) { _ in + self.mapController.updateAnnotations(newRegion: nil) + } + .onChange(of: [mapController.region.span.latitudeDelta, mapController.region.span.longitudeDelta]) { _ in + self.mapController.updateAnnotations(newRegion: nil) + } } diff --git a/GeoBus/App/Components/StopDetails/StopEstimations.swift b/GeoBus/App/Components/StopDetails/StopEstimations.swift index 5a85f393..e0c12884 100644 --- a/GeoBus/App/Components/StopDetails/StopEstimations.swift +++ b/GeoBus/App/Components/StopDetails/StopEstimations.swift @@ -16,7 +16,7 @@ struct EstimationsContainer: View { @State var estimations: [CarrisNetworkModel.Estimation]? - let refreshTimer = Timer.publish(every: 60 /* seconds */, on: .main, in: .common).autoconnect() + let refreshTimer = Timer.publish(every: 30 /* seconds */, on: .main, in: .common).autoconnect() func getEstimationsFromController(_ value: Any?) { diff --git a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift index 14a60d38..8822efc1 100644 --- a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift +++ b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift @@ -7,6 +7,7 @@ // import SwiftUI import Combine +import MapKit struct CarrisVehicleSheetView: View { @@ -17,23 +18,29 @@ struct CarrisVehicleSheetView: View { var body: some View { VStack(spacing: 0) { - if (appstate.vehicles == .error) { - SheetErrorScreen() - } else { - CarrisVehicleSheetHeader(vehicle: carrisNetworkController.activeVehicle) - ScrollView { - VStack(alignment: .leading, spacing: 15) { - CarrisVehicleLastSeenTime(vehicle: carrisNetworkController.activeVehicle) - if (carrisNetworkController.communityDataProviderStatus) { - // CarrisVehicleNextStop(vehicle: carrisNetworkController.activeVehicle) - CarrisVehicleRouteOverview(vehicle: carrisNetworkController.activeVehicle) - Disclaimer() - } else { - DataProvidersCard() + CarrisVehicleSheetHeader(vehicle: carrisNetworkController.activeVehicle) + ScrollView { + VStack(alignment: .leading, spacing: 15) { + if (appstate.carris_vehicleDetails == .error) { + SheetErrorScreen() + } else { + HStack(spacing: 15) { + CarrisVehicleLastSeenTime(vehicle: carrisNetworkController.activeVehicle) + CarrisVehicleToggleFollowOnMap() } } - .padding() + if (carrisNetworkController.communityDataProviderStatus) { + CarrisVehicleRouteSummary(vehicle: carrisNetworkController.activeVehicle) + Disclaimer() + } else { + DataProvidersCard() + .overlay() { + RoundedRectangle(cornerRadius: 10) + .stroke(Color(.systemTeal).opacity(0.1), lineWidth: 2) + } + } } + .padding() } } } @@ -79,7 +86,7 @@ struct CarrisVehicleLastSeenTime: View { func updateLastSeenTime(_ value: Any) { if (vehicle?.lastGpsTime != nil) { - self.lastSeenTime = Helpers.getTimeString(for: vehicle!.lastGpsTime!, in: .past, style: .full, units: [.hour, .minute, .second]) + self.lastSeenTime = Helpers.getTimeString(for: vehicle!.lastGpsTime!, in: .past, style: .short, units: [.hour, .minute, .second]) } } @@ -96,6 +103,91 @@ struct CarrisVehicleLastSeenTime: View { } } + + + + +struct CarrisVehicleToggleFollowOnMap: View { + + private let storageKeyForShouldFollowOnMap: String = "ui_shouldFollowOnMap" + + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var mapController = MapController.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + + @State private var shouldFollowOnMap: Bool + + + init() { + self.shouldFollowOnMap = UserDefaults.standard.bool(forKey: storageKeyForShouldFollowOnMap) + } + + + func centerMapOnActiveVehicle() { + if (shouldFollowOnMap && carrisNetworkController.activeVehicle != nil) { + mapController.moveMap(to: MKCoordinateRegion( + center: CLLocationCoordinate2D(latitude: carrisNetworkController.activeVehicle?.lat ?? 0, longitude: carrisNetworkController.activeVehicle?.lng ?? 0), + latitudinalMeters: CLLocationDistance(1500), longitudinalMeters: CLLocationDistance(1500) + )) + } + } + + var body: some View { + Button(action: { + TapticEngine.impact.feedback(.medium) + self.shouldFollowOnMap = !shouldFollowOnMap + UserDefaults.standard.set(shouldFollowOnMap, forKey: storageKeyForShouldFollowOnMap) + self.centerMapOnActiveVehicle() + }, label: { + VStack { + Image(systemName: shouldFollowOnMap ? "location.circle.fill" : "location.slash.circle") + .font(.system(size: 20, weight: .medium)) + } + .padding() + .foregroundColor(shouldFollowOnMap ? .white : Color(.systemBlue)) + .background(shouldFollowOnMap ? Color(.systemBlue) : Color(.secondaryLabel).opacity(0.1)) + .cornerRadius(10) + .onAppear(perform: centerMapOnActiveVehicle) + .onChange(of: [carrisNetworkController.activeVehicle?.lat, carrisNetworkController.activeVehicle?.lng]) { _ in + centerMapOnActiveVehicle() + } + }) + } + + + +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -103,7 +195,14 @@ struct CarrisVehicleLastSeenTime: View { -struct CarrisVehicleNextStop: View { + + + + + + + +struct CarrisVehicleRouteSummary: View { let vehicle: CarrisNetworkModel.Vehicle? @@ -111,73 +210,156 @@ struct CarrisVehicleNextStop: View { @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared - @State var nextStopIndex: Int = 0 + func findNextStop() -> Int? { + if (vehicle?.routeOverview != nil) { + + if let previousStop = vehicle!.routeOverview!.lastIndex(where: { + $0.hasArrived ?? false + }) { + if (previousStop + 1 < vehicle!.routeOverview!.count) { + let nextStop = vehicle!.routeOverview![previousStop + 1] + return nextStop.stopId + } + } + + } + return nil + } - func setNextStop(_ value: Any) { + func findNextStopIndex() -> Int? { if (vehicle?.routeOverview != nil) { if let previousStop = vehicle!.routeOverview!.lastIndex(where: { $0.hasArrived ?? false }) { - nextStopIndex = previousStop + 1 - } else { - nextStopIndex = 10 + return previousStop + 1 } } + return nil } - var placeholder: some View { - EmptyView() - } + @State var nextStopIndex = 0 + @State var showAllStops = false - var actualContent: some View { - VStack(spacing: 0) { + + + var allStopsToggle: some View { + VStack(alignment: .leading, spacing: 5) { HStack { - Text("Next stop:") - .font(Font.system(size: 10, weight: .bold)) - .textCase(.uppercase) - .foregroundColor(Color(.secondaryLabel)) + Button(action: { + TapticEngine.impact.feedback(.medium) + self.showAllStops = !showAllStops + }, label: { + if (showAllStops) { + Image(systemName: "minus.square") + .frame(width: 25) + Text("Hide Previous Stops") + } else { + Image(systemName: "plus.square") + .frame(width: 25) + Text("Show All Stops") + } + }) Spacer() - PulseLabel(accent: .blue, label: Text("Community")) + PulseLabel(accent: .teal, label: Text("Community")) } - .padding(.vertical, 10) - .padding(.horizontal) + .font(Font.system(size: 15, weight: .medium)) + .foregroundColor(Color(.secondaryLabel)) + } + } + + @State private var placeholderOpacity: Double = 1 + + + + var content: some View { + VStack(alignment: .leading, spacing: 0) { + + allStopsToggle + .padding() + Divider() - Button(action: { - _ = carrisNetworkController.select(stop: vehicle!.routeOverview![nextStopIndex].stopId) - appstate.present(sheet: .carris_stopDetails) - }, label: { - HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: nextStopIndex + 1, style: .standard) - Text(carrisNetworkController.find(stop: (vehicle!.routeOverview![nextStopIndex].stopId))?.name ?? "") - .font(Font.system(size: 17, weight: .medium)) - .lineLimit(1) - .foregroundColor(Color(.label)) - Spacer(minLength: 5) - TimeLeft(time: vehicle?.routeOverview![nextStopIndex].eta) + + VStack(alignment: .leading, spacing: 0) { + ForEach(nextStopIndex.. 0) { + Rectangle() + .frame(width: 3, height: 30) + .foregroundColor(Color("StopMutedBackground")) + .padding(.horizontal, 11) + } + + HStack(alignment: .center, spacing: 10) { + StopIcon(orderInRoute: thisStopIndex+1, style: .muted) + Text(carrisNetworkController.find(stop: estimationLine.stopId)?.name ?? "") + .font(Font.system(size: 17, weight: .medium)) + .lineLimit(1) + .foregroundColor(estimationLine.hasArrived ?? false ? Color("StopMutedText") : .black) + Spacer(minLength: 5) + Image(systemName: "checkmark.circle") + .font(Font.system(size: 15, weight: .medium)) + .foregroundColor(Color("StopMutedText")) + } + + } else if (nextStopId == estimationLine.stopId) { + + VStack(alignment: .center, spacing: -2) { + Rectangle() + .frame(width: 3, height: 30) + .foregroundColor(Color("StopMutedBackground")) + Image(systemName: "arrowtriangle.down.circle.fill") + .font(Font.system(size: 15, weight: .medium)) + .foregroundColor(Color(.systemBlue)) + Rectangle() + .frame(width: 5, height: 30) + .foregroundColor(Color(.systemBlue).opacity(0.5)) + } + .frame(width: 25) + + HStack(alignment: .center, spacing: 10) { + StopIcon(orderInRoute: thisStopIndex+1, style: .standard) + Text(carrisNetworkController.find(stop: estimationLine.stopId)?.name ?? "") + .font(Font.system(size: 17, weight: .medium)) + .lineLimit(1) + .foregroundColor(Color(.label)) + Spacer(minLength: 5) + TimeLeft(time: estimationLine.eta) + } + + } else { + + if (thisStopIndex > 0) { + Rectangle() + .frame(width: 5, height: 30) + .foregroundColor(Color(.systemBlue)) + .padding(.horizontal, 10) + } + + HStack(alignment: .center, spacing: 10) { + StopIcon(orderInRoute: thisStopIndex+1, style: .standard) + Text(carrisNetworkController.find(stop: estimationLine.stopId)?.name ?? "") + .font(Font.system(size: 17, weight: .medium)) + .lineLimit(1) + .foregroundColor(Color(.label)) + Spacer(minLength: 5) + TimeLeft(time: estimationLine.eta) + } + + } + + + } + + } + + +} @@ -242,8 +488,8 @@ struct CarrisVehicleRouteOverview: View { let vehicle: CarrisNetworkModel.Vehicle? - @ObservedObject var appstate = Appstate.shared - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared func findNextStop() -> Int? { @@ -263,93 +509,115 @@ struct CarrisVehicleRouteOverview: View { } + func findNextStopIndex() -> Int? { + if (vehicle?.routeOverview != nil) { + if let previousStop = vehicle!.routeOverview!.lastIndex(where: { + $0.hasArrived ?? false + }) { + return previousStop + 1 + } + } + return nil + } + + var content: some View { - VStack(alignment: .leading, spacing: 0) { - if (vehicle?.routeOverview != nil) { - - ForEach(Array(vehicle!.routeOverview!.enumerated()), id: \.offset) { index, element in - VStack(alignment: .leading, spacing: 0) { - - if (element.hasArrived ?? false) { - - if (index > 0) { - Rectangle() - .frame(width: 3, height: 30) - .foregroundColor(Color("StopMutedBackground")) - .padding(.horizontal, 11) - } - - HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: index+1, style: .muted) - Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") - .font(Font.system(size: 17, weight: .medium)) - .lineLimit(1) - .foregroundColor(element.hasArrived ?? false ? Color("StopMutedText") : .black) - Spacer(minLength: 5) - Image(systemName: "checkmark.circle") - .font(Font.system(size: 15, weight: .medium)) - .foregroundColor(Color("StopMutedText")) - } - - } else if (findNextStop() == element.stopId) { - - VStack(spacing: 0) { - Rectangle() - .frame(width: 3, height: 30) - .foregroundColor(Color("StopMutedBackground")) - .padding(.horizontal, 11) - Image(systemName: "arrowtriangle.down.circle.fill") - .font(Font.system(size: 15, weight: .medium)) - .foregroundColor(Color(.systemBlue)) - .padding(.vertical, -2) - Rectangle() - .frame(width: 5, height: 30) - .foregroundColor(Color(.systemBlue)) - .padding(.horizontal, 10) - } + ScrollViewReader { value in + Button("Jump to #8") { + if (findNextStopIndex() != nil) { + value.scrollTo(findNextStopIndex()) + } + } + + VStack(alignment: .leading, spacing: 0) { + if (vehicle?.routeOverview != nil) { + + ForEach(Array(vehicle!.routeOverview!.enumerated()), id: \.offset) { index, element in + VStack(alignment: .leading, spacing: 0) { - HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: index+1, style: .standard) - Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") - .font(Font.system(size: 17, weight: .medium)) - .lineLimit(1) - .foregroundColor(Color(.label)) - Spacer(minLength: 5) - TimeLeft(time: element.eta) + if (element.hasArrived ?? false) { + + if (index > 0) { + Rectangle() + .frame(width: 3, height: 30) + .foregroundColor(Color("StopMutedBackground")) + .padding(.horizontal, 11) + } + + HStack(alignment: .center, spacing: 10) { + StopIcon(orderInRoute: index+1, style: .muted) + Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") + .font(Font.system(size: 17, weight: .medium)) + .lineLimit(1) + .foregroundColor(element.hasArrived ?? false ? Color("StopMutedText") : .black) + Spacer(minLength: 5) + Image(systemName: "checkmark.circle") + .font(Font.system(size: 15, weight: .medium)) + .foregroundColor(Color("StopMutedText")) + } + + } else if (findNextStop() == element.stopId) { + + VStack(spacing: 0) { + Rectangle() + .frame(width: 3, height: 30) + .foregroundColor(Color("StopMutedBackground")) + .padding(.horizontal, 11) + Image(systemName: "arrowtriangle.down.circle.fill") + .font(Font.system(size: 15, weight: .medium)) + .foregroundColor(Color(.systemBlue)) + .padding(.vertical, -2) + Rectangle() + .frame(width: 5, height: 30) + .foregroundColor(Color(.systemBlue)) + .padding(.horizontal, 10) + } + + HStack(alignment: .center, spacing: 10) { + StopIcon(orderInRoute: index+1, style: .standard) + Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") + .font(Font.system(size: 17, weight: .medium)) + .lineLimit(1) + .foregroundColor(Color(.label)) + Spacer(minLength: 5) + TimeLeft(time: element.eta) + } + + } else { + + if (index > 0) { + Rectangle() + .frame(width: 5, height: 30) + .foregroundColor(Color(.systemBlue)) + .padding(.horizontal, 10) + } + + HStack(alignment: .center, spacing: 10) { + StopIcon(orderInRoute: index+1, style: .standard) + Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") + .font(Font.system(size: 17, weight: .medium)) + .lineLimit(1) + .foregroundColor(Color(.label)) + Spacer(minLength: 5) + TimeLeft(time: element.eta) + } + } - } else { - if (index > 0) { - Rectangle() - .frame(width: 5, height: 30) - .foregroundColor(Color(.systemBlue)) - .padding(.horizontal, 10) - } - - HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: index+1, style: .standard) - Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") - .font(Font.system(size: 17, weight: .medium)) - .lineLimit(1) - .foregroundColor(Color(.label)) - Spacer(minLength: 5) - TimeLeft(time: element.eta) - } - } - - - + .id(index) } + + + + } else { + Text("Is nil, loading?") } - - } else { - Text("Is nil, loading?") } } .padding() diff --git a/GeoBus/App/Controllers/Appstate.swift b/GeoBus/App/Controllers/Appstate.swift index 8325f298..1b573094 100644 --- a/GeoBus/App/Controllers/Appstate.swift +++ b/GeoBus/App/Controllers/Appstate.swift @@ -40,6 +40,7 @@ final class Appstate: ObservableObject { case routes case vehicles case estimations + case carris_vehicleDetails } @@ -102,6 +103,8 @@ final class Appstate: ObservableObject { @Published var vehicles: State = .idle @Published var estimations: State = .idle + @Published var carris_vehicleDetails: State = .idle + @Published var sheetIsPresented: Bool = false @Published var currentlyPresentedSheetView: PresentableSheetView? = nil @@ -126,6 +129,8 @@ final class Appstate: ObservableObject { self.vehicles = newState case .estimations: self.estimations = newState + case .carris_vehicleDetails: + self.carris_vehicleDetails = newState } // Change state of global module if (self.auth == .idle && self.vehicles == .idle) { diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index e3aacb5f..0371bf4c 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -9,7 +9,7 @@ import Foundation import MapKit import SwiftUI -//@MainActor +@MainActor class MapController: ObservableObject { /* * */ @@ -33,7 +33,7 @@ class MapController: ObservableObject { @Published var showLocationNotAllowedAlert: Bool = false @Published var visibleAnnotations: [GenericMapAnnotation] = [] - + @Published var allStopAnnotations: [GenericMapAnnotation] = [] /* * */ @@ -144,7 +144,7 @@ class MapController: ObservableObject { visibleAnnotations.removeAll(where: { switch $0.item { - case .carris_stop(_), .carris_connection(_), .carris_vehicle(_): + case .carris_stop(_), .carris_connection(_), .carris_vehicle(_), .ministop(_): return true } }) @@ -175,7 +175,7 @@ class MapController: ObservableObject { switch $0.item { case .carris_connection(_), .carris_stop(_): return true - case .carris_vehicle(_): + case .carris_vehicle(_), .ministop(_): return false } }) @@ -236,7 +236,7 @@ class MapController: ObservableObject { switch $0.item { case .carris_vehicle(_), .carris_stop(_): return true - case .carris_connection(_): + case .carris_connection(_), .ministop(_): return false } }) @@ -279,7 +279,7 @@ class MapController: ObservableObject { } else { return false } - case .carris_connection(_), .carris_stop(_): + case .carris_connection(_), .carris_stop(_), .ministop(_): return false } }) { @@ -339,4 +339,105 @@ class MapController: ObservableObject { // } + + + + /* * */ + /* MARK: - SECTION 8: UPDATE ANNOTATIONS WITH SELECTED CARRIS STOP */ + /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ + + func updateAnnotations(ministop: [CarrisNetworkModel.Stop]) { + + visibleAnnotations.removeAll(where: { + switch $0.item { + case .ministop(_): + return true + case .carris_vehicle(_), .carris_stop(_), .carris_connection(_): + return false + } + }) + + var tempNewAnnotations: [GenericMapAnnotation] = [] + + for stop in ministop { + tempNewAnnotations.append( + GenericMapAnnotation( + id: UUID(), + location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), + item: .ministop(stop) + ) + ) + } + + self.addAnnotations(tempNewAnnotations, zoom: true) + + } + + @ObservedObject var carrisNetworkController = CarrisNetworkController.shared + + + /* * */ + /* MARK: - SECTION 8: UPDATE ANNOTATIONS WITH SELECTED CARRIS STOP */ + /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ + + func updateAnnotations(newRegion: MKCoordinateRegion?) { + + if (allStopAnnotations.isEmpty) { + for stop in carrisNetworkController.allStops { + allStopAnnotations.append( + GenericMapAnnotation( + id: UUID(), + location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), + item: .ministop(stop) + ) + ) + } + } + + + if (region.span.latitudeDelta < 0.005 || region.span.longitudeDelta < 0.005) { + + let latTop = self.region.center.latitude + self.region.span.latitudeDelta + let latBottom = self.region.center.latitude - self.region.span.latitudeDelta + + let lngRight = self.region.center.longitude + self.region.span.longitudeDelta + let lngLeft = self.region.center.longitude - self.region.span.longitudeDelta + + + for annotation in allStopAnnotations { + + // Checks + let isBetweenLats = annotation.location.latitude > latBottom && annotation.location.latitude < latTop + let isBetweenLngs = annotation.location.longitude > lngLeft && annotation.location.longitude < lngRight + + if (isBetweenLats && isBetweenLngs) { + if visibleAnnotations.firstIndex(where: { + $0.id == annotation.id + }) == nil { + visibleAnnotations.append(annotation) + } + } else { + if let indexOfAnnotation = visibleAnnotations.firstIndex(where: { + $0.id == annotation.id + }) { + visibleAnnotations.remove(at: indexOfAnnotation) + } + } + + } + + } else { + visibleAnnotations.removeAll(where: { + switch $0.item { + case .ministop(_): + return true; + case .carris_connection(_), .carris_stop(_), .carris_vehicle(_): + return false; + } + }) + } + + } + + } diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index 71f0553c..f10a2d7a 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -161,6 +161,9 @@ class CarrisNetworkController: ObservableObject { UserDefaults.standard.removeObject(forKey: key) } + // Restore deleted stored values + UserDefaults.standard.set(communityDataProviderStatus, forKey: storageKeyForCommunityDataProviderStatus) + // Fetch the updated network from the API await fetchStopsFromCarrisAPI() await fetchRoutesFromCarrisAPI() @@ -194,11 +197,18 @@ class CarrisNetworkController: ObservableObject { // DEBUG ! // if (self.activeVehicle == nil) { -// self.select(vehicle: self.allVehicles[0].id) +// self.select(vehicle: self.allVehicles[1].id) // Appstate.shared.present(sheet: .carris_vehicleDetails) // } // ! DEBUG + // DEBUG ! +// if (self.activeRoute == nil) { +// _ = self.select(route: "758") +// // Appstate.shared.present(sheet: .carris_vehicleDetails) +// } + // ! DEBUG + // If there is an active vehicle, also refresh it's details if (self.activeVehicle != nil) { await self.fetchVehicleDetailsFromCarrisAPI(for: self.activeVehicle!.id) @@ -707,6 +717,18 @@ class CarrisNetworkController: ObservableObject { } + public func getDirectionFrom(string directionString: String?) -> CarrisNetworkModel.Direction? { + switch directionString { + case "ASC": + return .ascending + case "DESC": + return .descending + case "CIRC": + return .circular + default: + return nil + } + } @@ -914,6 +936,7 @@ class CarrisNetworkController: ObservableObject { allVehicles[indexOfVehicleInArray!].previousLatitude = vehicleSummary.previousLatitude ?? 0 allVehicles[indexOfVehicleInArray!].previousLongitude = vehicleSummary.previousLongitude ?? 0 allVehicles[indexOfVehicleInArray!].lastGpsTime = vehicleSummary.lastGpsTime ?? "" + allVehicles[indexOfVehicleInArray!].direction = getDirectionFrom(string: vehicleSummary.direction) } else { self.allVehicles.append( @@ -956,7 +979,7 @@ class CarrisNetworkController: ObservableObject { private func fetchVehicleDetailsFromCarrisAPI(for vehicleId: Int) async { - Appstate.shared.change(to: .loading, for: .vehicles) + Appstate.shared.change(to: .loading, for: .carris_vehicleDetails) print("GeoBus: Carris API: Vehicle Details: Starting update...") @@ -983,10 +1006,10 @@ class CarrisNetworkController: ObservableObject { print("GeoBus: Carris API: Vehicle Details: Update complete!") - Appstate.shared.change(to: .idle, for: .vehicles) + Appstate.shared.change(to: .idle, for: .carris_vehicleDetails) } catch { - Appstate.shared.change(to: .error, for: .vehicles) + Appstate.shared.change(to: .error, for: .carris_vehicleDetails) print("GeoBus: Carris API: Vehicles Details: Error found while updating. More info: \(error)") return } @@ -1002,7 +1025,7 @@ class CarrisNetworkController: ObservableObject { private func fetchVehicleDetailsFromCommunityAPI(for vehicleId: Int) async { - Appstate.shared.change(to: .loading, for: .vehicles) + Appstate.shared.change(to: .loading, for: .carris_vehicleDetails) print("GeoBus: Community API: Vehicle Details: Starting update...") @@ -1030,6 +1053,7 @@ class CarrisNetworkController: ObservableObject { ) } + let indexOfVehicleInArray = allVehicles.firstIndex(where: { $0.id == vehicleId }) @@ -1043,10 +1067,10 @@ class CarrisNetworkController: ObservableObject { print("GeoBus: Community API: Vehicle Details: Update complete!") - Appstate.shared.change(to: .idle, for: .vehicles) + Appstate.shared.change(to: .idle, for: .carris_vehicleDetails) } catch { - Appstate.shared.change(to: .error, for: .vehicles) + Appstate.shared.change(to: .error, for: .carris_vehicleDetails) print("GeoBus: Community API: Vehicles Details: Error found while updating. More info: \(error)") return } @@ -1100,7 +1124,6 @@ class CarrisNetworkController: ObservableObject { let rawDataCarrisAPIEstimations = try await CarrisAPI.shared.request(for: "Estimations/busStop/\(stopId)/top/5") let decodedCarrisAPIEstimations = try JSONDecoder().decode([CarrisAPIModel.Estimation].self, from: rawDataCarrisAPIEstimations) - var tempFormattedEstimations: [CarrisNetworkModel.Estimation] = [] @@ -1112,7 +1135,7 @@ class CarrisNetworkController: ObservableObject { routeNumber: apiEstimation.routeNumber, destination: apiEstimation.destination, eta: apiEstimation.time ?? "", - busNumber: Int(apiEstimation.busNumber ?? "-1") + busNumber: Int(apiEstimation.busNumber ?? "") ) ) } diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift index 775862bc..31027272 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift @@ -179,6 +179,7 @@ struct CarrisNetworkModel { var previousLatitude: Double? var previousLongitude: Double? var lastGpsTime: String? + var direction: Direction? // Carris API › Vehicle Details var vehiclePlate: String? diff --git a/GeoBus/App/GeoBusApp.swift b/GeoBus/App/GeoBusApp.swift index cd72805f..9a6a693c 100644 --- a/GeoBus/App/GeoBusApp.swift +++ b/GeoBus/App/GeoBusApp.swift @@ -5,8 +5,6 @@ import SwiftUI @main struct GeoBusApp: App { - @ObservedObject private var appstate = Appstate.shared - @ObservedObject private var mapController = MapController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared // @ObservedObject private var tcbNetworkController = TCBNetworkController.shared diff --git a/GeoBus/App/Layout/Chip.swift b/GeoBus/App/Layout/Chip.swift index e46376e5..6cdd3657 100644 --- a/GeoBus/App/Layout/Chip.swift +++ b/GeoBus/App/Layout/Chip.swift @@ -7,21 +7,24 @@ import SwiftUI -struct Chip: View { +struct Chip: View { let icon: Image let text: Text let color: Color let showContent: Bool + let customContent: CustomContent + @State private var placeholderOpacity: Double = 1 - init(icon: Image, text: Text, color: Color, showContent: Bool = true) { + init(icon: Image, text: Text, color: Color, showContent: Bool = true, customContent: () -> CustomContent = { EmptyView() }) { self.icon = icon self.text = text self.color = color self.showContent = showContent + self.customContent = customContent() } var actualContent: some View { @@ -33,17 +36,16 @@ struct Chip: View { Spacer() } .font(Font.system(size: 15, weight: .medium)) - .padding() .foregroundColor(color) + .padding() -// if (customContent != nil) { -// Divider() -// customContent!() -// .padding() -// } + if (type(of: customContent) != EmptyView.self) { + Divider() + customContent + .padding() + } } - .padding() .frame(maxWidth: .infinity) .background(color.opacity(0.1)) .cornerRadius(10) diff --git a/GeoBus/App/Layout/SheetErrorScreen.swift b/GeoBus/App/Layout/SheetErrorScreen.swift index 4d1a8d30..7ed8f953 100644 --- a/GeoBus/App/Layout/SheetErrorScreen.swift +++ b/GeoBus/App/Layout/SheetErrorScreen.swift @@ -12,6 +12,18 @@ struct SheetErrorScreen: View { @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { + Chip( + icon: Image(systemName: "exclamationmark.triangle.fill"), + text: Text("Error fetching data from server."), + color: Color(.systemOrange) + ) + } + + + + + + var bodyAlt: some View { VStack(alignment: .center, spacing: 15) { Image(systemName: "exclamationmark.octagon.fill") .foregroundColor(Color(.systemRed)) diff --git a/GeoBus/App/Layout/StopIcon.swift b/GeoBus/App/Layout/StopIcon.swift index 39496bb7..59d7cdaf 100644 --- a/GeoBus/App/Layout/StopIcon.swift +++ b/GeoBus/App/Layout/StopIcon.swift @@ -10,15 +10,24 @@ import SwiftUI struct StopIcon: View { public let orderInRoute: Int? - public let direction: CarrisNetworkModel.Direction? public let style: Style public let isSelected: Bool init(orderInRoute: Int? = nil, direction: CarrisNetworkModel.Direction? = nil, style: Style = .standard, isSelected: Bool = false) { self.orderInRoute = orderInRoute - self.direction = direction - self.style = style self.isSelected = isSelected + + switch direction { + case .ascending: + self.style = .ascending + case .descending: + self.style = .descending + case .circular: + self.style = .circular + case .none: + self.style = style + } + } From 113071e96dadbd87b4dfae9f1bedb7008817139b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Mon, 7 Nov 2022 22:15:09 +0000 Subject: [PATCH 43/63] Created new SheetController To centralize the management of the sheet views. --- GeoBus.xcodeproj/project.pbxproj | 4 + GeoBus/App/Components/ContentView.swift | 5 -- .../App/Components/Map/MapAnnotations.swift | 16 ++-- GeoBus/App/Components/NavBar.swift | 8 +- .../App/Components/PresentedSheetView.swift | 4 +- .../SelectRoute/FavoriteRoutes.swift | 4 +- .../SelectRoute/SelectRouteInput.swift | 4 +- .../Components/SelectRoute/SetOfRoutes.swift | 4 +- .../StopDetails/SearchStopInput.swift | 3 +- .../StopDetails/StopEstimations.swift | 4 +- .../Components/StopDetails/StopSearch.swift | 4 +- GeoBus/App/Controllers/Appstate.swift | 35 -------- GeoBus/App/Controllers/MapController.swift | 2 +- GeoBus/App/Controllers/SheetController.swift | 87 +++++++++++++++++++ GeoBus/App/GeoBusApp.swift | 8 +- GeoBus/App/Layout/SheetHeader.swift | 6 +- 16 files changed, 128 insertions(+), 70 deletions(-) create mode 100644 GeoBus/App/Controllers/SheetController.swift diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index b7f1ca3c..aea3ad42 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -68,6 +68,7 @@ CFDD014928D5114D0070FE4B /* SyncStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDD014828D5114D0070FE4B /* SyncStatus.swift */; }; CFDD014B28D535370070FE4B /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDD014A28D535370070FE4B /* Card.swift */; }; CFDD014D28D66D9B0070FE4B /* CloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDD014C28D66D9B0070FE4B /* CloseButton.swift */; }; + CFED5F8A2919B7820062045B /* SheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFED5F892919B7820062045B /* SheetController.swift */; }; CFEF85C228D34E4F00A29526 /* crowdin.yml in Resources */ = {isa = PBXBuildFile; fileRef = CFEF85C128D34E4F00A29526 /* crowdin.yml */; }; CFEF85C528D34E6300A29526 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = CFEF85C328D34E6300A29526 /* README.md */; }; CFEF85C628D34E6300A29526 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = CFEF85C428D34E6300A29526 /* LICENSE */; }; @@ -156,6 +157,7 @@ CFDD014828D5114D0070FE4B /* SyncStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatus.swift; sourceTree = ""; }; CFDD014A28D535370070FE4B /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; CFDD014C28D66D9B0070FE4B /* CloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseButton.swift; sourceTree = ""; }; + CFED5F892919B7820062045B /* SheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetController.swift; sourceTree = ""; }; CFEF85C128D34E4F00A29526 /* crowdin.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = crowdin.yml; sourceTree = ""; }; CFEF85C328D34E6300A29526 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; CFEF85C428D34E6300A29526 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; @@ -240,6 +242,7 @@ children = ( CF47994C28D3315E00B56D4B /* Appstate.swift */, CFFFAD8028F64E2000DFD5FD /* Analytics.swift */, + CFED5F892919B7820062045B /* SheetController.swift */, CF6C918128D3F1C6006C3F61 /* MapController.swift */, CF5094C028FB992D00EDD320 /* Networks */, ); @@ -541,6 +544,7 @@ CF181FE828CCB7D600248F72 /* ContentView.swift in Sources */, CF05F62028CD337200B4AD58 /* AppVersion.swift in Sources */, CF6C918628D3F8C8006C3F61 /* StopSearch.swift in Sources */, + CFED5F8A2919B7820062045B /* SheetController.swift in Sources */, CF18207228CCBD2300248F72 /* VariantPicker.swift in Sources */, CFAF0E7A28CE84F400DDAD5B /* MapAnnotations.swift in Sources */, CF548FF328D129B000668CB6 /* VehicleDetailsView.swift in Sources */, diff --git a/GeoBus/App/Components/ContentView.swift b/GeoBus/App/Components/ContentView.swift index f983bd08..904521e9 100644 --- a/GeoBus/App/Components/ContentView.swift +++ b/GeoBus/App/Components/ContentView.swift @@ -10,8 +10,6 @@ import SwiftUI struct ContentView: View { - @ObservedObject private var appstate = Appstate.shared - var body: some View { VStack(spacing: 0) { ZStack(alignment: .topTrailing) { @@ -28,9 +26,6 @@ struct ContentView: View { NavBar() .edgesIgnoringSafeArea(.vertical) } - .sheet(isPresented: $appstate.sheetIsPresented) { - PresentedSheetView() - } } } diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index c9614434..522efd4d 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -34,7 +34,7 @@ struct CarrisStopAnnotationView: View { public let stop: CarrisNetworkModel.Stop - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @@ -42,7 +42,7 @@ struct CarrisStopAnnotationView: View { Button(action: { TapticEngine.impact.feedback(.light) carrisNetworkController.select(stop: self.stop) - appstate.present(sheet: .carris_stopDetails) + sheetController.present(sheet: .carris_stopDetails) }) { StopIcon(isSelected: carrisNetworkController.activeStop?.id == self.stop.id) } @@ -56,7 +56,7 @@ struct CarrisConnectionAnnotationView: View { public let connection: CarrisNetworkModel.Connection - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @@ -68,7 +68,7 @@ struct CarrisConnectionAnnotationView: View { Button(action: { TapticEngine.impact.feedback(.light) carrisNetworkController.select(connection: self.connection) - appstate.present(sheet: .carris_connectionDetails) + sheetController.present(sheet: .carris_connectionDetails) }) { StopIcon( orderInRoute: self.connection.orderInRoute, @@ -89,7 +89,7 @@ struct CarrisVehicleAnnotationView: View { let vehicle: CarrisNetworkModel.Vehicle - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @@ -102,7 +102,7 @@ struct CarrisVehicleAnnotationView: View { Button(action: { TapticEngine.impact.feedback(.light) carrisNetworkController.select(vehicle: vehicle.id) - appstate.present(sheet: .carris_vehicleDetails) + sheetController.present(sheet: .carris_vehicleDetails) }) { ZStack(alignment: .init(horizontal: .leading, vertical: .center)) { switch (vehicle.kind) { @@ -128,7 +128,7 @@ struct CarrisMiniStopAnnotationView: View { public let stop: CarrisNetworkModel.Stop - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var mapController = MapController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @@ -137,7 +137,7 @@ struct CarrisMiniStopAnnotationView: View { Button(action: { TapticEngine.impact.feedback(.light) carrisNetworkController.select(stop: self.stop) - appstate.present(sheet: .carris_stopDetails) + sheetController.present(sheet: .carris_stopDetails) }) { Circle() .foregroundColor(.blue) diff --git a/GeoBus/App/Components/NavBar.swift b/GeoBus/App/Components/NavBar.swift index 047aa6c6..2850b4e8 100644 --- a/GeoBus/App/Components/NavBar.swift +++ b/GeoBus/App/Components/NavBar.swift @@ -10,7 +10,7 @@ import Combine struct NavBar: View { - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @State var showSelectRouteSheet: Bool = false @@ -21,7 +21,7 @@ struct NavBar: View { // Depending on the state, the button conveys different information. var routeSelector: some View { Button(action: { - appstate.present(sheet: .carris_RouteSelector) + sheetController.present(sheet: .carris_RouteSelector) }) { SelectRouteView() } @@ -33,9 +33,9 @@ struct NavBar: View { var routeDetails: some View { Button(action: { if (carrisNetworkController.activeRoute != nil) { - appstate.present(sheet: .carris_RouteDetails) + sheetController.present(sheet: .carris_RouteDetails) } else { - appstate.present(sheet: .carris_RouteSelector) + sheetController.present(sheet: .carris_RouteSelector) } }) { RouteDetailsView() diff --git a/GeoBus/App/Components/PresentedSheetView.swift b/GeoBus/App/Components/PresentedSheetView.swift index 18c08fd5..b9ec50fc 100644 --- a/GeoBus/App/Components/PresentedSheetView.swift +++ b/GeoBus/App/Components/PresentedSheetView.swift @@ -9,11 +9,11 @@ import SwiftUI struct PresentedSheetView: View { - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { - switch appstate.currentlyPresentedSheetView { + switch sheetController.currentlyPresentedSheetView { case .carris_RouteSelector: SelectRouteSheet() diff --git a/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift b/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift index 1d5a6a35..e84191df 100644 --- a/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift +++ b/GeoBus/App/Components/SelectRoute/FavoriteRoutes.swift @@ -12,7 +12,7 @@ struct FavoriteRoutes: View { @Environment(\.colorScheme) var colorScheme: ColorScheme - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @State var routes: [CarrisNetworkModel.Route] = [] @@ -37,7 +37,7 @@ struct FavoriteRoutes: View { Button(action: { _ = self.carrisNetworkController.select(route: route.number) Analytics.shared.capture(event: .Routes_Select_FromFavorites, properties: ["routeNumber": route.number]) - appstate.unpresent() + sheetController.dismiss() }){ RouteBadgeSquare(routeNumber: route.number) } diff --git a/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift b/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift index fd822ad7..9f62d754 100644 --- a/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift +++ b/GeoBus/App/Components/SelectRoute/SelectRouteInput.swift @@ -10,7 +10,7 @@ import SwiftUI struct SelectRouteInput: View { - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @State var showErrorLabel: Bool = false @@ -32,7 +32,7 @@ struct SelectRouteInput: View { let success = carrisNetworkController.select(route: self.routeNumber.uppercased()) if success { Analytics.shared.capture(event: .Routes_Select_FromTextInput, properties: ["routeNumber": self.routeNumber.uppercased()]) - appstate.unpresent() + sheetController.dismiss() } else { self.showErrorLabel = true } diff --git a/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift b/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift index e347d53e..e35c78c0 100644 --- a/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift +++ b/GeoBus/App/Components/SelectRoute/SetOfRoutes.swift @@ -10,7 +10,7 @@ import SwiftUI struct SetOfRoutes: View { - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared let title: Text @@ -35,7 +35,7 @@ struct SetOfRoutes: View { Button(action: { _ = self.carrisNetworkController.select(route: route.number) Analytics.shared.capture(event: .Routes_Select_FromList, properties: ["routeNumber": route.number]) - appstate.unpresent() + sheetController.dismiss() }){ RouteBadgeSquare(routeNumber: route.number) } diff --git a/GeoBus/App/Components/StopDetails/SearchStopInput.swift b/GeoBus/App/Components/StopDetails/SearchStopInput.swift index a388ce29..222ae879 100644 --- a/GeoBus/App/Components/StopDetails/SearchStopInput.swift +++ b/GeoBus/App/Components/StopDetails/SearchStopInput.swift @@ -11,6 +11,7 @@ import SwiftUI struct SearchStopInput: View { @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @FocusState private var stopIdInputIsFocused: Bool @@ -43,7 +44,7 @@ struct SearchStopInput: View { let success = self.carrisNetworkController.select(stop: Int(self.stopPublicId.uppercased()) ?? -1) if success { Analytics.shared.capture(event: .Stops_Select_FromTextInput, properties: ["stopPublicId": self.stopPublicId.uppercased()]) - appstate.present(sheet: .carris_stopDetails) + sheetController.present(sheet: .carris_stopDetails) } else { self.showErrorLabel = true } diff --git a/GeoBus/App/Components/StopDetails/StopEstimations.swift b/GeoBus/App/Components/StopDetails/StopEstimations.swift index e0c12884..e2b528a3 100644 --- a/GeoBus/App/Components/StopDetails/StopEstimations.swift +++ b/GeoBus/App/Components/StopDetails/StopEstimations.swift @@ -135,7 +135,7 @@ struct EstimationContainer: View { let estimation: CarrisNetworkModel.Estimation - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var mapController = MapController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared @@ -156,7 +156,7 @@ struct EstimationContainer: View { Button(action: { carrisNetworkController.select(vehicle: estimation.busNumber) // mapController.moveMap(to:) - appstate.present(sheet: .carris_vehicleDetails) + sheetController.present(sheet: .carris_vehicleDetails) }, label: { estimationLine }) diff --git a/GeoBus/App/Components/StopDetails/StopSearch.swift b/GeoBus/App/Components/StopDetails/StopSearch.swift index 39be3813..b84c9516 100644 --- a/GeoBus/App/Components/StopDetails/StopSearch.swift +++ b/GeoBus/App/Components/StopDetails/StopSearch.swift @@ -9,13 +9,13 @@ import SwiftUI struct StopSearch: View { - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared var body: some View { SquareButton(icon: "mail.and.text.magnifyingglass", size: 26) .onTapGesture() { TapticEngine.impact.feedback(.medium) - appstate.present(sheet: .carris_stopSelector) + sheetController.present(sheet: .carris_stopSelector) } } } diff --git a/GeoBus/App/Controllers/Appstate.swift b/GeoBus/App/Controllers/Appstate.swift index 1b573094..ae114789 100644 --- a/GeoBus/App/Controllers/Appstate.swift +++ b/GeoBus/App/Controllers/Appstate.swift @@ -45,38 +45,6 @@ final class Appstate: ObservableObject { - /* * */ - /* MARK: - SECTION 2: MODULES */ - /* These are the modules that publish state change events. This allows the UI to provide local */ - /* loading or error messages on the relevant functionality, increasing perception of stability. */ - - enum PresentableSheetView { - case carris_RouteSelector - case carris_RouteDetails - case carris_stopSelector - case carris_vehicleDetails - case carris_connectionDetails - case carris_stopDetails - } - - public func present(sheet: PresentableSheetView) { - if (sheetIsPresented) { - self.sheetIsPresented = false - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - self.currentlyPresentedSheetView = sheet - self.sheetIsPresented = true - } - } else { - self.currentlyPresentedSheetView = sheet - self.sheetIsPresented = true - } - } - - public func unpresent() { - self.sheetIsPresented = false - } - - /* * */ /* MARK: - SECTION 3: SHARED INSTANCE */ /* To allow the same instance of this class to be available accross the whole app, */ @@ -105,9 +73,6 @@ final class Appstate: ObservableObject { @Published var carris_vehicleDetails: State = .idle - @Published var sheetIsPresented: Bool = false - @Published var currentlyPresentedSheetView: PresentableSheetView? = nil - /* * */ diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index 0371bf4c..1a7b632d 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -10,7 +10,7 @@ import MapKit import SwiftUI @MainActor -class MapController: ObservableObject { +final class MapController: ObservableObject { /* * */ /* MARK: - SECTION 1: SETTINGS */ diff --git a/GeoBus/App/Controllers/SheetController.swift b/GeoBus/App/Controllers/SheetController.swift new file mode 100644 index 00000000..10e5f2af --- /dev/null +++ b/GeoBus/App/Controllers/SheetController.swift @@ -0,0 +1,87 @@ +// +// Appstate.swift +// GeoBus +// +// Created by João de Vasconcelos on 11/09/2022. +// + +import Foundation +import SwiftUI + +/* * */ +/* MARK: - APPSTATE */ +/* Appstate is a 'global' class that all controller modules use to set the current state of the app. */ +/* This state is immediatly reflected on the UI to inform the user of any loading or error events. */ +/* Using Appstate increases consistency in UI code and prevents direct access to controllers. */ + + +final class SheetController: ObservableObject { + + /* * */ + /* MARK: - 1: PRESENTABLE SHEET VIEWS */ + /* These are the available views that can be presented inside the sheet. */ + + enum PresentableSheetView { + case carris_RouteSelector + case carris_RouteDetails + case carris_stopSelector + case carris_vehicleDetails + case carris_connectionDetails + case carris_stopDetails + } + + + + /* * */ + /* MARK: - SECTION 2: PUBLISHED VARIABLES */ + /* Here are all the @Published variables refering to the above modules that can be consumed */ + /* by the UI. It is important to keep the names of this variables short, but descriptive, */ + /* to avoid clutter on the interface code. */ + + @Published var sheetIsPresented: Bool = false + @Published var currentlyPresentedSheetView: PresentableSheetView? = nil + + + + /* * */ + /* MARK: - 3: PRESENT SHEET */ + /* Call this function to present the view. If a sheet is already visible, */ + /* then dismiss it and after a delay present the desired sheet. */ + + public func present(sheet: PresentableSheetView) { + if (sheetIsPresented) { + self.sheetIsPresented = false + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + self.currentlyPresentedSheetView = sheet + self.sheetIsPresented = true + } + } else { + self.currentlyPresentedSheetView = sheet + self.sheetIsPresented = true + } + } + + + + /* * */ + /* MARK: - 4: DISMISS SHEET */ + /* Call this function to dismiss the sheet. */ + + public func dismiss() { + self.sheetIsPresented = false + } + + + + /* * */ + /* MARK: - SECTION 3: SHARED INSTANCE */ + /* To allow the same instance of this class to be available accross the whole app, */ + /* we create a Singleton. More info here: https://www.hackingwithswift.com/example-code/language/what-is-a-singleton */ + /* Adding a private initializer is important because it stops other code from creating a new class instance. */ + + static let shared = SheetController() + + private init() { } + + +} diff --git a/GeoBus/App/GeoBusApp.swift b/GeoBus/App/GeoBusApp.swift index 9a6a693c..7a75c256 100644 --- a/GeoBus/App/GeoBusApp.swift +++ b/GeoBus/App/GeoBusApp.swift @@ -5,10 +5,11 @@ import SwiftUI @main struct GeoBusApp: App { + @ObservedObject private var sheetController = SheetController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared // @ObservedObject private var tcbNetworkController = TCBNetworkController.shared - private let updateIntervalTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() + private let appRefreshTimer = Timer.publish(every: 20 /* seconds */, on: .main, in: .common).autoconnect() var body: some Scene { WindowGroup { @@ -16,10 +17,13 @@ struct GeoBusApp: App { .onAppear(perform: { Analytics.shared.capture(event: .App_Session_Start) }) - .onReceive(updateIntervalTimer) { event in + .onReceive(appRefreshTimer) { event in carrisNetworkController.refresh() Analytics.shared.capture(event: .App_Session_Ping) } + .sheet(isPresented: $sheetController.sheetIsPresented) { + PresentedSheetView() + } } } diff --git a/GeoBus/App/Layout/SheetHeader.swift b/GeoBus/App/Layout/SheetHeader.swift index fc93e762..a4c44db8 100644 --- a/GeoBus/App/Layout/SheetHeader.swift +++ b/GeoBus/App/Layout/SheetHeader.swift @@ -10,7 +10,7 @@ import SwiftUI struct SheetHeader: View { - @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var sheetController = SheetController.shared let title: Text @@ -18,7 +18,9 @@ struct SheetHeader: View { VStack { HStack { Spacer() - Button(action: { appstate.unpresent() }) { + Button(action: { + sheetController.dismiss() + }) { Text("Close") .fontWeight(.bold) } From 57d092190b6661cf4de587db23a2a6899d89ead1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Mon, 7 Nov 2022 22:22:10 +0000 Subject: [PATCH 44/63] Fixed comments on controllers --- GeoBus/App/Controllers/Analytics.swift | 9 ++------- GeoBus/App/Controllers/Appstate.swift | 9 +-------- GeoBus/App/Controllers/MapController.swift | 14 +++++++------- .../Networks/Carris/CarrisAPIModel.swift | 2 ++ .../Carris/CarrisCommunityAPIModel.swift | 2 ++ .../Carris/CarrisNetworkController.swift | 1 + .../Networks/Carris/CarrisNetworkModel.swift | 8 +------- GeoBus/App/Controllers/SheetController.swift | 16 ++++------------ GeoBus/App/GeoBusApp.swift | 2 ++ 9 files changed, 22 insertions(+), 41 deletions(-) diff --git a/GeoBus/App/Controllers/Analytics.swift b/GeoBus/App/Controllers/Analytics.swift index 2ca44d0e..6d600cc1 100644 --- a/GeoBus/App/Controllers/Analytics.swift +++ b/GeoBus/App/Controllers/Analytics.swift @@ -1,13 +1,7 @@ -// -// Analytics.swift -// GeoBus -// -// Created by João de Vasconcelos on 11/09/2022. -// - import Foundation import PostHog + /* * */ /* MARK: - ANALYTICS */ /* Wrapper to Posthog. For anyone reading this, this framework was selected */ @@ -16,6 +10,7 @@ import PostHog /* The point is to only record general events, without identifiers, to understand */ /* if calls to the API are working, which routes are selected and how many people are using it. */ + final class Analytics { /* * */ diff --git a/GeoBus/App/Controllers/Appstate.swift b/GeoBus/App/Controllers/Appstate.swift index ae114789..0a9dd9e5 100644 --- a/GeoBus/App/Controllers/Appstate.swift +++ b/GeoBus/App/Controllers/Appstate.swift @@ -1,12 +1,5 @@ -// -// Appstate.swift -// GeoBus -// -// Created by João de Vasconcelos on 11/09/2022. -// - import Foundation -import SwiftUI + /* * */ /* MARK: - APPSTATE */ diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index 1a7b632d..402643f4 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -1,14 +1,14 @@ -// -// MapController.swift -// GeoBus -// -// Created by João de Vasconcelos on 16/09/2022. -// - import Foundation import MapKit import SwiftUI + +/* * */ +/* MARK: - MAP CONTROLLER */ +/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi rutrum lectus */ +/* non interdum imperdiet. In hendrerit ligula velit, ac porta augue volutpat id. */ + + @MainActor final class MapController: ObservableObject { diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift index d6351023..097b9ec1 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift @@ -1,10 +1,12 @@ import Foundation + /* * */ /* MARK: - CARRIS API DATA MODEL */ /* Data model as provided by Carris API. */ /* Schema is available at https://joaodcp.github.io/Carris-API */ + struct CarrisAPIModel { struct RoutesList: Decodable { diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift index 361f40dc..16e767fe 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisCommunityAPIModel.swift @@ -1,10 +1,12 @@ import Foundation + /* * */ /* MARK: - CARRIS COMMUNITY API DATA MODEL */ /* Data model as provided by Community API for Carris network. */ /* Schema is available at https://github.com/ricardojorgerm/carril */ + struct CarrisCommunityAPIModel { struct Vehicle: Decodable { diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index f10a2d7a..1e88b61e 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -8,6 +8,7 @@ import Combine /* allows for code reuse, less plumbing passing objects from one class to another and less */ /* clutter overall. If the data is provided by Carris, it should be controlled by this class. */ + @MainActor class CarrisNetworkController: ObservableObject { diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift index 31027272..87f4d0c2 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift @@ -1,10 +1,3 @@ -// -// Routes.swift -// GeoBus -// -// Created by João de Vasconcelos on 09/09/2022. -// Copyright © 2022 João de Vasconcelos. All rights reserved. -// import Foundation import CoreLocation @@ -16,6 +9,7 @@ import CoreLocation /* For this app, the goal is to simplify and build upon this network model */ /* to prevent duplicated data and increase flexibility on updates to the views. */ + struct CarrisNetworkModel { enum Kind: Codable, Equatable { diff --git a/GeoBus/App/Controllers/SheetController.swift b/GeoBus/App/Controllers/SheetController.swift index 10e5f2af..c4dc9da6 100644 --- a/GeoBus/App/Controllers/SheetController.swift +++ b/GeoBus/App/Controllers/SheetController.swift @@ -1,18 +1,10 @@ -// -// Appstate.swift -// GeoBus -// -// Created by João de Vasconcelos on 11/09/2022. -// - import Foundation -import SwiftUI + /* * */ -/* MARK: - APPSTATE */ -/* Appstate is a 'global' class that all controller modules use to set the current state of the app. */ -/* This state is immediatly reflected on the UI to inform the user of any loading or error events. */ -/* Using Appstate increases consistency in UI code and prevents direct access to controllers. */ +/* MARK: - SHEET CONTROLLER */ +/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi rutrum lectus */ +/* non interdum imperdiet. In hendrerit ligula velit, ac porta augue volutpat id. */ final class SheetController: ObservableObject { diff --git a/GeoBus/App/GeoBusApp.swift b/GeoBus/App/GeoBusApp.swift index 7a75c256..b67cfce2 100644 --- a/GeoBus/App/GeoBusApp.swift +++ b/GeoBus/App/GeoBusApp.swift @@ -1,7 +1,9 @@ import SwiftUI + /* MARK: - GEOBUS */ + @main struct GeoBusApp: App { From 1af97637dff0265373106df7ddcd3d4c74079576 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 00:00:46 +0000 Subject: [PATCH 45/63] Create Debounce.swift Will be deleted later, but just for historic purposes. This might come handy later. --- GeoBus/App/Extensions/Debounce.swift | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 GeoBus/App/Extensions/Debounce.swift diff --git a/GeoBus/App/Extensions/Debounce.swift b/GeoBus/App/Extensions/Debounce.swift new file mode 100644 index 00000000..225c2315 --- /dev/null +++ b/GeoBus/App/Extensions/Debounce.swift @@ -0,0 +1,20 @@ +// +// Debounce.swift +// GeoBus +// +// Created by @quickthyme (https://stackoverflow.com/a/59296478) +// + +import Dispatch + +class Debounce { + + private init() {} + + static func input(_ input: T, comparedAgainst current: @escaping @autoclosure () -> (T), perform: @escaping (T) -> ()) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + if input == current() { perform(input) } + } + } + +} From 5dd37580b62fc6336fa98b87a718a30c23c311fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 00:01:17 +0000 Subject: [PATCH 46/63] Standardized appearance under style enum --- GeoBus/App/Layout/StopIcon.swift | 62 +++++++++++++++++++------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/GeoBus/App/Layout/StopIcon.swift b/GeoBus/App/Layout/StopIcon.swift index 59d7cdaf..4749f63c 100644 --- a/GeoBus/App/Layout/StopIcon.swift +++ b/GeoBus/App/Layout/StopIcon.swift @@ -9,8 +9,8 @@ import SwiftUI struct StopIcon: View { - public let orderInRoute: Int? public let style: Style + public let orderInRoute: Int? public let isSelected: Bool init(orderInRoute: Int? = nil, direction: CarrisNetworkModel.Direction? = nil, style: Style = .standard, isSelected: Bool = false) { @@ -31,8 +31,10 @@ struct StopIcon: View { } + enum Style { case standard + case mini case circular case ascending case descending @@ -41,32 +43,38 @@ struct StopIcon: View { } - // Properties: - // The defaults for the icon - private let size: CGFloat = 25 - private let multiplier: Double = 1.5 - private var viewSize: CGFloat { - if (self.isSelected) { - return self.size * self.multiplier - } else { - return self.size + switch style { + case .standard, .ascending, .descending, .circular, .muted: + return 25 + case .mini: + return 10 + case .selected: + return 25 * 1.5 } } + + private var borderWidth: CGFloat { return self.viewSize - self.viewSize / 5 } + + private var textSize: CGFloat { return self.viewSize / 2 } + + private var borderColor: Color { switch style { case .standard: return Color("StopCircularBorder") + case .mini: + return Color("StopCircularBorder") case .ascending: return Color("StopAscendingBorder") case .descending: @@ -80,10 +88,14 @@ struct StopIcon: View { } } + + private var backgroundColor: Color { switch style { case .standard: return Color("StopCircularBackground") + case .mini: + return Color("StopCircularBackground") case .ascending: return Color("StopAscendingBackground") case .descending: @@ -97,10 +109,14 @@ struct StopIcon: View { } } + + private var textColor: Color { switch style { case .standard: return Color("StopCircularText") + case .mini: + return Color("StopCircularText") case .ascending: return Color("StopAscendingText") case .descending: @@ -115,28 +131,26 @@ struct StopIcon: View { } + var body: some View { ZStack { Circle() .foregroundColor(self.borderColor) .frame(width: self.viewSize, height: self.viewSize) - .animation(.default, value: self.borderColor) - .animation(.default, value: self.viewSize) Circle() .foregroundColor(self.backgroundColor) .frame(width: self.borderWidth, height: self.borderWidth) - .animation(.default, value: self.backgroundColor) - .animation(.default, value: self.borderWidth) - if (self.orderInRoute != nil) { - Text(String(self.orderInRoute!)) - .font(.system(size: self.textSize, weight: .bold)) - .foregroundColor(textColor) - .animation(.default, value: self.textSize) - } else { - Image(systemName: "mappin") - .font(.system(size: self.textSize, weight: .bold)) - .foregroundColor(textColor) - .animation(.default, value: self.textSize) + switch style { + case .standard, .selected: + Image(systemName: "mappin") + .font(.system(size: self.textSize, weight: .bold)) + .foregroundColor(textColor) + case .ascending, .descending, .circular, .muted: + Text(String(self.orderInRoute!)) + .font(.system(size: self.textSize, weight: .bold)) + .foregroundColor(textColor) + case .mini: + EmptyView() } } } From fd2b0da02876196bdee6fe4a5defdefb3f6e7cf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 00:02:09 +0000 Subject: [PATCH 47/63] Let's face it, the app only has carris now --- GeoBus/App/Components/NavBar.swift | 6 +++--- .../App/Components/PresentedSheetView.swift | 20 +++++++++---------- .../StopDetails/SearchStopInput.swift | 2 +- .../StopDetails/StopEstimations.swift | 2 +- .../Components/StopDetails/StopSearch.swift | 2 +- GeoBus/App/Controllers/SheetController.swift | 12 +++++------ 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/GeoBus/App/Components/NavBar.swift b/GeoBus/App/Components/NavBar.swift index 2850b4e8..406e802c 100644 --- a/GeoBus/App/Components/NavBar.swift +++ b/GeoBus/App/Components/NavBar.swift @@ -21,7 +21,7 @@ struct NavBar: View { // Depending on the state, the button conveys different information. var routeSelector: some View { Button(action: { - sheetController.present(sheet: .carris_RouteSelector) + sheetController.present(sheet: .RouteSelector) }) { SelectRouteView() } @@ -33,9 +33,9 @@ struct NavBar: View { var routeDetails: some View { Button(action: { if (carrisNetworkController.activeRoute != nil) { - sheetController.present(sheet: .carris_RouteDetails) + sheetController.present(sheet: .RouteDetails) } else { - sheetController.present(sheet: .carris_RouteSelector) + sheetController.present(sheet: .RouteSelector) } }) { RouteDetailsView() diff --git a/GeoBus/App/Components/PresentedSheetView.swift b/GeoBus/App/Components/PresentedSheetView.swift index b9ec50fc..b9f0fb4f 100644 --- a/GeoBus/App/Components/PresentedSheetView.swift +++ b/GeoBus/App/Components/PresentedSheetView.swift @@ -15,30 +15,30 @@ struct PresentedSheetView: View { var body: some View { switch sheetController.currentlyPresentedSheetView { - case .carris_RouteSelector: + case .RouteSelector: SelectRouteSheet() .presentationDetents([.large]) .presentationDragIndicator(.hidden) - case .carris_RouteDetails: + case .RouteDetails: RouteDetailsSheet() .presentationDetents([.large]) .presentationDragIndicator(.hidden) - case .carris_stopSelector: + case .StopSelector: StopSearchView() .presentationDetents([.medium]) .presentationDragIndicator(.hidden) - case .carris_vehicleDetails: - CarrisVehicleSheetView() + case .StopDetails: + StopSheetView() .presentationDetents([.medium, .large]) .presentationDragIndicator(.hidden) .onDisappear() { - carrisNetworkController.deselect([.vehicle]) + carrisNetworkController.deselect([.stop]) } - case .carris_connectionDetails: + case .ConnectionDetails: ConnectionSheetView() .presentationDetents([.medium, .large]) .presentationDragIndicator(.hidden) @@ -46,12 +46,12 @@ struct PresentedSheetView: View { carrisNetworkController.deselect([.connection]) } - case .carris_stopDetails: - StopSheetView() + case .VehicleDetails: + CarrisVehicleSheetView() .presentationDetents([.medium, .large]) .presentationDragIndicator(.hidden) .onDisappear() { - carrisNetworkController.deselect([.stop]) + carrisNetworkController.deselect([.vehicle]) } case .none: diff --git a/GeoBus/App/Components/StopDetails/SearchStopInput.swift b/GeoBus/App/Components/StopDetails/SearchStopInput.swift index 222ae879..f83a53d5 100644 --- a/GeoBus/App/Components/StopDetails/SearchStopInput.swift +++ b/GeoBus/App/Components/StopDetails/SearchStopInput.swift @@ -44,7 +44,7 @@ struct SearchStopInput: View { let success = self.carrisNetworkController.select(stop: Int(self.stopPublicId.uppercased()) ?? -1) if success { Analytics.shared.capture(event: .Stops_Select_FromTextInput, properties: ["stopPublicId": self.stopPublicId.uppercased()]) - sheetController.present(sheet: .carris_stopDetails) + sheetController.present(sheet: .StopDetails) } else { self.showErrorLabel = true } diff --git a/GeoBus/App/Components/StopDetails/StopEstimations.swift b/GeoBus/App/Components/StopDetails/StopEstimations.swift index e2b528a3..3157be5a 100644 --- a/GeoBus/App/Components/StopDetails/StopEstimations.swift +++ b/GeoBus/App/Components/StopDetails/StopEstimations.swift @@ -156,7 +156,7 @@ struct EstimationContainer: View { Button(action: { carrisNetworkController.select(vehicle: estimation.busNumber) // mapController.moveMap(to:) - sheetController.present(sheet: .carris_vehicleDetails) + sheetController.present(sheet: .VehicleDetails) }, label: { estimationLine }) diff --git a/GeoBus/App/Components/StopDetails/StopSearch.swift b/GeoBus/App/Components/StopDetails/StopSearch.swift index b84c9516..9ca5cf31 100644 --- a/GeoBus/App/Components/StopDetails/StopSearch.swift +++ b/GeoBus/App/Components/StopDetails/StopSearch.swift @@ -15,7 +15,7 @@ struct StopSearch: View { SquareButton(icon: "mail.and.text.magnifyingglass", size: 26) .onTapGesture() { TapticEngine.impact.feedback(.medium) - sheetController.present(sheet: .carris_stopSelector) + sheetController.present(sheet: .StopSelector) } } } diff --git a/GeoBus/App/Controllers/SheetController.swift b/GeoBus/App/Controllers/SheetController.swift index c4dc9da6..a8b04afc 100644 --- a/GeoBus/App/Controllers/SheetController.swift +++ b/GeoBus/App/Controllers/SheetController.swift @@ -14,12 +14,12 @@ final class SheetController: ObservableObject { /* These are the available views that can be presented inside the sheet. */ enum PresentableSheetView { - case carris_RouteSelector - case carris_RouteDetails - case carris_stopSelector - case carris_vehicleDetails - case carris_connectionDetails - case carris_stopDetails + case RouteSelector + case RouteDetails + case StopSelector + case StopDetails + case ConnectionDetails + case VehicleDetails } From 969fe36b80cdc44c1422c7f2b4b19ce727b004db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 00:02:17 +0000 Subject: [PATCH 48/63] . --- GeoBus.xcodeproj/project.pbxproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index aea3ad42..6696ae4c 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ CFDD014B28D535370070FE4B /* Card.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDD014A28D535370070FE4B /* Card.swift */; }; CFDD014D28D66D9B0070FE4B /* CloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDD014C28D66D9B0070FE4B /* CloseButton.swift */; }; CFED5F8A2919B7820062045B /* SheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFED5F892919B7820062045B /* SheetController.swift */; }; + CFED5F8C2919CBBD0062045B /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFED5F8B2919CBBD0062045B /* Debounce.swift */; }; CFEF85C228D34E4F00A29526 /* crowdin.yml in Resources */ = {isa = PBXBuildFile; fileRef = CFEF85C128D34E4F00A29526 /* crowdin.yml */; }; CFEF85C528D34E6300A29526 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = CFEF85C328D34E6300A29526 /* README.md */; }; CFEF85C628D34E6300A29526 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = CFEF85C428D34E6300A29526 /* LICENSE */; }; @@ -158,6 +159,7 @@ CFDD014A28D535370070FE4B /* Card.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Card.swift; sourceTree = ""; }; CFDD014C28D66D9B0070FE4B /* CloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseButton.swift; sourceTree = ""; }; CFED5F892919B7820062045B /* SheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetController.swift; sourceTree = ""; }; + CFED5F8B2919CBBD0062045B /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; CFEF85C128D34E4F00A29526 /* crowdin.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = crowdin.yml; sourceTree = ""; }; CFEF85C328D34E6300A29526 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; CFEF85C428D34E6300A29526 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; @@ -322,6 +324,7 @@ CFFFAD8228F6754400DFD5FD /* Helpers.swift */, CFDC15EE28D292FB00A4BE49 /* ViewSize.swift */, CF5094C428FC279A00EDD320 /* Array.swift */, + CFED5F8B2919CBBD0062045B /* Debounce.swift */, ); path = Extensions; sourceTree = ""; @@ -566,6 +569,7 @@ CFDD014B28D535370070FE4B /* Card.swift in Sources */, CF18202928CCBBDC00248F72 /* TapticEngine.swift in Sources */, CF548FF628D14BA400668CB6 /* VehicleIdentifier.swift in Sources */, + CFED5F8C2919CBBD0062045B /* Debounce.swift in Sources */, CF5094CB28FC50E900EDD320 /* CarrisNetworkModel.swift in Sources */, CF18208728CCBD3A00248F72 /* SelectRouteInput.swift in Sources */, CF0C256E290324A600B03052 /* CarrisCommunityAPI.swift in Sources */, From 03ab5305a7f517e0aee249ff1b53a473e06cb798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 00:02:46 +0000 Subject: [PATCH 49/63] Stop Annotations are now always visible depending on the map region --- .../App/Components/Map/MapAnnotations.swift | 74 ++++++------------- 1 file changed, 22 insertions(+), 52 deletions(-) diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index 522efd4d..1eb23b4a 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -21,37 +21,13 @@ struct GenericMapAnnotation: Identifiable { case carris_stop(CarrisNetworkModel.Stop) case carris_connection(CarrisNetworkModel.Connection) case carris_vehicle(CarrisNetworkModel.Vehicle) - case ministop(CarrisNetworkModel.Stop) +// case ministop(CarrisNetworkModel.Stop) } } - - -struct CarrisStopAnnotationView: View { - - public let stop: CarrisNetworkModel.Stop - - @ObservedObject private var sheetController = SheetController.shared - @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared - - - var body: some View { - Button(action: { - TapticEngine.impact.feedback(.light) - carrisNetworkController.select(stop: self.stop) - sheetController.present(sheet: .carris_stopDetails) - }) { - StopIcon(isSelected: carrisNetworkController.activeStop?.id == self.stop.id) - } - .frame(width: 40, height: 40, alignment: .center) - } - -} - - struct CarrisConnectionAnnotationView: View { public let connection: CarrisNetworkModel.Connection @@ -68,7 +44,7 @@ struct CarrisConnectionAnnotationView: View { Button(action: { TapticEngine.impact.feedback(.light) carrisNetworkController.select(connection: self.connection) - sheetController.present(sheet: .carris_connectionDetails) + sheetController.present(sheet: .ConnectionDetails) }) { StopIcon( orderInRoute: self.connection.orderInRoute, @@ -102,7 +78,7 @@ struct CarrisVehicleAnnotationView: View { Button(action: { TapticEngine.impact.feedback(.light) carrisNetworkController.select(vehicle: vehicle.id) - sheetController.present(sheet: .carris_vehicleDetails) + sheetController.present(sheet: .VehicleDetails) }) { ZStack(alignment: .init(horizontal: .leading, vertical: .center)) { switch (vehicle.kind) { @@ -124,37 +100,31 @@ struct CarrisVehicleAnnotationView: View { -struct CarrisMiniStopAnnotationView: View { +struct StopAnnotationView: View { public let stop: CarrisNetworkModel.Stop - @ObservedObject private var sheetController = SheetController.shared - @ObservedObject private var mapController = MapController.shared - @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + @StateObject private var sheetController = SheetController.shared + @StateObject private var mapController = MapController.shared + @StateObject private var carrisNetworkController = CarrisNetworkController.shared - - var icon: some View { - Button(action: { + var body: some View { + VStack { + if (carrisNetworkController.activeStop?.id == self.stop.id) { + StopIcon(style: .selected) + } else if (mapController.region.span.latitudeDelta < 0.0025 || mapController.region.span.longitudeDelta < 0.0025) { + StopIcon(style: .standard) + } else { + StopIcon(style: .mini) + } + } + .onTapGesture { TapticEngine.impact.feedback(.light) carrisNetworkController.select(stop: self.stop) - sheetController.present(sheet: .carris_stopDetails) - }) { - Circle() - .foregroundColor(.blue) - .background(Color(.blue)) - } - .frame(width: 15, height: 15, alignment: .center) - } - - - var body: some View { - if (mapController.region.span.latitudeDelta < 0.0025 || mapController.region.span.longitudeDelta < 0.0025) { - icon - } else { - Circle() - .foregroundColor(.blue) - .background(Color(.blue)) - .frame(width: 5, height: 5) + sheetController.present(sheet: .StopDetails) + withAnimation(.easeIn(duration: 0.5)) { + mapController.centerMapOnCoordinates(lat: self.stop.lat, lng: self.stop.lng) + } } } From 339a1b6feec7653d539f22bca62476e572a14111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 00:03:00 +0000 Subject: [PATCH 50/63] Use of combine debouncer --- GeoBus/App/Components/Map/MapView.swift | 35 +++++-------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/GeoBus/App/Components/Map/MapView.swift b/GeoBus/App/Components/Map/MapView.swift index 5a48cd6b..e1aa3967 100644 --- a/GeoBus/App/Components/Map/MapView.swift +++ b/GeoBus/App/Components/Map/MapView.swift @@ -12,8 +12,8 @@ import MapKit struct MapView: View { - @ObservedObject private var mapController = MapController.shared - @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + @StateObject private var mapController = MapController.shared + @StateObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { @@ -27,46 +27,25 @@ struct MapView: View { MapAnnotation(coordinate: annotation.location) { switch (annotation.item) { case .carris_stop(let item): - CarrisStopAnnotationView(stop: item) + StopAnnotationView(stop: item) case .carris_connection(let item): CarrisConnectionAnnotationView(connection: item) case .carris_vehicle(let item): CarrisVehicleAnnotationView(vehicle: item) - case .ministop(let item): - CarrisMiniStopAnnotationView(stop: item) } } } - .onChange(of: carrisNetworkController.activeStop) { newStop in - if (newStop != nil) { - self.mapController.updateAnnotations(with: newStop!) - } - } - .onChange(of: carrisNetworkController.activeVariant) { newVariant in + .onReceive(carrisNetworkController.$activeVariant) { newVariant in if (newVariant != nil) { self.mapController.updateAnnotations(with: newVariant!) } } -// .onChange(of: carrisNetworkController.activeVehicle) { newVehicle in -// if (newVehicle != nil) { -// self.mapController.updateAnnotations(with: newVehicle!) -// } -// } - .onChange(of: carrisNetworkController.activeVehicles) { newVehiclesList in + .onReceive(carrisNetworkController.$activeVehicles) { newVehiclesList in self.mapController.updateAnnotations(with: newVehiclesList) } -// .onAppear() { -// self.mapController.updateAnnotations(ministop: carrisNetworkController.allStops) -// } -// .onChange(of: carrisNetworkController.allStops) { allStops in -// self.mapController.updateAnnotations(ministop: allStops) -// } - .onChange(of: [mapController.region.center.latitude, mapController.region.center.longitude]) { _ in - self.mapController.updateAnnotations(newRegion: nil) - } - .onChange(of: [mapController.region.span.latitudeDelta, mapController.region.span.longitudeDelta]) { _ in - self.mapController.updateAnnotations(newRegion: nil) + .onReceive(mapController.$region.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)) { newRegion in + self.mapController.updateAnnotations(for: newRegion, with: carrisNetworkController.allStops) } } From 8be938b46e5549bda758fdaa535816fab8b663ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 00:04:14 +0000 Subject: [PATCH 51/63] Conform MKCoordinateRegion to Equatable Remove SwiftUI dependency. Animations should be in views, not in controllers. --- GeoBus/App/Controllers/MapController.swift | 105 ++++++++++++--------- 1 file changed, 63 insertions(+), 42 deletions(-) diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index 402643f4..11ed1d7d 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -1,6 +1,5 @@ import Foundation import MapKit -import SwiftUI /* * */ @@ -33,7 +32,6 @@ final class MapController: ObservableObject { @Published var showLocationNotAllowedAlert: Bool = false @Published var visibleAnnotations: [GenericMapAnnotation] = [] - @Published var allStopAnnotations: [GenericMapAnnotation] = [] /* * */ @@ -65,11 +63,20 @@ final class MapController: ObservableObject { /* Helper function to animate the Map changing region. */ func moveMap(to newRegion: MKCoordinateRegion) { - DispatchQueue.main.async { - withAnimation(.easeIn(duration: 0.5)) { - self.region = newRegion - } - } + self.region = newRegion + } + + + + /* * */ + /* MARK: - SECTION 6: CENTER MAP ON COORDINATES */ + /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ + + func centerMapOnCoordinates(lat: Double, lng: Double, andZoom: Bool = false) { + self.region = MKCoordinateRegion( + center: CLLocationCoordinate2D(latitude: lat, longitude: lng), + span: self.region.span + ) } @@ -140,28 +147,28 @@ final class MapController: ObservableObject { /* MARK: - SECTION 8: UPDATE ANNOTATIONS WITH SELECTED CARRIS STOP */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ - func updateAnnotations(with activeStop: CarrisNetworkModel.Stop) { - - visibleAnnotations.removeAll(where: { - switch $0.item { - case .carris_stop(_), .carris_connection(_), .carris_vehicle(_), .ministop(_): - return true - } - }) - - var tempNewAnnotations: [GenericMapAnnotation] = [] - - tempNewAnnotations.append( - GenericMapAnnotation( - id: UUID(), - location: CLLocationCoordinate2D(latitude: activeStop.lat, longitude: activeStop.lng), - item: .carris_stop(activeStop) - ) - ) - - self.addAnnotations(tempNewAnnotations, zoom: true) - - } +// func updateAnnotations(with activeStop: CarrisNetworkModel.Stop) { +// +// visibleAnnotations.removeAll(where: { +// switch $0.item { +// case .carris_stop(_), .carris_connection(_), .carris_vehicle(_: +// return true +// } +// }) +// +// var tempNewAnnotations: [GenericMapAnnotation] = [] +// +// tempNewAnnotations.append( +// GenericMapAnnotation( +// id: UUID(), +// location: CLLocationCoordinate2D(latitude: activeStop.lat, longitude: activeStop.lng), +// item: .carris_stop(activeStop) +// ) +// ) +// +// self.addAnnotations(tempNewAnnotations, zoom: true) +// +// } @@ -175,7 +182,7 @@ final class MapController: ObservableObject { switch $0.item { case .carris_connection(_), .carris_stop(_): return true - case .carris_vehicle(_), .ministop(_): + case .carris_vehicle(_): return false } }) @@ -236,7 +243,7 @@ final class MapController: ObservableObject { switch $0.item { case .carris_vehicle(_), .carris_stop(_): return true - case .carris_connection(_), .ministop(_): + case .carris_connection(_): return false } }) @@ -279,7 +286,7 @@ final class MapController: ObservableObject { } else { return false } - case .carris_connection(_), .carris_stop(_), .ministop(_): + case .carris_connection(_), .carris_stop(_): return false } }) { @@ -350,9 +357,9 @@ final class MapController: ObservableObject { visibleAnnotations.removeAll(where: { switch $0.item { - case .ministop(_): + case .carris_stop(_): return true - case .carris_vehicle(_), .carris_stop(_), .carris_connection(_): + case .carris_vehicle(_), .carris_connection(_): return false } }) @@ -364,7 +371,7 @@ final class MapController: ObservableObject { GenericMapAnnotation( id: UUID(), location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), - item: .ministop(stop) + item: .carris_stop(stop) ) ) } @@ -373,22 +380,22 @@ final class MapController: ObservableObject { } - @ObservedObject var carrisNetworkController = CarrisNetworkController.shared - /* * */ /* MARK: - SECTION 8: UPDATE ANNOTATIONS WITH SELECTED CARRIS STOP */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ - func updateAnnotations(newRegion: MKCoordinateRegion?) { + private var allStopAnnotations: [GenericMapAnnotation] = [] + + func updateAnnotations(for newMapRegion: MKCoordinateRegion?, with allStops: [CarrisNetworkModel.Stop]) { if (allStopAnnotations.isEmpty) { - for stop in carrisNetworkController.allStops { + for stop in allStops { allStopAnnotations.append( GenericMapAnnotation( id: UUID(), location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), - item: .ministop(stop) + item: .carris_stop(stop) ) ) } @@ -429,9 +436,9 @@ final class MapController: ObservableObject { } else { visibleAnnotations.removeAll(where: { switch $0.item { - case .ministop(_): + case .carris_stop(_): return true; - case .carris_connection(_), .carris_stop(_), .carris_vehicle(_): + case .carris_connection(_), .carris_vehicle(_): return false; } }) @@ -441,3 +448,17 @@ final class MapController: ObservableObject { } + + + + + +extension MKCoordinateRegion: Equatable { + public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool { + if (lhs.center.latitude != rhs.center.latitude) { return false } + if (lhs.center.longitude != rhs.center.longitude) { return false } + if (lhs.span.latitudeDelta != rhs.span.latitudeDelta) { return false } + if (lhs.span.longitudeDelta != rhs.span.longitudeDelta) { return false } + return true + } +} From 3ae28033bcd1ce1bc2666758162bd6926e7cb63b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 01:39:45 +0000 Subject: [PATCH 52/63] Tweaks to StopIcon --- GeoBus/App/Layout/StopIcon.swift | 16 ++++---- .../Colors/Stops/Mini/Contents.json | 6 +++ .../StopMiniBackground.colorset/Contents.json | 38 +++++++++++++++++++ .../StopMiniBorder.colorset/Contents.json | 38 +++++++++++++++++++ .../Mini/StopMiniText.colorset/Contents.json | 38 +++++++++++++++++++ .../Colors/Stops/Standard/Contents.json | 6 +++ .../Contents.json | 38 +++++++++++++++++++ .../StopStandardBorder.colorset/Contents.json | 38 +++++++++++++++++++ .../StopStandardText.colorset/Contents.json | 38 +++++++++++++++++++ 9 files changed, 247 insertions(+), 9 deletions(-) create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Mini/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniBackground.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniBorder.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniText.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Standard/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardBackground.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardBorder.colorset/Contents.json create mode 100644 GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardText.colorset/Contents.json diff --git a/GeoBus/App/Layout/StopIcon.swift b/GeoBus/App/Layout/StopIcon.swift index 4749f63c..a3f867ef 100644 --- a/GeoBus/App/Layout/StopIcon.swift +++ b/GeoBus/App/Layout/StopIcon.swift @@ -11,11 +11,9 @@ struct StopIcon: View { public let style: Style public let orderInRoute: Int? - public let isSelected: Bool - init(orderInRoute: Int? = nil, direction: CarrisNetworkModel.Direction? = nil, style: Style = .standard, isSelected: Bool = false) { + init(style: Style = .standard, orderInRoute: Int? = nil, direction: CarrisNetworkModel.Direction? = nil) { self.orderInRoute = orderInRoute - self.isSelected = isSelected switch direction { case .ascending: @@ -72,9 +70,9 @@ struct StopIcon: View { private var borderColor: Color { switch style { case .standard: - return Color("StopCircularBorder") + return Color("StopStandardBorder") case .mini: - return Color("StopCircularBorder") + return Color("StopMiniBorder") case .ascending: return Color("StopAscendingBorder") case .descending: @@ -93,9 +91,9 @@ struct StopIcon: View { private var backgroundColor: Color { switch style { case .standard: - return Color("StopCircularBackground") + return Color("StopStandardBackground") case .mini: - return Color("StopCircularBackground") + return Color("StopMiniBackground") case .ascending: return Color("StopAscendingBackground") case .descending: @@ -114,9 +112,9 @@ struct StopIcon: View { private var textColor: Color { switch style { case .standard: - return Color("StopCircularText") + return Color("StopStandardText") case .mini: - return Color("StopCircularText") + return Color("StopMiniText") case .ascending: return Color("StopAscendingText") case .descending: diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Mini/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Mini/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Mini/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniBackground.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniBackground.colorset/Contents.json new file mode 100644 index 00000000..c27ad0fe --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.510", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.667", + "green" : "0.235", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniBorder.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniBorder.colorset/Contents.json new file mode 100644 index 00000000..91ecaedc --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniBorder.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.863", + "green" : "0.431", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.510", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniText.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniText.colorset/Contents.json new file mode 100644 index 00000000..22c4bb0a --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Mini/StopMiniText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Standard/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Standard/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Standard/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardBackground.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardBackground.colorset/Contents.json new file mode 100644 index 00000000..c27ad0fe --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.510", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.667", + "green" : "0.235", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardBorder.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardBorder.colorset/Contents.json new file mode 100644 index 00000000..91ecaedc --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardBorder.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.863", + "green" : "0.431", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.510", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardText.colorset/Contents.json b/GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardText.colorset/Contents.json new file mode 100644 index 00000000..22c4bb0a --- /dev/null +++ b/GeoBus/Assets.xcassets/Colors/Stops/Standard/StopStandardText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} From c70f431df5e8e274792668c67620ea2399726f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 01:40:59 +0000 Subject: [PATCH 53/63] Always show Vehicle Route Overview from Community Even if status is disabled. --- .../VehicleDetails/VehicleDetailsView.swift | 25 ++++++------------- .../Carris/CarrisNetworkController.swift | 5 +--- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift index 8822efc1..4b08cf85 100644 --- a/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift +++ b/GeoBus/App/Components/VehicleDetails/VehicleDetailsView.swift @@ -29,16 +29,8 @@ struct CarrisVehicleSheetView: View { CarrisVehicleToggleFollowOnMap() } } - if (carrisNetworkController.communityDataProviderStatus) { - CarrisVehicleRouteSummary(vehicle: carrisNetworkController.activeVehicle) - Disclaimer() - } else { - DataProvidersCard() - .overlay() { - RoundedRectangle(cornerRadius: 10) - .stroke(Color(.systemTeal).opacity(0.1), lineWidth: 2) - } - } + CarrisVehicleRouteSummary(vehicle: carrisNetworkController.activeVehicle) + Disclaimer() } .padding() } @@ -399,7 +391,7 @@ struct CarrisVehicleRouteOverviewEstimationLine: View { } HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: thisStopIndex+1, style: .muted) + StopIcon(style: .muted, orderInRoute: thisStopIndex+1) Text(carrisNetworkController.find(stop: estimationLine.stopId)?.name ?? "") .font(Font.system(size: 17, weight: .medium)) .lineLimit(1) @@ -426,7 +418,7 @@ struct CarrisVehicleRouteOverviewEstimationLine: View { .frame(width: 25) HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: thisStopIndex+1, style: .standard) + StopIcon(style: .circular, orderInRoute: thisStopIndex+1) Text(carrisNetworkController.find(stop: estimationLine.stopId)?.name ?? "") .font(Font.system(size: 17, weight: .medium)) .lineLimit(1) @@ -445,7 +437,7 @@ struct CarrisVehicleRouteOverviewEstimationLine: View { } HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: thisStopIndex+1, style: .standard) + StopIcon(style: .circular, orderInRoute: thisStopIndex+1) Text(carrisNetworkController.find(stop: estimationLine.stopId)?.name ?? "") .font(Font.system(size: 17, weight: .medium)) .lineLimit(1) @@ -548,7 +540,7 @@ struct CarrisVehicleRouteOverview: View { } HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: index+1, style: .muted) + StopIcon(style: .muted, orderInRoute: index+1) Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") .font(Font.system(size: 17, weight: .medium)) .lineLimit(1) @@ -577,7 +569,7 @@ struct CarrisVehicleRouteOverview: View { } HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: index+1, style: .standard) + StopIcon(style: .standard, orderInRoute: index+1) Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") .font(Font.system(size: 17, weight: .medium)) .lineLimit(1) @@ -596,7 +588,7 @@ struct CarrisVehicleRouteOverview: View { } HStack(alignment: .center, spacing: 10) { - StopIcon(orderInRoute: index+1, style: .standard) + StopIcon(style: .standard, orderInRoute: index+1) Text(carrisNetworkController.find(stop: element.stopId)?.name ?? "") .font(Font.system(size: 17, weight: .medium)) .lineLimit(1) @@ -609,7 +601,6 @@ struct CarrisVehicleRouteOverview: View { } - .id(index) } diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index 1e88b61e..4bdde3a5 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -213,10 +213,7 @@ class CarrisNetworkController: ObservableObject { // If there is an active vehicle, also refresh it's details if (self.activeVehicle != nil) { await self.fetchVehicleDetailsFromCarrisAPI(for: self.activeVehicle!.id) - // If Community provider is also enabled, then also refresh those details - if (self.communityDataProviderStatus) { - await self.fetchVehicleDetailsFromCommunityAPI(for: self.activeVehicle!.id) - } + await self.fetchVehicleDetailsFromCommunityAPI(for: self.activeVehicle!.id) } // Update the list of active vehicles (the current selected route) self.populateActiveVehicles() From 8b48566799a6211909f2ad1345c271cbeb5ce7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 01:41:18 +0000 Subject: [PATCH 54/63] Update stop annotations to new style --- .../App/Components/Map/MapAnnotations.swift | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index 1eb23b4a..5f249a26 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -21,7 +21,7 @@ struct GenericMapAnnotation: Identifiable { case carris_stop(CarrisNetworkModel.Stop) case carris_connection(CarrisNetworkModel.Connection) case carris_vehicle(CarrisNetworkModel.Vehicle) -// case ministop(CarrisNetworkModel.Stop) + // case ministop(CarrisNetworkModel.Stop) } } @@ -46,11 +46,17 @@ struct CarrisConnectionAnnotationView: View { carrisNetworkController.select(connection: self.connection) sheetController.present(sheet: .ConnectionDetails) }) { - StopIcon( - orderInRoute: self.connection.orderInRoute, - direction: self.connection.direction, - isSelected: carrisNetworkController.activeConnection == self.connection - ) + if (carrisNetworkController.activeConnection?.id == self.connection.id) { + StopIcon(style: .selected, orderInRoute: self.connection.orderInRoute) + } else if (self.connection.direction == .ascending) { + StopIcon(style: .ascending, orderInRoute: self.connection.orderInRoute) + } else if (self.connection.direction == .descending) { + StopIcon(style: .descending, orderInRoute: self.connection.orderInRoute) + } else if (self.connection.direction == .circular) { + StopIcon(style: .circular, orderInRoute: self.connection.orderInRoute) + } else { + StopIcon(style: .standard) + } } .frame(width: 40, height: 40, alignment: .center) } @@ -122,9 +128,9 @@ struct StopAnnotationView: View { TapticEngine.impact.feedback(.light) carrisNetworkController.select(stop: self.stop) sheetController.present(sheet: .StopDetails) - withAnimation(.easeIn(duration: 0.5)) { - mapController.centerMapOnCoordinates(lat: self.stop.lat, lng: self.stop.lng) - } + // withAnimation(.easeIn(duration: 0.5)) { + // mapController.centerMapOnCoordinates(lat: self.stop.lat, lng: self.stop.lng) + // } } } From a9b251866f7fde1abf907ff0f96af4625db681e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 18:39:22 +0000 Subject: [PATCH 55/63] Renamed .carris_stop and .carris_vehicle to .stop and .vehicle --- .../App/Components/Map/MapAnnotations.swift | 5 ++-- GeoBus/App/Components/Map/MapView.swift | 4 +-- GeoBus/App/Controllers/MapController.swift | 30 +++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index 5f249a26..ca7707ff 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -18,10 +18,9 @@ struct GenericMapAnnotation: Identifiable { var item: AnnotationItem enum AnnotationItem { - case carris_stop(CarrisNetworkModel.Stop) + case stop(CarrisNetworkModel.Stop) case carris_connection(CarrisNetworkModel.Connection) - case carris_vehicle(CarrisNetworkModel.Vehicle) - // case ministop(CarrisNetworkModel.Stop) + case vehicle(CarrisNetworkModel.Vehicle) } } diff --git a/GeoBus/App/Components/Map/MapView.swift b/GeoBus/App/Components/Map/MapView.swift index e1aa3967..83e20b3b 100644 --- a/GeoBus/App/Components/Map/MapView.swift +++ b/GeoBus/App/Components/Map/MapView.swift @@ -26,11 +26,11 @@ struct MapView: View { MapAnnotation(coordinate: annotation.location) { switch (annotation.item) { - case .carris_stop(let item): + case .stop(let item): StopAnnotationView(stop: item) case .carris_connection(let item): CarrisConnectionAnnotationView(connection: item) - case .carris_vehicle(let item): + case .vehicle(let item): CarrisVehicleAnnotationView(vehicle: item) } } diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index 11ed1d7d..3595915a 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -151,7 +151,7 @@ final class MapController: ObservableObject { // // visibleAnnotations.removeAll(where: { // switch $0.item { -// case .carris_stop(_), .carris_connection(_), .carris_vehicle(_: +// case .stop(_), .carris_connection(_), .vehicle(_: // return true // } // }) @@ -162,7 +162,7 @@ final class MapController: ObservableObject { // GenericMapAnnotation( // id: UUID(), // location: CLLocationCoordinate2D(latitude: activeStop.lat, longitude: activeStop.lng), -// item: .carris_stop(activeStop) +// item: .stop(activeStop) // ) // ) // @@ -180,9 +180,9 @@ final class MapController: ObservableObject { visibleAnnotations.removeAll(where: { switch $0.item { - case .carris_connection(_), .carris_stop(_): + case .carris_connection(_), .stop(_): return true - case .carris_vehicle(_): + case .vehicle(_): return false } }) @@ -241,7 +241,7 @@ final class MapController: ObservableObject { visibleAnnotations.removeAll(where: { switch $0.item { - case .carris_vehicle(_), .carris_stop(_): + case .vehicle(_), .stop(_): return true case .carris_connection(_): return false @@ -256,7 +256,7 @@ final class MapController: ObservableObject { GenericMapAnnotation( id: UUID(), location: vehicle.coordinate, - item: .carris_vehicle(vehicle) + item: .vehicle(vehicle) ) ) } @@ -280,13 +280,13 @@ final class MapController: ObservableObject { if let activeVehicleAnnotation = visibleAnnotations.first(where: { switch $0.item { - case .carris_vehicle(let item): + case .vehicle(let item): if (item.id == activeVehicle.id) { return true } else { return false } - case .carris_connection(_), .carris_stop(_): + case .carris_connection(_), .stop(_): return false } }) { @@ -301,7 +301,7 @@ final class MapController: ObservableObject { GenericMapAnnotation( id: UUID(), location: activeVehicle.coordinate, - item: .carris_vehicle(activeVehicle) + item: .vehicle(activeVehicle) ) ) @@ -357,9 +357,9 @@ final class MapController: ObservableObject { visibleAnnotations.removeAll(where: { switch $0.item { - case .carris_stop(_): + case .stop(_): return true - case .carris_vehicle(_), .carris_connection(_): + case .vehicle(_), .carris_connection(_): return false } }) @@ -371,7 +371,7 @@ final class MapController: ObservableObject { GenericMapAnnotation( id: UUID(), location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), - item: .carris_stop(stop) + item: .stop(stop) ) ) } @@ -395,7 +395,7 @@ final class MapController: ObservableObject { GenericMapAnnotation( id: UUID(), location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), - item: .carris_stop(stop) + item: .stop(stop) ) ) } @@ -436,9 +436,9 @@ final class MapController: ObservableObject { } else { visibleAnnotations.removeAll(where: { switch $0.item { - case .carris_stop(_): + case .stop(_): return true; - case .carris_connection(_), .carris_vehicle(_): + case .carris_connection(_), .vehicle(_): return false; } }) From ecb5f239cb32622d835283296e3bdceba006a79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 22:58:16 +0000 Subject: [PATCH 56/63] What a mess --- .../App/Components/Map/MapAnnotations.swift | 40 ++- GeoBus/App/Components/Map/MapView.swift | 22 +- GeoBus/App/Controllers/MapController.swift | 324 ++++++++++++------ .../Networks/Carris/CarrisNetworkModel.swift | 10 +- 4 files changed, 273 insertions(+), 123 deletions(-) diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index ca7707ff..277fe530 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -11,13 +11,29 @@ import SwiftUI -struct GenericMapAnnotation: Identifiable { +struct GenericMapAnnotation: Identifiable, Equatable, Hashable { + + static func == (lhs: GenericMapAnnotation, rhs: GenericMapAnnotation) -> Bool { + return lhs.item == rhs.item + } + + func hash(into hasher: inout Hasher) { + hasher.combine(item) + } + + + init(location: CLLocationCoordinate2D, item: AnnotationItem) { + self.id = UUID() + self.location = location + self.item = item + } + let id: UUID var location: CLLocationCoordinate2D var item: AnnotationItem - enum AnnotationItem { + enum AnnotationItem: Equatable { case stop(CarrisNetworkModel.Stop) case carris_connection(CarrisNetworkModel.Connection) case vehicle(CarrisNetworkModel.Vehicle) @@ -27,6 +43,26 @@ struct GenericMapAnnotation: Identifiable { +extension GenericMapAnnotation.AnnotationItem: Hashable { + func hash(into hasher: inout Hasher) { + switch self { + case .stop(let item): + hasher.combine(item) // combine with associated value, if it's not `Hashable` map it to some `Hashable` type and then combine result + case .carris_connection(let item): + hasher.combine(item) // combine with associated value, if it's not `Hashable` map it to some `Hashable` type and then combine result + case .vehicle(let item): + hasher.combine(item) + break + } + } +} + + + + + + + struct CarrisConnectionAnnotationView: View { public let connection: CarrisNetworkModel.Connection diff --git a/GeoBus/App/Components/Map/MapView.swift b/GeoBus/App/Components/Map/MapView.swift index 83e20b3b..7259c66e 100644 --- a/GeoBus/App/Components/Map/MapView.swift +++ b/GeoBus/App/Components/Map/MapView.swift @@ -36,17 +36,23 @@ struct MapView: View { } } - .onReceive(carrisNetworkController.$activeVariant) { newVariant in - if (newVariant != nil) { - self.mapController.updateAnnotations(with: newVariant!) - } + .onReceive(carrisNetworkController.$allStops) { allStopsList in + self.mapController.createAnnotations(for: allStopsList) } - .onReceive(carrisNetworkController.$activeVehicles) { newVehiclesList in - self.mapController.updateAnnotations(with: newVehiclesList) + .onReceive(carrisNetworkController.$allVehicles) { allVehicles in + self.mapController.createAnnotations(for: allVehicles) } - .onReceive(mapController.$region.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)) { newRegion in - self.mapController.updateAnnotations(for: newRegion, with: carrisNetworkController.allStops) +// .onReceive(carrisNetworkController.$activeVariant) { newVariant in +// if (newVariant != nil) { +// self.mapController.updateAnnotations(with: newVariant!) +// } +// } + .onReceive(carrisNetworkController.$activeVehicles) { activeVehicles in + self.mapController.showAnnotations(for: activeVehicles) } +// .onReceive(mapController.$region.debounce(for: .seconds(0.05), scheduler: DispatchQueue.main)) { newRegion in +// self.mapController.updateAnnotations(for: newRegion) +// } } diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index 3595915a..b62b3163 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -4,15 +4,13 @@ import MapKit /* * */ /* MARK: - MAP CONTROLLER */ -/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi rutrum lectus */ -/* non interdum imperdiet. In hendrerit ligula velit, ac porta augue volutpat id. */ +/* This class is responsible for controlling the Map and its annotations. */ -@MainActor final class MapController: ObservableObject { /* * */ - /* MARK: - SECTION 1: SETTINGS */ + /* MARK: - 1: SETTINGS */ /* Static settings for the Map view. */ private let initialMapRegion = CLLocationCoordinate2D(latitude: 38.721917, longitude: -9.137732) @@ -143,6 +141,21 @@ final class MapController: ObservableObject { + + private func addAnnotations(_ newAnnotations: [GenericMapAnnotation], zoom: Bool = false) { + // Add the annotations to the map + self.visibleAnnotations.append(contentsOf: newAnnotations) + // Remove annotations with duplicate IDs (ex: same stop on different itineraries) + self.visibleAnnotations.uniqueInPlace(for: \.id) + // Adjust map region to annotations + if (zoom) { + self.zoomToFitMapAnnotations(annotations: newAnnotations) + } + } + + + + /* * */ /* MARK: - SECTION 8: UPDATE ANNOTATIONS WITH SELECTED CARRIS STOP */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ @@ -195,7 +208,6 @@ final class MapController: ObservableObject { for connection in activeVariant.circularItinerary! { tempNewAnnotations.append( GenericMapAnnotation( - id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), item: .carris_connection(connection) ) @@ -207,7 +219,6 @@ final class MapController: ObservableObject { for connection in activeVariant.ascendingItinerary! { tempNewAnnotations.append( GenericMapAnnotation( - id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), item: .carris_connection(connection) ) @@ -219,7 +230,6 @@ final class MapController: ObservableObject { for connection in activeVariant.descendingItinerary! { tempNewAnnotations.append( GenericMapAnnotation( - id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), item: .carris_connection(connection) ) @@ -233,81 +243,114 @@ final class MapController: ObservableObject { + + + + + + + + + + + /* * */ /* MARK: - SECTION 10: UPDATE ANNOTATIONS WITH LIST OF ACTIVE CARRIS VEHICLES */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ - func updateAnnotations(with activeVehiclesList: [CarrisNetworkModel.Vehicle]) { - - visibleAnnotations.removeAll(where: { - switch $0.item { - case .vehicle(_), .stop(_): - return true - case .carris_connection(_): - return false - } - }) + private var allStopAnnotations: [GenericMapAnnotation] = [] + + func createAnnotations(for allStops: [CarrisNetworkModel.Stop]) { + allStopAnnotations.removeAll() - var tempNewAnnotations: [GenericMapAnnotation] = [] - - for vehicle in activeVehiclesList { - tempNewAnnotations.append( + for stop in allStops { + allStopAnnotations.append( GenericMapAnnotation( - id: UUID(), - location: vehicle.coordinate, - item: .vehicle(vehicle) + location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), + item: .stop(stop) ) ) } - self.addAnnotations(tempNewAnnotations) + } + + + + private var allVehicleAnnotations: [GenericMapAnnotation] = [] + + func createAnnotations(for allVehicles: [CarrisNetworkModel.Vehicle]) { + + allVehicleAnnotations.removeAll() + + for vehicle in allVehicles { + if (vehicle.lat != nil && vehicle.lng != nil) { + allVehicleAnnotations.append( + GenericMapAnnotation( + location: CLLocationCoordinate2D(latitude: vehicle.lat!, longitude: vehicle.lng!), + item: .vehicle(vehicle) + ) + ) + } + } } - /* * */ - /* MARK: - SECTION 10: UPDATE ANNOTATIONS WITH SINGLE ACTIVE CARRIS VEHICLE */ - /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ - func updateAnnotations(with activeVehicle: CarrisNetworkModel.Vehicle) { + + + + + + func showAnnotations(for activeVehicles: [CarrisNetworkModel.Vehicle]) { -// let indexOfVehicleInArray = allVehicles.firstIndex(where: { -// $0.id == vehicleId -// }) + + activeVehicles.forEach({ vehicle in + + + for + + + + + + + let index = allVehicleAnnotations.firstIndex(where: { annotation in + switch annotation.item { + case .vehicle(let item): + vehicle.id == item.id + case .stop(_), .carris_connection(_): + return false + } + }) + + if (index != nil) { + visibleAnnotations.append(allVehicleAnnotations[index!]) + } + + }) - if let activeVehicleAnnotation = visibleAnnotations.first(where: { - switch $0.item { + + let newVisibleAnnotations = allVehicleAnnotations.filter({ annotation in + + switch annotation.item { case .vehicle(let item): - if (item.id == activeVehicle.id) { + if activeVehicles.firstIndex(where: { + $0.id == item.id + }) == nil { + visibleAnnotations.append(annotation) return true - } else { - return false } + return false case .carris_connection(_), .stop(_): return false } - }) { - - self.zoomToFitMapAnnotations(annotations: [activeVehicleAnnotation]) - - } else { - - var tempNewAnnotations: [GenericMapAnnotation] = [] - tempNewAnnotations.append( - GenericMapAnnotation( - id: UUID(), - location: activeVehicle.coordinate, - item: .vehicle(activeVehicle) - ) - ) - - self.addAnnotations(tempNewAnnotations, zoom: true) - - } + }) + } @@ -320,95 +363,156 @@ final class MapController: ObservableObject { - private func addAnnotations(_ newAnnotations: [GenericMapAnnotation], zoom: Bool = false) { - DispatchQueue.main.async { - // Add the annotations to the map - self.visibleAnnotations.append(contentsOf: newAnnotations) - // Remove annotations with duplicate IDs (ex: same stop on different itineraries) - self.visibleAnnotations.uniqueInPlace(for: \.id) - // Adjust map region to annotations - if (zoom) { - self.zoomToFitMapAnnotations(annotations: newAnnotations) - } - } - } -// private func removeAnnotations(ofType annotationTypes: [GenericMapAnnotation.AnnotationItem]) { + + + + + + +// func updateAnnotations(with activeVehiclesList: [CarrisNetworkModel.Vehicle]) { +// +// var tempNewAnnotations: [GenericMapAnnotation] = [] +// +// for vehicle in activeVehiclesList { +// tempNewAnnotations.append( +// GenericMapAnnotation( +// id: UUID(), +// location: vehicle.coordinate, +// item: .vehicle(vehicle) +// ) +// ) +// } +// +// } + + + + + + + + + + func updateAnnotations(with activeVehiclesList: [CarrisNetworkModel.Vehicle]) { + +// for activeVehicle in activeVehiclesList { +// +// +// +// } +// +// +// +// +// activeVehiclesList.forEach({ activeVehicle in +// visibleAnnotations.insert( +// GenericMapAnnotation( +// location: activeVehicle.coordinate, +// item: .vehicle(activeVehicle) +// ) +// ) +// }) +// +// +// print("GBDEBUG: visibleAnnotations_NEW: \(visibleAnnotations)") + + + + // visibleAnnotations.removeAll(where: { -// for type in annotationTypes { -// if ($0.item == type) { +// switch $0.item { +// case .vehicle(_): // return true -// } +// case .stop(_), .carris_connection(_): +// return false // } -// return false // }) -// } - - +// +// +// var tempNewAnnotations: [GenericMapAnnotation] = [] +// +// for vehicle in activeVehiclesList { +// tempNewAnnotations.append( +// GenericMapAnnotation( +// location: vehicle.coordinate, +// item: .vehicle(vehicle) +// ) +// ) +// } +// +// self.addAnnotations(tempNewAnnotations) + + } /* * */ - /* MARK: - SECTION 8: UPDATE ANNOTATIONS WITH SELECTED CARRIS STOP */ + /* MARK: - SECTION 10: UPDATE ANNOTATIONS WITH SINGLE ACTIVE CARRIS VEHICLE */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ - func updateAnnotations(ministop: [CarrisNetworkModel.Stop]) { + func updateAnnotations(with activeVehicle: CarrisNetworkModel.Vehicle) { - visibleAnnotations.removeAll(where: { +// let indexOfVehicleInArray = allVehicles.firstIndex(where: { +// $0.id == vehicleId +// }) + + + if let activeVehicleAnnotation = visibleAnnotations.first(where: { switch $0.item { - case .stop(_): - return true - case .vehicle(_), .carris_connection(_): + case .vehicle(let item): + if (item.id == activeVehicle.id) { + return true + } else { + return false + } + case .carris_connection(_), .stop(_): return false } - }) - - var tempNewAnnotations: [GenericMapAnnotation] = [] - - for stop in ministop { + }) { + + self.zoomToFitMapAnnotations(annotations: [activeVehicleAnnotation]) + + } else { + + var tempNewAnnotations: [GenericMapAnnotation] = [] + tempNewAnnotations.append( GenericMapAnnotation( - id: UUID(), - location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), - item: .stop(stop) + location: activeVehicle.coordinate, + item: .vehicle(activeVehicle) ) ) + + self.addAnnotations(tempNewAnnotations, zoom: true) + } - self.addAnnotations(tempNewAnnotations, zoom: true) - } + + + + /* * */ /* MARK: - SECTION 8: UPDATE ANNOTATIONS WITH SELECTED CARRIS STOP */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ - private var allStopAnnotations: [GenericMapAnnotation] = [] - - func updateAnnotations(for newMapRegion: MKCoordinateRegion?, with allStops: [CarrisNetworkModel.Stop]) { + func updateAnnotations(for newMapRegion: MKCoordinateRegion?) { - if (allStopAnnotations.isEmpty) { - for stop in allStops { - allStopAnnotations.append( - GenericMapAnnotation( - id: UUID(), - location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), - item: .stop(stop) - ) - ) - } + guard newMapRegion != nil else { + return } - - if (region.span.latitudeDelta < 0.005 || region.span.longitudeDelta < 0.005) { + if (newMapRegion!.span.latitudeDelta < 0.005 || newMapRegion!.span.longitudeDelta < 0.005) { - let latTop = self.region.center.latitude + self.region.span.latitudeDelta - let latBottom = self.region.center.latitude - self.region.span.latitudeDelta + let latTop = newMapRegion!.center.latitude + newMapRegion!.span.latitudeDelta + let latBottom = newMapRegion!.center.latitude - newMapRegion!.span.latitudeDelta - let lngRight = self.region.center.longitude + self.region.span.longitudeDelta - let lngLeft = self.region.center.longitude - self.region.span.longitudeDelta + let lngRight = newMapRegion!.center.longitude + newMapRegion!.span.longitudeDelta + let lngLeft = newMapRegion!.center.longitude - newMapRegion!.span.longitudeDelta for annotation in allStopAnnotations { diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift index 87f4d0c2..4924cfcf 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift @@ -75,7 +75,7 @@ struct CarrisNetworkModel { // CONNECTION // Connections are a thin wrapper before stops in order to be able // to hold a ‹orderInRoute› number. Connections are identified by this value. - struct Connection: Codable, Equatable, Identifiable { + struct Connection: Codable, Equatable, Identifiable, Hashable { let id: Int let direction: Direction let orderInRoute: Int @@ -94,7 +94,7 @@ struct CarrisNetworkModel { /* MARK: - STOP */ /* Stops are identified by its ‹publicId› value. */ /* They have a name and a location. */ - struct Stop: Codable, Equatable, Identifiable { + struct Stop: Codable, Equatable, Identifiable, Hashable { let id: Int let name: String let lat, lng: Double @@ -137,12 +137,16 @@ struct CarrisNetworkModel { /* MARK: - CARRIS VEHICLE */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia. */ - class Vehicle: Identifiable, Equatable { + class Vehicle: Identifiable, Equatable, Hashable { static func == (lhs: CarrisNetworkModel.Vehicle, rhs: CarrisNetworkModel.Vehicle) -> Bool { return false } + func hash(into hasher: inout Hasher) { + hasher.combine(self.lat) + } + // IDENTIFIER // The unique identifier for this model. From 7e9297d10b31fa02b060fb47814d00d39d98a2cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 22:58:25 +0000 Subject: [PATCH 57/63] Revert "What a mess" This reverts commit ecb5f239cb32622d835283296e3bdceba006a79f. --- .../App/Components/Map/MapAnnotations.swift | 40 +-- GeoBus/App/Components/Map/MapView.swift | 22 +- GeoBus/App/Controllers/MapController.swift | 324 ++++++------------ .../Networks/Carris/CarrisNetworkModel.swift | 10 +- 4 files changed, 123 insertions(+), 273 deletions(-) diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index 277fe530..ca7707ff 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -11,29 +11,13 @@ import SwiftUI -struct GenericMapAnnotation: Identifiable, Equatable, Hashable { - - static func == (lhs: GenericMapAnnotation, rhs: GenericMapAnnotation) -> Bool { - return lhs.item == rhs.item - } - - func hash(into hasher: inout Hasher) { - hasher.combine(item) - } - - - init(location: CLLocationCoordinate2D, item: AnnotationItem) { - self.id = UUID() - self.location = location - self.item = item - } - +struct GenericMapAnnotation: Identifiable { let id: UUID var location: CLLocationCoordinate2D var item: AnnotationItem - enum AnnotationItem: Equatable { + enum AnnotationItem { case stop(CarrisNetworkModel.Stop) case carris_connection(CarrisNetworkModel.Connection) case vehicle(CarrisNetworkModel.Vehicle) @@ -43,26 +27,6 @@ struct GenericMapAnnotation: Identifiable, Equatable, Hashable { -extension GenericMapAnnotation.AnnotationItem: Hashable { - func hash(into hasher: inout Hasher) { - switch self { - case .stop(let item): - hasher.combine(item) // combine with associated value, if it's not `Hashable` map it to some `Hashable` type and then combine result - case .carris_connection(let item): - hasher.combine(item) // combine with associated value, if it's not `Hashable` map it to some `Hashable` type and then combine result - case .vehicle(let item): - hasher.combine(item) - break - } - } -} - - - - - - - struct CarrisConnectionAnnotationView: View { public let connection: CarrisNetworkModel.Connection diff --git a/GeoBus/App/Components/Map/MapView.swift b/GeoBus/App/Components/Map/MapView.swift index 7259c66e..83e20b3b 100644 --- a/GeoBus/App/Components/Map/MapView.swift +++ b/GeoBus/App/Components/Map/MapView.swift @@ -36,23 +36,17 @@ struct MapView: View { } } - .onReceive(carrisNetworkController.$allStops) { allStopsList in - self.mapController.createAnnotations(for: allStopsList) + .onReceive(carrisNetworkController.$activeVariant) { newVariant in + if (newVariant != nil) { + self.mapController.updateAnnotations(with: newVariant!) + } } - .onReceive(carrisNetworkController.$allVehicles) { allVehicles in - self.mapController.createAnnotations(for: allVehicles) + .onReceive(carrisNetworkController.$activeVehicles) { newVehiclesList in + self.mapController.updateAnnotations(with: newVehiclesList) } -// .onReceive(carrisNetworkController.$activeVariant) { newVariant in -// if (newVariant != nil) { -// self.mapController.updateAnnotations(with: newVariant!) -// } -// } - .onReceive(carrisNetworkController.$activeVehicles) { activeVehicles in - self.mapController.showAnnotations(for: activeVehicles) + .onReceive(mapController.$region.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)) { newRegion in + self.mapController.updateAnnotations(for: newRegion, with: carrisNetworkController.allStops) } -// .onReceive(mapController.$region.debounce(for: .seconds(0.05), scheduler: DispatchQueue.main)) { newRegion in -// self.mapController.updateAnnotations(for: newRegion) -// } } diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index b62b3163..3595915a 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -4,13 +4,15 @@ import MapKit /* * */ /* MARK: - MAP CONTROLLER */ -/* This class is responsible for controlling the Map and its annotations. */ +/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi rutrum lectus */ +/* non interdum imperdiet. In hendrerit ligula velit, ac porta augue volutpat id. */ +@MainActor final class MapController: ObservableObject { /* * */ - /* MARK: - 1: SETTINGS */ + /* MARK: - SECTION 1: SETTINGS */ /* Static settings for the Map view. */ private let initialMapRegion = CLLocationCoordinate2D(latitude: 38.721917, longitude: -9.137732) @@ -141,21 +143,6 @@ final class MapController: ObservableObject { - - private func addAnnotations(_ newAnnotations: [GenericMapAnnotation], zoom: Bool = false) { - // Add the annotations to the map - self.visibleAnnotations.append(contentsOf: newAnnotations) - // Remove annotations with duplicate IDs (ex: same stop on different itineraries) - self.visibleAnnotations.uniqueInPlace(for: \.id) - // Adjust map region to annotations - if (zoom) { - self.zoomToFitMapAnnotations(annotations: newAnnotations) - } - } - - - - /* * */ /* MARK: - SECTION 8: UPDATE ANNOTATIONS WITH SELECTED CARRIS STOP */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ @@ -208,6 +195,7 @@ final class MapController: ObservableObject { for connection in activeVariant.circularItinerary! { tempNewAnnotations.append( GenericMapAnnotation( + id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), item: .carris_connection(connection) ) @@ -219,6 +207,7 @@ final class MapController: ObservableObject { for connection in activeVariant.ascendingItinerary! { tempNewAnnotations.append( GenericMapAnnotation( + id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), item: .carris_connection(connection) ) @@ -230,6 +219,7 @@ final class MapController: ObservableObject { for connection in activeVariant.descendingItinerary! { tempNewAnnotations.append( GenericMapAnnotation( + id: UUID(), location: CLLocationCoordinate2D(latitude: connection.stop.lat, longitude: connection.stop.lng), item: .carris_connection(connection) ) @@ -243,206 +233,35 @@ final class MapController: ObservableObject { - - - - - - - - - - - /* * */ /* MARK: - SECTION 10: UPDATE ANNOTATIONS WITH LIST OF ACTIVE CARRIS VEHICLES */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ - private var allStopAnnotations: [GenericMapAnnotation] = [] - - func createAnnotations(for allStops: [CarrisNetworkModel.Stop]) { - - allStopAnnotations.removeAll() - - for stop in allStops { - allStopAnnotations.append( - GenericMapAnnotation( - location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), - item: .stop(stop) - ) - ) - } - - } - - - - private var allVehicleAnnotations: [GenericMapAnnotation] = [] - - func createAnnotations(for allVehicles: [CarrisNetworkModel.Vehicle]) { - - allVehicleAnnotations.removeAll() - - for vehicle in allVehicles { - if (vehicle.lat != nil && vehicle.lng != nil) { - allVehicleAnnotations.append( - GenericMapAnnotation( - location: CLLocationCoordinate2D(latitude: vehicle.lat!, longitude: vehicle.lng!), - item: .vehicle(vehicle) - ) - ) - } - } - - } - - - - - - - - - - func showAnnotations(for activeVehicles: [CarrisNetworkModel.Vehicle]) { - - - activeVehicles.forEach({ vehicle in - - - for - - - - - - - let index = allVehicleAnnotations.firstIndex(where: { annotation in - switch annotation.item { - case .vehicle(let item): - vehicle.id == item.id - case .stop(_), .carris_connection(_): - return false - } - }) - - if (index != nil) { - visibleAnnotations.append(allVehicleAnnotations[index!]) - } - - }) - - + func updateAnnotations(with activeVehiclesList: [CarrisNetworkModel.Vehicle]) { - let newVisibleAnnotations = allVehicleAnnotations.filter({ annotation in - - switch annotation.item { - case .vehicle(let item): - if activeVehicles.firstIndex(where: { - $0.id == item.id - }) == nil { - visibleAnnotations.append(annotation) - return true - } - return false - case .carris_connection(_), .stop(_): + visibleAnnotations.removeAll(where: { + switch $0.item { + case .vehicle(_), .stop(_): + return true + case .carris_connection(_): return false } - }) - } - - - - - - - - - - - - - - - - - - -// func updateAnnotations(with activeVehiclesList: [CarrisNetworkModel.Vehicle]) { -// -// var tempNewAnnotations: [GenericMapAnnotation] = [] -// -// for vehicle in activeVehiclesList { -// tempNewAnnotations.append( -// GenericMapAnnotation( -// id: UUID(), -// location: vehicle.coordinate, -// item: .vehicle(vehicle) -// ) -// ) -// } -// -// } - - - - - - - - - - func updateAnnotations(with activeVehiclesList: [CarrisNetworkModel.Vehicle]) { - -// for activeVehicle in activeVehiclesList { -// -// -// -// } -// -// -// -// -// activeVehiclesList.forEach({ activeVehicle in -// visibleAnnotations.insert( -// GenericMapAnnotation( -// location: activeVehicle.coordinate, -// item: .vehicle(activeVehicle) -// ) -// ) -// }) -// -// -// print("GBDEBUG: visibleAnnotations_NEW: \(visibleAnnotations)") - - + var tempNewAnnotations: [GenericMapAnnotation] = [] + for vehicle in activeVehiclesList { + tempNewAnnotations.append( + GenericMapAnnotation( + id: UUID(), + location: vehicle.coordinate, + item: .vehicle(vehicle) + ) + ) + } -// visibleAnnotations.removeAll(where: { -// switch $0.item { -// case .vehicle(_): -// return true -// case .stop(_), .carris_connection(_): -// return false -// } -// }) -// -// -// var tempNewAnnotations: [GenericMapAnnotation] = [] -// -// for vehicle in activeVehiclesList { -// tempNewAnnotations.append( -// GenericMapAnnotation( -// location: vehicle.coordinate, -// item: .vehicle(vehicle) -// ) -// ) -// } -// -// self.addAnnotations(tempNewAnnotations) + self.addAnnotations(tempNewAnnotations) } @@ -480,6 +299,7 @@ final class MapController: ObservableObject { tempNewAnnotations.append( GenericMapAnnotation( + id: UUID(), location: activeVehicle.coordinate, item: .vehicle(activeVehicle) ) @@ -492,7 +312,40 @@ final class MapController: ObservableObject { } - + + + + + + + + + private func addAnnotations(_ newAnnotations: [GenericMapAnnotation], zoom: Bool = false) { + DispatchQueue.main.async { + // Add the annotations to the map + self.visibleAnnotations.append(contentsOf: newAnnotations) + // Remove annotations with duplicate IDs (ex: same stop on different itineraries) + self.visibleAnnotations.uniqueInPlace(for: \.id) + // Adjust map region to annotations + if (zoom) { + self.zoomToFitMapAnnotations(annotations: newAnnotations) + } + } + } + + +// private func removeAnnotations(ofType annotationTypes: [GenericMapAnnotation.AnnotationItem]) { +// visibleAnnotations.removeAll(where: { +// for type in annotationTypes { +// if ($0.item == type) { +// return true +// } +// } +// return false +// }) +// } + + @@ -500,19 +353,62 @@ final class MapController: ObservableObject { /* MARK: - SECTION 8: UPDATE ANNOTATIONS WITH SELECTED CARRIS STOP */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ - func updateAnnotations(for newMapRegion: MKCoordinateRegion?) { + func updateAnnotations(ministop: [CarrisNetworkModel.Stop]) { - guard newMapRegion != nil else { - return + visibleAnnotations.removeAll(where: { + switch $0.item { + case .stop(_): + return true + case .vehicle(_), .carris_connection(_): + return false + } + }) + + var tempNewAnnotations: [GenericMapAnnotation] = [] + + for stop in ministop { + tempNewAnnotations.append( + GenericMapAnnotation( + id: UUID(), + location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), + item: .stop(stop) + ) + ) + } + + self.addAnnotations(tempNewAnnotations, zoom: true) + + } + + + /* * */ + /* MARK: - SECTION 8: UPDATE ANNOTATIONS WITH SELECTED CARRIS STOP */ + /* Lorem ipsum dolor sit amet consectetur adipisicing elit. */ + + private var allStopAnnotations: [GenericMapAnnotation] = [] + + func updateAnnotations(for newMapRegion: MKCoordinateRegion?, with allStops: [CarrisNetworkModel.Stop]) { + + if (allStopAnnotations.isEmpty) { + for stop in allStops { + allStopAnnotations.append( + GenericMapAnnotation( + id: UUID(), + location: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), + item: .stop(stop) + ) + ) + } } - if (newMapRegion!.span.latitudeDelta < 0.005 || newMapRegion!.span.longitudeDelta < 0.005) { + + if (region.span.latitudeDelta < 0.005 || region.span.longitudeDelta < 0.005) { - let latTop = newMapRegion!.center.latitude + newMapRegion!.span.latitudeDelta - let latBottom = newMapRegion!.center.latitude - newMapRegion!.span.latitudeDelta + let latTop = self.region.center.latitude + self.region.span.latitudeDelta + let latBottom = self.region.center.latitude - self.region.span.latitudeDelta - let lngRight = newMapRegion!.center.longitude + newMapRegion!.span.longitudeDelta - let lngLeft = newMapRegion!.center.longitude - newMapRegion!.span.longitudeDelta + let lngRight = self.region.center.longitude + self.region.span.longitudeDelta + let lngLeft = self.region.center.longitude - self.region.span.longitudeDelta for annotation in allStopAnnotations { diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift index 4924cfcf..87f4d0c2 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift @@ -75,7 +75,7 @@ struct CarrisNetworkModel { // CONNECTION // Connections are a thin wrapper before stops in order to be able // to hold a ‹orderInRoute› number. Connections are identified by this value. - struct Connection: Codable, Equatable, Identifiable, Hashable { + struct Connection: Codable, Equatable, Identifiable { let id: Int let direction: Direction let orderInRoute: Int @@ -94,7 +94,7 @@ struct CarrisNetworkModel { /* MARK: - STOP */ /* Stops are identified by its ‹publicId› value. */ /* They have a name and a location. */ - struct Stop: Codable, Equatable, Identifiable, Hashable { + struct Stop: Codable, Equatable, Identifiable { let id: Int let name: String let lat, lng: Double @@ -137,16 +137,12 @@ struct CarrisNetworkModel { /* MARK: - CARRIS VEHICLE */ /* Lorem ipsum dolor sit amet consectetur adipisicing elit. Maxime mollitia. */ - class Vehicle: Identifiable, Equatable, Hashable { + class Vehicle: Identifiable, Equatable { static func == (lhs: CarrisNetworkModel.Vehicle, rhs: CarrisNetworkModel.Vehicle) -> Bool { return false } - func hash(into hasher: inout Hasher) { - hasher.combine(self.lat) - } - // IDENTIFIER // The unique identifier for this model. From ee63eff5a2499525d6aa9f1cf0445da78822ff34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 22:59:09 +0000 Subject: [PATCH 58/63] Revert "Delete NewMapView.swift" This reverts commit 5e71d44777277923e7c8eacfd94e4da8693a8404. --- GeoBus/App/Components/NewMap/NewMapView.swift | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 GeoBus/App/Components/NewMap/NewMapView.swift diff --git a/GeoBus/App/Components/NewMap/NewMapView.swift b/GeoBus/App/Components/NewMap/NewMapView.swift new file mode 100644 index 00000000..f103192e --- /dev/null +++ b/GeoBus/App/Components/NewMap/NewMapView.swift @@ -0,0 +1,222 @@ +// +// MapView.swift +// GeoBus +// +// Created by João de Vasconcelos on 14/04/2020. +// Copyright © 2020 João de Vasconcelos. All rights reserved. +// + + + +import SwiftUI +import MapKit + +struct NewMapView: UIViewRepresentable { + + private let mapView = MKMapView() + + @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + + + func makeUIView(context: UIViewRepresentableContext) -> MKMapView { + + mapView.delegate = context.coordinator + mapView.mapType = MKMapType.standard + mapView.showsUserLocation = true + mapView.showsTraffic = true + mapView.isRotateEnabled = false + mapView.isPitchEnabled = false + + mapView.register(NewConnectionAnnotationView.self, forAnnotationViewWithReuseIdentifier: "stop") + + // Set initial location in Lisbon + let lisbon = CLLocation(latitude: 38.721917, longitude: -9.137732) + let lisbonArea = MKCoordinateRegion(center: lisbon.coordinate, latitudinalMeters: 15000, longitudinalMeters: 15000) + mapView.setRegion(lisbonArea, animated: true) + + return mapView + } + + + func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext) { + + var annotationsToAdd: [MKAnnotation] = [] + + print("MAPTEST: is called updateUIView") +// print("MAPTEST: carrisNetworkController.activeRoute: \(carrisNetworkController.activeRoute)") + + if (carrisNetworkController.activeRoute != nil) { + for connection in carrisNetworkController.activeRoute?.variants[0].ascendingItinerary ?? [] { + annotationsToAdd.append( + NewConnectionAnnotation( + name: connection.stop.name, + publicId: connection.stop.id, + latitude: connection.stop.lat, + longitude: connection.stop.lng, + connection: connection + ) + ) + } + } + + + // Update whatever was set to update +// mapView.removeAnnotations([]) + uiView.addAnnotations(annotationsToAdd) + + uiView.showAnnotations(annotationsToAdd, animated: true) + + } + + + func makeCoordinator() -> NewMapView.Coordinator { + Coordinator(control: self) + } + + + + // MARK: - MKMapViewDelegate + + final class Coordinator: NSObject, MKMapViewDelegate { + + private let control: NewMapView + + private let appstate = Appstate.shared + private let carrisNetworkController = CarrisNetworkController.shared + + init(control: NewMapView) { + self.control = control + } + + + + @MainActor func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + print("MAPTEST: mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)") + if view.isKind(of: NewConnectionAnnotationView.self) { + + let selectedStopAnnotationView = view as! NewConnectionAnnotationView + let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation + +// selectedStopAnnotationView.marker.image = UIImage(named: "GreenInfo") + + TapticEngine.impact.feedback(.light) + _ = carrisNetworkController.select(connection: stopAnnotation.connection) + appstate.present(sheet: .carris_connectionDetails) + + } + } + + + + @MainActor func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { + print("MAPTEST: mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView)") + if view.isKind(of: NewConnectionAnnotationView.self) { + +// let selectedStopAnnotationView = view as! NewConnectionAnnotationView +// let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation + +// selectedStopAnnotationView.marker.image = stopAnnotation.markerSymbol + + carrisNetworkController.deselect([.connection]) + + } + } + + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + print("MAPTEST: mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?") + if annotation.isKind(of: NewConnectionAnnotation.self) { + +// return MKAnnotationView(annotation: annotation, reuseIdentifier: "stop") + + let identifier = "stop" + var view: MKAnnotationView + + if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) { + dequeuedView.annotation = annotation + view = dequeuedView + } else { + view = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier) + } + + return view + + } else { + + return nil + + } + + } + + + } + +} + + + + + + + + + + + +class NewConnectionAnnotationView: MKAnnotationView { + + override var annotation: MKAnnotation? { + + willSet { + guard let annotation = newValue as? NewConnectionAnnotation else { + return + } + + canShowCallout = false + +// marker.image = annotation.markerSymbol +// marker.frame = CGRect(x: 0, y: 0, width: 35, height: 35) +// frame = marker.frame + let swiftUIView = CarrisConnectionAnnotationView(connection: annotation.connection) // swiftUIView is View + let viewCtrl = UIHostingController(rootView: swiftUIView) + addSubview(viewCtrl.view) + + } + + } + + + +} + + + +class NewConnectionAnnotation: NSObject, MKAnnotation { + + let name: String + let publicId: Int + + let coordinate: CLLocationCoordinate2D + + let connection: CarrisNetworkModel.Connection + + + init(name: String?, publicId: Int?, latitude: Double, longitude: Double, connection: CarrisNetworkModel.Connection) { + self.name = name ?? "-" + self.publicId = publicId ?? -1 + self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + self.connection = connection + super.init() + } + + +// var title: String? = nil +// +// var subtitle: String? = nil + + + var markerSymbol = UIImage(named: "PinkArrowUp") + +} + From 2092797ae8622d8a534002800852a6849f94b28d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Tue, 8 Nov 2022 23:00:30 +0000 Subject: [PATCH 59/63] Bring back mapkit --- GeoBus.xcodeproj/project.pbxproj | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index 6696ae4c..e103fc55 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -57,6 +57,7 @@ CF82BB0928D7F19A007F0CDB /* LocationCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF82BB0828D7F19A007F0CDB /* LocationCard.swift */; }; CF82BB0B28D7F1C6007F0CDB /* ShareCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF82BB0A28D7F1C6007F0CDB /* ShareCard.swift */; }; CF82BB0D28D7F202007F0CDB /* ContactsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF82BB0C28D7F202007F0CDB /* ContactsCard.swift */; }; + CF88E97A291B170200E22E82 /* NewMapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF88E979291B170200E22E82 /* NewMapView.swift */; }; CFAF0E7A28CE84F400DDAD5B /* MapAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFAF0E7928CE84F400DDAD5B /* MapAnnotations.swift */; }; CFB5D45728EEFE21002368BC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = CFB5D45528EEFE21002368BC /* InfoPlist.strings */; }; CFB5D45A28EEFE6B002368BC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CFB5D45828EEFE6B002368BC /* Localizable.strings */; }; @@ -136,6 +137,7 @@ CF82BB0828D7F19A007F0CDB /* LocationCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationCard.swift; sourceTree = ""; }; CF82BB0A28D7F1C6007F0CDB /* ShareCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareCard.swift; sourceTree = ""; }; CF82BB0C28D7F202007F0CDB /* ContactsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsCard.swift; sourceTree = ""; }; + CF88E979291B170200E22E82 /* NewMapView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewMapView.swift; sourceTree = ""; }; CFAF0E7928CE84F400DDAD5B /* MapAnnotations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapAnnotations.swift; sourceTree = ""; }; CFAF0E7B28CEC78C00DDAD5B /* GeoBus.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = GeoBus.entitlements; sourceTree = ""; }; CFB5D45628EEFE21002368BC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; @@ -193,6 +195,7 @@ CF181FE728CCB7D600248F72 /* ContentView.swift */, CF05F61928CD09A000B4AD58 /* NavBar.swift */, CF47FCB429035D8300AE33B0 /* PresentedSheetView.swift */, + CF88E978291B170200E22E82 /* NewMap */, CF18208828CCBD4600248F72 /* Map */, CF05F62528CD60BD00B4AD58 /* SelectRoute */, CF05F61828CD097700B4AD58 /* RouteDetails */, @@ -397,6 +400,14 @@ path = VehicleDetails; sourceTree = ""; }; + CF88E978291B170200E22E82 /* NewMap */ = { + isa = PBXGroup; + children = ( + CF88E979291B170200E22E82 /* NewMapView.swift */, + ); + path = NewMap; + sourceTree = ""; + }; CFF4875F28D3D35F00E2C13D /* About */ = { isa = PBXGroup; children = ( @@ -540,6 +551,7 @@ CF18207728CCBD2300248F72 /* RouteDetailsAddToFavorites.swift in Sources */, CF6C918428D3F2C9006C3F61 /* UserLocation.swift in Sources */, CFFFAD8928F8ECDE00DFD5FD /* Spinner.swift in Sources */, + CF88E97A291B170200E22E82 /* NewMapView.swift in Sources */, CF18207528CCBD2300248F72 /* VariantWarning.swift in Sources */, CF6C918C28D4B452006C3F61 /* SearchStopInput.swift in Sources */, CF18207628CCBD2300248F72 /* ConnectionsList.swift in Sources */, From db1d6cbcaa3e49376dac5cf0a8e323325fd2a619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Thu, 10 Nov 2022 02:44:20 +0000 Subject: [PATCH 60/63] MapKit advances --- GeoBus/App/Components/ContentView.swift | 24 +- .../App/Components/Map/MapAnnotations.swift | 50 +++ GeoBus/App/Components/NewMap/NewMapView.swift | 386 ++++++++++++++---- GeoBus/App/Controllers/MapController.swift | 9 + 4 files changed, 385 insertions(+), 84 deletions(-) diff --git a/GeoBus/App/Components/ContentView.swift b/GeoBus/App/Components/ContentView.swift index 904521e9..15be084e 100644 --- a/GeoBus/App/Components/ContentView.swift +++ b/GeoBus/App/Components/ContentView.swift @@ -7,14 +7,34 @@ // import SwiftUI +import MapKit struct ContentView: View { + @StateObject private var carrisNetworkController = CarrisNetworkController.shared + @StateObject private var mapController = MapController.shared + + @State var annotations: [GeoBusMKAnnotation] = [ +// GeoBusMKAnnotation(id: 120, coordinate: CLLocationCoordinate2D(latitude: 38.736946, longitude: -9.142685), type: .stop), +// GeoBusMKAnnotation(id: 120, coordinate: CLLocationCoordinate2D(latitude: 38.736946, longitude: -9.142685), type: .stop) + ] + var body: some View { VStack(spacing: 0) { ZStack(alignment: .topTrailing) { - MapView() - .edgesIgnoringSafeArea(.vertical) + MapViewSwiftUI( + region: $mapController.region, + annotations: $annotations + ) + .edgesIgnoringSafeArea(.vertical) + .onAppear() { + print("HOWMANY willAdd Stops: \(carrisNetworkController.allStops.count)") + for stop in carrisNetworkController.allStops { + self.annotations.append( + GeoBusMKAnnotation(id: stop.id, coordinate: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), type: .stop) + ) + } + } VStack(spacing: 15) { AboutGeoBus() Spacer() diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index ca7707ff..3139596f 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -134,3 +134,53 @@ struct StopAnnotationView: View { } } + + + + + + + + + +struct NewStopMKAnnotationView: View { + + public let stopId: Int + + @State private var stop: CarrisNetworkModel.Stop? + + @StateObject private var sheetController = SheetController.shared + @StateObject private var mapController = MapController.shared + @StateObject private var carrisNetworkController = CarrisNetworkController.shared + + var body: some View { + VStack { + if (stop != nil) { +// VStack { + if (carrisNetworkController.activeStop?.id == self.stop?.id) { + StopIcon(style: .selected) + } else if (mapController.region.span.latitudeDelta < 0.0025 || mapController.region.span.longitudeDelta < 0.0025) { + StopIcon(style: .standard) + } else { + StopIcon(style: .mini) + } +// } +// .onTapGesture { +// TapticEngine.impact.feedback(.light) +// carrisNetworkController.select(stop: self.stop!) +// sheetController.present(sheet: .StopDetails) +// // withAnimation(.easeIn(duration: 0.5)) { +// // mapController.centerMapOnCoordinates(lat: self.stop.lat, lng: self.stop.lng) +// // } +// } + } else { + Circle().foregroundColor(.red) + } + } + .onAppear() { + self.stop = carrisNetworkController.find(stop: stopId) + print("HEYSTOP: stop find result: \(self.stop?.id)") + } + } + +} diff --git a/GeoBus/App/Components/NewMap/NewMapView.swift b/GeoBus/App/Components/NewMap/NewMapView.swift index f103192e..ae07c9e0 100644 --- a/GeoBus/App/Components/NewMap/NewMapView.swift +++ b/GeoBus/App/Components/NewMap/NewMapView.swift @@ -11,6 +11,163 @@ import SwiftUI import MapKit +struct MapViewSwiftUI: UIViewRepresentable { + + private let mapView = MKMapView() + + @Binding var region: MKCoordinateRegion + @Binding var annotations: [GeoBusMKAnnotation] + + + func makeUIView(context: UIViewRepresentableContext) -> MKMapView { + mapView.delegate = context.coordinator + mapView.region = self.region + mapView.showsUserLocation = true + mapView.userTrackingMode = .follow + + mapView.register(StopMKAnnotationView.self, forAnnotationViewWithReuseIdentifier: StopMKAnnotationView.reuseIdentifier) + + return mapView + } + + + func updateUIView(_ uiView: MKMapView, context: Context) { + + // First, make sure we are dealing with annotation of type GeoBusMKAnnotation + guard let currentAnnotations = uiView.annotations as? [GeoBusMKAnnotation] else { return } + + // Find out the excess annotations that should be removed from the map + // The following works as: [a, b, c, d, e] - [a, c, e] = [b, d] + let annotationsToRemove = Array(Set(currentAnnotations).subtracting(annotations)) + + // Update the view with annotations + uiView.removeAnnotations(annotationsToRemove) + uiView.addAnnotations(annotations) + + print("HOWMANY removeAnnotations: \(annotationsToRemove.count)") + print("HOWMANY uiView displayed annotations: \(uiView.annotations.count)") + print("HOWMANY ----------") + } + + + func makeCoordinator() -> MapViewSwiftUICoordinator { + MapViewSwiftUICoordinator(self) + } + +} + + + +final class MapViewSwiftUICoordinator: NSObject, MKMapViewDelegate { + + var parentSwiftUIView: MapViewSwiftUI + + init(_ parentSwiftUIView: MapViewSwiftUI) { + self.parentSwiftUIView = parentSwiftUIView + } + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + guard let annotation = annotation as? GeoBusMKAnnotation else { return nil } + + switch annotation.type { + case .stop: + return mapView.dequeueReusableAnnotationView(withIdentifier: StopMKAnnotationView.reuseIdentifier, for: annotation) + default: + return nil + } + + } + + + func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) { + guard let annotation = annotation as? GeoBusMKAnnotation else { return nil } + + switch annotation.type { + case .stop: + return \\ activate stop in CarrisNetworkController + default: + return nil + } + } + +} + + + + + + +final class GeoBusMKAnnotation: NSObject, MKAnnotation { + + let id: Int + let type: AnnotationType + var coordinate: CLLocationCoordinate2D + + enum AnnotationType { + case stop + case vehicle + } + + init(id: Int, coordinate: CLLocationCoordinate2D, type: AnnotationType) { + self.id = id + self.coordinate = coordinate + self.type = type + } + + override func isEqual(_ object: Any?) -> Bool { + if let annot = object as? GeoBusMKAnnotation{ + // Add your defintion of equality here. i.e what determines if two Annotations are equal. + let equalCoordinates = annot.coordinate == self.coordinate + let equalType = annot.type == self.type + return equalCoordinates && equalType +// return annot.id == id +// return annot.coordinate.latitude == coordinate.latitude && annot.coordinate.longitude == coordinate.longitude + } + return false + } + +} + + +final class StopMKAnnotationView: MKAnnotationView { + + static let reuseIdentifier = "stop" + + override var annotation: MKAnnotation? { + willSet { + guard let newValue = newValue as? GeoBusMKAnnotation else { return } + +// clusteringIdentifier = "stop" + canShowCallout = false + + let swiftUIView = NewStopMKAnnotationView(stopId: newValue.id) + let uiKitView = UIHostingController(rootView: swiftUIView) + addSubview(uiKitView.view) + + } + + } + +} + + + + + + + + +// -------------------------------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------------------------------------------------------- + + + + + + struct NewMapView: UIViewRepresentable { private let mapView = MKMapView() @@ -24,10 +181,10 @@ struct NewMapView: UIViewRepresentable { mapView.mapType = MKMapType.standard mapView.showsUserLocation = true mapView.showsTraffic = true - mapView.isRotateEnabled = false - mapView.isPitchEnabled = false + mapView.isRotateEnabled = true + mapView.isPitchEnabled = true - mapView.register(NewConnectionAnnotationView.self, forAnnotationViewWithReuseIdentifier: "stop") + mapView.register(NewStopAnnotationView.self, forAnnotationViewWithReuseIdentifier: "stop") // Set initial location in Lisbon let lisbon = CLLocation(latitude: 38.721917, longitude: -9.137732) @@ -38,33 +195,56 @@ struct NewMapView: UIViewRepresentable { } + + + +// override func viewDidLoad() { +// super.viewDidLoad() +// setupCompassButton() +// setupUserTrackingButtonAndScaleView() +// registerAnnotationViewClasses() +// +// locationManager.delegate = self +// locationManager.requestWhenInUseAuthorization() +// locationManager.startUpdatingLocation() +// +// loadDataForMapRegionAndBikes() +// } + + + func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext) { - var annotationsToAdd: [MKAnnotation] = [] - print("MAPTEST: is called updateUIView") // print("MAPTEST: carrisNetworkController.activeRoute: \(carrisNetworkController.activeRoute)") - if (carrisNetworkController.activeRoute != nil) { - for connection in carrisNetworkController.activeRoute?.variants[0].ascendingItinerary ?? [] { + + if (uiView.annotations.isEmpty) { + + var annotationsToAdd: [MKAnnotation] = [] + + carrisNetworkController.allStops.forEach({ annotationsToAdd.append( - NewConnectionAnnotation( - name: connection.stop.name, - publicId: connection.stop.id, - latitude: connection.stop.lat, - longitude: connection.stop.lng, - connection: connection + NewStopAnnotation( + name: $0.name, + publicId: $0.id, + latitude: $0.lat, + longitude: $0.lng, + stop: $0 ) ) - } + }) + + uiView.addAnnotations(annotationsToAdd) + } // Update whatever was set to update -// mapView.removeAnnotations([]) - uiView.addAnnotations(annotationsToAdd) +// uiView.removeAnnotations(uiView.annotations) +// uiView.addAnnotations(annotationsToAdd) - uiView.showAnnotations(annotationsToAdd, animated: true) +// uiView.showAnnotations(annotationsToAdd, animated: true) } @@ -81,7 +261,7 @@ struct NewMapView: UIViewRepresentable { private let control: NewMapView - private let appstate = Appstate.shared + private let sheetController = SheetController.shared private let carrisNetworkController = CarrisNetworkController.shared init(control: NewMapView) { @@ -90,19 +270,35 @@ struct NewMapView: UIViewRepresentable { - @MainActor func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { +// @MainActor func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { +// print("MAPTEST: mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)") +// if view.isKind(of: NewConnectionAnnotationView.self) { +// +// let selectedStopAnnotationView = view as! NewConnectionAnnotationView +// let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation +// +//// selectedStopAnnotationView.marker.image = UIImage(named: "GreenInfo") +// +// TapticEngine.impact.feedback(.light) +// _ = carrisNetworkController.select(connection: stopAnnotation.connection) +// sheetController.present(sheet: .ConnectionDetails) +// +// } +// } + + + @MainActor func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) { print("MAPTEST: mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)") - if view.isKind(of: NewConnectionAnnotationView.self) { - - let selectedStopAnnotationView = view as! NewConnectionAnnotationView - let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation - -// selectedStopAnnotationView.marker.image = UIImage(named: "GreenInfo") - + if annotation.isKind(of: NewStopAnnotation.self) { + + let stopAnnotation = annotation as! NewStopAnnotation + + print("MAPTEST: selected annotation stop id: \(stopAnnotation.stop.id)") + TapticEngine.impact.feedback(.light) - _ = carrisNetworkController.select(connection: stopAnnotation.connection) - appstate.present(sheet: .carris_connectionDetails) - + _ = carrisNetworkController.select(stop: stopAnnotation.stop) + sheetController.present(sheet: .ConnectionDetails) + } } @@ -110,46 +306,66 @@ struct NewMapView: UIViewRepresentable { @MainActor func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { print("MAPTEST: mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView)") - if view.isKind(of: NewConnectionAnnotationView.self) { - -// let selectedStopAnnotationView = view as! NewConnectionAnnotationView -// let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation - -// selectedStopAnnotationView.marker.image = stopAnnotation.markerSymbol - - carrisNetworkController.deselect([.connection]) - - } +// if view.isKind(of: NewConnectionAnnotationView.self) { +// +//// let selectedStopAnnotationView = view as! NewConnectionAnnotationView +//// let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation +// +//// selectedStopAnnotationView.marker.image = stopAnnotation.markerSymbol +// + carrisNetworkController.deselect([.stop]) +// +// } } + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { - print("MAPTEST: mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?") - if annotation.isKind(of: NewConnectionAnnotation.self) { - -// return MKAnnotationView(annotation: annotation, reuseIdentifier: "stop") - - let identifier = "stop" - var view: MKAnnotationView - - if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) { - dequeuedView.annotation = annotation - view = dequeuedView - } else { - view = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier) - } - - return view - + guard let annotation = annotation as? NewStopAnnotation else { return nil } + + let identifier = "stop" + var view: MKAnnotationView + + if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) { + dequeuedView.annotation = annotation + view = dequeuedView } else { - - return nil - + view = NewStopAnnotationView(annotation: annotation, reuseIdentifier: NewStopAnnotationView.ReuseID) //MKAnnotationView(annotation: annotation, reuseIdentifier: identifier) } + return view +// return NewStopAnnotationView(annotation: annotation, reuseIdentifier: NewStopAnnotationView.ReuseID) } +// func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { +// print("MAPTEST: mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?") +// if annotation.isKind(of: NewStopAnnotation.self) { +// +//// return MKAnnotationView(annotation: annotation, reuseIdentifier: "stop") +// +// let identifier = "stop" +// var view: MKAnnotationView +// +// if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) { +// dequeuedView.annotation = annotation +// view = dequeuedView +// } else { +// view = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier) +// } +//// +// return view +// +// } else { +// +// return nil +// +// } +// +// } + + } } @@ -159,26 +375,40 @@ struct NewMapView: UIViewRepresentable { - - - - - -class NewConnectionAnnotationView: MKAnnotationView { +class NewStopAnnotationView: MKAnnotationView { + + static let ReuseID = "stop" + +// override init(annotation: MKAnnotation?, reuseIdentifier: String?) { +// super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) +// clusteringIdentifier = "stop" +// } +// +// required init?(coder aDecoder: NSCoder) { +// fatalError("init(coder:) has not been implemented") +// } + +// override func prepareForDisplay() { +// super.prepareForDisplay() +// displayPriority = .defaultLow +//// markerTintColor = UIColor.unicycleColor +//// glyphImage = #imageLiteral(resourceName: "unicycle") +// let swiftUIView = Circle().foregroundColor(.blue).frame(width: 5, height: 5) // swiftUIView is View +// let viewCtrl = UIHostingController(rootView: swiftUIView) +// addSubview(viewCtrl.view) +// } override var annotation: MKAnnotation? { willSet { - guard let annotation = newValue as? NewConnectionAnnotation else { + guard let annotation = newValue as? NewStopAnnotation else { return } + clusteringIdentifier = "stop" canShowCallout = false -// marker.image = annotation.markerSymbol -// marker.frame = CGRect(x: 0, y: 0, width: 35, height: 35) -// frame = marker.frame - let swiftUIView = CarrisConnectionAnnotationView(connection: annotation.connection) // swiftUIView is View + let swiftUIView = StopAnnotationView(stop: annotation.stop) // swiftUIView is View let viewCtrl = UIHostingController(rootView: swiftUIView) addSubview(viewCtrl.view) @@ -192,31 +422,23 @@ class NewConnectionAnnotationView: MKAnnotationView { -class NewConnectionAnnotation: NSObject, MKAnnotation { +class NewStopAnnotation: NSObject, MKAnnotation { let name: String let publicId: Int let coordinate: CLLocationCoordinate2D - let connection: CarrisNetworkModel.Connection + let stop: CarrisNetworkModel.Stop - init(name: String?, publicId: Int?, latitude: Double, longitude: Double, connection: CarrisNetworkModel.Connection) { + init(name: String?, publicId: Int?, latitude: Double, longitude: Double, stop: CarrisNetworkModel.Stop) { self.name = name ?? "-" self.publicId = publicId ?? -1 self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - self.connection = connection + self.stop = stop super.init() } - -// var title: String? = nil -// -// var subtitle: String? = nil - - - var markerSymbol = UIImage(named: "PinkArrowUp") - } diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index 3595915a..697b0ec0 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -462,3 +462,12 @@ extension MKCoordinateRegion: Equatable { return true } } + + +extension CLLocationCoordinate2D: Equatable { + public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool { + if (lhs.latitude != rhs.latitude) { return false } + if (lhs.longitude != rhs.longitude) { return false } + return true + } +} From af99b23469a630996cd38f3acc92bcbdcbcd79b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sun, 13 Nov 2022 02:18:09 +0000 Subject: [PATCH 61/63] Messy code to save --- GeoBus.xcodeproj/project.pbxproj | 4 + GeoBus/App/Components/ContentView.swift | 83 +++++++++-- .../App/Components/Map/MapAnnotations.swift | 42 +----- GeoBus/App/Components/NewMap/NewMapView.swift | 138 ++++++++++++++++-- .../RouteDetails/RouteDetailsView.swift | 8 +- GeoBus/App/Controllers/MapController.swift | 8 +- GeoBus/App/Extensions/BackgroundThreads.swift | 40 +++++ GeoBus/App/Layout/StopIcon.swift | 7 +- 8 files changed, 261 insertions(+), 69 deletions(-) create mode 100644 GeoBus/App/Extensions/BackgroundThreads.swift diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index e103fc55..cf90877b 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -71,6 +71,7 @@ CFDD014D28D66D9B0070FE4B /* CloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFDD014C28D66D9B0070FE4B /* CloseButton.swift */; }; CFED5F8A2919B7820062045B /* SheetController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFED5F892919B7820062045B /* SheetController.swift */; }; CFED5F8C2919CBBD0062045B /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFED5F8B2919CBBD0062045B /* Debounce.swift */; }; + CFEE4EA229202637005DFA1B /* BackgroundThreads.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFEE4EA129202637005DFA1B /* BackgroundThreads.swift */; }; CFEF85C228D34E4F00A29526 /* crowdin.yml in Resources */ = {isa = PBXBuildFile; fileRef = CFEF85C128D34E4F00A29526 /* crowdin.yml */; }; CFEF85C528D34E6300A29526 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = CFEF85C328D34E6300A29526 /* README.md */; }; CFEF85C628D34E6300A29526 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = CFEF85C428D34E6300A29526 /* LICENSE */; }; @@ -162,6 +163,7 @@ CFDD014C28D66D9B0070FE4B /* CloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloseButton.swift; sourceTree = ""; }; CFED5F892919B7820062045B /* SheetController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetController.swift; sourceTree = ""; }; CFED5F8B2919CBBD0062045B /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = ""; }; + CFEE4EA129202637005DFA1B /* BackgroundThreads.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundThreads.swift; sourceTree = ""; }; CFEF85C128D34E4F00A29526 /* crowdin.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = crowdin.yml; sourceTree = ""; }; CFEF85C328D34E6300A29526 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; CFEF85C428D34E6300A29526 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; @@ -328,6 +330,7 @@ CFDC15EE28D292FB00A4BE49 /* ViewSize.swift */, CF5094C428FC279A00EDD320 /* Array.swift */, CFED5F8B2919CBBD0062045B /* Debounce.swift */, + CFEE4EA129202637005DFA1B /* BackgroundThreads.swift */, ); path = Extensions; sourceTree = ""; @@ -567,6 +570,7 @@ CFB71F82290CAFB500B37E69 /* LoadingSheet.swift in Sources */, CF0C256C29031B2C00B03052 /* CarrisCommunityAPIModel.swift in Sources */, CF82BB0928D7F19A007F0CDB /* LocationCard.swift in Sources */, + CFEE4EA229202637005DFA1B /* BackgroundThreads.swift in Sources */, CF18207928CCBD2300248F72 /* RouteDetailsVehiclesQuantity.swift in Sources */, CF181FE628CCB7D600248F72 /* GeoBusApp.swift in Sources */, CF18205028CCBCE900248F72 /* OpenExternalLinkButton.swift in Sources */, diff --git a/GeoBus/App/Components/ContentView.swift b/GeoBus/App/Components/ContentView.swift index 15be084e..86b98fb0 100644 --- a/GeoBus/App/Components/ContentView.swift +++ b/GeoBus/App/Components/ContentView.swift @@ -14,27 +14,84 @@ struct ContentView: View { @StateObject private var carrisNetworkController = CarrisNetworkController.shared @StateObject private var mapController = MapController.shared - @State var annotations: [GeoBusMKAnnotation] = [ -// GeoBusMKAnnotation(id: 120, coordinate: CLLocationCoordinate2D(latitude: 38.736946, longitude: -9.142685), type: .stop), -// GeoBusMKAnnotation(id: 120, coordinate: CLLocationCoordinate2D(latitude: 38.736946, longitude: -9.142685), type: .stop) - ] - var body: some View { VStack(spacing: 0) { ZStack(alignment: .topTrailing) { MapViewSwiftUI( region: $mapController.region, - annotations: $annotations + annotations: $mapController.newAnnotations ) .edgesIgnoringSafeArea(.vertical) - .onAppear() { - print("HOWMANY willAdd Stops: \(carrisNetworkController.allStops.count)") - for stop in carrisNetworkController.allStops { - self.annotations.append( - GeoBusMKAnnotation(id: stop.id, coordinate: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), type: .stop) - ) +// .onAppear() { +// .onReceive(mapController.$region.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)) { newRegion in + .onChange(of: mapController.region, perform: { newRegion in + var tempNewAnnotations: [GeoBusMKAnnotation] = [] + if (newRegion.span.latitudeDelta < 0.01 || newRegion.span.longitudeDelta < 0.01) { + + let latTop = newRegion.center.latitude + newRegion.span.latitudeDelta + let latBottom = newRegion.center.latitude - newRegion.span.latitudeDelta + + let lngRight = newRegion.center.longitude + newRegion.span.longitudeDelta + let lngLeft = newRegion.center.longitude - newRegion.span.longitudeDelta + + for stop in carrisNetworkController.allStops { + + let isBetweenLats = stop.lat > latBottom && stop.lat < latTop + let isBetweenLngs = stop.lng > lngLeft && stop.lng < lngRight + + if (isBetweenLats && isBetweenLngs) { + tempNewAnnotations.append( + GeoBusMKAnnotation(type: .stop, id: stop.id, coordinate: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng)) + ) + } + } } - } +// mapController.newAnnotations.removeAll() + mapController.newAnnotations = tempNewAnnotations + }) +// .onChange(of: mapController.$region, perform: { newRegion in +// .onReceive(mapController.$region.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)) { newRegion in +// if (newRegion.span.latitudeDelta < 0.005 || newRegion.span.longitudeDelta < 0.005) { +// +// var tempMatchingAnnotations: [GeoBusMKAnnotation] = [] +// +// let latTop = newRegion.center.latitude + newRegion.span.latitudeDelta +// let latBottom = newRegion.center.latitude - newRegion.span.latitudeDelta +// +// let lngRight = newRegion.center.longitude + newRegion.span.longitudeDelta +// let lngLeft = newRegion.center.longitude - newRegion.span.longitudeDelta +// +// +// for stop in carrisNetworkController.allStops { +// +// // Checks +// let isBetweenLats = stop.lat > latBottom && stop.lat < latTop +// let isBetweenLngs = stop.lng > lngLeft && stop.lng < lngRight +// +// if (isBetweenLats && isBetweenLngs) { +// tempMatchingAnnotations.append( +// GeoBusMKAnnotation( +// id: stop.id, +// coordinate: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), +// type: .stop +// ) +// ) +// } +// +// } +// +// mapController.newAnnotations = tempMatchingAnnotations +// +// } else { +// mapController.newAnnotations.removeAll() +// } +// } + + + + + + VStack(spacing: 15) { AboutGeoBus() Spacer() diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index 3139596f..48304666 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -143,44 +143,4 @@ struct StopAnnotationView: View { -struct NewStopMKAnnotationView: View { - - public let stopId: Int - - @State private var stop: CarrisNetworkModel.Stop? - - @StateObject private var sheetController = SheetController.shared - @StateObject private var mapController = MapController.shared - @StateObject private var carrisNetworkController = CarrisNetworkController.shared - - var body: some View { - VStack { - if (stop != nil) { -// VStack { - if (carrisNetworkController.activeStop?.id == self.stop?.id) { - StopIcon(style: .selected) - } else if (mapController.region.span.latitudeDelta < 0.0025 || mapController.region.span.longitudeDelta < 0.0025) { - StopIcon(style: .standard) - } else { - StopIcon(style: .mini) - } -// } -// .onTapGesture { -// TapticEngine.impact.feedback(.light) -// carrisNetworkController.select(stop: self.stop!) -// sheetController.present(sheet: .StopDetails) -// // withAnimation(.easeIn(duration: 0.5)) { -// // mapController.centerMapOnCoordinates(lat: self.stop.lat, lng: self.stop.lng) -// // } -// } - } else { - Circle().foregroundColor(.red) - } - } - .onAppear() { - self.stop = carrisNetworkController.find(stop: stopId) - print("HEYSTOP: stop find result: \(self.stop?.id)") - } - } - -} + diff --git a/GeoBus/App/Components/NewMap/NewMapView.swift b/GeoBus/App/Components/NewMap/NewMapView.swift index ae07c9e0..421ed55f 100644 --- a/GeoBus/App/Components/NewMap/NewMapView.swift +++ b/GeoBus/App/Components/NewMap/NewMapView.swift @@ -18,12 +18,15 @@ struct MapViewSwiftUI: UIViewRepresentable { @Binding var region: MKCoordinateRegion @Binding var annotations: [GeoBusMKAnnotation] + @StateObject var carrisNetworkController = CarrisNetworkController.shared + func makeUIView(context: UIViewRepresentableContext) -> MKMapView { mapView.delegate = context.coordinator mapView.region = self.region mapView.showsUserLocation = true mapView.userTrackingMode = .follow + mapView.preferredConfiguration = MKStandardMapConfiguration(elevationStyle: .realistic, emphasisStyle: .muted) mapView.register(StopMKAnnotationView.self, forAnnotationViewWithReuseIdentifier: StopMKAnnotationView.reuseIdentifier) @@ -41,6 +44,7 @@ struct MapViewSwiftUI: UIViewRepresentable { let annotationsToRemove = Array(Set(currentAnnotations).subtracting(annotations)) // Update the view with annotations +// uiView.removeAnnotations(uiView.annotations) uiView.removeAnnotations(annotationsToRemove) uiView.addAnnotations(annotations) @@ -66,12 +70,77 @@ final class MapViewSwiftUICoordinator: NSObject, MKMapViewDelegate { self.parentSwiftUIView = parentSwiftUIView } +// +// func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { + @MainActor func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + DispatchQueue.main.async { [self] in + self.parentSwiftUIView.region = mapView.region + } + } + +// @MainActor func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { +//// @MainActor func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { +// DispatchQueue.main.async { [self] in +// self.parentSwiftUIView.region = mapView.region +// if (mapView.region.span.latitudeDelta < 0.005 || mapView.region.span.longitudeDelta < 0.005) { +// +// var tempMatchingAnnotations: [GeoBusMKAnnotation] = [] +// +// let latTop = mapView.region.center.latitude + mapView.region.span.latitudeDelta +// let latBottom = mapView.region.center.latitude - mapView.region.span.latitudeDelta +// +// let lngRight = mapView.region.center.longitude + mapView.region.span.longitudeDelta +// let lngLeft = mapView.region.center.longitude - mapView.region.span.longitudeDelta +// +// +// for stop in parentSwiftUIView.carrisNetworkController.allStops { +// +// // Checks +// let isBetweenLats = stop.lat > latBottom && stop.lat < latTop +// let isBetweenLngs = stop.lng > lngLeft && stop.lng < lngRight +// +// if (isBetweenLats && isBetweenLngs) { +// tempMatchingAnnotations.append( +// GeoBusMKAnnotation( +// id: stop.id, +// coordinate: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), +// type: .stop +// ) +// ) +// } +// +// } +// +// self.parentSwiftUIView.annotations = tempMatchingAnnotations +// +// // First, make sure we are dealing with annotation of type GeoBusMKAnnotation +//// guard let currentAnnotations = mapView.annotations as? [GeoBusMKAnnotation] else { return } +// +// // Find out the excess annotations that should be removed from the map +// // The following works as: [a, b, c, d, e] - [a, c, e] = [b, d] +//// let annotationsToRemove = Array(Set(currentAnnotations).subtracting(tempMatchingAnnotations)) +// +// // Update the view with annotations +//// mapView.removeAnnotations(annotationsToRemove) +//// mapView.addAnnotations(tempMatchingAnnotations) +// +//// annotations = tempMatchingAnnotations +// +// } else { +// self.parentSwiftUIView.annotations = [] +//// mapView.removeAnnotations(mapView.annotations) +// } +// } +// } + + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { guard let annotation = annotation as? GeoBusMKAnnotation else { return nil } switch annotation.type { case .stop: - return mapView.dequeueReusableAnnotationView(withIdentifier: StopMKAnnotationView.reuseIdentifier, for: annotation) +// return mapView.dequeueReusableAnnotationView(withIdentifier: StopMKAnnotationView.reuseIdentifier, for: annotation) + return StopMKAnnotationView(annotation: annotation, reuseIdentifier: "stop") default: return nil } @@ -79,14 +148,30 @@ final class MapViewSwiftUICoordinator: NSObject, MKMapViewDelegate { } - func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) { - guard let annotation = annotation as? GeoBusMKAnnotation else { return nil } + @MainActor func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) { + guard let annotation = annotation as? GeoBusMKAnnotation else { return } switch annotation.type { case .stop: - return \\ activate stop in CarrisNetworkController + DispatchQueue.main.async { [self] in + _ = parentSwiftUIView.carrisNetworkController.select(stop: annotation.id) + SheetController.shared.present(sheet: .StopDetails) + } default: - return nil + return + } + } + + + @MainActor func mapView(_ mapView: MKMapView, didDeselect annotation: MKAnnotation) { + guard let annotation = annotation as? GeoBusMKAnnotation else { return } + + switch annotation.type { + case .stop: + parentSwiftUIView.carrisNetworkController.deselect([.stop]) + SheetController.shared.dismiss() + default: + return } } @@ -108,7 +193,7 @@ final class GeoBusMKAnnotation: NSObject, MKAnnotation { case vehicle } - init(id: Int, coordinate: CLLocationCoordinate2D, type: AnnotationType) { + init(type: AnnotationType, id: Int, coordinate: CLLocationCoordinate2D) { self.id = id self.coordinate = coordinate self.type = type @@ -120,10 +205,9 @@ final class GeoBusMKAnnotation: NSObject, MKAnnotation { let equalCoordinates = annot.coordinate == self.coordinate let equalType = annot.type == self.type return equalCoordinates && equalType -// return annot.id == id -// return annot.coordinate.latitude == coordinate.latitude && annot.coordinate.longitude == coordinate.longitude + } else { + return false } - return false } } @@ -137,13 +221,11 @@ final class StopMKAnnotationView: MKAnnotationView { willSet { guard let newValue = newValue as? GeoBusMKAnnotation else { return } -// clusteringIdentifier = "stop" canShowCallout = false let swiftUIView = NewStopMKAnnotationView(stopId: newValue.id) let uiKitView = UIHostingController(rootView: swiftUIView) addSubview(uiKitView.view) - } } @@ -153,6 +235,38 @@ final class StopMKAnnotationView: MKAnnotationView { +struct NewStopMKAnnotationView: View { + + public let stopId: Int + + @StateObject private var carrisNetworkController = CarrisNetworkController.shared + + var body: some View { + VStack { + if (carrisNetworkController.activeStop?.id == self.stopId) { + StopIcon(style: .selected) + } else { + StopIcon(style: .mini) + } + } + } + +} + + + + + + + + + + + + + + + @@ -427,7 +541,7 @@ class NewStopAnnotation: NSObject, MKAnnotation { let name: String let publicId: Int - let coordinate: CLLocationCoordinate2D + @objc dynamic var coordinate: CLLocationCoordinate2D let stop: CarrisNetworkModel.Stop diff --git a/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift b/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift index 671ed612..64be92da 100644 --- a/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift +++ b/GeoBus/App/Components/RouteDetails/RouteDetailsView.swift @@ -11,8 +11,10 @@ import SwiftUI struct RouteDetailsView: View { @ObservedObject private var appstate = Appstate.shared + @ObservedObject private var mapController = MapController.shared @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared + @State var mapspan: Double = 0.0 // Initial screen simply explaining how to select a route, // also with app version and build. @@ -21,9 +23,13 @@ struct RouteDetailsView: View { VStack(alignment: .leading) { Spacer() HStack { - Text("← Choose a Route") +// Text("← Choose a Route") + Text("Span: \(mapspan)") .font(Font.system(size: 15, weight: .bold, design: .default)) .foregroundColor(Color(.secondaryLabel)) + .onReceive(mapController.$region, perform: { newRegion in + self.mapspan = newRegion.span.latitudeDelta + }) Spacer() } Spacer() diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index 697b0ec0..0011402b 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -26,7 +26,10 @@ final class MapController: ObservableObject { /* MARK: - SECTION 2: PUBLISHED PROPERTIES */ /* Here are all the @Published variables that can be consumed by the app views. */ - @Published var region = MKCoordinateRegion() + @Published var region = MKCoordinateRegion( + center: CLLocationCoordinate2D(latitude: 38.721917, longitude: -9.137732), + span: MKCoordinateSpan(latitudeDelta: CLLocationDistance(15000), longitudeDelta: CLLocationDistance(15000)) + ) @Published var locationManager = CLLocationManager() @Published var showLocationNotAllowedAlert: Bool = false @@ -34,6 +37,9 @@ final class MapController: ObservableObject { @Published var visibleAnnotations: [GenericMapAnnotation] = [] + @Published var newAnnotations: [GeoBusMKAnnotation] = [] + + /* * */ /* MARK: - SECTION 3: SHARED INSTANCE */ /* To allow the same instance of this class to be available accross the whole app, */ diff --git a/GeoBus/App/Extensions/BackgroundThreads.swift b/GeoBus/App/Extensions/BackgroundThreads.swift new file mode 100644 index 00000000..50d3b80b --- /dev/null +++ b/GeoBus/App/Extensions/BackgroundThreads.swift @@ -0,0 +1,40 @@ +// +// BackgroundThreads.swift +// GeoBus +// +// Created by João de Vasconcelos on 12/11/2022. +// + +import Foundation + +extension DispatchQueue { + + static func background(delay: Double = 0.0, background: (()->Void)? = nil, completion: (() -> Void)? = nil) { + DispatchQueue.global(qos: .background).async { + background?() + if let completion = completion { + DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: { + completion() + }) + } + } + } +} + +// USAGE + +//DispatchQueue.background(delay: 3.0, background: { +// // do something in background +//}, completion: { +// // when background job finishes, wait 3 seconds and do something in main thread +//}) +// +//DispatchQueue.background(background: { +// // do something in background +//}, completion:{ +// // when background job finished, do something in main thread +//}) +// +//DispatchQueue.background(delay: 3.0, completion:{ +// // do something in main thread after 3 seconds +//}) diff --git a/GeoBus/App/Layout/StopIcon.swift b/GeoBus/App/Layout/StopIcon.swift index a3f867ef..aa26f93a 100644 --- a/GeoBus/App/Layout/StopIcon.swift +++ b/GeoBus/App/Layout/StopIcon.swift @@ -12,6 +12,8 @@ struct StopIcon: View { public let style: Style public let orderInRoute: Int? + @ObservedObject private var mapController = MapController.shared + init(style: Style = .standard, orderInRoute: Int? = nil, direction: CarrisNetworkModel.Direction? = nil) { self.orderInRoute = orderInRoute @@ -47,7 +49,10 @@ struct StopIcon: View { case .standard, .ascending, .descending, .circular, .muted: return 25 case .mini: - return 10 + let mapZoom = mapController.region.span.latitudeDelta + var size = -440.0 * mapZoom + 12 + if (size < 0) { size = 0 } + return size case .selected: return 25 * 1.5 } From 05eda4e66c34fba1272ee4389e7e194a9a776f71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sun, 13 Nov 2022 19:32:10 +0000 Subject: [PATCH 62/63] Functioning annotations and vehicles with MKMap --- GeoBus/App/Components/ContentView.swift | 81 ++-- GeoBus/App/Components/NewMap/NewMapView.swift | 458 ++++-------------- GeoBus/App/Controllers/MapController.swift | 48 +- .../Carris/CarrisNetworkController.swift | 2 +- 4 files changed, 170 insertions(+), 419 deletions(-) diff --git a/GeoBus/App/Components/ContentView.swift b/GeoBus/App/Components/ContentView.swift index 86b98fb0..87b6ef40 100644 --- a/GeoBus/App/Components/ContentView.swift +++ b/GeoBus/App/Components/ContentView.swift @@ -11,28 +11,40 @@ import MapKit struct ContentView: View { - @StateObject private var carrisNetworkController = CarrisNetworkController.shared @StateObject private var mapController = MapController.shared + @StateObject private var carrisNetworkController = CarrisNetworkController.shared var body: some View { VStack(spacing: 0) { ZStack(alignment: .topTrailing) { MapViewSwiftUI( region: $mapController.region, - annotations: $mapController.newAnnotations + camera: $mapController.mapCamera, + annotations: $mapController.allAnnotations ) .edgesIgnoringSafeArea(.vertical) -// .onAppear() { -// .onReceive(mapController.$region.debounce(for: .seconds(0.5), scheduler: DispatchQueue.main)) { newRegion in + .onReceive(carrisNetworkController.$activeVehicles) { newVehiclesList in + var tempNewAnnotations: [GeoBusMKAnnotation] = [] + for vehicle in carrisNetworkController.activeVehicles { + tempNewAnnotations.append( + GeoBusMKAnnotation( + type: .vehicle, + id: vehicle.id, + coordinate: CLLocationCoordinate2D(latitude: vehicle.lat ?? 0, longitude: vehicle.lng ?? 0) + ) + ) + } + mapController.add(annotations: tempNewAnnotations, ofType: .vehicle) + } .onChange(of: mapController.region, perform: { newRegion in var tempNewAnnotations: [GeoBusMKAnnotation] = [] if (newRegion.span.latitudeDelta < 0.01 || newRegion.span.longitudeDelta < 0.01) { - let latTop = newRegion.center.latitude + newRegion.span.latitudeDelta - let latBottom = newRegion.center.latitude - newRegion.span.latitudeDelta + let latTop = newRegion.center.latitude + newRegion.span.latitudeDelta + 0.01 + let latBottom = newRegion.center.latitude - newRegion.span.latitudeDelta - 0.01 - let lngRight = newRegion.center.longitude + newRegion.span.longitudeDelta - let lngLeft = newRegion.center.longitude - newRegion.span.longitudeDelta + let lngRight = newRegion.center.longitude + newRegion.span.longitudeDelta + 0.01 + let lngLeft = newRegion.center.longitude - newRegion.span.longitudeDelta - 0.01 for stop in carrisNetworkController.allStops { @@ -41,57 +53,18 @@ struct ContentView: View { if (isBetweenLats && isBetweenLngs) { tempNewAnnotations.append( - GeoBusMKAnnotation(type: .stop, id: stop.id, coordinate: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng)) + GeoBusMKAnnotation( + type: .stop, + id: stop.id, + coordinate: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng) + ) ) } + } } -// mapController.newAnnotations.removeAll() - mapController.newAnnotations = tempNewAnnotations + mapController.add(annotations: tempNewAnnotations, ofType: .stop) }) -// .onChange(of: mapController.$region, perform: { newRegion in -// .onReceive(mapController.$region.debounce(for: .seconds(0.1), scheduler: DispatchQueue.main)) { newRegion in -// if (newRegion.span.latitudeDelta < 0.005 || newRegion.span.longitudeDelta < 0.005) { -// -// var tempMatchingAnnotations: [GeoBusMKAnnotation] = [] -// -// let latTop = newRegion.center.latitude + newRegion.span.latitudeDelta -// let latBottom = newRegion.center.latitude - newRegion.span.latitudeDelta -// -// let lngRight = newRegion.center.longitude + newRegion.span.longitudeDelta -// let lngLeft = newRegion.center.longitude - newRegion.span.longitudeDelta -// -// -// for stop in carrisNetworkController.allStops { -// -// // Checks -// let isBetweenLats = stop.lat > latBottom && stop.lat < latTop -// let isBetweenLngs = stop.lng > lngLeft && stop.lng < lngRight -// -// if (isBetweenLats && isBetweenLngs) { -// tempMatchingAnnotations.append( -// GeoBusMKAnnotation( -// id: stop.id, -// coordinate: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), -// type: .stop -// ) -// ) -// } -// -// } -// -// mapController.newAnnotations = tempMatchingAnnotations -// -// } else { -// mapController.newAnnotations.removeAll() -// } -// } - - - - - - VStack(spacing: 15) { AboutGeoBus() Spacer() diff --git a/GeoBus/App/Components/NewMap/NewMapView.swift b/GeoBus/App/Components/NewMap/NewMapView.swift index 421ed55f..9230e253 100644 --- a/GeoBus/App/Components/NewMap/NewMapView.swift +++ b/GeoBus/App/Components/NewMap/NewMapView.swift @@ -16,6 +16,7 @@ struct MapViewSwiftUI: UIViewRepresentable { private let mapView = MKMapView() @Binding var region: MKCoordinateRegion + @Binding var camera: MKMapCamera @Binding var annotations: [GeoBusMKAnnotation] @StateObject var carrisNetworkController = CarrisNetworkController.shared @@ -29,6 +30,7 @@ struct MapViewSwiftUI: UIViewRepresentable { mapView.preferredConfiguration = MKStandardMapConfiguration(elevationStyle: .realistic, emphasisStyle: .muted) mapView.register(StopMKAnnotationView.self, forAnnotationViewWithReuseIdentifier: StopMKAnnotationView.reuseIdentifier) + mapView.register(VehicleMKAnnotationView.self, forAnnotationViewWithReuseIdentifier: VehicleMKAnnotationView.reuseIdentifier) return mapView } @@ -36,19 +38,42 @@ struct MapViewSwiftUI: UIViewRepresentable { func updateUIView(_ uiView: MKMapView, context: Context) { - // First, make sure we are dealing with annotation of type GeoBusMKAnnotation - guard let currentAnnotations = uiView.annotations as? [GeoBusMKAnnotation] else { return } + var tempAnnotationsToAdd: [GeoBusMKAnnotation] = [] + var tempAnnotationsToRemove: [GeoBusMKAnnotation] = [] + + let tempCurrentAnnotations: [GeoBusMKAnnotation] = uiView.annotations.compactMap({ + // Make sure we are dealing with annotation of type GeoBusMKAnnotation + return $0 as? GeoBusMKAnnotation + }) + + // Find out which annotations should be added to the map + for newAnnotation in annotations { + // se esta nova anotation ainda não estiver já na UiView + // adicionar à 'tempAnnotationsToAdd' + let indexOfThisNewAnnotationInUiView = tempCurrentAnnotations.firstIndex(of: newAnnotation) + if (indexOfThisNewAnnotationInUiView == nil) { + tempAnnotationsToAdd.append(newAnnotation) + } + } // Find out the excess annotations that should be removed from the map - // The following works as: [a, b, c, d, e] - [a, c, e] = [b, d] - let annotationsToRemove = Array(Set(currentAnnotations).subtracting(annotations)) + for currentAnnotation in tempCurrentAnnotations { + // se esta anotation que está visível não estiver na 'annotations' + // adicionar à 'tempAnnotationsToRemove' + let indexOfThisCurrentAnnotationInNextAnnotations = annotations.firstIndex(of: currentAnnotation) + if (indexOfThisCurrentAnnotationInNextAnnotations == nil) { + tempAnnotationsToRemove.append(currentAnnotation) + } + } + // Update the view with annotations -// uiView.removeAnnotations(uiView.annotations) - uiView.removeAnnotations(annotationsToRemove) - uiView.addAnnotations(annotations) + uiView.removeAnnotations(tempAnnotationsToRemove) + uiView.addAnnotations(tempAnnotationsToAdd) - print("HOWMANY removeAnnotations: \(annotationsToRemove.count)") + print("HOWMANY currentAnnotations: \(tempCurrentAnnotations.count)") + print("HOWMANY tempAnnotationsToAdd: \(tempAnnotationsToAdd.count)") + print("HOWMANY tempAnnotationsToRemove: \(tempAnnotationsToRemove.count)") print("HOWMANY uiView displayed annotations: \(uiView.annotations.count)") print("HOWMANY ----------") } @@ -70,68 +95,21 @@ final class MapViewSwiftUICoordinator: NSObject, MKMapViewDelegate { self.parentSwiftUIView = parentSwiftUIView } -// -// func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { + @MainActor func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { DispatchQueue.main.async { [self] in self.parentSwiftUIView.region = mapView.region + self.parentSwiftUIView.camera = mapView.camera } } -// @MainActor func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { -//// @MainActor func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { -// DispatchQueue.main.async { [self] in + @MainActor func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { + DispatchQueue.main.async { [self] in // self.parentSwiftUIView.region = mapView.region -// if (mapView.region.span.latitudeDelta < 0.005 || mapView.region.span.longitudeDelta < 0.005) { -// -// var tempMatchingAnnotations: [GeoBusMKAnnotation] = [] -// -// let latTop = mapView.region.center.latitude + mapView.region.span.latitudeDelta -// let latBottom = mapView.region.center.latitude - mapView.region.span.latitudeDelta -// -// let lngRight = mapView.region.center.longitude + mapView.region.span.longitudeDelta -// let lngLeft = mapView.region.center.longitude - mapView.region.span.longitudeDelta -// -// -// for stop in parentSwiftUIView.carrisNetworkController.allStops { -// -// // Checks -// let isBetweenLats = stop.lat > latBottom && stop.lat < latTop -// let isBetweenLngs = stop.lng > lngLeft && stop.lng < lngRight -// -// if (isBetweenLats && isBetweenLngs) { -// tempMatchingAnnotations.append( -// GeoBusMKAnnotation( -// id: stop.id, -// coordinate: CLLocationCoordinate2D(latitude: stop.lat, longitude: stop.lng), -// type: .stop -// ) -// ) -// } -// -// } -// -// self.parentSwiftUIView.annotations = tempMatchingAnnotations -// -// // First, make sure we are dealing with annotation of type GeoBusMKAnnotation -//// guard let currentAnnotations = mapView.annotations as? [GeoBusMKAnnotation] else { return } -// -// // Find out the excess annotations that should be removed from the map -// // The following works as: [a, b, c, d, e] - [a, c, e] = [b, d] -//// let annotationsToRemove = Array(Set(currentAnnotations).subtracting(tempMatchingAnnotations)) -// -// // Update the view with annotations -//// mapView.removeAnnotations(annotationsToRemove) -//// mapView.addAnnotations(tempMatchingAnnotations) -// -//// annotations = tempMatchingAnnotations -// -// } else { -// self.parentSwiftUIView.annotations = [] -//// mapView.removeAnnotations(mapView.annotations) -// } -// } -// } + self.parentSwiftUIView.camera = mapView.camera + } + } + func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { @@ -139,10 +117,9 @@ final class MapViewSwiftUICoordinator: NSObject, MKMapViewDelegate { switch annotation.type { case .stop: -// return mapView.dequeueReusableAnnotationView(withIdentifier: StopMKAnnotationView.reuseIdentifier, for: annotation) - return StopMKAnnotationView(annotation: annotation, reuseIdentifier: "stop") - default: - return nil + return StopMKAnnotationView(annotation: annotation, reuseIdentifier: StopMKAnnotationView.reuseIdentifier) + case .vehicle: + return VehicleMKAnnotationView(annotation: annotation, reuseIdentifier: VehicleMKAnnotationView.reuseIdentifier) } } @@ -154,24 +131,16 @@ final class MapViewSwiftUICoordinator: NSObject, MKMapViewDelegate { switch annotation.type { case .stop: DispatchQueue.main.async { [self] in + TapticEngine.impact.feedback(.light) _ = parentSwiftUIView.carrisNetworkController.select(stop: annotation.id) SheetController.shared.present(sheet: .StopDetails) } - default: - return - } - } - - - @MainActor func mapView(_ mapView: MKMapView, didDeselect annotation: MKAnnotation) { - guard let annotation = annotation as? GeoBusMKAnnotation else { return } - - switch annotation.type { - case .stop: - parentSwiftUIView.carrisNetworkController.deselect([.stop]) - SheetController.shared.dismiss() - default: - return + case .vehicle: + DispatchQueue.main.async { [self] in + TapticEngine.impact.feedback(.light) + _ = parentSwiftUIView.carrisNetworkController.select(vehicle: annotation.id) + SheetController.shared.present(sheet: .VehicleDetails) + } } } @@ -202,9 +171,10 @@ final class GeoBusMKAnnotation: NSObject, MKAnnotation { override func isEqual(_ object: Any?) -> Bool { if let annot = object as? GeoBusMKAnnotation{ // Add your defintion of equality here. i.e what determines if two Annotations are equal. - let equalCoordinates = annot.coordinate == self.coordinate - let equalType = annot.type == self.type - return equalCoordinates && equalType +// let equalCoordinates = annot.coordinate == self.coordinate +// let equalType = annot.type == self.type +// return equalCoordinates && equalType + return annot.id == self.id } else { return false } @@ -213,6 +183,16 @@ final class GeoBusMKAnnotation: NSObject, MKAnnotation { } + + + + + + + + +// STOP ANNOTATIONS + final class StopMKAnnotationView: MKAnnotationView { static let reuseIdentifier = "stop" @@ -223,19 +203,16 @@ final class StopMKAnnotationView: MKAnnotationView { canShowCallout = false - let swiftUIView = NewStopMKAnnotationView(stopId: newValue.id) + let swiftUIView = StopSwiftUIAnnotationView(stopId: newValue.id) let uiKitView = UIHostingController(rootView: swiftUIView) addSubview(uiKitView.view) } - } } - - -struct NewStopMKAnnotationView: View { +struct StopSwiftUIAnnotationView: View { public let stopId: Int @@ -263,296 +240,51 @@ struct NewStopMKAnnotationView: View { +// VEHICLE ANNOTATIONS - - - - - - -// -------------------------------------------------------------------------------------------------------------------------------------------- -// -------------------------------------------------------------------------------------------------------------------------------------------- -// -------------------------------------------------------------------------------------------------------------------------------------------- -// -------------------------------------------------------------------------------------------------------------------------------------------- -// -------------------------------------------------------------------------------------------------------------------------------------------- - - - - - - -struct NewMapView: UIViewRepresentable { - - private let mapView = MKMapView() - - @ObservedObject private var carrisNetworkController = CarrisNetworkController.shared - - - func makeUIView(context: UIViewRepresentableContext) -> MKMapView { - - mapView.delegate = context.coordinator - mapView.mapType = MKMapType.standard - mapView.showsUserLocation = true - mapView.showsTraffic = true - mapView.isRotateEnabled = true - mapView.isPitchEnabled = true - - mapView.register(NewStopAnnotationView.self, forAnnotationViewWithReuseIdentifier: "stop") - - // Set initial location in Lisbon - let lisbon = CLLocation(latitude: 38.721917, longitude: -9.137732) - let lisbonArea = MKCoordinateRegion(center: lisbon.coordinate, latitudinalMeters: 15000, longitudinalMeters: 15000) - mapView.setRegion(lisbonArea, animated: true) - - return mapView - } - - - - - -// override func viewDidLoad() { -// super.viewDidLoad() -// setupCompassButton() -// setupUserTrackingButtonAndScaleView() -// registerAnnotationViewClasses() -// -// locationManager.delegate = self -// locationManager.requestWhenInUseAuthorization() -// locationManager.startUpdatingLocation() -// -// loadDataForMapRegionAndBikes() -// } - - - - func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext) { - - print("MAPTEST: is called updateUIView") -// print("MAPTEST: carrisNetworkController.activeRoute: \(carrisNetworkController.activeRoute)") - - - if (uiView.annotations.isEmpty) { - - var annotationsToAdd: [MKAnnotation] = [] - - carrisNetworkController.allStops.forEach({ - annotationsToAdd.append( - NewStopAnnotation( - name: $0.name, - publicId: $0.id, - latitude: $0.lat, - longitude: $0.lng, - stop: $0 - ) - ) - }) - - uiView.addAnnotations(annotationsToAdd) - - } - - - // Update whatever was set to update -// uiView.removeAnnotations(uiView.annotations) -// uiView.addAnnotations(annotationsToAdd) - -// uiView.showAnnotations(annotationsToAdd, animated: true) - - } - - - func makeCoordinator() -> NewMapView.Coordinator { - Coordinator(control: self) - } - - - - // MARK: - MKMapViewDelegate - - final class Coordinator: NSObject, MKMapViewDelegate { - - private let control: NewMapView - - private let sheetController = SheetController.shared - private let carrisNetworkController = CarrisNetworkController.shared - - init(control: NewMapView) { - self.control = control - } - - - -// @MainActor func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { -// print("MAPTEST: mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)") -// if view.isKind(of: NewConnectionAnnotationView.self) { -// -// let selectedStopAnnotationView = view as! NewConnectionAnnotationView -// let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation -// -//// selectedStopAnnotationView.marker.image = UIImage(named: "GreenInfo") -// -// TapticEngine.impact.feedback(.light) -// _ = carrisNetworkController.select(connection: stopAnnotation.connection) -// sheetController.present(sheet: .ConnectionDetails) -// -// } -// } - - - @MainActor func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) { - print("MAPTEST: mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView)") - if annotation.isKind(of: NewStopAnnotation.self) { - - let stopAnnotation = annotation as! NewStopAnnotation - - print("MAPTEST: selected annotation stop id: \(stopAnnotation.stop.id)") - - TapticEngine.impact.feedback(.light) - _ = carrisNetworkController.select(stop: stopAnnotation.stop) - sheetController.present(sheet: .ConnectionDetails) - - } - } - - - - @MainActor func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) { - print("MAPTEST: mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView)") -// if view.isKind(of: NewConnectionAnnotationView.self) { -// -//// let selectedStopAnnotationView = view as! NewConnectionAnnotationView -//// let stopAnnotation = selectedStopAnnotationView.annotation as! NewConnectionAnnotation -// -//// selectedStopAnnotationView.marker.image = stopAnnotation.markerSymbol -// - carrisNetworkController.deselect([.stop]) -// -// } - } - - - - - func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { - guard let annotation = annotation as? NewStopAnnotation else { return nil } - - let identifier = "stop" - var view: MKAnnotationView - - if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) { - dequeuedView.annotation = annotation - view = dequeuedView - } else { - view = NewStopAnnotationView(annotation: annotation, reuseIdentifier: NewStopAnnotationView.ReuseID) //MKAnnotationView(annotation: annotation, reuseIdentifier: identifier) - } - - return view -// return NewStopAnnotationView(annotation: annotation, reuseIdentifier: NewStopAnnotationView.ReuseID) - } - - -// func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { -// print("MAPTEST: mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?") -// if annotation.isKind(of: NewStopAnnotation.self) { -// -//// return MKAnnotationView(annotation: annotation, reuseIdentifier: "stop") -// -// let identifier = "stop" -// var view: MKAnnotationView -// -// if let dequeuedView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) { -// dequeuedView.annotation = annotation -// view = dequeuedView -// } else { -// view = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier) -// } -//// -// return view -// -// } else { -// -// return nil -// -// } -// -// } - - - } - -} - - - - - - -class NewStopAnnotationView: MKAnnotationView { +final class VehicleMKAnnotationView: MKAnnotationView { - static let ReuseID = "stop" - -// override init(annotation: MKAnnotation?, reuseIdentifier: String?) { -// super.init(annotation: annotation, reuseIdentifier: reuseIdentifier) -// clusteringIdentifier = "stop" -// } -// -// required init?(coder aDecoder: NSCoder) { -// fatalError("init(coder:) has not been implemented") -// } - -// override func prepareForDisplay() { -// super.prepareForDisplay() -// displayPriority = .defaultLow -//// markerTintColor = UIColor.unicycleColor -//// glyphImage = #imageLiteral(resourceName: "unicycle") -// let swiftUIView = Circle().foregroundColor(.blue).frame(width: 5, height: 5) // swiftUIView is View -// let viewCtrl = UIHostingController(rootView: swiftUIView) -// addSubview(viewCtrl.view) -// } + static let reuseIdentifier = "vehicle" override var annotation: MKAnnotation? { - willSet { - guard let annotation = newValue as? NewStopAnnotation else { - return - } - - clusteringIdentifier = "stop" + guard let newValue = newValue as? GeoBusMKAnnotation else { return } + canShowCallout = false - - let swiftUIView = StopAnnotationView(stop: annotation.stop) // swiftUIView is View - let viewCtrl = UIHostingController(rootView: swiftUIView) - addSubview(viewCtrl.view) - + + let swiftUIView = VehicleSwiftUIAnnotationView(vehicleId: newValue.id) + let uiKitView = UIHostingController(rootView: swiftUIView) + addSubview(uiKitView.view) } - } - - } - -class NewStopAnnotation: NSObject, MKAnnotation { +struct VehicleSwiftUIAnnotationView: View { - let name: String - let publicId: Int + public let vehicleId: Int - @objc dynamic var coordinate: CLLocationCoordinate2D - - let stop: CarrisNetworkModel.Stop + @StateObject private var mapController = MapController.shared + @StateObject private var carrisNetworkController = CarrisNetworkController.shared + @State private var vehicle: CarrisNetworkModel.Vehicle? - init(name: String?, publicId: Int?, latitude: Double, longitude: Double, stop: CarrisNetworkModel.Stop) { - self.name = name ?? "-" - self.publicId = publicId ?? -1 - self.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - self.stop = stop - super.init() + var body: some View { + VStack { + switch (vehicle?.kind) { + case .tram, .elevator: + Image("Tram") + case .neighborhood, .night, .regular, .none: + Image("RegularService") + } + } + .rotationEffect(.radians(vehicle?.angleInRadians ?? 0) + .degrees(-mapController.mapCamera.heading)) + .animation(.default, value: vehicle?.angleInRadians) + .onAppear() { + self.vehicle = carrisNetworkController.find(vehicle: self.vehicleId) + } } } - diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index 0011402b..103475cd 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -31,13 +31,17 @@ final class MapController: ObservableObject { span: MKCoordinateSpan(latitudeDelta: CLLocationDistance(15000), longitudeDelta: CLLocationDistance(15000)) ) + + @Published var mapCamera: MKMapCamera = MKMapCamera() + + @Published var locationManager = CLLocationManager() @Published var showLocationNotAllowedAlert: Bool = false @Published var visibleAnnotations: [GenericMapAnnotation] = [] - @Published var newAnnotations: [GeoBusMKAnnotation] = [] + @Published var allAnnotations: [GeoBusMKAnnotation] = [] /* * */ @@ -61,6 +65,48 @@ final class MapController: ObservableObject { longitudinalMeters: self.initialMapZoom ) } + + + + + + + // ADD ANNOTATIONS + func add(annotations newAnnotationsArray: [GeoBusMKAnnotation], ofType annotationsType: GeoBusMKAnnotation.AnnotationType) { + self.allAnnotations.removeAll(where: { $0.type == annotationsType }) + self.allAnnotations.append(contentsOf: newAnnotationsArray) + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index 4bdde3a5..f3f32202 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -666,7 +666,7 @@ class CarrisNetworkController: ObservableObject { /* These functions search for the provided object identifier in the storage arrays */ /* and return it if found or nil if not found. */ - private func find(vehicle vehicleId: Int) -> CarrisNetworkModel.Vehicle? { + public func find(vehicle vehicleId: Int) -> CarrisNetworkModel.Vehicle? { if let requestedVehicleObject = self.allVehicles[withId: vehicleId] { return requestedVehicleObject } else { From 45bd8ffbe345213da6e2225dc9177570a09ed02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20de=20Vasconcelos?= Date: Sun, 13 Nov 2022 23:53:49 +0000 Subject: [PATCH 63/63] Touching with overlays --- GeoBus/App/Components/ContentView.swift | 30 +++- GeoBus/App/Components/NewMap/NewMapView.swift | 128 ++++++++++++++++-- GeoBus/App/Controllers/MapController.swift | 1 + .../Networks/Carris/CarrisAPIModel.swift | 12 ++ .../Carris/CarrisNetworkController.swift | 9 +- .../Networks/Carris/CarrisNetworkModel.swift | 5 +- 6 files changed, 167 insertions(+), 18 deletions(-) diff --git a/GeoBus/App/Components/ContentView.swift b/GeoBus/App/Components/ContentView.swift index 87b6ef40..92fcc77f 100644 --- a/GeoBus/App/Components/ContentView.swift +++ b/GeoBus/App/Components/ContentView.swift @@ -20,9 +20,33 @@ struct ContentView: View { MapViewSwiftUI( region: $mapController.region, camera: $mapController.mapCamera, - annotations: $mapController.allAnnotations + annotations: $mapController.allAnnotations, + overlays: $mapController.allOverlays ) .edgesIgnoringSafeArea(.vertical) + .onReceive(carrisNetworkController.$activeVariant) { newVariant in + if (newVariant != nil) { + if (newVariant?.circularShape != nil) { + var tempLinePoints: [CLLocationCoordinate2D] = [] + do { + let dataStringData = Data(newVariant!.circularShape!.utf8) + let dataObject = try JSONDecoder().decode(CarrisAPIModel.Shape2.self, from: dataStringData) + print("HJBUYY/TGUYINUHBGVFCTVYGBUHNJHBGVFCDVTGYBHUN") + print(dataObject) + for point in dataObject.coordinates { + tempLinePoints.append( + CLLocationCoordinate2D(latitude: point[1], longitude: point[0]) + ) + } + } catch { + print(error) + } + self.mapController.allOverlays.append( + MKPolyline(coordinates: tempLinePoints, count: tempLinePoints.count) + ) + } + } + } .onReceive(carrisNetworkController.$activeVehicles) { newVehiclesList in var tempNewAnnotations: [GeoBusMKAnnotation] = [] for vehicle in carrisNetworkController.activeVehicles { @@ -36,7 +60,7 @@ struct ContentView: View { } mapController.add(annotations: tempNewAnnotations, ofType: .vehicle) } - .onChange(of: mapController.region, perform: { newRegion in + .onReceive(mapController.$region) { newRegion in var tempNewAnnotations: [GeoBusMKAnnotation] = [] if (newRegion.span.latitudeDelta < 0.01 || newRegion.span.longitudeDelta < 0.01) { @@ -64,7 +88,7 @@ struct ContentView: View { } } mapController.add(annotations: tempNewAnnotations, ofType: .stop) - }) + } VStack(spacing: 15) { AboutGeoBus() Spacer() diff --git a/GeoBus/App/Components/NewMap/NewMapView.swift b/GeoBus/App/Components/NewMap/NewMapView.swift index 9230e253..2ef8a6c5 100644 --- a/GeoBus/App/Components/NewMap/NewMapView.swift +++ b/GeoBus/App/Components/NewMap/NewMapView.swift @@ -18,6 +18,7 @@ struct MapViewSwiftUI: UIViewRepresentable { @Binding var region: MKCoordinateRegion @Binding var camera: MKMapCamera @Binding var annotations: [GeoBusMKAnnotation] + @Binding var overlays: [MKPolyline] @StateObject var carrisNetworkController = CarrisNetworkController.shared @@ -48,8 +49,7 @@ struct MapViewSwiftUI: UIViewRepresentable { // Find out which annotations should be added to the map for newAnnotation in annotations { - // se esta nova anotation ainda não estiver já na UiView - // adicionar à 'tempAnnotationsToAdd' + // If this annotation is not yet on the view, add it let indexOfThisNewAnnotationInUiView = tempCurrentAnnotations.firstIndex(of: newAnnotation) if (indexOfThisNewAnnotationInUiView == nil) { tempAnnotationsToAdd.append(newAnnotation) @@ -58,8 +58,7 @@ struct MapViewSwiftUI: UIViewRepresentable { // Find out the excess annotations that should be removed from the map for currentAnnotation in tempCurrentAnnotations { - // se esta anotation que está visível não estiver na 'annotations' - // adicionar à 'tempAnnotationsToRemove' + // If this visible annotation is not in [annotations], remove it let indexOfThisCurrentAnnotationInNextAnnotations = annotations.firstIndex(of: currentAnnotation) if (indexOfThisCurrentAnnotationInNextAnnotations == nil) { tempAnnotationsToRemove.append(currentAnnotation) @@ -71,6 +70,19 @@ struct MapViewSwiftUI: UIViewRepresentable { uiView.removeAnnotations(tempAnnotationsToRemove) uiView.addAnnotations(tempAnnotationsToAdd) + + + // OVERLAYS + + var tempOverlaysToAdd: [MKPolyline] = [] + + for overlay in overlays { + tempOverlaysToAdd.append(overlay) + } + + uiView.addOverlays(tempOverlaysToAdd) + + print("HOWMANY currentAnnotations: \(tempCurrentAnnotations.count)") print("HOWMANY tempAnnotationsToAdd: \(tempAnnotationsToAdd.count)") print("HOWMANY tempAnnotationsToRemove: \(tempAnnotationsToRemove.count)") @@ -105,13 +117,12 @@ final class MapViewSwiftUICoordinator: NSObject, MKMapViewDelegate { @MainActor func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) { DispatchQueue.main.async { [self] in -// self.parentSwiftUIView.region = mapView.region + // self.parentSwiftUIView.region = mapView.region self.parentSwiftUIView.camera = mapView.camera } } - func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { guard let annotation = annotation as? GeoBusMKAnnotation else { return nil } @@ -144,6 +155,21 @@ final class MapViewSwiftUICoordinator: NSObject, MKMapViewDelegate { } } + + + @MainActor func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { + print("HETERERERERERE : \(overlay)") + if let polyline = overlay as? MKPolyline { + print("HETERERERERERE27e53672") + var testlineRenderer = MKPolylineRenderer(polyline: polyline) + testlineRenderer.strokeColor = .systemPink + testlineRenderer.lineWidth = 2.0 + return testlineRenderer + } + fatalError("Something wrong...") + } + + } @@ -153,9 +179,10 @@ final class MapViewSwiftUICoordinator: NSObject, MKMapViewDelegate { final class GeoBusMKAnnotation: NSObject, MKAnnotation { - let id: Int let type: AnnotationType - var coordinate: CLLocationCoordinate2D + + let id: Int + dynamic var coordinate: CLLocationCoordinate2D enum AnnotationType { case stop @@ -168,12 +195,52 @@ final class GeoBusMKAnnotation: NSObject, MKAnnotation { self.type = type } + func update(coordinate newCoordinate: CLLocationCoordinate2D) { + self.coordinate = newCoordinate + } + override func isEqual(_ object: Any?) -> Bool { if let annot = object as? GeoBusMKAnnotation{ // Add your defintion of equality here. i.e what determines if two Annotations are equal. -// let equalCoordinates = annot.coordinate == self.coordinate -// let equalType = annot.type == self.type -// return equalCoordinates && equalType + let equalId = annot.id == self.id + let equalCoordinates = annot.coordinate == self.coordinate + let equalType = annot.type == self.type + return equalId && equalCoordinates && equalType + } else { + return false + } + } + +} + + + + +final class GeoBusMKOverlay: NSObject, MKOverlay { + + let type: OverlayType + + let id: Int + var coordinate: CLLocationCoordinate2D + var boundingMapRect: MKMapRect + + enum OverlayType { + case route + } + + init(type: OverlayType, id: Int, coordinate: CLLocationCoordinate2D, boundingMapRect: MKMapRect) { + self.type = type + self.id = id + self.coordinate = coordinate + self.boundingMapRect = boundingMapRect + } + + override func isEqual(_ object: Any?) -> Bool { + if let annot = object as? GeoBusMKAnnotation{ + // Add your defintion of equality here. i.e what determines if two Annotations are equal. + // let equalCoordinates = annot.coordinate == self.coordinate + // let equalType = annot.type == self.type + // return equalCoordinates && equalType return annot.id == self.id } else { return false @@ -191,6 +258,10 @@ final class GeoBusMKAnnotation: NSObject, MKAnnotation { + + + + // STOP ANNOTATIONS final class StopMKAnnotationView: MKAnnotationView { @@ -203,6 +274,8 @@ final class StopMKAnnotationView: MKAnnotationView { canShowCallout = false + zPriority = MKAnnotationViewZPriority(0) + let swiftUIView = StopSwiftUIAnnotationView(stopId: newValue.id) let uiKitView = UIHostingController(rootView: swiftUIView) addSubview(uiKitView.view) @@ -253,6 +326,8 @@ final class VehicleMKAnnotationView: MKAnnotationView { canShowCallout = false + zPriority = MKAnnotationViewZPriority(1) + let swiftUIView = VehicleSwiftUIAnnotationView(vehicleId: newValue.id) let uiKitView = UIHostingController(rootView: swiftUIView) addSubview(uiKitView.view) @@ -288,3 +363,34 @@ struct VehicleSwiftUIAnnotationView: View { } } + + + + + + + + + +// ROUTE VARIANT OVERLAY RENDERER + + +//final class VehicleMKAnnotationView: MKOverlayRenderer { +// +// static let reuseIdentifier = "vehicle" +// +// override var annotation: MKAnnotation? { +// willSet { +// guard let newValue = newValue as? GeoBusMKAnnotation else { return } +// +// canShowCallout = false +// +// zPriority = MKAnnotationViewZPriority(1) +// +// let swiftUIView = VehicleSwiftUIAnnotationView(vehicleId: newValue.id) +// let uiKitView = UIHostingController(rootView: swiftUIView) +// addSubview(uiKitView.view) +// } +// } +// +//} diff --git a/GeoBus/App/Controllers/MapController.swift b/GeoBus/App/Controllers/MapController.swift index 103475cd..eddfea64 100644 --- a/GeoBus/App/Controllers/MapController.swift +++ b/GeoBus/App/Controllers/MapController.swift @@ -42,6 +42,7 @@ final class MapController: ObservableObject { @Published var allAnnotations: [GeoBusMKAnnotation] = [] + @Published var allOverlays: [MKPolyline] = [] /* * */ diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift index 097b9ec1..b0927c43 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisAPIModel.swift @@ -37,9 +37,21 @@ struct CarrisAPIModel { struct Itinerary: Decodable { let id: Int? let type: String? + let shape: String? let connections: [Connection]? } + + struct Shape: Decodable { + let type: String? + let coordinates: String? + } + + struct Shape2: Decodable { + let type: String? + let coordinates: [[Double]] + } + struct Connection: Decodable { let id, distance, orderNum: Int? let busStop: Stop? diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift index f3f32202..3f3906dc 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkController.swift @@ -348,6 +348,9 @@ class CarrisNetworkController: ObservableObject { // Request Route Detail for ‹routeNumber› let rawDataCarrisAPIRouteDetail = try await CarrisAPI.shared.request(for: "Routes/\(availableRoute.routeNumber ?? "-")") + + print("rawDataCarrisAPIRouteDetail: \(String(decoding: rawDataCarrisAPIRouteDetail, as: UTF8.self))") + let decodedAPIRouteDetail = try JSONDecoder().decode(CarrisAPIModel.Route.self, from: rawDataCarrisAPIRouteDetail) // Define a temporary variable to store formatted route variants @@ -457,7 +460,8 @@ class CarrisNetworkController: ObservableObject { name: tempVariantName, circularItinerary: tempCircularConnections, ascendingItinerary: tempAscendingConnections, - descendingItinerary: tempDescendingConnections + descendingItinerary: tempDescendingConnections, + circularShape: rawVariant.circItinerary?.shape ) } @@ -799,6 +803,7 @@ class CarrisNetworkController: ObservableObject { public func select(variant: CarrisNetworkModel.Variant) { self.activeVariant = variant +// print("rawVariant.circItinerary?.shape: \(variant.circularShape)") } @@ -811,14 +816,12 @@ class CarrisNetworkController: ObservableObject { } public func select(stop: CarrisNetworkModel.Stop) { - self.deselect([.all]) self.activeStop = stop } public func select(stop stopId: Int) -> Bool { let stop = self.find(stop: stopId) if (stop != nil) { - self.deselect([.all]) self.activeStop = stop // self.select(stop: stop!) return true diff --git a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift index 87f4d0c2..6fe82a5a 100644 --- a/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift +++ b/GeoBus/App/Controllers/Networks/Carris/CarrisNetworkModel.swift @@ -61,13 +61,16 @@ struct CarrisNetworkModel { let ascendingItinerary: [Connection]? let descendingItinerary: [Connection]? - init(number: Int, name: String, circularItinerary: [Connection]? = nil, ascendingItinerary: [Connection]? = nil, descendingItinerary: [Connection]? = nil) { + let circularShape: String? + + init(number: Int, name: String, circularItinerary: [Connection]? = nil, ascendingItinerary: [Connection]? = nil, descendingItinerary: [Connection]? = nil, circularShape: String?) { self.id = number self.number = number self.name = name self.circularItinerary = circularItinerary self.ascendingItinerary = ascendingItinerary self.descendingItinerary = descendingItinerary + self.circularShape = circularShape } }