diff --git a/GeoBus.xcodeproj/project.pbxproj b/GeoBus.xcodeproj/project.pbxproj index 7787e4e..605bfaa 100644 --- a/GeoBus.xcodeproj/project.pbxproj +++ b/GeoBus.xcodeproj/project.pbxproj @@ -69,6 +69,7 @@ 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 */; }; + CFB16ADA28EA6F730095D2ED /* Map in Frameworks */ = {isa = PBXBuildFile; productRef = CFB16AD928EA6F730095D2ED /* Map */; }; CFB5D45728EEFE21002368BC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = CFB5D45528EEFE21002368BC /* InfoPlist.strings */; }; CFB5D45A28EEFE6B002368BC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = CFB5D45828EEFE6B002368BC /* Localizable.strings */; }; CFC80FCC28D2C2FF003D059D /* DragAndDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = CFC80FCB28D2C2FF003D059D /* DragAndDrop.swift */; }; @@ -179,6 +180,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + CFB16ADA28EA6F730095D2ED /* Map in Frameworks */, CFF71E7028CCC47200C498FE /* Lottie in Frameworks */, CFFF2D3D28D7C99C00E035E0 /* PostHog in Frameworks */, ); @@ -425,6 +427,7 @@ packageProductDependencies = ( CFF71E6F28CCC47200C498FE /* Lottie */, CFFF2D3C28D7C99C00E035E0 /* PostHog */, + CFB16AD928EA6F730095D2ED /* Map */, ); productName = GeoBus; productReference = CF181FE228CCB7D600248F72 /* GeoBus.app */; @@ -462,6 +465,7 @@ packageReferences = ( CFF71E6E28CCC47200C498FE /* XCRemoteSwiftPackageReference "lottie-ios" */, CFFF2D3B28D7C99C00E035E0 /* XCRemoteSwiftPackageReference "posthog-ios" */, + CFB16AD828EA6F730095D2ED /* XCRemoteSwiftPackageReference "Map" */, ); productRefGroup = CF181FE328CCB7D600248F72 /* Products */; projectDirPath = ""; @@ -842,6 +846,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + CFB16AD828EA6F730095D2ED /* XCRemoteSwiftPackageReference "Map" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pauljohanneskraft/Map"; + requirement = { + branch = main; + kind = branch; + }; + }; CFF71E6E28CCC47200C498FE /* XCRemoteSwiftPackageReference "lottie-ios" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/airbnb/lottie-ios.git"; @@ -861,6 +873,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + CFB16AD928EA6F730095D2ED /* Map */ = { + isa = XCSwiftPackageProductDependency; + package = CFB16AD828EA6F730095D2ED /* XCRemoteSwiftPackageReference "Map" */; + productName = Map; + }; CFF71E6F28CCC47200C498FE /* Lottie */ = { isa = XCSwiftPackageProductDependency; package = CFF71E6E28CCC47200C498FE /* XCRemoteSwiftPackageReference "lottie-ios" */; diff --git a/GeoBus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/GeoBus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 4c9c267..ec0faaa 100644 --- a/GeoBus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/GeoBus.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -3,12 +3,21 @@ { "identity" : "lottie-ios", "kind" : "remoteSourceControl", - "location" : "https://github.com/airbnb/lottie-ios.git", + "location" : "https://github.com/airbnb/lottie-ios", "state" : { "revision" : "314537ec697719fa33a9d48210f4d06a463286d3", "version" : "3.4.3" } }, + { + "identity" : "map", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pauljohanneskraft/Map", + "state" : { + "branch" : "main", + "revision" : "3dbb081d903b46f64fa7149d93b848861a5cf66a" + } + }, { "identity" : "posthog-ios", "kind" : "remoteSourceControl", diff --git a/GeoBus/App/Components/Map/MapAnnotations.swift b/GeoBus/App/Components/Map/MapAnnotations.swift index 3218646..11ce28f 100644 --- a/GeoBus/App/Components/Map/MapAnnotations.swift +++ b/GeoBus/App/Components/Map/MapAnnotations.swift @@ -10,10 +10,14 @@ import MapKit import SwiftUI -struct GenericMapAnnotation: Identifiable { +class GenericMapAnnotation: Identifiable, Equatable, ObservableObject { + static func == (lhs: GenericMapAnnotation, rhs: GenericMapAnnotation) -> Bool { + lhs.location.latitude == rhs.location.latitude && lhs.location.longitude == rhs.location.longitude + } + - let id = UUID() - let location: CLLocationCoordinate2D + let id: String + @Published var location: CLLocationCoordinate2D let format: Format enum Format { @@ -26,6 +30,7 @@ struct GenericMapAnnotation: Identifiable { var stop: Stop? init(lat: Double, lng: Double, format: Format, stop: Stop) { + self.id = UUID().uuidString //stop.publicId self.location = CLLocationCoordinate2D(latitude: lat, longitude: lng) self.format = format self.stop = stop @@ -36,6 +41,7 @@ struct GenericMapAnnotation: Identifiable { var vehicle: VehicleSummary? init(lat: Double, lng: Double, format: Format, vehicle: VehicleSummary) { + self.id = vehicle.busNumber self.location = CLLocationCoordinate2D(latitude: lat, longitude: lng) self.format = format self.stop = nil diff --git a/GeoBus/App/Components/Map/MapView.swift b/GeoBus/App/Components/Map/MapView.swift index 1ff73d6..d5d1c26 100644 --- a/GeoBus/App/Components/Map/MapView.swift +++ b/GeoBus/App/Components/Map/MapView.swift @@ -7,7 +7,9 @@ // import SwiftUI +import Map import MapKit +import Combine struct MapView: View { @@ -15,28 +17,28 @@ struct MapView: View { @EnvironmentObject var stopsController: StopsController @EnvironmentObject var routesController: RoutesController @EnvironmentObject var vehiclesController: VehiclesController - - + + var body: some View { + Map( coordinateRegion: $mapController.region, - interactionModes: [.all], - showsUserLocation: true, - annotationItems: mapController.visibleAnnotations - ) { annotation in - - MapAnnotation(coordinate: annotation.location) { - switch (annotation.format) { - case .stop: - StopAnnotationView(stop: annotation.stop!, isPresentedOnAppear: false) - case .vehicle: - VehicleAnnotationView(vehicle: annotation.vehicle!, isPresentedOnAppear: false) - case .singleStop: - StopAnnotationView(stop: annotation.stop!, isPresentedOnAppear: true) + interactionModes: [.pan, .zoom], + annotationItems: mapController.visibleAnnotations, + annotationContent: { item in + let annotation = UpdatingMapAnnotation(coordinate: item.location, publisher: item.$location) + ViewMapAnnotation(annotation: annotation) { + switch (item.format) { + case .stop: + StopAnnotationView(stop: item.stop!, isPresentedOnAppear: false) + case .vehicle: + VehicleAnnotationView(vehicle: item.vehicle!, isPresentedOnAppear: false) + case .singleStop: + StopAnnotationView(stop: item.stop!, isPresentedOnAppear: true) + } } } - - } + ) .onChange(of: stopsController.selectedStop) { newStop in if (newStop != nil) { mapController.updateAnnotations(with: newStop!) @@ -50,7 +52,34 @@ struct MapView: View { .onChange(of: vehiclesController.vehicles) { newVehiclesList in mapController.updateAnnotations(with: newVehiclesList) } + } + +} + + +@objc +class UpdatingMapAnnotation: NSObject, MKAnnotation { + + dynamic var coordinate: CLLocationCoordinate2D + + private var coordinateCancellable: Cancellable? + + init(coordinate: CLLocationCoordinate2D, publisher: P) where P.Output == CLLocationCoordinate2D, P.Failure == Never { + self.coordinate = coordinate + + super.init() + + coordinateCancellable = publisher + .sink { [weak self] newValue in + // I changed unowned to weak, since we are now in another async context + // and the instance could (although highly unlikely) be gone until the animation is performed + UIView.animate(withDuration: 0.25) { + self?.coordinate = newValue + } + } + } + } diff --git a/GeoBus/App/Models/Routes.swift b/GeoBus/App/Models/Routes.swift index fd0379a..40b505c 100644 --- a/GeoBus/App/Models/Routes.swift +++ b/GeoBus/App/Models/Routes.swift @@ -40,6 +40,7 @@ struct APIRouteVariant: Decodable { } struct APIRouteVariantItinerary: Decodable { + let shape: String? let id: Int? let type: String? let connections: [APIRouteVariantItineraryConnection]? @@ -77,11 +78,11 @@ struct Route: Codable, Equatable, Identifiable { let name: String let kind: Kind var variants: [Variant] - + var id: String { return self.number } - + } @@ -90,7 +91,7 @@ struct Variant: Codable, Equatable, Identifiable { var name: String = "" let isCircular: Bool var upItinerary, downItinerary, circItinerary: [Stop]? - + var id: String { return self.name } diff --git a/GeoBus/App/Models/Stops.swift b/GeoBus/App/Models/Stops.swift index 6726183..64a7838 100644 --- a/GeoBus/App/Models/Stops.swift +++ b/GeoBus/App/Models/Stops.swift @@ -26,7 +26,7 @@ struct APIStop: Decodable { /* MARK: - Stop */ -// Data models adjusted for the app. +// Data model adjusted for the app. struct Stop: Codable, Equatable, Identifiable { let publicId: String @@ -36,6 +36,7 @@ struct Stop: Codable, Equatable, Identifiable { let direction: Direction? var id: String { - return self.publicId //UUID().uuidString + return self.publicId +// return UUID().uuidString } } diff --git a/GeoBus/App/State/MapController.swift b/GeoBus/App/State/MapController.swift index 0701992..9ce8684 100644 --- a/GeoBus/App/State/MapController.swift +++ b/GeoBus/App/State/MapController.swift @@ -11,25 +11,25 @@ import SwiftUI @MainActor class MapController: ObservableObject { - + @Published var region = MKCoordinateRegion( center: CLLocationCoordinate2D(latitude: 38.721917, longitude: -9.137732), latitudinalMeters: 15000, longitudinalMeters: 15000 ) - + @Published var locationManager = CLLocationManager() @Published var showLocationNotAllowedAlert: Bool = false - + private var stopAnnotations: [GenericMapAnnotation] = [] private var vehicleAnnotations: [GenericMapAnnotation] = [] @Published var visibleAnnotations: [GenericMapAnnotation] = [] - - - + + + /* MARK: - RECEIVE APPSTATE & AUTHENTICATION */ - + var appstate = Appstate() - + func receive(state: Appstate) { self.appstate = state } @@ -55,7 +55,7 @@ class MapController: ObservableObject { func centerMapOnUserLocation(andZoom: Bool) { locationManager.requestWhenInUseAuthorization() - + if (locationManager.authorizationStatus == .authorizedWhenInUse) { self.appstate.capture(event: "Location-Status-Allowed") if (andZoom) { @@ -73,19 +73,19 @@ class MapController: ObservableObject { self.appstate.capture(event: "Location-Status-Denied") self.showLocationNotAllowedAlert = true } - + } - - - + + + /* MARK: - UPDATE ANNOTATIONS WITH SELECTED STOP */ // ..... func updateAnnotations(with selectedStop: Stop) { - + stopAnnotations = [] - + stopAnnotations.append( GenericMapAnnotation( lat: selectedStop.lat, @@ -94,12 +94,12 @@ class MapController: ObservableObject { stop: selectedStop ) ) - + visibleAnnotations.removeAll() visibleAnnotations.append(contentsOf: stopAnnotations) - + zoomToFitMapAnnotations(annotations: visibleAnnotations) - + } @@ -109,9 +109,9 @@ class MapController: ObservableObject { // ..... func updateAnnotations(with selectedVariant: Variant) { - + stopAnnotations = [] - + if (selectedVariant.upItinerary != nil) { for stop in selectedVariant.upItinerary! { stopAnnotations.append( @@ -119,7 +119,7 @@ class MapController: ObservableObject { ) } } - + if (selectedVariant.downItinerary != nil) { for stop in selectedVariant.downItinerary! { stopAnnotations.append( @@ -127,7 +127,7 @@ class MapController: ObservableObject { ) } } - + if (selectedVariant.circItinerary != nil) { for stop in selectedVariant.circItinerary! { stopAnnotations.append( @@ -135,34 +135,47 @@ class MapController: ObservableObject { ) } } - + visibleAnnotations.removeAll() visibleAnnotations.append(contentsOf: stopAnnotations) - + zoomToFitMapAnnotations(annotations: visibleAnnotations) - + } - - - - /* MARK: - UPDATE ANNOTATIONS WITH VEHICLES LIST */ - // ..... + + + /* MARK: - UPDATE VEHICLE ANNOTATIONS */ + + // On receiving a new list of Vehicles, loop through each one + // to check if it is already present in the map. If it is, then update + // it's the coordinates. If it is not, add it to the list of visible annotations. func updateAnnotations(with vehiclesList: [VehicleSummary]) { - - vehicleAnnotations = [] - + + // Loop through the new list of vehicles for vehicle in vehiclesList { - vehicleAnnotations.append( - GenericMapAnnotation(lat: vehicle.lat, lng: vehicle.lng, format: .vehicle, vehicle: vehicle) - ) + + // Check if busNumber is already visible in the Map + let indexOfVehicleAnnotation = visibleAnnotations.firstIndex { + $0.id == vehicle.busNumber + } + + if (indexOfVehicleAnnotation != nil) { + // If it is, update it's coordinates + self.visibleAnnotations[indexOfVehicleAnnotation!].location = CLLocationCoordinate2D( + latitude: vehicle.lat, longitude: vehicle.lng + ) + } else { + // If it is not, add it to the map + visibleAnnotations.append(GenericMapAnnotation(lat: vehicle.lat, lng: vehicle.lng, format: .vehicle, vehicle: vehicle)) + } + } - visibleAnnotations.removeAll() - visibleAnnotations.append(contentsOf: vehicleAnnotations) - visibleAnnotations.append(contentsOf: stopAnnotations) - + + // MISSING: Remove vehicles in visibleAnnotations that are not in the list + } @@ -194,9 +207,9 @@ class MapController: ObservableObject { newRegion.center.longitude = topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5 newRegion.span.latitudeDelta = fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * spanMargin newRegion.span.longitudeDelta = fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * spanMargin - + self.moveMap(to: newRegion) - + } - + } diff --git a/GeoBus/App/State/RoutesController.swift b/GeoBus/App/State/RoutesController.swift index 0915044..99ed9a3 100644 --- a/GeoBus/App/State/RoutesController.swift +++ b/GeoBus/App/State/RoutesController.swift @@ -11,29 +11,29 @@ import Combine @MainActor class RoutesController: ObservableObject { - + /* MARK: - Variables */ - + private let storageKeyForAllRoutes: String = "routes_allRoutes" @Published var allRoutes: [Route] = [] - + private let storageKeyForLastUpdatedRoutes: String = "routes_lastUpdatedRoutes" private var lastUpdatedRoutes: String? = nil - + private let storageKeyForFavoriteRoutes: String = "routes_favoriteRoutes" @Published var favorites: [Route] = [] - + @Published var selectedRoute: Route? @Published var selectedVariant: Variant? - + @Published var totalRoutesLeftToUpdate: Int? = nil - - - + + + /* MARK: - INITIALIZER */ - + // Retrieve data from UserDefaults on init. - + init() { // Unwrap and Decode all stops from Storage if let unwrappedAllRoutes = UserDefaults.standard.data(forKey: storageKeyForAllRoutes) { @@ -46,37 +46,37 @@ class RoutesController: ObservableObject { self.lastUpdatedRoutes = unwrappedLastUpdatedRoutes } } - - - + + + /* MARK: - RECEIVE APPSTATE & AUTHENTICATION */ - + var appstate = Appstate() var authentication = Authentication() - + func receive(state: Appstate, auth: Authentication) { self.appstate = state self.authentication = auth } - - - + + + /* MARK: - Selectors */ // Getters and Setters for published and private variables. - + private func select(route: Route) { self.selectedRoute = route self.select(variant: route.variants[0]) } - + func select(route routeNumber: String) { let route = self.findRoute(by: routeNumber) if (route != nil) { self.select(route: route!) } } - + func select(route routeNumber: String, returnResult: Bool) -> Bool { let route = self.findRoute(by: routeNumber) if (route != nil) { @@ -86,30 +86,30 @@ class RoutesController: ObservableObject { return false } } - - + + func select(variant: Variant) { self.selectedVariant = variant } - - + + func deselect() { self.selectedRoute = nil self.selectedVariant = nil } - - - + + + /* MARK: - Check for Updates from Carris API */ - + // This function decides whether to update available routes // if they are outdated. For now, do this once every 5 days. - + func update(forced: Bool = false) { Task { - + let formatter = ISO8601DateFormatter() - + if (lastUpdatedRoutes == nil || allRoutes.isEmpty || forced) { await fetchRoutesFromAPI() let timestamp = formatter.string(from: Date.now) @@ -118,37 +118,37 @@ class RoutesController: ObservableObject { // Calculate time interval let formattedDateObj = formatter.date(from: lastUpdatedRoutes!) let secondsPassed = Int(formattedDateObj?.timeIntervalSinceNow ?? -1) - + if ( (secondsPassed * -1) > (86400 * 5) ) { // 86400 seconds * 5 = 5 days await fetchRoutesFromAPI() let timestamp = formatter.string(from: Date.now) UserDefaults.standard.set(timestamp, forKey: storageKeyForLastUpdatedRoutes) } } - + // Retrieve favorites at app launch self.retrieveFavorites() - + } } - - - + + + /* MARK: - Retrieve Favourite Routes from iCloud KVS */ - + // This function retrieves favorites from iCloud Key-Value-Storage. - + func retrieveFavorites() { - + // Get from iCloud let iCloudKeyStore = NSUbiquitousKeyValueStore() iCloudKeyStore.synchronize() - + let savedFavorites = iCloudKeyStore.array(forKey: storageKeyForFavoriteRoutes) as? [String] ?? [] - + // Clear current favorites array self.favorites.removeAll() - + // Save to array for routeNumber in savedFavorites { let route = findRoute(by: routeNumber) @@ -156,17 +156,17 @@ class RoutesController: ObservableObject { favorites.append(route!) } } - + } - - - + + + /* MARK: - Save Favorite Routes to iCloud KVS */ - + // This function saves a representation of the routes stored in the favorites array // to iCloud Key-Value-Store. This function should be called whenever a change // in favorites occurs, to ensure consistency across devices. - + func saveFavorites() { var favoritesToSave: [String] = [] for favRoute in favorites { @@ -176,16 +176,16 @@ class RoutesController: ObservableObject { iCloudKeyStore.set(favoritesToSave, forKey: storageKeyForFavoriteRoutes) iCloudKeyStore.synchronize() } - - - + + + /* MARK: - Toggle Route as Favorite */ - + // This function marks a route as favorite if it is not, // and removes it from favorites if it is. - + func toggleFavorite(route: Route) { - + if let index = self.favorites.firstIndex(of: route) { self.favorites.remove(at: index) self.appstate.capture(event: "Routes-Details-RemoveFromFavorites", properties: ["routeNumber": route.number]) @@ -193,52 +193,52 @@ class RoutesController: ObservableObject { self.favorites.append(route) self.appstate.capture(event: "Routes-Details-AddToFavorites", properties: ["routeNumber": route.number]) } - + saveFavorites() - + } - - - + + + /* MARK: - Reorder Favorites */ - + // This function marks a route as favorite if it is not, - + func reorderFavorites(fromOffsets: IndexSet, toOffset: Int) { - + self.favorites.move(fromOffsets: fromOffsets, toOffset: toOffset) - + saveFavorites() - + } - - - + + + /* MARK: - Is Favourite */ - + // This function checks if a route is marked as favorite. - + func isFavourite(route: Route) -> Bool { return favorites.contains(route) } - - - + + + /* MARK: - 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 fetchRoutesFromAPI() async { - + self.appstate.capture(event: "Routes-Sync-START") appstate.change(to: .loading, for: .routes) - + print("Fetching Routes: Starting...") - + do { // Request API Routes List var requestAPIRoutesList = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/Routes")!) @@ -247,7 +247,7 @@ class RoutesController: ObservableObject { requestAPIRoutesList.setValue("Bearer \(authentication.authToken ?? "invalid_token")", forHTTPHeaderField: "Authorization") let (rawDataAPIRoutesList, rawResponseAPIRoutesList) = try await URLSession.shared.data(for: requestAPIRoutesList) let responseAPIRoutesList = rawResponseAPIRoutesList as? HTTPURLResponse - + // Check status of response if (responseAPIRoutesList?.statusCode == 401) { await self.authentication.authenticate() @@ -257,24 +257,24 @@ class RoutesController: ObservableObject { print(responseAPIRoutesList as Any) throw Appstate.CarrisAPIError.unavailable } - + let decodedAPIRoutesList = try JSONDecoder().decode([APIRoutesList].self, from: rawDataAPIRoutesList) - - + + self.totalRoutesLeftToUpdate = decodedAPIRoutesList.count - - + + // Define a temporary variable to store routes // before saving them to the device storage. var tempAllRoutes: [Route] = [] - + // For each available route in the API, for availableRoute in decodedAPIRoutesList { - + if (availableRoute.isPublicVisible ?? false) { - + print("Route: Route.\(String(describing: availableRoute.routeNumber)) starting...") - + // Request Route Detail for .routeNumber var requestAPIRouteDetail = URLRequest(url: URL(string: "https://gateway.carris.pt/gateway/xtranpassengerapi/api/v2.10/Routes/\(availableRoute.routeNumber ?? "invalid-route-number")")!) requestAPIRouteDetail.addValue("application/json", forHTTPHeaderField: "Content-Type") @@ -282,7 +282,7 @@ class RoutesController: ObservableObject { requestAPIRouteDetail.setValue("Bearer \(authentication.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) { Task { @@ -294,13 +294,14 @@ class RoutesController: ObservableObject { print(responseAPIRouteDetail as Any) throw Appstate.CarrisAPIError.unavailable } - - + + let decodedAPIRouteDetail = try JSONDecoder().decode(APIRoute.self, from: rawDataAPIRouteDetail) - + + // Define a temporary variable to store formatted route variants var formattedRouteVariants: [Variant] = [] - + // For each variant in route, // check if it is currently active, format it // and append the result to the temporary variable. @@ -314,7 +315,7 @@ class RoutesController: ObservableObject { ) } } - + // Build the formatted route object let formattedRoute = Route( number: decodedAPIRouteDetail.routeNumber ?? "-", @@ -322,20 +323,20 @@ class RoutesController: ObservableObject { kind: Globals().getKind(by: decodedAPIRouteDetail.routeNumber ?? "-"), variants: formattedRouteVariants ) - + // Save the formatted route object in the allRoutes temporary variable tempAllRoutes.append(formattedRoute) - + self.totalRoutesLeftToUpdate! -= 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.allRoutes.removeAll() @@ -343,12 +344,12 @@ class RoutesController: ObservableObject { if let encodedAllRoutes = try? JSONEncoder().encode(self.allRoutes) { UserDefaults.standard.set(encodedAllRoutes, forKey: storageKeyForAllRoutes) } - + print("Fetching Routes: Complete!") - + appstate.capture(event: "Routes-Sync-OK") appstate.change(to: .idle, for: .routes) - + } catch { appstate.capture(event: "Routes-Sync-ERROR") appstate.change(to: .error, for: .routes) @@ -356,17 +357,21 @@ class RoutesController: ObservableObject { print(error) print("************") } - + } - - - + + + /* MARK: - Format Route Variants */ - + // Parse and simplify the data model for variants - + func formatRawRouteVariant(rawVariant: APIRouteVariant, isCircular: Bool) -> Variant { - + + print("GB SHAPE: \(String(describing: rawVariant.upItinerary?.shape))") + print("GB SHAPE: \(String(describing: rawVariant.downItinerary?.shape))") + print("GB SHAPE: \(String(describing: rawVariant.circItinerary?.shape))") + // Create a temporary variable to store the final RouteVariant var formattedVariant = Variant( number: rawVariant.variantNumber ?? -1, @@ -375,20 +380,20 @@ class RoutesController: ObservableObject { downItinerary: nil, circItinerary: nil ) - + // For each Itinerary type, // check if it is defined (not nil) in the raw object - + // For UpItinerary: if (rawVariant.upItinerary != nil) { - + // Change the temporary variable property to an empty array formattedVariant.upItinerary = [] - + // For each connection, // convert the nested objects into a simplified RouteStop object for rawConnection in rawVariant.upItinerary!.connections ?? [] { - + // Append new values to the temporary variable property directly formattedVariant.upItinerary!.append( Stop( @@ -400,24 +405,24 @@ class RoutesController: ObservableObject { direction: .ascending ) ) - + } - + // Sort the stops formattedVariant.upItinerary!.sort(by: { $0.orderInRoute! < $1.orderInRoute! }) - + } - + // For DownItinerary: if (rawVariant.downItinerary != nil) { - + // Change the temporary variable property to an empty array formattedVariant.downItinerary = [] - + // For each connection, // convert the nested objects into a simplified RouteStop object for rawConnection in rawVariant.downItinerary!.connections ?? [] { - + // Append new values to the temporary variable property directly formattedVariant.downItinerary!.append( Stop( @@ -429,24 +434,24 @@ class RoutesController: ObservableObject { direction: .descending ) ) - + } - + // Sort the stops formattedVariant.downItinerary!.sort(by: { $0.orderInRoute! < $1.orderInRoute! }) - + } - + // For CircItinerary: if (rawVariant.circItinerary != nil) { - + // Change the temporary variable property to an empty array formattedVariant.circItinerary = [] - + // For each connection, // convert the nested objects into a simplified RouteStop object for rawConnection in rawVariant.circItinerary!.connections ?? [] { - + // Append new values to the temporary variable property directly formattedVariant.circItinerary!.append( Stop( @@ -458,14 +463,14 @@ class RoutesController: ObservableObject { direction: .circular ) ) - + } - + // Sort the stops formattedVariant.circItinerary!.sort(by: { $0.orderInRoute! < $1.orderInRoute! }) - + } - + if (formattedVariant.isCircular) { formattedVariant.name = getTerminalStopNameForVariant(variant: formattedVariant, direction: .circular) } else { @@ -473,52 +478,52 @@ class RoutesController: ObservableObject { let lastStop = getTerminalStopNameForVariant(variant: formattedVariant, direction: .descending) formattedVariant.name = "\(firstStop) ⇄ \(lastStop)" } - + // Finally, return the temporary variable to the caller return formattedVariant - + } - - - + + + /* 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? { - + // Find index of route matching requested routeNumber let indexOfRouteInArray = 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 allRoutes[indexOfRouteInArray!] } else { return nil } - + } - - - + + + /* 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 ?? "-") + 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 ?? "-") } } - - - + + + }