diff --git a/Apps/MTA/AppDelegate.h b/Apps/MTA/AppDelegate.h new file mode 100644 index 000000000..9307ad1f1 --- /dev/null +++ b/Apps/MTA/AppDelegate.h @@ -0,0 +1,18 @@ +// +// AppDelegate.h +// OBANetworking +// +// Copyright © Open Transit Software Foundation +// This source code is licensed under the Apache 2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +#import + +@interface AppDelegate : UIResponder + +@property (strong, nonatomic) UIWindow *window; + + +@end + diff --git a/Apps/MTA/AppDelegate.m b/Apps/MTA/AppDelegate.m new file mode 100644 index 000000000..879456340 --- /dev/null +++ b/Apps/MTA/AppDelegate.m @@ -0,0 +1,134 @@ +// +// AppDelegate.m +// OBANetworking +// +// Copyright © Open Transit Software Foundation +// This source code is licensed under the Apache 2.0 license found in the +// LICENSE file in the root directory of this source tree. +// + +#import "AppDelegate.h" +@import OBAKitCore; +@import OBAKit; +#import "App-Swift.h" + +@interface AppDelegate () +@property(nonatomic,strong) OBAApplication *app; +@property(nonatomic,strong) NSUserDefaults *userDefaults; +@property(nonatomic,strong) OBAClassicApplicationRootController *rootController; +@end + +@implementation AppDelegate + +- (instancetype)init { + self = [super init]; + + if (self) { + NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil]; + [NSURLCache setSharedURLCache:urlCache]; + + NSString *appGroup = NSBundle.mainBundle.appGroup; + assert(appGroup); + + _userDefaults = [[NSUserDefaults alloc] initWithSuiteName:appGroup]; + + [_userDefaults registerDefaults:@{ + OBAAnalyticsKeys.reportingEnabledUserDefaultsKey: @(NO) + }]; + + OBAAppConfig *appConfig = [[OBAAppConfig alloc] initWithAppBundle:NSBundle.mainBundle userDefaults:_userDefaults analytics:nil]; + + _app = [[OBAApplication alloc] initWithConfig:appConfig]; + _app.delegate = self; + } + + return self; +} + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + self.window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; + [self.window makeKeyAndVisible]; + + // This method will call -applicationReloadRootInterface:, which creates the + // application's UI and attaches it to the window, so no need to do that here. + [self.app application:application didFinishLaunching:launchOptions]; + + return YES; +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + [self.app applicationDidBecomeActive:application]; +} + +- (void)applicationWillResignActive:(UIApplication *)application { + [self.app applicationWillResignActive:application]; +} + +#pragma mark - OBAApplicationDelegate + +- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { + return [self.app application:app open:url options:options]; +} + +- (UIApplication*)uiApplication { + return [UIApplication sharedApplication]; +} + +- (void)performTestCrash { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-value" + @[][1]; +#pragma clang diagnostic pop +} + +- (void)setIdleTimerDisabled:(BOOL)idleTimerDisabled { + UIApplication.sharedApplication.idleTimerDisabled = idleTimerDisabled; +} + +- (BOOL)idleTimerDisabled { + return UIApplication.sharedApplication.idleTimerDisabled; +} + +- (BOOL)registeredForRemoteNotifications { + return UIApplication.sharedApplication.registeredForRemoteNotifications; +} + +- (void)applicationReloadRootInterface:(OBAApplication*)application { + void(^showRootController)(void) = ^{ + self.rootController = [[OBAClassicApplicationRootController alloc] initWithApplication:application]; + self.window.rootViewController = self.rootController; + }; + + if ([OBAOnboardingNavigationController needsToOnboardWithApplication:application]) { + self.window.rootViewController = [[OBAOnboardingNavigationController alloc] initWithApplication:application completion:^{ + showRootController(); + [UIView transitionWithView:self.window duration:0.5 options:UIViewAnimationOptionTransitionFlipFromLeft animations:nil completion:nil]; + }]; + } else { + showRootController(); + } +} + +- (BOOL)canOpenURL:(NSURL*)url { + return [UIApplication.sharedApplication canOpenURL:url]; +} + +- (void)open:(NSURL * _Nonnull)url options:(NSDictionary * _Nonnull)options completionHandler:(void (^ _Nullable)(BOOL))completion { + [UIApplication.sharedApplication openURL:url options:options completionHandler:completion]; +} + +- (NSDictionary*)credits { + return @{@"Firebase": @"https://raw.githubusercontent.com/firebase/firebase-ios-sdk/master/LICENSE"}; +} + +- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void(^)(NSArray> *restorableObjects))restorationHandler { + return [self.app application:application continue:userActivity restorationHandler:restorationHandler]; +} + +#pragma mark - Push Notifications + +- (BOOL)isRegisteredForRemoteNotifications { + return NO; +} + +@end diff --git a/Apps/MTA/Assets.xcassets/AccentColor.colorset/Contents.json b/Apps/MTA/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..59a784e50 --- /dev/null +++ b/Apps/MTA/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA6", + "green" : "0x39", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/MTA/Assets.xcassets/AppIcon.appiconset/Contents.json b/Apps/MTA/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..d45ddf978 --- /dev/null +++ b/Apps/MTA/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "MTA-logo.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/MTA/Assets.xcassets/AppIcon.appiconset/MTA-logo.png b/Apps/MTA/Assets.xcassets/AppIcon.appiconset/MTA-logo.png new file mode 100644 index 000000000..ab809af77 Binary files /dev/null and b/Apps/MTA/Assets.xcassets/AppIcon.appiconset/MTA-logo.png differ diff --git a/Apps/MTA/Assets.xcassets/Colors/Contents.json b/Apps/MTA/Assets.xcassets/Colors/Contents.json new file mode 100644 index 000000000..da4a164c9 --- /dev/null +++ b/Apps/MTA/Assets.xcassets/Colors/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Apps/MTA/Assets.xcassets/Colors/brand.colorset/Contents.json b/Apps/MTA/Assets.xcassets/Colors/brand.colorset/Contents.json new file mode 100644 index 000000000..59a784e50 --- /dev/null +++ b/Apps/MTA/Assets.xcassets/Colors/brand.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA6", + "green" : "0x39", + "red" : "0x00" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/MTA/Assets.xcassets/Contents.json b/Apps/MTA/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Apps/MTA/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Apps/MTA/Base.lproj/LaunchScreen.storyboard b/Apps/MTA/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..4e4ffebf0 --- /dev/null +++ b/Apps/MTA/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Apps/MTA/Onboarding/OnboardingNavigationController.swift b/Apps/MTA/Onboarding/OnboardingNavigationController.swift new file mode 100644 index 000000000..56c230613 --- /dev/null +++ b/Apps/MTA/Onboarding/OnboardingNavigationController.swift @@ -0,0 +1,138 @@ +// +// OnboardingNavigationController.swift +// OneBusAway +// +// Created by Alan Chu on 1/28/23. +// + +import OBAKit +import OBAKitCore +import SwiftUI + +/// Displays Onboarding steps. For performance, consider checking ``needsToOnboard(application:)`` before initializing and presenting this controller. +/// This is the default Onboarder, created for migrating data from the classic-codebase and for selecting a region from a list. +@objc(OBAOnboardingNavigationController) +public class OnboardingNavigationController: UINavigationController { + enum Page: UInt, CaseIterable { + case migration + case location + case regionPicker + + case debugPageA + case debugPageB + } + + private let application: Application + private let regionsService: RegionsService + private let regionPickerCoordinator: RegionPickerCoordinator + private let dataMigrator: DataMigrator + + private var page: Page? + + var completion: VoidBlock + + private static var testOnboarding: Bool = { + #if DEBUG + let envVar = ProcessInfo.processInfo.environment["TEST_ONBOARDING"] ?? "0" + return (envVar as NSString).boolValue + #else + return false + #endif + }() + + @objc static public func needsToOnboard(application: Application) -> Bool { + if testOnboarding { + return true + } + + return application.regionsService.currentRegion == nil || application.shouldPerformMigration + } + + public init(application: Application, dataMigrator: DataMigrator = .standard, completion: @escaping VoidBlock) { + self.application = application + self.regionsService = application.regionsService + self.regionPickerCoordinator = RegionPickerCoordinator(regionsService: regionsService) + self.dataMigrator = dataMigrator + + self.completion = completion + + super.init(nibName: nil, bundle: nil) + } + + @objc public convenience init(application: Application, completion: @escaping VoidBlock) { + self.init(application: application, dataMigrator: .standard, completion: completion) + } + + override public func viewDidLoad() { + super.viewDidLoad() + + if application.hasDataToMigrate { + self.page = .migration + } else if application.regionsService.currentRegion == nil || Self.testOnboarding { + self.page = .location + } else { + self.page = nil + } + + self.showPage() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @ViewBuilder private func view(for page: Page) -> some View { + let dismissBlock: VoidBlock = { [weak self] in + self?.nextPage() + } + + switch page { + case .location: + RegionPickerLocationAuthorizationView(regionProvider: regionPickerCoordinator, dismissBlock: dismissBlock) + case .regionPicker: + RegionPickerView(regionProvider: regionPickerCoordinator, dismissBlock: dismissBlock) + case .migration: + DataMigrationView(dismissBlock: dismissBlock) + case .debugPageA: + Button("Debug A", action: dismissBlock) + case .debugPageB: + Button("Debug B", action: dismissBlock) + } + } + + private func showPage() { + guard let page else { + return self.dismiss(animated: true, completion: self.completion) + } + + let view = view(for: page) + .environment(\.coreApplication, application) + + self.setViewControllers([UIHostingController(rootView: view)], animated: true) + } + + private func nextPage() { + guard let page else { + return + } + + switch page { + case .migration: + self.page = .location + case .location: + self.page = .regionPicker + case .regionPicker: + if Self.testOnboarding { + self.page = .debugPageA + } else { + self.page = nil + } + case .debugPageA: + self.page = .debugPageB + case .debugPageB: + self.page = nil + } + + self.showPage() + } +} diff --git a/Apps/MTA/Resources/regions.json b/Apps/MTA/Resources/regions.json new file mode 100644 index 000000000..6526380b3 --- /dev/null +++ b/Apps/MTA/Resources/regions.json @@ -0,0 +1,610 @@ +{ + "code": 200, + "text": "OK", + "version": 3, + "data": { + "list": [ + { + "siriBaseUrl": "https://tampa.onebusaway.org/onebusaway-api-webapp/siri/", + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "onebusaway@gohart.org", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://api.tampa.onebusaway.org/api/", + "id": 0, + "regionName": "Tampa Bay", + "obaVersionInfo": "1.1.11-SNAPSHOT|1|1|11|SNAPSHOT|6950d86123a7a9e5f12065bcbec0c516f35d86d9", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": true, + "otpContactEmail": "otp-tampa@onebusaway.org", + "otpBaseUrl": "https://otp.prod.obahart.org/otp/", + "paymentWarningBody": null, + "supportsSiriRealtimeApis": true, + "twitterUrl": "https://mobile.twitter.com/OBA_tampa", + "paymentAndroidAppId": "co.bytemark.flamingo", + "paymentiOSAppStoreIdentifier": "1487465395", + "paymentiOSAppUrlScheme": "fb313213768708402HART", + "active": true, + "open311Servers": [ + { + "juridisctionId": null, + "apiKey": "937033cad3054ec58a1a8156dcdd6ad8a416af2f", + "baseUrl": "https://seeclickfix.com/open311/v2/" + } + ], + "paymentWarningTitle": null, + "language": "en_US", + "bounds": [ + { + "lat": 27.976910500000002, + "latSpan": 0.5424609999999994, + "lon": -82.445851, + "lonSpan": 0.576357999999999 + }, + { + "lat": 27.919249999999998, + "latSpan": 0.47208000000000183, + "lon": -82.652145, + "lonSpan": 0.3967700000000036 + } + ], + "facebookUrl": "", + "stopInfoUrl": null, + "experimental": false + }, + { + "siriBaseUrl": "https://pugetsound.onebusaway.org/onebusaway-api-webapp/siri/", + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "onebusaway@soundtransit.org", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://api.pugetsound.onebusaway.org/", + "id": 1, + "regionName": "Puget Sound", + "obaVersionInfo": "1.1.7|1|1|7||c8ee3d4906dd55ecafdd124f31f39c0f54a37b52", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": "otp-pugetsound@onebusaway.org", + "otpBaseUrl": "https://tpng.api.soundtransit.org/tripplanner/st/", + "paymentWarningBody": "The mobile fare payment app for Puget Sound does not support all transit service shown in OneBusAway. Please check that a ticket is eligible for your agency and route before you purchase!", + "supportsSiriRealtimeApis": true, + "twitterUrl": "https://mobile.twitter.com/oba_pugetsound", + "paymentAndroidAppId": "co.bytemark.tgt", + "paymentiOSAppStoreIdentifier": "1131345078", + "paymentiOSAppUrlScheme": "co.bytemark.tgt", + "active": true, + "open311Servers": [], + "paymentWarningTitle": "Check before you buy!", + "language": "en_US", + "bounds": [ + { + "lat": 47.221315, + "latSpan": 0.33704, + "lon": -122.4051325, + "lonSpan": 0.440483 + }, + { + "lat": 47.5607395, + "latSpan": 0.743251, + "lon": -122.1462785, + "lonSpan": 0.720901 + }, + { + "lat": 47.556288, + "latSpan": 0.090694, + "lon": -122.4013255, + "lonSpan": 0.126793 + }, + { + "lat": 47.093563, + "latSpan": 0.320892, + "lon": -122.701637, + "lonSpan": 0.55098 + }, + { + "lat": 47.5346090123, + "latSpan": 0.889378024643, + "lon": -122.3294835, + "lonSpan": 0.621109 + }, + { + "lat": 47.9747595, + "latSpan": 1.336481, + "lon": -122.8512, + "lonSpan": 1.0904 + }, + { + "lat": 47.6204755, + "latSpan": 0.014397, + "lon": -122.335392, + "lonSpan": 0.00635600000001 + }, + { + "lat": 47.64585, + "latSpan": 0.0669, + "lon": -122.2963, + "lonSpan": 0.0802 + }, + { + "lat": 47.9347358907, + "latSpan": 0.68796117128, + "lon": -121.993246104, + "lonSpan": 0.784555996061 + } + ], + "facebookUrl": "https://www.facebook.com/pages/OneBusAway/216091804930", + "stopInfoUrl": null, + "experimental": false + }, + { + "siriBaseUrl": "https://bustime.mta.info/api/", + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": false, + "contactEmail": "MTABusTime@mtahq.org", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://bustime.mta.info/", + "id": 2, + "regionName": "MTA New York", + "obaVersionInfo": "", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": null, + "otpBaseUrl": null, + "paymentWarningBody": null, + "supportsSiriRealtimeApis": true, + "twitterUrl": "https://mobile.twitter.com/nyctbusstop", + "paymentAndroidAppId": null, + "paymentiOSAppStoreIdentifier": null, + "paymentiOSAppUrlScheme": null, + "active": true, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "en_US", + "bounds": [ + { + "lat": 40.707678, + "latSpan": 0.4093900000000019, + "lon": -74.01768100000001, + "lonSpan": 0.4686659999999989 + }, + { + "lat": 40.8192825, + "latSpan": 0.228707, + "lon": -73.89908199999999, + "lonSpan": 0.23146799999999246 + } + ], + "facebookUrl": "https://www.facebook.com/pages/MTA-New-York-City-Transit/232635164606", + "stopInfoUrl": null, + "experimental": false + }, + { + "siriBaseUrl": null, + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "onebusaway@atlantaregional.com", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://atlanta.onebusaway.org/api/", + "id": 3, + "regionName": "Atlanta", + "obaVersionInfo": "1.1.14-SNAPSHOT|1|1|14|SNAPSHOT|440e5cb692a1ed195de7f0686d69f5ceecbe9a41", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": "onebusaway@atlantaregional.com", + "otpBaseUrl": "https://opentrip.atlantaregion.com/otp", + "paymentWarningBody": null, + "supportsSiriRealtimeApis": false, + "twitterUrl": "https://mobile.twitter.com/OBA_atlanta", + "paymentAndroidAppId": null, + "paymentiOSAppStoreIdentifier": null, + "paymentiOSAppUrlScheme": null, + "active": false, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "en_US", + "bounds": [ + { + "lat": 33.7901797681045, + "latSpan": 0.002537628406997783, + "lon": -84.39483216212469, + "lonSpan": 0.016058977126604645 + }, + { + "lat": 33.84859251766493, + "latSpan": 0.006806584025866869, + "lon": -84.36189486914657, + "lonSpan": 0.035245473959491846 + }, + { + "lat": 34.22444908498577, + "latSpan": 0.06626826841780087, + "lon": -84.48419886031866, + "lonSpan": 0.051677923063323306 + }, + { + "lat": 33.78784752284655, + "latSpan": 0.026695928046905237, + "lon": -84.31082746240949, + "lonSpan": 0.028927748099008 + }, + { + "lat": 33.8079649176, + "latSpan": 0.8443565223999983, + "lon": -84.34070523855, + "lonSpan": 0.8666740198999889 + }, + { + "lat": 33.7842061834795, + "latSpan": 0.030916824823002287, + "lon": -84.36385552465549, + "lonSpan": 0.08416295068897739 + }, + { + "lat": 33.8105225, + "latSpan": 0.5874290000000002, + "lon": -84.37966800000001, + "lonSpan": 0.5820399999999921 + } + ], + "facebookUrl": "https://www.facebook.com/pages/ObaAtlanta/136662306506627", + "stopInfoUrl": null, + "experimental": false + }, + { + "siriBaseUrl": "https://buseta.wmata.com/onebusaway-api-webapp/siri/", + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "feedback@wmata.com", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://buseta.wmata.com/onebusaway-api-webapp/", + "id": 4, + "regionName": "Washington, D.C.", + "obaVersionInfo": "", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": null, + "otpBaseUrl": null, + "paymentWarningBody": null, + "supportsSiriRealtimeApis": true, + "twitterUrl": "https://mobile.twitter.com/Metrobusinfo", + "paymentAndroidAppId": null, + "paymentiOSAppStoreIdentifier": null, + "paymentiOSAppUrlScheme": null, + "active": true, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "en_US", + "bounds": [ + { + "lat": 38.895092500000004, + "latSpan": 0.5927969999999974, + "lon": -77.059196, + "lonSpan": 0.7805199999999957 + } + ], + "facebookUrl": "", + "stopInfoUrl": null, + "experimental": false + }, + { + "siriBaseUrl": null, + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "transitinfo@york.ca", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://oba.yrt.ca/", + "id": 5, + "regionName": "York", + "obaVersionInfo": "1.1.7|1|1|7||c8ee3d4906dd55ecafdd124f31f39c0f54a37b52", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": null, + "otpBaseUrl": null, + "paymentWarningBody": null, + "supportsSiriRealtimeApis": false, + "twitterUrl": "https://mobile.twitter.com/YRTViva", + "paymentAndroidAppId": null, + "paymentiOSAppStoreIdentifier": null, + "paymentiOSAppUrlScheme": null, + "active": true, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "en_CA", + "bounds": [ + { + "lat": 44.0248945, + "latSpan": 0.6089630000000028, + "lon": -79.43752, + "lonSpan": 0.49329600000000084 + } + ], + "facebookUrl": "https://www.facebook.com/198178906967045", + "stopInfoUrl": null, + "experimental": false + }, + { + "siriBaseUrl": null, + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "bear-transit@v-a.io", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://bt.v-a.io/onebusaway/", + "id": 6, + "regionName": "Bear Transit (beta)", + "obaVersionInfo": "1.1.7|1|1|7|d3bbb9109a652359845bdee516dc2cbd1ba35e49", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": null, + "otpBaseUrl": null, + "paymentWarningBody": null, + "supportsSiriRealtimeApis": false, + "twitterUrl": "https://mobile.twitter.com/CalParking", + "paymentAndroidAppId": null, + "paymentiOSAppStoreIdentifier": null, + "paymentiOSAppUrlScheme": null, + "active": false, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "en_US", + "bounds": [ + { + "lat": 37.8917275, + "latSpan": 0.0492929999999987, + "lon": -122.28957750000001, + "lonSpan": 0.10181500000000199 + } + ], + "facebookUrl": "https://www.facebook.com/pages/Bear-Transit/109669175726418", + "stopInfoUrl": null, + "experimental": true + }, + { + "siriBaseUrl": null, + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "sbrown@camsys.com", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://app.dev.mbta.obaweb.org/onebusaway-api-webapp/", + "id": 7, + "regionName": "Boston (beta)", + "obaVersionInfo": "", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": null, + "otpBaseUrl": null, + "paymentWarningBody": null, + "supportsSiriRealtimeApis": false, + "twitterUrl": "", + "paymentAndroidAppId": null, + "paymentiOSAppStoreIdentifier": null, + "paymentiOSAppUrlScheme": null, + "active": true, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "en_US", + "bounds": [ + { + "lat": 42.367244, + "latSpan": 0.014910000000000423, + "lon": -71.023894, + "lonSpan": 0.011874000000005935 + }, + { + "lat": 42.1893185, + "latSpan": 1.2170369999999977, + "lon": -71.210252, + "lonSpan": 1.1692720000000065 + } + ], + "facebookUrl": "", + "stopInfoUrl": null, + "experimental": true + }, + { + "siriBaseUrl": null, + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "obasupport@octo3.fi", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://194.89.230.196:8080/", + "id": 8, + "regionName": "Lappeenranta (beta)", + "obaVersionInfo": "1.1.13|1|1|13||ef9f836500eafee955381b17799b3105b525e93b", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": null, + "otpBaseUrl": null, + "paymentWarningBody": null, + "supportsSiriRealtimeApis": false, + "twitterUrl": "", + "paymentAndroidAppId": null, + "paymentiOSAppStoreIdentifier": null, + "paymentiOSAppUrlScheme": null, + "active": true, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "fi_FI", + "bounds": [ + { + "lat": 61.05999415916385, + "latSpan": 0.07303466470629871, + "lon": 28.197898391353952, + "lonSpan": 0.2674852336517013 + } + ], + "facebookUrl": "", + "stopInfoUrl": null, + "experimental": true + }, + { + "siriBaseUrl": null, + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "info@rvtd.org", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://oba.rvtd.org/onebusaway-api-webapp/", + "id": 9, + "regionName": "Rogue Valley, Oregon", + "obaVersionInfo": "", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": null, + "otpBaseUrl": null, + "paymentWarningBody": null, + "supportsSiriRealtimeApis": false, + "twitterUrl": "", + "paymentAndroidAppId": null, + "paymentiOSAppStoreIdentifier": null, + "paymentiOSAppUrlScheme": null, + "active": true, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "en_US", + "bounds": [ + { + "lat": 42.309802394046, + "latSpan": 0.2679752119080021, + "lon": -122.82026350000001, + "lonSpan": 0.2974530000000044 + } + ], + "facebookUrl": "", + "stopInfoUrl": null, + "experimental": false + }, + { + "siriBaseUrl": null, + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "jespejo@sanjoaquinrtd.com", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://www.obartd.com/onebusaway-api-webapp/", + "id": 10, + "regionName": "San Joaquin RTD (beta)", + "obaVersionInfo": "1.1.12-SNAPSHOT|1|1|12|SNAPSHOT|", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": null, + "otpBaseUrl": null, + "paymentWarningBody": null, + "supportsSiriRealtimeApis": false, + "twitterUrl": "", + "paymentAndroidAppId": null, + "paymentiOSAppStoreIdentifier": null, + "paymentiOSAppUrlScheme": null, + "active": false, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "en_US", + "bounds": [ + { + "lat": 37.9337345, + "latSpan": 0.39840300000000184, + "lon": -121.3456095, + "lonSpan": 0.4220969999999937 + } + ], + "facebookUrl": "", + "stopInfoUrl": null, + "experimental": true + }, + { + "siriBaseUrl": null, + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "customerfeedback@sdmts.com", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://realtime.sdmts.com/api/", + "id": 11, + "regionName": "San Diego", + "obaVersionInfo": "1.1.14|1|1|14|73aea0d ", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": "customerfeedback@sdmts.com", + "otpBaseUrl": "https://realtime.sdmts.com:9091/otp", + "paymentWarningBody": null, + "supportsSiriRealtimeApis": false, + "twitterUrl": "", + "paymentAndroidAppId": "org.sdmts.riderapp", + "paymentiOSAppStoreIdentifier": "1212568295", + "paymentiOSAppUrlScheme": "org.sdmts.riderapp.compassmobile.payments", + "active": true, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "en_US", + "bounds": [ + { + "lat": 32.731591, + "latSpan": 0.0011640000000028294, + "lon": -117.1896335, + "lonSpan": 0.027426999999988766 + }, + { + "lat": 33.0727675231135, + "latSpan": 0.7105120739810005, + "lon": -117.2316382462345, + "lonSpan": 0.7237252935990028 + }, + { + "lat": 32.8998529712665, + "latSpan": 0.7140676589169956, + "lon": -116.73142875280399, + "lonSpan": 1.093941754416008 + } + ], + "facebookUrl": "", + "stopInfoUrl": null, + "experimental": false + }, + { + "siriBaseUrl": null, + "supportsEmbeddedSocial": false, + "supportsObaRealtimeApis": true, + "contactEmail": "oba4spokanetransit@gmail.com", + "enrollParticipantsInStudy": true, + "obaBaseUrl": "https://www.oba4spokane.com/api/", + "id": 12, + "regionName": "Spokane", + "obaVersionInfo": "2.0.0-SNAPSHOT|2|0|0|SNAPSHOT|", + "travelBehaviorDataCollectionEnabled": true, + "supportsObaDiscoveryApis": true, + "supportsOtpBikeshare": false, + "otpContactEmail": null, + "otpBaseUrl": null, + "paymentWarningBody": null, + "supportsSiriRealtimeApis": false, + "twitterUrl": "", + "paymentAndroidAppId": null, + "paymentiOSAppStoreIdentifier": null, + "paymentiOSAppUrlScheme": null, + "active": true, + "open311Servers": [], + "paymentWarningTitle": null, + "language": "en_US", + "bounds": [ + { + "lat": 47.622654, + "latSpan": 0.29196599999999506, + "lon": -117.397625, + "lonSpan": 0.6162599999999969 + } + ], + "facebookUrl": "", + "stopInfoUrl": null, + "experimental": false + } + ] + } +} \ No newline at end of file diff --git a/Apps/MTA/gpx_files/README.markdown b/Apps/MTA/gpx_files/README.markdown new file mode 100644 index 000000000..f70d991a8 --- /dev/null +++ b/Apps/MTA/gpx_files/README.markdown @@ -0,0 +1,27 @@ +About +==== + +GPX, or GPS Exchange Format, is an XML schema designed as a common GPS data format for software applications. + +It can be used to describe waypoints, tracks, and routes. The format is open and can be used without the need to pay license fees. Its tags store location, elevation, and time and can in this way be used to interchange data between GPS devices and software packages. Such computer programs allow users, for example, to view their tracks, project their tracks on satellite images or other maps, annotate maps, and tag photographs with the geolocation in the Exif metadata. + +(courtesy of Wikipedia - http://en.wikipedia.org/wiki/GPS_Exchange_Format) + +Purpose in OneBusAway +=== + +We use GPX files to provide simulated locations to the app while running in the iOS Simulator. + +Files for the following regions are included: + +* [Atlanta, Georgia](atlanta.gpx) +* [Rogue Valley, Oregon](rvtd.gpx) +* [San Diego, California](sandiego.gpx) +* [Seattle/Tacoma/Puget Sound, Washington](capitolhill.gpx) +* [Tampa Bay, Florida](tampa.gpx) +* [Washington, D.C.](washingtondc.gpx) +* [York Region Transit (Canada)](yorkca.gpx) + +The following files are also included for test scenarios: + +* [Invalid location (0.0, 0.0)](invalid.gpx) \ No newline at end of file diff --git a/Apps/MTA/gpx_files/_SEA_10_Layover.gpx b/Apps/MTA/gpx_files/_SEA_10_Layover.gpx new file mode 100644 index 000000000..3ab7c3b39 --- /dev/null +++ b/Apps/MTA/gpx_files/_SEA_10_Layover.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/gpx_files/atlanta.gpx b/Apps/MTA/gpx_files/atlanta.gpx new file mode 100644 index 000000000..da99494f9 --- /dev/null +++ b/Apps/MTA/gpx_files/atlanta.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/gpx_files/boston.gpx b/Apps/MTA/gpx_files/boston.gpx new file mode 100644 index 000000000..0c8ec63b8 --- /dev/null +++ b/Apps/MTA/gpx_files/boston.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/gpx_files/capitolhill.gpx b/Apps/MTA/gpx_files/capitolhill.gpx new file mode 100644 index 000000000..cb30fa223 --- /dev/null +++ b/Apps/MTA/gpx_files/capitolhill.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/gpx_files/invalid.gpx b/Apps/MTA/gpx_files/invalid.gpx new file mode 100644 index 000000000..26abb1bc4 --- /dev/null +++ b/Apps/MTA/gpx_files/invalid.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/gpx_files/rvtd.gpx b/Apps/MTA/gpx_files/rvtd.gpx new file mode 100644 index 000000000..890f20bf0 --- /dev/null +++ b/Apps/MTA/gpx_files/rvtd.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/gpx_files/san_joaquin.gpx b/Apps/MTA/gpx_files/san_joaquin.gpx new file mode 100644 index 000000000..27eb5a812 --- /dev/null +++ b/Apps/MTA/gpx_files/san_joaquin.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/gpx_files/sandiego.gpx b/Apps/MTA/gpx_files/sandiego.gpx new file mode 100644 index 000000000..c450dca28 --- /dev/null +++ b/Apps/MTA/gpx_files/sandiego.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/gpx_files/tampa.gpx b/Apps/MTA/gpx_files/tampa.gpx new file mode 100644 index 000000000..0915e46bd --- /dev/null +++ b/Apps/MTA/gpx_files/tampa.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/gpx_files/washingtondc.gpx b/Apps/MTA/gpx_files/washingtondc.gpx new file mode 100644 index 000000000..2fec531fe --- /dev/null +++ b/Apps/MTA/gpx_files/washingtondc.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/gpx_files/yorkca.gpx b/Apps/MTA/gpx_files/yorkca.gpx new file mode 100644 index 000000000..2d953dc90 --- /dev/null +++ b/Apps/MTA/gpx_files/yorkca.gpx @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/Apps/MTA/pl.lproj/InfoPlist.strings b/Apps/MTA/pl.lproj/InfoPlist.strings new file mode 100644 index 000000000..eceae4767 --- /dev/null +++ b/Apps/MTA/pl.lproj/InfoPlist.strings @@ -0,0 +1,11 @@ +/** + * Apple Strings File + * Generated by Twine 1.0.6 + * Language: pl + */ + +/********** Uncategorized **********/ + +"NSLocationAlwaysAndWhenInUseUsageDescription" = "Pokaż swoją lokalizację na mapie"; + +"NSLocationWhenInUseUsageDescription" = "Pokaż swoją lokalizację na mapie"; diff --git a/Apps/MTA/project.yml b/Apps/MTA/project.yml new file mode 100644 index 000000000..a4f5cf005 --- /dev/null +++ b/Apps/MTA/project.yml @@ -0,0 +1,94 @@ +############ +# OneBusAway +############ + +options: + bundleIdPrefix: info.mta + +targets: + App: + sources: + - path: Apps/MTA + name: App + group: MTA + - path: Apps/Shared/CommonClient + name: Common + group: MTA + entitlements: + path: Apps/MTA/MTA.entitlements + properties: + com.apple.security.application-groups: + - group.info.mta.bustime + com.apple.developer.associated-domains: + - applinks:onebusaway.co + - applinks:www.onebusaway.co + - applinks:alerts.onebusaway.org + info: + path: Apps/MTA/Info.plist + properties: + CFBundleDisplayName: MTA Bus Time + CFBundleURLTypes: [{CFBundleTypeRole: "Editor", CFBundleURLIconFile: "", CFBundleURLName: "onebusaway", CFBundleURLSchemes: ["onebusaway"]}] + LSApplicationQueriesSchemes: + - fb + - twitter + - comgooglemaps + - fb313213768708402HART + - org.sdmts.riderapp.compassmobile.payments + - org.sdmts.pronto + - co.bytemark.tgt + NSAppTransportSecurity: + NSAllowsArbitraryLoads: false + NSExceptionDomains: { + "onebusaway.co": { + NSIncludesSubdomains: true, + NSExceptionAllowsInsecureHTTPLoads: true + } + } + NSHumanReadableCopyright: © Metropolitan Transportation Authority + NSUserActivityTypes: + - info.mta.bustime.user_activity.stop + - info.mta.bustime.user_activity.trip + OBAKitConfig: + AppDevelopersEmailAddress: iphone-app@onebusaway.org + AppGroup: group.info.mta.bustime + BundledRegionsFileName: regions.json + DeepLinkServerBaseAddress: https://onebusaway.co + ExtensionURLScheme: bustime + PrivacyPolicyURL: https://new.mta.info/privacy-policy + PushNotificationAPIKey: d5d0d28a-6091-46cd-9627-0ce01ffa9f9e + RESTServerAPIKey: test + RegionsServerBaseAddress: https://regions.onebusaway.org + RegionsServerAPIPath: /regions-v3.json + settings: + base: + DEVELOPMENT_TEAM: 4ZQCMA634J + PRODUCT_BUNDLE_IDENTIFIER: info.mta.bustime + TodayView: + sources: ["Apps/MTA/Assets.xcassets"] + entitlements: + properties: + com.apple.security.application-groups: + - group.info.mta.bustime + info: + properties: + CFBundleDisplayName: MTA Bus Time + OBAKitConfig: + AppGroup: group.info.mta.bustime + BundledRegionsFileName: regions.json + DeepLinkServerBaseAddress: https://onebusaway.co + ExtensionURLScheme: bustime + RESTServerAPIKey: test + RegionsServerBaseAddress: https://regions.onebusaway.org + RegionsServerAPIPath: /regions-v3.json + settings: + base: + DEVELOPMENT_TEAM: 4ZQCMA634J + PRODUCT_BUNDLE_IDENTIFIER: info.mta.bustime.TodayView + +include: + - path: Apps/Shared/app_shared.yml + - path: OBAKitCore/project.yml + - path: OBAKit/project.yml + - path: OBAKitTests/project.yml + - path: OBAKitUITests/project.yml + - path: TodayView/project.yml diff --git a/OBAKitCore/Models/AgencyAlertsStore.swift b/OBAKitCore/Models/AgencyAlertsStore.swift index 731b3a52c..af3022f1f 100644 --- a/OBAKitCore/Models/AgencyAlertsStore.swift +++ b/OBAKitCore/Models/AgencyAlertsStore.swift @@ -72,7 +72,7 @@ public class AgencyAlertsStore: NSObject, RegionsServiceDelegate { // Get agency alerts from OBA and Obaco. let agencyAlerts = try await withThrowingTaskGroup(of: [AgencyAlert].self) { group -> [AgencyAlert] in group.addTask { - try await self.fetchRegionalAlerts(service: apiService) + await self.fetchRegionalAlerts(service: apiService) } group.addTask { @@ -104,8 +104,8 @@ public class AgencyAlertsStore: NSObject, RegionsServiceDelegate { } // MARK: - REST API - private func fetchRegionalAlerts(service: RESTAPIService) async throws -> [AgencyAlert] { - return try await service.getAlerts(agencies: agencies) + private func fetchRegionalAlerts(service: RESTAPIService) async -> [AgencyAlert] { + return await service.getAlerts(agencies: agencies) } // MARK: - Obaco diff --git a/OBAKitCore/Network/APIService+GetData.swift b/OBAKitCore/Network/APIService+GetData.swift index 4d6534d9e..c52826ee0 100644 --- a/OBAKitCore/Network/APIService+GetData.swift +++ b/OBAKitCore/Network/APIService+GetData.swift @@ -70,6 +70,13 @@ extension APIService { throw APIError.invalidContentType(originalError: nil, expectedContentType: "json", actualContentType: response.contentType) } + if data.count < 10 && String(data: data, encoding: .utf8) == "null" { + // 10 ^^^ is arbitrary. This could really be 8 or maybe even 4, but this is a + // belt and suspenders check. + logger.error("Decoder failed for \(url, privacy: .public): endpoint returned the string 'null' instead of a real value.") + throw APIError.invalidContentType(originalError: nil, expectedContentType: "json", actualContentType: "nothing") + } + do { return try decoder.decode(T.self, from: data) } catch { diff --git a/OBAKitCore/Network/RESTAPIService/RESTAPIService+GetAgencyAlerts.swift b/OBAKitCore/Network/RESTAPIService/RESTAPIService+GetAgencyAlerts.swift index 65a8792f1..6fe6d4504 100644 --- a/OBAKitCore/Network/RESTAPIService/RESTAPIService+GetAgencyAlerts.swift +++ b/OBAKitCore/Network/RESTAPIService/RESTAPIService+GetAgencyAlerts.swift @@ -7,33 +7,38 @@ extension RESTAPIService { // MARK: - Regional Alerts - public nonisolated func getAlerts(agencies: [AgencyWithCoverage]) async throws -> [AgencyAlert] { - return try await withThrowingTaskGroup(of: [AgencyAlert].self) { group -> [AgencyAlert] in + public nonisolated func getAlerts(agencies: [AgencyWithCoverage]) async -> [AgencyAlert] { + return await withTaskGroup(of: [AgencyAlert].self) { group -> [AgencyAlert] in for agency in agencies { group.addTask { - return try await self.getAlerts(agency: agency) + return await self.getAlerts(agency: agency) } } var alerts: [AgencyAlert] = [] - for try await result in group { + for await result in group { alerts.append(contentsOf: result) } return alerts } } - public nonisolated func getAlerts(agency: AgencyWithCoverage) async throws -> [AgencyAlert] { + public nonisolated func getAlerts(agency: AgencyWithCoverage) async -> [AgencyAlert] { let url = urlBuilder.getRESTRegionalAlerts(agencyID: agency.agencyID) - let (data, _) = try await self.getData(for: url) - let message = try TransitRealtime_FeedMessage(serializedData: data) - return message.entity - .filter(isQualifiedAlert) - .compactMap { - // TODO: Don't swallow error - try? AgencyAlert(feedEntity: $0, agencies: [agency]) - } + do { + let (data, _) = try await self.getData(for: url) + let message = try TransitRealtime_FeedMessage(serializedData: data) + return message.entity + .filter(isQualifiedAlert) + .compactMap { + // TODO: Don't swallow error + try? AgencyAlert(feedEntity: $0, agencies: [agency]) + } + } catch { + logger.error("getAlerts() failed for \(agency.agency.name, privacy: .public): \(error, privacy: .public)") + return [] + } } private nonisolated func isQualifiedAlert(_ entity: TransitRealtime_FeedEntity) -> Bool {