diff --git a/.gitignore b/.gitignore index 52b9332cd..051e9cde9 100644 --- a/.gitignore +++ b/.gitignore @@ -125,3 +125,4 @@ android/app/google-services.json !**/ios/**/default.pbxuser !**/ios/**/default.perspectivev3 !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +arcgis_maps_core/ diff --git a/android/app/build.gradle b/android/app/build.gradle index af8272248..41b1347bf 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -15,8 +15,8 @@ if (keystorePropertiesFile.exists()) { android { namespace = "edu.ucsd" - compileSdk = flutter.compileSdkVersion - ndkVersion = "27.0.12077973" + compileSdk = 36 + ndkVersion = "28.2.13676358" compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 @@ -34,7 +34,7 @@ android { defaultConfig { applicationId = "edu.ucsd" - minSdkVersion = 23 + minSdkVersion = 28 targetSdk = flutter.targetSdkVersion versionCode = flutter.versionCode versionName = flutter.versionName diff --git a/android/settings.gradle b/android/settings.gradle index a7ce4fe0c..868031436 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -19,7 +19,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "8.9.1" apply false - id "org.jetbrains.kotlin.android" version "2.1.10" apply false + id "org.jetbrains.kotlin.android" version "2.3.0" apply false } include ":app" diff --git a/arcgis_maps_core b/arcgis_maps_core new file mode 120000 index 000000000..50b594453 --- /dev/null +++ b/arcgis_maps_core @@ -0,0 +1 @@ +/Users/alessioyu/.pub-cache/hosted/pub.dev/arcgis_maps-300.0.0+4935/arcgis_maps_core \ No newline at end of file diff --git a/ios/Podfile b/ios/Podfile index b18002d35..8f22b17e1 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '15.0' +platform :ios, '17.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' @@ -31,6 +31,9 @@ target 'Runner' do use_frameworks! use_modular_headers! + pod 'Runtimecore', :podspec => '../arcgis_maps_core/ios/Runtimecore.podspec' + pod 'arcgis_maps_ffi', :podspec => '../arcgis_maps_core/ios/arcgis_maps_ffi.podspec' + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) end @@ -41,7 +44,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) target.build_configurations.each do |config| - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '14.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0' # Remove unused permissions here # More information: https://github.com/BaseflowIT/flutter-permission-handler/blob/develop/permission_handler/ios/Classes/PermissionHandlerEnums.h # e.g. 'PERMISSION_CAMERA=0' for to remove camera permission diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2bd3bf343..9f51f6269 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,88 +1,92 @@ PODS: - app_links (0.0.1): - Flutter + - arcgis_maps (300.0.0.4935): + - arcgis_maps_ffi + - Flutter + - Runtimecore + - arcgis_maps_ffi (300.0.0.4935) - connectivity_plus (0.0.1): - Flutter - - FlutterMacOS - device_info_plus (0.0.1): - Flutter - - Firebase/CoreOnly (12.6.0): - - FirebaseCore (~> 12.6.0) - - Firebase/Crashlytics (12.6.0): + - Firebase/CoreOnly (12.12.0): + - FirebaseCore (~> 12.12.0) + - Firebase/Crashlytics (12.12.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 12.6.0) - - Firebase/Messaging (12.6.0): + - FirebaseCrashlytics (~> 12.12.0) + - Firebase/Messaging (12.12.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 12.6.0) - - firebase_analytics (12.1.0): + - FirebaseMessaging (~> 12.12.0) + - firebase_analytics (12.3.0): - firebase_core - - FirebaseAnalytics (= 12.6.0) + - FirebaseAnalytics (= 12.12.0) - Flutter - - firebase_core (4.3.0): - - Firebase/CoreOnly (= 12.6.0) + - firebase_core (4.7.0): + - Firebase/CoreOnly (= 12.12.0) - Flutter - - firebase_crashlytics (5.0.6): - - Firebase/Crashlytics (= 12.6.0) + - firebase_crashlytics (5.2.0): + - Firebase/Crashlytics (= 12.12.0) - firebase_core - Flutter - - firebase_messaging (16.1.0): - - Firebase/Messaging (= 12.6.0) + - firebase_messaging (16.2.0): + - Firebase/Messaging (= 12.12.0) - firebase_core - Flutter - - FirebaseAnalytics (12.6.0): - - FirebaseAnalytics/Default (= 12.6.0) - - FirebaseCore (~> 12.6.0) - - FirebaseInstallations (~> 12.6.0) + - FirebaseAnalytics (12.12.0): + - FirebaseAnalytics/Default (= 12.12.0) + - FirebaseCore (~> 12.12.0) + - FirebaseInstallations (~> 12.12.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/Network (~> 8.1) - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - FirebaseAnalytics/Default (12.6.0): - - FirebaseCore (~> 12.6.0) - - FirebaseInstallations (~> 12.6.0) - - GoogleAppMeasurement/Default (= 12.6.0) + - FirebaseAnalytics/Default (12.12.0): + - FirebaseCore (~> 12.12.0) + - FirebaseInstallations (~> 12.12.0) + - GoogleAppMeasurement/Default (= 12.12.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/Network (~> 8.1) - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - FirebaseCore (12.6.0): - - FirebaseCoreInternal (~> 12.6.0) + - FirebaseCore (12.12.1): + - FirebaseCoreInternal (~> 12.12.0) - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Logger (~> 8.1) - - FirebaseCoreExtension (12.6.0): - - FirebaseCore (~> 12.6.0) - - FirebaseCoreInternal (12.6.0): + - FirebaseCoreExtension (12.12.0): + - FirebaseCore (~> 12.12.0) + - FirebaseCoreInternal (12.12.0): - "GoogleUtilities/NSData+zlib (~> 8.1)" - - FirebaseCrashlytics (12.6.0): - - FirebaseCore (~> 12.6.0) - - FirebaseInstallations (~> 12.6.0) - - FirebaseRemoteConfigInterop (~> 12.6.0) - - FirebaseSessions (~> 12.6.0) + - FirebaseCrashlytics (12.12.1): + - FirebaseCore (~> 12.12.0) + - FirebaseInstallations (~> 12.12.0) + - FirebaseRemoteConfigInterop (~> 12.12.0) + - FirebaseSessions (~> 12.12.0) - GoogleDataTransport (~> 10.1) - GoogleUtilities/Environment (~> 8.1) - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) - - FirebaseInstallations (12.6.0): - - FirebaseCore (~> 12.6.0) + - FirebaseInstallations (12.12.0): + - FirebaseCore (~> 12.12.0) - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1) - PromisesObjC (~> 2.4) - - FirebaseMessaging (12.6.0): - - FirebaseCore (~> 12.6.0) - - FirebaseInstallations (~> 12.6.0) + - FirebaseMessaging (12.12.0): + - FirebaseCore (~> 12.12.0) + - FirebaseInstallations (~> 12.12.0) - GoogleDataTransport (~> 10.1) - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Reachability (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1) - nanopb (~> 3.30910.0) - - FirebaseRemoteConfigInterop (12.6.0) - - FirebaseSessions (12.6.0): - - FirebaseCore (~> 12.6.0) - - FirebaseCoreExtension (~> 12.6.0) - - FirebaseInstallations (~> 12.6.0) + - FirebaseRemoteConfigInterop (12.12.0) + - FirebaseSessions (12.12.0): + - FirebaseCore (~> 12.12.0) + - FirebaseCoreExtension (~> 12.12.0) + - FirebaseInstallations (~> 12.12.0) - GoogleDataTransport (~> 10.1) - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/UserDefaults (~> 8.1) @@ -91,7 +95,10 @@ PODS: - Flutter (1.0.0) - flutter_local_notifications (0.0.1): - Flutter - - flutter_secure_storage (6.0.0): + - flutter_secure_storage_darwin (10.0.0): + - Flutter + - FlutterMacOS + - flutter_web_auth_2 (5.0.0): - Flutter - geolocator_apple (1.2.0): - Flutter @@ -102,28 +109,28 @@ PODS: - Flutter - Google-Maps-iOS-Utils (< 7.0, >= 5.0) - GoogleMaps (< 10.0, >= 8.4) - - GoogleAdsOnDeviceConversion (3.2.0): + - GoogleAdsOnDeviceConversion (3.4.2): - GoogleUtilities/Environment (~> 8.1) - GoogleUtilities/Logger (~> 8.1) - GoogleUtilities/Network (~> 8.1) - nanopb (~> 3.30910.0) - - GoogleAppMeasurement/Core (12.6.0): + - GoogleAppMeasurement/Core (12.12.0): - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/Network (~> 8.1) - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - GoogleAppMeasurement/Default (12.6.0): - - GoogleAdsOnDeviceConversion (~> 3.2.0) - - GoogleAppMeasurement/Core (= 12.6.0) - - GoogleAppMeasurement/IdentitySupport (= 12.6.0) + - GoogleAppMeasurement/Default (12.12.0): + - GoogleAdsOnDeviceConversion (~> 3.4.0) + - GoogleAppMeasurement/Core (= 12.12.0) + - GoogleAppMeasurement/IdentitySupport (= 12.12.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/Network (~> 8.1) - "GoogleUtilities/NSData+zlib (~> 8.1)" - nanopb (~> 3.30910.0) - - GoogleAppMeasurement/IdentitySupport (12.6.0): - - GoogleAppMeasurement/Core (= 12.6.0) + - GoogleAppMeasurement/IdentitySupport (12.12.0): + - GoogleAppMeasurement/Core (= 12.12.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.1) - GoogleUtilities/MethodSwizzler (~> 8.1) - GoogleUtilities/Network (~> 8.1) @@ -162,6 +169,8 @@ PODS: - GoogleUtilities/UserDefaults (8.1.0): - GoogleUtilities/Logger - GoogleUtilities/Privacy + - haptic_feedback (0.5.1): + - Flutter - nanopb (3.30910.0): - nanopb/decode (= 3.30910.0) - nanopb/encode (= 3.30910.0) @@ -169,14 +178,12 @@ PODS: - nanopb/encode (3.30910.0) - package_info_plus (0.4.5): - Flutter - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - permission_handler_apple (9.3.0): - Flutter - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) + - Runtimecore (300.0.0.4935) - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -190,7 +197,9 @@ PODS: DEPENDENCIES: - app_links (from `.symlinks/plugins/app_links/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) + - arcgis_maps (from `.symlinks/plugins/arcgis_maps/ios`) + - arcgis_maps_ffi (from `../arcgis_maps_core/ios/arcgis_maps_ffi.podspec`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) - firebase_core (from `.symlinks/plugins/firebase_core/ios`) @@ -198,12 +207,14 @@ DEPENDENCIES: - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) + - flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`) + - flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`) - geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`) - google_maps_flutter_ios (from `.symlinks/plugins/google_maps_flutter_ios/ios`) + - haptic_feedback (from `.symlinks/plugins/haptic_feedback/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - Runtimecore (from `../arcgis_maps_core/ios/Runtimecore.podspec`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) @@ -234,8 +245,12 @@ SPEC REPOS: EXTERNAL SOURCES: app_links: :path: ".symlinks/plugins/app_links/ios" + arcgis_maps: + :path: ".symlinks/plugins/arcgis_maps/ios" + arcgis_maps_ffi: + :podspec: "../arcgis_maps_core/ios/arcgis_maps_ffi.podspec" connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/darwin" + :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" firebase_analytics: @@ -250,18 +265,22 @@ EXTERNAL SOURCES: :path: Flutter flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" - flutter_secure_storage: - :path: ".symlinks/plugins/flutter_secure_storage/ios" + flutter_secure_storage_darwin: + :path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin" + flutter_web_auth_2: + :path: ".symlinks/plugins/flutter_web_auth_2/ios" geolocator_apple: :path: ".symlinks/plugins/geolocator_apple/darwin" google_maps_flutter_ios: :path: ".symlinks/plugins/google_maps_flutter_ios/ios" + haptic_feedback: + :path: ".symlinks/plugins/haptic_feedback/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" - path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" + Runtimecore: + :podspec: "../arcgis_maps_core/ios/Runtimecore.podspec" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" url_launcher_ios: @@ -273,44 +292,48 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: c5161ac5ab5383ad046884568b4b91cb52df5d91 - connectivity_plus: b21496ab28d1324eb59885d888a4d83b98531f01 + arcgis_maps: 6751f496cf10a6ab40be99650db808fba3c79479 + arcgis_maps_ffi: 0db05f5860c64735cd0bfacd40ea0dc32108dd0d + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe - Firebase: a451a7b61536298fd5cbfe3a746fd40443a50679 - firebase_analytics: 4f9cca09e65f6c2944a862c6dc86f6bed9fb769c - firebase_core: ba00a168e719694f38960502ceb560285603d073 - firebase_crashlytics: 13f4b77e9ce2a84b1f8ea07f293db5b6213ce1cf - firebase_messaging: bf0e29321927edc02a563c984dbfa5b063864b15 - FirebaseAnalytics: d0a97a0db6425e5a5d966340b87f92ca7b13a557 - FirebaseCore: 0e38ad5d62d980a47a64b8e9301ffa311457be04 - FirebaseCoreExtension: 032fd6f8509e591fda8cb76f6651f20d926b121f - FirebaseCoreInternal: 69bf1306a05b8ac43004f6cc1f804bb7b05b229e - FirebaseCrashlytics: 3d6248c50726ee7832aef0e53cb84c9e64d9fa7e - FirebaseInstallations: 631b38da2e11a83daa4bfb482f79d286a5dfa7ad - FirebaseMessaging: a61bc42dcab3f7a346d94bbb54dab2c9435b18b2 - FirebaseRemoteConfigInterop: 3443b8cb8fffd76bb3e03b2a84bfd3db952fcda4 - FirebaseSessions: 2e8f808347e665dff3e5843f275715f07045297d - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 - flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + Firebase: aa154fee4e9b8eac17aa42344988865b3e857d33 + firebase_analytics: e5f4faccf1f7040d3788e835b7eb7b57dc84a911 + firebase_core: 9156a152117c843440b0b990c785aa0259bc5447 + firebase_crashlytics: e24acd48861c5edf6e0f6c134d6a0b28593c76d7 + firebase_messaging: 0d962ab44ff24ed36deb8fa2ee043c4671858269 + FirebaseAnalytics: db0dca7e35a503ddf6869e457f4926dc35afe216 + FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284 + FirebaseCoreExtension: ff6fd42eb5287e71d3e160450de6509733d9ead7 + FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9 + FirebaseCrashlytics: 03f4e20d0c9b7fd6338cb9066f4bfb69d3f42fd0 + FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e + FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31 + FirebaseRemoteConfigInterop: 23996ab7397494722df4fdd1fd398024389d5da8 + FirebaseSessions: 804bd321f2d2f2ddafe74ef7856062aa19f179c2 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb + flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23 + flutter_web_auth_2: 646fc9df97a01c59e5eea99b237da2c6360f8439 geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e Google-Maps-iOS-Utils: 0a484b05ed21d88c9f9ebbacb007956edd508a96 google_maps_flutter_ios: 0291eb2aa252298a769b04d075e4a9d747ff7264 - GoogleAdsOnDeviceConversion: d68c69dd9581a0f5da02617b6f377e5be483970f - GoogleAppMeasurement: 3bf40aff49a601af5da1c3345702fcb4991d35ee + GoogleAdsOnDeviceConversion: 02fc8fe599acd867e328321effb0d9b2d023a38a + GoogleAppMeasurement: 3b4687de50ab25ee2d4d541849f10ca8df862a12 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleMaps: 0608099d4870cac8754bdba9b6953db543432438 GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1 + haptic_feedback: 1f22d7ad3abb43bb814aa42fdf548dc6d9e8245a nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - url_launcher_ios: 694010445543906933d732453a59da0a173ae33d - webview_flutter_wkwebview: 1821ceac936eba6f7984d89a9f3bcb4dea99ebb2 + Runtimecore: 58f0dbf24f3ed9e38aaf552c7723ecb64f78cc1d + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d wifi_connection: a73e16eb2fe4e40ca32a6d69c8609130603bb62d -PODFILE CHECKSUM: c12be671b3fe135b062a73a5acc25fefbe51c40b +PODFILE CHECKSUM: 327a920f85ee4988ff213b8c0b4a413604d472f9 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 096808a0b..d3a0320ba 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -15,7 +15,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - F98DB421A362DEC1AA6FF298 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E1731BD34CEF0DCB3E993F02 /* Pods_Runner.framework */; }; + D8CA97443716260D5654DC3A /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 43D592C4668B7B9255F41E36 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -32,17 +32,17 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0E940A07290D20B33171D413 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 2F5012E47A1F3F57C687EABE /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 346B293D23F32D2500DD909D /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 52F88FE7DDB502D18A26B7EA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 43D592C4668B7B9255F41E36 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 74BC29D11E57AC63D5E725D0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 774267092416F93F00AE2519 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8D760C497D5B2DA31FF10CAF /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -50,7 +50,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; explicitFileType = text.plist.info; path = Info.plist; sourceTree = ""; }; - E1731BD34CEF0DCB3E993F02 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EA15149A7ECBEEAFCB060618 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -58,7 +58,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - F98DB421A362DEC1AA6FF298 /* Pods_Runner.framework in Frameworks */, + D8CA97443716260D5654DC3A /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -83,7 +83,7 @@ 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, FB73A6E03F3079B1691E9055 /* Pods */, - C522BDF92E7051EA82B83309 /* Frameworks */, + A0E05FAA68B331E57CDF099A /* Frameworks */, ); sourceTree = ""; }; @@ -120,10 +120,10 @@ name = "Supporting Files"; sourceTree = ""; }; - C522BDF92E7051EA82B83309 /* Frameworks */ = { + A0E05FAA68B331E57CDF099A /* Frameworks */ = { isa = PBXGroup; children = ( - E1731BD34CEF0DCB3E993F02 /* Pods_Runner.framework */, + 43D592C4668B7B9255F41E36 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -131,9 +131,9 @@ FB73A6E03F3079B1691E9055 /* Pods */ = { isa = PBXGroup; children = ( - 0E940A07290D20B33171D413 /* Pods-Runner.debug.xcconfig */, - 52F88FE7DDB502D18A26B7EA /* Pods-Runner.release.xcconfig */, - 8D760C497D5B2DA31FF10CAF /* Pods-Runner.profile.xcconfig */, + 74BC29D11E57AC63D5E725D0 /* Pods-Runner.debug.xcconfig */, + EA15149A7ECBEEAFCB060618 /* Pods-Runner.release.xcconfig */, + 2F5012E47A1F3F57C687EABE /* Pods-Runner.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -145,7 +145,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - DFEBA31F248FFF58128BD6EE /* [CP] Check Pods Manifest.lock */, + 3A232C58A713A3B0E30B33C2 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -153,8 +153,8 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 348EE9E2258A901800712F2E /* ShellScript */, - B19D24E1170B60724162B13B /* [CP] Embed Pods Frameworks */, - BAA8B7F5B962BB7F0187FF6A /* [CP] Copy Pods Resources */, + 4207515FC91A96EADBE8A0AD /* [CP] Embed Pods Frameworks */, + 2C1278E8790F8AB292D138B8 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -176,7 +176,6 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; - DevelopmentTeam = G789749RTK; LastSwiftMigration = 0910; ProvisioningStyle = Automatic; }; @@ -217,6 +216,30 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 2C1278E8790F8AB292D138B8 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleMaps/GoogleMapsResources.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/firebase_messaging/firebase_messaging_Privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/google_maps_flutter_ios/google_maps_flutter_ios_privacy.bundle", + "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMapsResources.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/firebase_messaging_Privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/google_maps_flutter_ios_privacy.bundle", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 348EE9E2258A901800712F2E /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -234,38 +257,45 @@ shellPath = /bin/sh; shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n${PODS_ROOT}/FirebaseCrashlytics/run\n"; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 3A232C58A713A3B0E30B33C2 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); - name = "Run Script"; + name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - B19D24E1170B60724162B13B /* [CP] Embed Pods Frameworks */ = { + 4207515FC91A96EADBE8A0AD /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -284,19 +314,23 @@ "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", "${BUILT_PRODUCTS_DIR}/PromisesObjC/FBLPromises.framework", "${BUILT_PRODUCTS_DIR}/PromisesSwift/Promises.framework", + "${BUILT_PRODUCTS_DIR}/app_links/app_links.framework", + "${BUILT_PRODUCTS_DIR}/arcgis_maps/arcgis_maps.framework", "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", - "${BUILT_PRODUCTS_DIR}/flutter_secure_storage/flutter_secure_storage.framework", + "${BUILT_PRODUCTS_DIR}/flutter_secure_storage_darwin/flutter_secure_storage_darwin.framework", + "${BUILT_PRODUCTS_DIR}/flutter_web_auth_2/flutter_web_auth_2.framework", "${BUILT_PRODUCTS_DIR}/geolocator_apple/geolocator_apple.framework", + "${BUILT_PRODUCTS_DIR}/haptic_feedback/haptic_feedback.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", - "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", - "${BUILT_PRODUCTS_DIR}/uni_links2/uni_links2.framework", "${BUILT_PRODUCTS_DIR}/url_launcher_ios/url_launcher_ios.framework", - "${BUILT_PRODUCTS_DIR}/webview_flutter/webview_flutter.framework", + "${BUILT_PRODUCTS_DIR}/webview_flutter_wkwebview/webview_flutter_wkwebview.framework", "${BUILT_PRODUCTS_DIR}/wifi_connection/wifi_connection.framework", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/Runtimecore/Runtimecore.framework/Runtimecore", + "${PODS_XCFRAMEWORKS_BUILD_DIR}/arcgis_maps_ffi/arcgis_maps_ffi.framework/arcgis_maps_ffi", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( @@ -312,70 +346,43 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBLPromises.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Promises.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/app_links.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/arcgis_maps.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_secure_storage_darwin.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_web_auth_2.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/geolocator_apple.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/haptic_feedback.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/uni_links2.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_ios.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/webview_flutter.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/webview_flutter_wkwebview.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wifi_connection.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Runtimecore.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/arcgis_maps_ffi.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - BAA8B7F5B962BB7F0187FF6A /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/GoogleMaps/GoogleMapsResources.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/firebase_messaging/firebase_messaging_Privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/google_maps_flutter_ios/google_maps_flutter_ios_privacy.bundle", - "${PODS_CONFIGURATION_BUILD_DIR}/permission_handler_apple/permission_handler_apple_privacy.bundle", - ); - name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMapsResources.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/firebase_messaging_Privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/google_maps_flutter_ios_privacy.bundle", - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/permission_handler_apple_privacy.bundle", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - DFEBA31F248FFF58128BD6EE /* [CP] Check Pods Manifest.lock */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - ); inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( ); + name = "Run Script"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -477,7 +484,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -490,8 +497,13 @@ PRODUCT_BUNDLE_IDENTIFIER = edu.ucsd.ucsandiego; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Profile; @@ -620,7 +632,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -633,9 +645,14 @@ PRODUCT_BUNDLE_IDENTIFIER = edu.ucsd.ucsandiego; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -657,7 +674,7 @@ "$(PROJECT_DIR)/Flutter", ); INFOPLIST_FILE = Runner/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -670,8 +687,13 @@ PRODUCT_BUNDLE_IDENTIFIER = edu.ucsd.ucsandiego; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; + SUPPORTS_MACCATALYST = NO; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = 1; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 2ba47ae65..2c31b4c90 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> @@ -34,11 +35,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" launchStyle = "0" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> diff --git a/lib/app_router.dart b/lib/app_router.dart index 137062593..52418ccf7 100644 --- a/lib/app_router.dart +++ b/lib/app_router.dart @@ -35,6 +35,7 @@ import 'package:campus_mobile_experimental/ui/shuttle/add_shuttle_stops_view.dar import 'package:campus_mobile_experimental/ui/shuttle/manage_shuttle_view.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:campus_mobile_experimental/ui/esrimap/esrimap.dart'; class Router { static Route generateRoute(RouteSettings settings) { @@ -51,7 +52,7 @@ class Router { return Home(); }); case RoutePaths.MAP: - return MaterialPageRoute(builder: (_) => prefix0.Maps()); + return MaterialPageRoute(builder: (_) => EsriMap()); case RoutePaths.MAP_SEARCH: return MaterialPageRoute(builder: (_) => MapSearchView()); case RoutePaths.NOTIFICATIONS: diff --git a/lib/main.dart b/lib/main.dart index a7fc8234b..6255348d6 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,7 @@ import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:google_maps_flutter_android/google_maps_flutter_android.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:arcgis_maps/arcgis_maps.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; diff --git a/lib/ui/esrimap/esrimap.dart b/lib/ui/esrimap/esrimap.dart new file mode 100644 index 000000000..e3e287e46 --- /dev/null +++ b/lib/ui/esrimap/esrimap.dart @@ -0,0 +1,3014 @@ +import 'dart:convert'; +import 'dart:math' as math; +import 'dart:async'; +import 'package:arcgis_maps/arcgis_maps.dart'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'esrimap_basemaps.dart'; +import 'esrimap_fab.dart'; +import 'esrimap_layers_panel.dart'; +import 'esrimap_scene.dart'; +import 'esrimap_config.dart'; +import 'esrimap_ai_search.dart'; + +// ----------------------------------------------------------------------------- +// Model +// ----------------------------------------------------------------------------- + +/// Unified search result model for both Buildings and POIs. +class MapSearchResult { + final String name; + final String subtitle; + final double latitude; + final double longitude; + final MapSearchSource source; + final String address; + final String description; + final String? websiteUrl; + + const MapSearchResult({ + required this.name, + required this.subtitle, + required this.latitude, + required this.longitude, + required this.source, + this.address = '', + this.description = '', + this.websiteUrl, + }); + + /// Serialize to JSON for SharedPreferences storage. + Map toJson() => { + 'name': name, + 'subtitle': subtitle, + 'latitude': latitude, + 'longitude': longitude, + 'source': source.index, + 'address': address, + 'description': description, + 'websiteUrl': websiteUrl, + }; + + /// Deserialize from JSON. + factory MapSearchResult.fromJson(Map json) { + return MapSearchResult( + name: json['name'] as String? ?? '', + subtitle: json['subtitle'] as String? ?? '', + latitude: (json['latitude'] as num?)?.toDouble() ?? 0.0, + longitude: (json['longitude'] as num?)?.toDouble() ?? 0.0, + source: MapSearchSource.values[json['source'] as int? ?? 0], + address: json['address'] as String? ?? '', + description: json['description'] as String? ?? '', + websiteUrl: json['websiteUrl'] as String?, + ); + } +} + +enum MapSearchSource { building, poi } + +// ----------------------------------------------------------------------------- +// Category definitions +// ----------------------------------------------------------------------------- + +class _SearchCategory { + final String label; + final IconData icon; + final String poiClassValue; + final Color color; + + const _SearchCategory({ + required this.label, + required this.icon, + required this.poiClassValue, + this.color = const Color(0xFFF5F0E6), + }); +} + +IconData _iconDataForName(String name) { + switch (name) { + case 'restaurant': return Icons.restaurant; + case 'menu_book': return Icons.menu_book; + case 'local_parking': return Icons.local_parking; + case 'fitness_center': return Icons.fitness_center; + case 'directions_bus': return Icons.directions_bus; + default: return Icons.place; + } +} + +Color _colorFromHex(String? hex) { + if (hex == null || hex.isEmpty) return const Color(0xFFF5F0E6); + final stripped = hex.replaceFirst('#', ''); + final value = int.tryParse(stripped, radix: 16); + if (value == null) return const Color(0xFFF5F0E6); + return Color(0xFF000000 | value); +} + +// ----------------------------------------------------------------------------- +// Slide-over header delegate +// ----------------------------------------------------------------------------- + +class _SlideOverHeaderDelegate extends SliverPersistentHeaderDelegate { + final Widget child; + final double height; + final Color backgroundColor; + + _SlideOverHeaderDelegate({ + required this.child, + required this.height, + required this.backgroundColor, + }); + + @override + double get minExtent => height; + @override + double get maxExtent => height; + + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + return Container( + height: height, + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: shrinkOffset == 0 + ? const BorderRadius.vertical(top: Radius.circular(16)) + : null, + ), + child: child, + ); + } + + @override + bool shouldRebuild(covariant _SlideOverHeaderDelegate oldDelegate) { + return child != oldDelegate.child || + height != oldDelegate.height || + backgroundColor != oldDelegate.backgroundColor; + } +} + +// ----------------------------------------------------------------------------- +// Widget +// ----------------------------------------------------------------------------- + +class EsriMap extends StatefulWidget { + const EsriMap({Key? key}) : super(key: key); + + @override + State createState() => _EsriMapState(); +} + +class _EsriMapState extends State with AutomaticKeepAliveClientMixin { + final _mapViewController = ArcGISMapView.createController(); + late final ArcGISMap _map; + final _graphicsOverlay = GraphicsOverlay(); + final _searchController = TextEditingController(); + final _focusNode = FocusNode(); + + // Slide-over controllers + final _categorySheetController = DraggableScrollableController(); + final _detailSheetController = DraggableScrollableController(); + + // Service base URL + static const _baseUrl = 'https://appzxi70zi.execute-api.us-west-2.amazonaws.com/test/ArcGIS-Map'; + + // Config -- loaded on init, gates map setup + EsriMapConfig? _config; + + // SharedPreferences key for recent searches + static const _recentSearchesKey = 'esri_map_recent_searches'; + static const _maxRecentSearches = 5; + + // Search state + List _searchResults = []; + List _allPoiClasses = []; // all distinct Class values from the server + List _matchingPoiClasses = []; // live-filtered subset while typing + bool _showResults = false; + bool _isSearching = false; + bool _showSuggestions = false; + + // Recent searches + List _recentSearches = []; + + // Detail slide-over state + MapSearchResult? _selectedResult; + MapSearchResult? _lastSelectedResult; + + // Results currently plotted as pins on the map (from category search) + List _mappedResults = []; + + // Full unfiltered result set for the active category (used by "See All") + List _allCategoryResults = []; + + // Whether to show the full category list panel + bool _showCategoryList = false; + + // The active category (for display in the "See All" label) + _SearchCategory? _activeCategory; + + // FAB active-button highlight state + bool _isLocationActive = false; + bool _isRecenterActive = false; + bool _ignoreViewpointReset = false; + + // Location display + final _locationDataSource = SystemLocationDataSource(); + bool _locationStarted = false; + + // Routing + final _routeGraphicsOverlay = GraphicsOverlay(); + bool _isRouting = false; + bool _hasRoute = false; + Envelope? _routePaddedExtent; + bool _routeFailed = false; + String _travelMode = 'Walking'; + double _routeTravelTimeMinutes = 0; + List _routeManeuvers = []; + bool _showRouteFields = false; + final _fromController = TextEditingController(); + final _toController = TextEditingController(); + final _fromFocusNode = FocusNode(); + final _toFocusNode = FocusNode(); + String? _activeRouteField; + (double, double)? _fromLatLng; + MapSearchResult? _routeDestination; + + // Basemap switcher + BasemapType _currentBasemapType = BasemapType.defaultMap; + final Map _basemaps = {}; + bool _showLayersPanel = false; + + // Operational layers — keyed by config layer key + final Map _layerVisible = {}; + final Map _layerLoading = {}; + final Map> _layerInstances = {}; + final Map _layerTimers = {}; + + // Compass + double _mapRotation = 0.0; + StreamSubscription? _viewpointChangedSubscription; + + final _mapReadyCompleter = Completer(); + + List<_SearchCategory> get _categories { + if (_config == null) return []; + return _config!.searchCategories + .map((c) => _SearchCategory( + label: c.label, + icon: _iconDataForName(c.icon), + poiClassValue: c.poiClass, + color: _colorFromHex(c.color), + )) + .toList(); + } + + // Scene mode key — matches keys in config.scenes ('default' | 'building3d' | 'droneView') + String _sceneMode = 'default'; + EsriSceneWidget? _scene3DWidget; + EsriSceneWidget? _sceneDroneWidget; + final _scene3DKey = GlobalKey(); + final _sceneDroneKey = GlobalKey(); + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _fetchConfigThenInit(); + _loadRecentSearches(); + _focusNode.addListener(_onFocusChanged); + _fromFocusNode.addListener(_onFromFocusChanged); + _toFocusNode.addListener(_onToFocusChanged); + } + + Future _fetchConfigThenInit() async { + try { + final config = await EsriMapConfigService.instance.fetch(); + if (!mounted) return; + setState(() => _config = config); + _initMap(config); + _fetchAllPoiClasses(); + } catch (e) { + debugPrint('Config fetch failed: $e'); + _mapReadyCompleter.completeError(e); + } + } + + @override + void dispose() { + _categorySheetController.dispose(); + _detailSheetController.dispose(); + _searchController.dispose(); + _fromController.dispose(); + _toController.dispose(); + _fromFocusNode.dispose(); + _toFocusNode.dispose(); + _focusNode.dispose(); + for (final timer in _layerTimers.values) { + timer.cancel(); + } + _viewpointChangedSubscription?.cancel(); + super.dispose(); + } + + void _setupAgeAuthChallengeHandler() { + ArcGISEnvironment + .authenticationManager + .arcGISAuthenticationChallengeHandler = _AgeAuthChallengeHandler( + EsriMapConfigService.instance.tokensUrl, + ); + } + + void _initMap(EsriMapConfig config) { + for (final type in BasemapType.values) { + _basemaps[type] = buildBasemap(type, config); + } + + _map = ArcGISMap.withBasemap(_basemaps[_currentBasemapType]!); + _map.initialViewpoint = Viewpoint.fromCenter( + ArcGISPoint( + x: -117.2340, + y: 32.8801, + spatialReference: SpatialReference.wgs84, + ), + scale: 24000, + ); + _mapReadyCompleter.complete(); + } + + void _onMapViewReady() async { + await _mapReadyCompleter.future; + _setupAgeAuthChallengeHandler(); + _mapViewController.arcGISMap = _map; + _mapViewController.interactionOptions.rotateEnabled = true; + if (!_mapViewController.graphicsOverlays.contains(_graphicsOverlay)) { + _mapViewController.graphicsOverlays.add(_graphicsOverlay); + } + if (!_mapViewController.graphicsOverlays.contains(_routeGraphicsOverlay)) { + _mapViewController.graphicsOverlays.add(_routeGraphicsOverlay); + } + + // Wire up location display — blue dot, no auto-pan on start + _mapViewController.locationDisplay.dataSource = _locationDataSource; + _mapViewController.locationDisplay.autoPanMode = + LocationDisplayAutoPanMode.off; + + _startLocationDisplay(); + _preloadAlternateBasemaps(); + + _viewpointChangedSubscription = + _mapViewController.onViewpointChanged.listen((_) { + final vp = _mapViewController.getCurrentViewpoint( + ViewpointType.centerAndScale, + ); + if (vp != null && mounted) { + setState(() { + _mapRotation = vp.rotation; + if (!_ignoreViewpointReset && (_isLocationActive || _isRecenterActive)) { + _isLocationActive = false; + _isRecenterActive = false; + } + }); + } + }); + } + + void _snapToNorth() { + if (_sceneMode != 'default') { + if (_sceneMode == 'building3d') { + _scene3DKey.currentState?.snapToNorth(); + } else { + _sceneDroneKey.currentState?.snapToNorth(); + } + return; + } + _mapViewController.setViewpointRotation(angleDegrees: 0); + } + + ///Loads metadata and style sheets; tile content still fetches lazily once the basemap is applied to the MapView. + void _preloadAlternateBasemaps() { + for (final entry in _basemaps.entries) { + if (entry.key == _currentBasemapType) continue; + entry.value.load().catchError((Object e) { + debugPrint('Failed to preload basemap ${entry.key}: $e'); + }); + } + } + + void _switchBasemap(BasemapType newType) { + if (newType == _currentBasemapType) return; + final newBasemap = _basemaps[newType]; + if (newBasemap == null) return; + + // Setting Map.basemap swaps the underlying layers without changing the current Viewpoint, so the user's view frame is preserved. + setState(() { + _map.basemap = newBasemap; + _currentBasemapType = newType; + }); + } + + void _setSceneMode(String key) { + if (key == _sceneMode) return; + setState(() { + _sceneMode = key; + if (key != 'default') _showLayersPanel = false; + if (_config == null) return; + final scene = _config!.scenes[key]; + if (scene == null || scene.type != 'scene') return; + final portalUrl = scene.portalKey != null + ? _config!.portals[scene.portalKey!] + : null; + if (portalUrl == null || scene.itemId == null) return; + if (key == 'building3d' && _scene3DWidget == null) { + _scene3DWidget = EsriSceneWidget( + key: _scene3DKey, + portalUri: portalUrl, + itemId: scene.itemId!, + onHeadingChanged: (h) => setState(() { + _mapRotation = h; + if (!_ignoreViewpointReset && (_isLocationActive || _isRecenterActive)) { + _isLocationActive = false; + _isRecenterActive = false; + } + }), + ); + } else if (key == 'droneView' && _sceneDroneWidget == null) { + _sceneDroneWidget = EsriSceneWidget( + key: _sceneDroneKey, + portalUri: portalUrl, + itemId: scene.itemId!, + onHeadingChanged: (h) => setState(() { + _mapRotation = h; + if (!_ignoreViewpointReset && (_isLocationActive || _isRecenterActive)) { + _isLocationActive = false; + _isRecenterActive = false; + } + }), + ); + } + }); + } + +void _toggleLayer(String key) async { + final entry = _config?.layers[key]; + if (entry == null) return; + + if (_layerVisible[key] == true) { + _layerTimers[key]?.cancel(); + _layerTimers.remove(key); + for (final layer in _layerInstances[key] ?? []) { + if (layer != null) _map.operationalLayers.remove(layer); + } + _layerInstances.remove(key); + setState(() => _layerVisible[key] = false); + return; + } + + setState(() => _layerLoading[key] = true); + + final instances = _buildLayerInstances(entry); + _layerInstances[key] = instances; + + for (final layer in instances) { + if (layer != null) _map.operationalLayers.add(layer); + } + + try { + await Future.wait(instances.whereType().map((l) => l.load())); + _applyLayerSpecialCases(key, instances); + } catch (e) { + debugPrint('Layer $key load error: $e'); + } + + _startLayerRefreshTimer(key, entry); + + setState(() { + _layerVisible[key] = true; + _layerLoading[key] = false; + }); + } + + List _buildLayerInstances(LayerEntry entry) { + if (entry.hasSublayers) { + return entry.sublayers!.map(_layerFromSublayer).toList(); + } + return [_layerFromEntry(entry)]; + } + + Layer? _layerFromSublayer(SublayerEntry sub) { + if (sub.source == 'url' && sub.url != null) { + return sub.url!.contains('FeatureServer') + ? FeatureLayer.withFeatureTable( + ServiceFeatureTable.withUri(Uri.parse(sub.url!))) + : ArcGISMapImageLayer.withUri(Uri.parse(sub.url!)); + } + return null; + } + + Layer? _layerFromEntry(LayerEntry entry) { + if (entry.source == 'url' && entry.url != null) { + return entry.url!.contains('FeatureServer') + ? FeatureLayer.withFeatureTable( + ServiceFeatureTable.withUri(Uri.parse(entry.url!))) + : ArcGISMapImageLayer.withUri(Uri.parse(entry.url!)); + } + return null; + } + + void _applyLayerSpecialCases(String key, List instances) { + // campusDistricts: show only sublayer id 4 + if (key == 'campusDistricts' && + instances.isNotEmpty && + instances.first is ArcGISMapImageLayer) { + final imageLayer = instances.first as ArcGISMapImageLayer; + for (final sub in imageLayer.mapImageSublayers) { + sub.isVisible = sub.id == 4; + } + } + } + + void _startLayerRefreshTimer(String key, LayerEntry entry) { + final subs = entry.sublayers; + if (subs == null) return; + + int? capturedIdx; + int? interval; + for (int i = 0; i < subs.length; i++) { + if (subs[i].refreshInterval > 0) { + capturedIdx = i; + interval = subs[i].refreshInterval; + break; + } + } + if (capturedIdx == null || interval == null) return; + + final idx = capturedIdx; + final sub = subs[idx]; + + _layerTimers[key] = Timer.periodic(Duration(seconds: interval), (_) async { + if (!mounted) return; + final instances = _layerInstances[key]; + if (instances == null || idx >= instances.length) return; + + final oldLayer = instances[idx]; + if (oldLayer != null) _map.operationalLayers.remove(oldLayer); + + final newLayer = _layerFromSublayer(sub); + instances[idx] = newLayer; + if (newLayer != null) { + _map.operationalLayers.add(newLayer); + try { + await newLayer.load(); + } catch (e) { + debugPrint('Refresh error $key[$idx]: $e'); + } + } + }); + } + + Future _startLocationDisplay() async { + try { + await _locationDataSource.start(); + setState(() => _locationStarted = true); + } on ArcGISException catch (e) { + debugPrint('Location error: ${e.message}'); + } catch (e) { + debugPrint('Location error: $e'); + } + } + + void _onFocusChanged() { + if (_focusNode.hasFocus && _searchController.text.isEmpty) { + setState(() { + _showSuggestions = true; + _showResults = false; + }); + } + } + + void _onFromFocusChanged() { + if (_fromFocusNode.hasFocus) { + setState(() { + _activeRouteField = 'from'; + if (_fromController.text.isEmpty) { + _showSuggestions = true; + _showResults = false; + } + }); + if (_detailSheetController.isAttached) { + _detailSheetController.animateTo( + 0.15, + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + ); + } + } + } + + void _onToFocusChanged() { + if (_toFocusNode.hasFocus) { + setState(() { + _activeRouteField = 'to'; + if (_toController.text.isEmpty) { + _showSuggestions = true; + _showResults = false; + } + }); + if (_detailSheetController.isAttached) { + _detailSheetController.animateTo( + 0.15, + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + ); + } + } + } + + // --------------------------------------------------------------------------- + // Recent searches persistence + // --------------------------------------------------------------------------- + + Future _loadRecentSearches() async { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = prefs.getString(_recentSearchesKey); + if (jsonStr != null) { + try { + final list = jsonDecode(jsonStr) as List; + setState(() { + _recentSearches = list + .map((e) => + MapSearchResult.fromJson(e as Map)) + .toList(); + }); + } catch (_) { + // ignore corrupted data + } + } + } + + Future _saveRecentSearches() async { + final prefs = await SharedPreferences.getInstance(); + final jsonStr = + jsonEncode(_recentSearches.map((r) => r.toJson()).toList()); + await prefs.setString(_recentSearchesKey, jsonStr); + } + + void _addToRecentSearches(MapSearchResult result) { + // Remove duplicate if exists (match by name + source) + _recentSearches.removeWhere( + (r) => r.name == result.name && r.source == result.source); + // Insert at front + _recentSearches.insert(0, result); + // Trim to max + if (_recentSearches.length > _maxRecentSearches) { + _recentSearches = _recentSearches.sublist(0, _maxRecentSearches); + } + _saveRecentSearches(); + } + + void _removeFromRecentSearches(int index) { + setState(() { + _recentSearches.removeAt(index); + }); + _saveRecentSearches(); + } + + // --------------------------------------------------------------------------- + // Query helpers + // --------------------------------------------------------------------------- + + /// GET from the REST API. + Future> _apiGet(String path, [Map? params]) async { + final uri = Uri.parse('$_baseUrl/$path').replace(queryParameters: params); + final response = await http.get(uri); + if (response.statusCode != 200) { + throw Exception('API error ${response.statusCode}: ${response.body}'); + } + return jsonDecode(response.body) as Map; + } + + Future> _apiPost(String path, Map body) async { + final response = await http.post( + Uri.parse('$_baseUrl/$path'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(body), + ); + if (response.statusCode != 200) { + throw Exception('API error ${response.statusCode}: ${response.body}'); + } + return jsonDecode(response.body) as Map; + } + + Future> _queryBuildings(String query) async { + final data = await _apiGet('buildings', {'q': query}); + return (data['results'] as List? ?? []).map((r) { + return MapSearchResult( + name: r['name'] as String? ?? 'Unknown Building', + subtitle: r['subtitle'] as String? ?? 'Building', + latitude: (r['latitude'] as num?)?.toDouble() ?? 0.0, + longitude: (r['longitude'] as num?)?.toDouble() ?? 0.0, + source: MapSearchSource.building, + address: r['address'] as String? ?? '', + ); + }).where((r) => r.latitude != 0.0 && r.longitude != 0.0).toList(); + } + + Future _fetchAllPoiClasses() async { + try { + final data = await _apiGet('poi/classes'); + final classes = (data['classes'] as List? ?? []) + .map((c) => c as String) + .toList(); + setState(() => _allPoiClasses = classes); + } catch (e) { + debugPrint('Failed to fetch POI classes: $e'); + } + } + + Future> _queryPOIs(String query) async { + final data = await _apiGet('poi', {'q': query}); + return _parsePOIResults(data); + } + + Future> _queryPOIsByClass(String classValue) async { + final data = await _apiGet('poi', {'class': classValue, 'limit': '100'}); + return _parsePOIResults(data); + } + + List _parsePOIResults(Map data) { + return (data['results'] as List? ?? []).map((r) { + return MapSearchResult( + name: r['name'] as String? ?? 'Unknown POI', + subtitle: r['subtitle'] as String? ?? '', + latitude: (r['latitude'] as num?)?.toDouble() ?? 0.0, + longitude: (r['longitude'] as num?)?.toDouble() ?? 0.0, + source: MapSearchSource.poi, + description: r['description'] as String? ?? '', + websiteUrl: r['websiteUrl'] as String?, + ); + }).where((r) => r.latitude != 0.0 && r.longitude != 0.0).toList(); + } + + // --------------------------------------------------------------------------- + // Search actions + // --------------------------------------------------------------------------- + + /// Text search: query both buildings and POIs in parallel. + Future _performSearch(String query) async { + if (query.trim().isEmpty) return; + + // Recompute category matches on submit + final q = query.toLowerCase(); + final matched = _allPoiClasses + .where((c) => c.toLowerCase().contains(q)) + .toList(); + + for (final cat in _categories) { + if (cat.poiClassValue.toLowerCase().contains(q) && + !matched.contains(cat.poiClassValue)) { + matched.insert(0, cat.poiClassValue); + } + } + + setState(() { + _isSearching = true; + _showResults = true; + _showSuggestions = false; + _selectedResult = null; + _matchingPoiClasses = matched; + }); + + try { + final results = await Future.wait([ + _queryBuildings(query), + _queryPOIs(query), + ]); + final merged = [...results[0], ...results[1]]; + setState(() { + _searchResults = merged; + _isSearching = false; + }); + } catch (e) { + print('Search error: $e'); + setState(() { + _searchResults = []; + _isSearching = false; + }); + } + } + + /// Returns the current visible map extent projected to WGS84. + Envelope? _getViewportEnvelopeWGS84() { + final vp = _mapViewController.getCurrentViewpoint( + ViewpointType.boundingGeometry, + ); + if (vp == null) return null; + final geom = vp.targetGeometry; + if (geom == null) return null; + final projected = GeometryEngine.project( + geom, + outputSpatialReference: SpatialReference.wgs84, + ); + return projected as Envelope?; + } + + /// Filters a result list to only those within the current map viewport. + /// Returns the full list unchanged if the envelope can't be determined. + List _filterToViewport(List results) { + final env = _getViewportEnvelopeWGS84(); + if (env == null) return results; + return results.where((r) { + return r.latitude >= env.yMin && + r.latitude <= env.yMax && + r.longitude >= env.xMin && + r.longitude <= env.xMax; + }).toList(); + } + + /// distance in meters between two WGS84 points + double _distanceMeters( + double lat1, double lng1, double lat2, double lng2) { + const r = 6371000.0; + final dLat = (lat2 - lat1) * math.pi / 180; + final dLng = (lng2 - lng1) * math.pi / 180; + final a = math.sin(dLat / 2) * math.sin(dLat / 2) + + math.cos(lat1 * math.pi / 180) * + math.cos(lat2 * math.pi / 180) * + math.sin(dLng / 2) * + math.sin(dLng / 2); + return r * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a)); + } + + /// Returns the user's current WGS84 lat/lng, or null if unavailable. + (double lat, double lng)? _getUserLatLng() { + final pos = _mapViewController.locationDisplay.location?.position; + if (pos == null) return null; + final wgs = GeometryEngine.project( + pos, + outputSpatialReference: SpatialReference.wgs84, + ) as ArcGISPoint?; + if (wgs == null) return null; + return (wgs.y, wgs.x); + } + + /// Plot a list of results as pins on the map. + void _plotResultsOnMap(List results) { + _graphicsOverlay.graphics.clear(); + _mappedResults = results; + for (int i = 0; i < results.length; i++) { + final result = results[i]; + final point = ArcGISPoint( + x: result.longitude, + y: result.latitude, + spatialReference: SpatialReference.wgs84, + ); + final graphic = Graphic( + geometry: point, + symbol: SimpleMarkerSymbol( + style: SimpleMarkerSymbolStyle.circle, + color: Colors.red, + size: 14, + ), + ); + // Store the list index so we can retrieve the result on tap + graphic.attributes['resultIndex'] = i; + _graphicsOverlay.graphics.add(graphic); + } + } + + /// Zoom the map to fit all mapped results, with padding. + void _zoomToResults(List results) { + if (results.isEmpty) return; + + if (results.length == 1) { + final r = results.first; + _mapViewController.setViewpointAnimated( + Viewpoint.fromCenter( + ArcGISPoint( + x: r.longitude, + y: r.latitude, + spatialReference: SpatialReference.wgs84, + ), + scale: 5000, + ), + ); + return; + } + + double minLat = results.map((r) => r.latitude).reduce((a, b) => a < b ? a : b); + double maxLat = results.map((r) => r.latitude).reduce((a, b) => a > b ? a : b); + double minLng = results.map((r) => r.longitude).reduce((a, b) => a < b ? a : b); + double maxLng = results.map((r) => r.longitude).reduce((a, b) => a > b ? a : b); + + const padding = 0.005; + final envelope = Envelope.fromXY( + xMin: minLng - padding, + yMin: minLat - padding, + xMax: maxLng + padding, + yMax: maxLat + padding, + spatialReference: SpatialReference.wgs84, + ); + _mapViewController.setViewpointAnimated( + Viewpoint.fromTargetExtent(envelope), + ); + } + + /// Category search: query POIs filtered by Class. + /// Category search: query POIs filtered by Class and plot all as map pins. + Future _performCategorySearch(_SearchCategory category) async { + setState(() { + _isSearching = true; + _showResults = false; + _showSuggestions = false; + _matchingPoiClasses = []; + _selectedResult = null; + _showCategoryList = false; + _activeCategory = category; + _searchController.text = category.label; + }); + _focusNode.unfocus(); + _fromFocusNode.unfocus(); + _toFocusNode.unfocus(); + + try { + final allResults = await _queryPOIsByClass(category.poiClassValue); + + // Always plot ALL pins — no zoom, no viewport filtering. + // The user pans/zooms freely to discover them. + _plotResultsOnMap(allResults); + + setState(() { + _allCategoryResults = allResults; + _showCategoryList = allResults.isNotEmpty; + _isSearching = false; + }); + } catch (e) { + print('Category search error: $e'); + setState(() { + _mappedResults = []; + _allCategoryResults = []; + _isSearching = false; + }); + } + } + + /// Triggers a category-style search from a raw POI class string. + Future _performClassSearch(String classValue) async { + final category = _SearchCategory( + label: _labelForClass(classValue), + icon: _iconForClass(classValue), + poiClassValue: classValue, + ); + await _performCategorySearch(category); + } + + /// Plots all results for the active category + void _seeAllCategoryResults() { + if (_allCategoryResults.isEmpty) return; + _zoomToResults(_allCategoryResults); + } + + void _selectResult(MapSearchResult result) { + // Handle result selection within route fields + if (_showRouteFields && _activeRouteField != null) { + if (_activeRouteField == 'from') { + _fromController.text = result.name; + _fromLatLng = (result.latitude, result.longitude); + _graphicsOverlay.graphics.clear(); + setState(() { + _showResults = false; + _showSuggestions = false; + _mappedResults = []; + _allCategoryResults = []; + _showCategoryList = false; + _activeCategory = null; + }); + _fromFocusNode.unfocus(); + // Re-solve route if we already have a destination + if (_routeDestination != null) { + _solveRoute(_routeDestination!, originLatLng: _fromLatLng); + } + } else if (_activeRouteField == 'to') { + _toController.text = result.name; + _routeDestination = result; + _graphicsOverlay.graphics.clear(); + setState(() { + _selectedResult = result; + _lastSelectedResult = result; + _showResults = false; + _showSuggestions = false; + _mappedResults = []; + _allCategoryResults = []; + _showCategoryList = false; + _activeCategory = null; + }); + _toFocusNode.unfocus(); + _solveRoute(result, originLatLng: _fromLatLng); + } + _addToRecentSearches(result); + return; + } + + _graphicsOverlay.graphics.clear(); + + final point = ArcGISPoint( + x: result.longitude, + y: result.latitude, + spatialReference: SpatialReference.wgs84, + ); + + final graphic = Graphic( + geometry: point, + symbol: SimpleMarkerSymbol( + style: SimpleMarkerSymbolStyle.circle, + color: result.source == MapSearchSource.building + ? Colors.blue + : Colors.red, + size: 14, + ), + ); + _graphicsOverlay.graphics.add(graphic); + + _mapViewController.setViewpointAnimated( + Viewpoint.fromCenter(point, scale: 5000), + ); + + setState(() { + _showResults = false; + _showSuggestions = false; + _searchController.text = result.name; + _selectedResult = result; + _lastSelectedResult = result; + }); + _focusNode.unfocus(); + + // Save to recent searches + _addToRecentSearches(result); + } + + /// Called when the ArcGISMapView is tapped + /// Identifies any graphic at the tap location and shows its detail panel + Future _onMapTap(Offset screenPoint) async { + // Dismiss any open dropdowns first + if (_showSuggestions || _showResults) { + setState(() { + _showSuggestions = false; + _showResults = false; + }); + _focusNode.unfocus(); + return; + } + + // Only attempt identify when pins are on the map + if (_mappedResults.isEmpty) { + if (_selectedResult != null) _closeDetail(); + return; + } + + try { + final identifyResult = await _mapViewController.identifyGraphicsOverlay( + _graphicsOverlay, + screenPoint: screenPoint, + tolerance: 12.0, + maximumResults: 1, + ); + + if (identifyResult.graphics.isNotEmpty) { + final tappedGraphic = identifyResult.graphics.first; + final index = tappedGraphic.attributes['resultIndex'] as int?; + if (index != null && index >= 0 && index < _mappedResults.length) { + _selectResultFromPin(_mappedResults[index]); + } + } else { + // Tapped empty space — close detail if open + if (_selectedResult != null) _closeDetail(); + } + } catch (e) { + print('Identify error: $e'); + } + } + + /// Select a result from a map pin tap + /// keeps all existing category pins visible + void _selectResultFromPin(MapSearchResult result) { + // In routing mode, treat pin tap based on active route field + if (_showRouteFields) { + if (_activeRouteField == 'from') { + _fromController.text = result.name; + _fromLatLng = (result.latitude, result.longitude); + _graphicsOverlay.graphics.clear(); + setState(() { + _showResults = false; + _showSuggestions = false; + _mappedResults = []; + _allCategoryResults = []; + _showCategoryList = false; + _activeCategory = null; + }); + _fromFocusNode.unfocus(); + if (_routeDestination != null) { + _solveRoute(_routeDestination!, originLatLng: _fromLatLng); + } + } else { + _toController.text = result.name; + _routeDestination = result; + _graphicsOverlay.graphics.clear(); + setState(() { + _selectedResult = result; + _lastSelectedResult = result; + _showResults = false; + _showSuggestions = false; + _mappedResults = []; + _allCategoryResults = []; + _showCategoryList = false; + _activeCategory = null; + }); + _toFocusNode.unfocus(); + _solveRoute(result, originLatLng: _fromLatLng); + } + _addToRecentSearches(result); + return; + } + + final point = ArcGISPoint( + x: result.longitude, + y: result.latitude, + spatialReference: SpatialReference.wgs84, + ); + + _mapViewController.setViewpointAnimated( + Viewpoint.fromCenter(point, scale: 5000), + ); + + setState(() { + _selectedResult = result; + _lastSelectedResult = result; + _searchController.text = result.name; + _showResults = false; + _showSuggestions = false; + _showCategoryList = false; + }); + _focusNode.unfocus(); + _addToRecentSearches(result); + } + + void _closeDetail() { + // If a category search is active and not in routing mode, reopen the list view + if (_allCategoryResults.isNotEmpty && + !_showRouteFields && !_hasRoute && !_routeFailed) { + setState(() { + _selectedResult = null; + _showCategoryList = true; + }); + } else { + _clearSearch(); + } + } + + void _recenterOnUser() { + if (!_locationStarted) { + _startLocationDisplay(); + return; + } + final loc = _mapViewController.locationDisplay.location; + if (loc == null) return; + + // Briefly snap to recenter mode, then release back to free pan + _mapViewController.locationDisplay.autoPanMode = + LocationDisplayAutoPanMode.recenter; + _ignoreViewpointReset = true; + setState(() => _isLocationActive = true); + Future.delayed(const Duration(milliseconds: 1500), () { + if (mounted) { + _ignoreViewpointReset = false; + _mapViewController.locationDisplay.autoPanMode = + LocationDisplayAutoPanMode.off; + } + }); + } + + void _recenterOnView() { + _ignoreViewpointReset = true; + setState(() => _isRecenterActive = true); + Future.delayed(const Duration(milliseconds: 1500), () { + if (mounted) _ignoreViewpointReset = false; + }); + if (_sceneMode != 'default') { + if (_sceneMode == 'building3d') { + _scene3DKey.currentState?.resetCamera(); + } else { + _sceneDroneKey.currentState?.resetCamera(); + } + return; + } + if (_selectedResult != null) { + _mapViewController.setViewpointAnimated( + Viewpoint.fromCenter( + ArcGISPoint( + x: _selectedResult!.longitude, + y: _selectedResult!.latitude, + spatialReference: SpatialReference.wgs84, + ), + scale: 5000, + ), + ); + } else if (_lastSelectedResult != null && _mappedResults.isEmpty) { + // Single pin on map, slide-over closed + _mapViewController.setViewpointAnimated( + Viewpoint.fromCenter( + ArcGISPoint( + x: _lastSelectedResult!.longitude, + y: _lastSelectedResult!.latitude, + spatialReference: SpatialReference.wgs84, + ), + scale: 5000, + ), + ); + } else if (_mappedResults.isNotEmpty) { + // Category pins on map + _zoomToResults(_mappedResults); + } else { + _mapViewController.setViewpointAnimated( + Viewpoint.fromCenter( + ArcGISPoint( + x: -117.2340, + y: 32.8801, + spatialReference: SpatialReference.wgs84, + ), + scale: 24000, + ), + ); + } + } + + void _clearSearch() { + _searchController.clear(); + _fromController.clear(); + _toController.clear(); + _graphicsOverlay.graphics.clear(); + _routeGraphicsOverlay.graphics.clear(); + setState(() { + _hasRoute = false; + _routeFailed = false; + _travelMode = 'Walking'; + _routeTravelTimeMinutes = 0; + _routeManeuvers = []; + _showRouteFields = false; + _searchResults = []; + _matchingPoiClasses = []; + _mappedResults = []; + _allCategoryResults = []; + _showResults = false; + _showSuggestions = false; + _showCategoryList = false; + _activeCategory = null; + _selectedResult = null; + _lastSelectedResult = null; + _activeRouteField = null; + _fromLatLng = null; + _routeDestination = null; + }); + } + + Future _launchWebsite(String url) async { + final uri = Uri.parse(url); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } + + // --------------------------------------------------------------------------- + // Routing + // --------------------------------------------------------------------------- + +/* Future _launchDirections(MapSearchResult result) async { + final uri = Uri.parse( + 'https://www.google.com/maps/dir/?api=1' + '&destination=${result.latitude},${result.longitude}', + ); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } */ + + static const _routeServiceUrl = + 'https://admin-enterprise-gis.ucsd.edu/server/rest/services/' + 'Wayfinding/Campus_Wayfinding_Network/NAServer/Route'; + + Future _solveRoute(MapSearchResult destination, {String? travelMode, (double, double)? originLatLng}) async { + // Use provided origin or fall back to GPS + final userLatLng = originLatLng ?? _getUserLatLng(); + if (userLatLng == null) { + setState(() => _isRouting = false); + return; + } + + final mode = travelMode ?? _travelMode; + + setState(() { + _isRouting = true; + _routeFailed = false; + _travelMode = mode; + }); + + try { + final routeTask = RouteTask.withUri(Uri.parse(_routeServiceUrl)); + await routeTask.load(); + + final params = await routeTask.createDefaultParameters(); + params.returnDirections = true; + final taskInfo = routeTask.getRouteTaskInfo(); + final matchingMode = taskInfo.travelModes + .where((m) => m.name == mode) + .firstOrNull; + if (matchingMode != null) { + params.travelMode = matchingMode; + } + + final origin = Stop(ArcGISPoint( + x: userLatLng.$2, + y: userLatLng.$1, + spatialReference: SpatialReference.wgs84, + )); + final dest = Stop(ArcGISPoint( + x: destination.longitude, + y: destination.latitude, + spatialReference: SpatialReference.wgs84, + )); + params.setStops([origin, dest]); + + final result = await routeTask.solveRoute(params); + if (result.routes.isEmpty) { + setState(() { + _isRouting = false; + _routeFailed = true; + }); + return; + } + + final route = result.routes.first; + _routeTravelTimeMinutes = route.totalTime; + _routeManeuvers = route.directionManeuvers; + + final routeGeometry = route.routeGeometry; + if (routeGeometry == null) { + setState(() { + _isRouting = false; + _routeFailed = true; + }); + return; + } + + _routeGraphicsOverlay.graphics.clear(); + final routeGraphic = Graphic( + geometry: routeGeometry, + symbol: SimpleLineSymbol( + style: SimpleLineSymbolStyle.solid, + color: Colors.blue, + width: 4, + ), + ); + _routeGraphicsOverlay.graphics.add(routeGraphic); + + // End point marker + final whiteOutline = SimpleLineSymbol( + style: SimpleLineSymbolStyle.solid, + color: Colors.white, + width: 2, + ); + _routeGraphicsOverlay.graphics.add(Graphic( + geometry: ArcGISPoint( + x: destination.longitude, + y: destination.latitude, + spatialReference: SpatialReference.wgs84, + ), + symbol: SimpleMarkerSymbol( + style: SimpleMarkerSymbolStyle.circle, + color: Colors.red, + size: 12, + )..outline = whiteOutline, + )); + + // Start point marker (only when origin is not GPS) + if (originLatLng != null) { + _routeGraphicsOverlay.graphics.add(Graphic( + geometry: ArcGISPoint( + x: originLatLng.$2, + y: originLatLng.$1, + spatialReference: SpatialReference.wgs84, + ), + symbol: SimpleMarkerSymbol( + style: SimpleMarkerSymbolStyle.circle, + color: Colors.blue, + size: 12, + )..outline = whiteOutline, + )); + } + + // Zoom to the route extent with extra bottom padding so the line + // stays centred in the visible map area above the detail slide-over. + final extent = routeGeometry.extent; + final dx = extent.width * 0.2; + final dyTop = extent.height * 0.5; + final dyBottom = extent.height * 0.75; + final paddedExtent = Envelope.fromXY( + xMin: extent.xMin - dx, + yMin: extent.yMin - dyBottom, + xMax: extent.xMax + dx, + yMax: extent.yMax + dyTop, + spatialReference: extent.spatialReference, + ); + _routePaddedExtent = paddedExtent; + _mapViewController.setViewpointAnimated( + Viewpoint.fromTargetExtent(paddedExtent), + ); + + _graphicsOverlay.graphics.clear(); + _mappedResults = []; + _allCategoryResults = []; + + setState(() { + _isRouting = false; + _hasRoute = true; + _showCategoryList = false; + _activeCategory = null; + _selectedResult = destination; + _lastSelectedResult = destination; + }); + } catch (e) { + debugPrint('Route solve error: $e'); + setState(() { + _isRouting = false; + _routeFailed = true; + }); + } + } + + void _onAiLocationSelected(AiSearchResult result) { + _searchController.text = result.name; + final point = ArcGISPoint( + x: result.longitude, + y: result.latitude, + spatialReference: SpatialReference.wgs84, + ); + _graphicsOverlay.graphics.clear(); + _graphicsOverlay.graphics.add(Graphic( + geometry: point, + symbol: SimpleMarkerSymbol( + style: SimpleMarkerSymbolStyle.circle, + color: Colors.red, + size: 14, + ), + )); + _mapViewController.setViewpointAnimated( + Viewpoint.fromCenter(point, scale: 5000), + ); + final mapResult = MapSearchResult( + name: result.name, + subtitle: result.subtitle, + latitude: result.latitude, + longitude: result.longitude, + source: MapSearchSource.poi, + address: result.address, + ); + setState(() { + _selectedResult = mapResult; + _lastSelectedResult = mapResult; + _showResults = false; + _showSuggestions = false; + }); + } + + void _onAiRouteRequested(List stops) { + if (stops.length < 2) return; + final first = stops.first; + final last = stops.last; + final destination = MapSearchResult( + name: last.name, + subtitle: last.subtitle, + latitude: last.latitude, + longitude: last.longitude, + source: MapSearchSource.poi, + address: last.address, + ); + _toController.text = destination.name; + _routeDestination = destination; + final gps = _getUserLatLng(); + _fromController.text = gps != null ? 'My Location' : first.name; + _fromLatLng = gps ?? (first.latitude, first.longitude); + _graphicsOverlay.graphics.clear(); + setState(() { + _showRouteFields = true; + _selectedResult = destination; + _lastSelectedResult = destination; + _mappedResults = []; + _allCategoryResults = []; + _showCategoryList = false; + _activeCategory = null; + }); + _solveRoute(destination, originLatLng: _fromLatLng); + } + + void _clearRoute() { + _routeGraphicsOverlay.graphics.clear(); + _fromController.clear(); + _toController.clear(); + _routePaddedExtent = null; + + // Re-add destination pin and center on it if a result is still selected + final destination = _selectedResult; + if (destination != null) { + final point = ArcGISPoint( + x: destination.longitude, + y: destination.latitude, + spatialReference: SpatialReference.wgs84, + ); + _graphicsOverlay.graphics.clear(); + _graphicsOverlay.graphics.add(Graphic( + geometry: point, + symbol: SimpleMarkerSymbol( + style: SimpleMarkerSymbolStyle.circle, + color: destination.source == MapSearchSource.building + ? Colors.blue + : Colors.red, + size: 14, + ), + )); + _mapViewController.setViewpointAnimated( + Viewpoint.fromCenter(point, scale: 5000), + ); + } + + setState(() { + _hasRoute = false; + _routeFailed = false; + _travelMode = 'Walking'; + _routeTravelTimeMinutes = 0; + _routeManeuvers = []; + _showRouteFields = false; + _activeRouteField = null; + _fromLatLng = null; + _routeDestination = null; + }); + } + + IconData _iconForResult(MapSearchResult result) { + return result.source == MapSearchSource.building + ? Icons.business + : Icons.place; + } + + /// Returns the icon for a POI class — uses the hardcoded category icon if one + /// exists, otherwise falls back to a generic place icon. + IconData _iconForClass(String classValue) { + return _categories + .firstWhere( + (c) => c.poiClassValue.toLowerCase() == classValue.toLowerCase(), + orElse: () => const _SearchCategory( + label: '', icon: Icons.place, poiClassValue: ''), + ) + .icon; + } + + /// Converts a raw class value like "Food and Beverage" into a display label. + /// Uses the hardcoded category label if available, otherwise returns as-is. + String _labelForClass(String classValue) { + final match = _categories.where( + (c) => c.poiClassValue.toLowerCase() == classValue.toLowerCase()); + return match.isNotEmpty ? match.first.label : classValue; + } + + // --------------------------------------------------------------------------- + // Suggestions panel (categories + recent searches) + // --------------------------------------------------------------------------- + + Widget _buildSuggestionsPanel(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final bgColor = isDark ? Colors.grey[850] : Colors.white; + + return Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + color: bgColor, + child: Padding( + padding: EdgeInsets.only( + top: (_showRouteFields && _activeRouteField == 'from') ? 0 : 12, + bottom: 12, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // user location from from field in routing mode + if (_showRouteFields && _activeRouteField == 'from') ...[ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + final gps = _getUserLatLng(); + if (gps == null) return; + _fromController.text = 'My Location'; + _fromLatLng = gps; + setState(() { + _showSuggestions = false; + _showResults = false; + }); + _fromFocusNode.unfocus(); + if (_routeDestination != null) { + _solveRoute(_routeDestination!, originLatLng: _fromLatLng); + } + }, + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 20), + child: Row( + children: [ + Icon( + Icons.my_location, + size: 20, + color: isDark ? Colors.grey[500] : Theme.of(context).colorScheme.primary, + ), + SizedBox(width: 12), + Text( + 'Current Location', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: isDark ? Colors.white : Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + ), + ), + Divider(height: 1), + SizedBox(height: 12), + ], + // Category section header + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'Suggested', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + ), + SizedBox(height: 12), + + // Category icons row + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: _categories + .map((cat) => _buildCategoryChip(context, cat)) + .toList(), + ), + ), + + // Recent searches section + if (_recentSearches.isNotEmpty) ...[ + SizedBox(height: 16), + Divider(height: 1), + SizedBox(height: 8), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Text( + 'Recently viewed', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + ), + SizedBox(height: 4), + ...List.generate(_recentSearches.length, (index) { + final recent = _recentSearches[index]; + return ListTile( + splashColor: Colors.transparent, + dense: true, + leading: Icon( + Icons.history, + size: 20, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), + title: Text( + recent.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + trailing: GestureDetector( + onTap: () => _removeFromRecentSearches(index), + child: Icon( + Icons.close, + size: 16, + color: isDark ? Colors.grey[500] : Colors.grey[400], + ), + ), + onTap: () => _selectResult(recent), + ); + }), + ], + ], + ), + ), + ); + } + + Widget _buildCategoryChip(BuildContext context, _SearchCategory category) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return GestureDetector( + onTap: () => _performCategorySearch(category), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + color: category.color, + shape: BoxShape.circle, + ), + child: Icon( + category.icon, + size: 24, + color: Colors.black, + ), + ), + SizedBox(height: 6), + Text( + category.label, + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + ], + ), + ); + } + + // --------------------------------------------------------------------------- + // Map interaction → collapse slideover + // --------------------------------------------------------------------------- + + void _onMapPointerDown(PointerDownEvent _) { + // Close layers panel when user touches the map + if (_showLayersPanel) { + setState(() => _showLayersPanel = false); + return; + } + // Collapse whichever sheet is active to the minimum snap + if (_showCategoryList && _selectedResult == null) { + if (_categorySheetController.isAttached) { + _categorySheetController.animateTo( + 0.15, + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + ); + } + } + if (_selectedResult != null) { + if (_detailSheetController.isAttached) { + _detailSheetController.animateTo( + 0.15, + duration: const Duration(milliseconds: 250), + curve: Curves.easeOut, + ); + } + } + } + + // --------------------------------------------------------------------------- + // Shared slide-over template + // --------------------------------------------------------------------------- + + /// Builds a consistent DraggableScrollableSheet with a pinned header. + /// [headerTitle] and [headerIcon] are shown in the header row. + /// [onClose] is called when the X button is tapped. + /// [trailing] is an optional widget shown before the close button (e.g. "Search here"). + /// [sliverBody] is the scrollable content below the header. + static const _slideOverHeaderHeight = 80.0; + + Widget _buildSlideOverContent({ + required BuildContext context, + required ScrollController scrollController, + required String headerTitle, + IconData? headerIcon, + required VoidCallback onClose, + Widget? trailing, + required List sliverBody, + }) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final bgColor = isDark ? Colors.grey[900]! : Colors.white; + + return Material( + elevation: 8, + color: bgColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + clipBehavior: Clip.antiAlias, + child: CustomScrollView( + controller: scrollController, + slivers: [ + SliverPersistentHeader( + pinned: true, + delegate: _SlideOverHeaderDelegate( + height: _slideOverHeaderHeight, + backgroundColor: bgColor, + child: Column( + children: [ + // Drag handle + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 4), + child: Container( + width: 36, + height: 4, + decoration: BoxDecoration( + color: isDark ? Colors.grey[700] : Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + ), + // Header row + Padding( + padding: const EdgeInsets.fromLTRB(20, 6, 20, 8), + child: Row( + children: [ + if (headerIcon != null) ...[ + Icon( + headerIcon, + size: 18, + color: isDark ? Colors.white70 : Colors.grey[700], + ), + const SizedBox(width: 8), + ], + Expanded( + child: Text( + headerTitle, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.grey[900], + ), + ), + ), + if (trailing != null) trailing!, + GestureDetector( + onTap: onClose, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + isDark ? Colors.grey[800] : Colors.grey[200], + ), + child: Icon( + Icons.close, + size: 18, + color: isDark ? Colors.white70 : Colors.grey[700], + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + // Divider below header + SliverToBoxAdapter( + child: Divider(height: 1), + ), + ...sliverBody, + ], + ), + ); + } + + // --------------------------------------------------------------------------- + // List view + // --------------------------------------------------------------------------- + Widget _buildCategoryListPanel(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final label = _activeCategory?.label ?? 'Results'; + + final viewport = _filterToViewport(_allCategoryResults); + final userPos = _getUserLatLng(); + final results = List.from(viewport); + if (userPos != null) { + results.sort((a, b) => + _distanceMeters(userPos.$1, userPos.$2, a.latitude, a.longitude) + .compareTo( + _distanceMeters( + userPos.$1, userPos.$2, b.latitude, b.longitude))); + } + + // Content-aware sizing: estimate content height as fraction of screen + final screenHeight = MediaQuery.of(context).size.height; + // Each list tile ~56px + header ~80px + divider + final contentHeight = + _slideOverHeaderHeight + (results.length * 56.0).clamp(56.0, 600.0); + final contentFraction = (contentHeight / screenHeight).clamp(0.15, 0.80); + final initialSize = + contentFraction < 0.45 ? contentFraction : 0.45; + final maxSize = contentFraction < 0.80 ? contentFraction.clamp(0.45, 0.80) : 0.80; + // Snap sizes must be strictly increasing and within [min, max] + final snaps = [0.15]; + if (initialSize > 0.15 + 0.01) snaps.add(initialSize); + + return DraggableScrollableSheet( + controller: _categorySheetController, + initialChildSize: initialSize, + minChildSize: 0.15, + maxChildSize: maxSize, + snap: true, + snapSizes: snaps, + builder: (context, scrollController) { + final headerTitle = results.isEmpty + ? '$label - None in view' + : '$label - ${results.length} in view'; + + return _buildSlideOverContent( + context: context, + scrollController: scrollController, + headerTitle: headerTitle, + headerIcon: _activeCategory?.icon ?? Icons.place, + onClose: _clearSearch, + trailing: _allCategoryResults.length > results.length + ? TextButton.icon( + icon: Icon( + Icons.layers_outlined, + size: 16, + color: isDark ? Colors.white54 : Colors.grey[600], + ), + label: Text( + 'See all ${_allCategoryResults.length} results', + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.white54 : Colors.grey[600], + ), + ), + style: TextButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + minimumSize: Size.zero, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + ), + onPressed: _seeAllCategoryResults, + ) + : null, + sliverBody: results.isEmpty + ? [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.location_off, + size: 32, + color: isDark + ? Colors.grey[600] + : Colors.grey[400]), + const SizedBox(height: 8), + Text( + 'No $label in current view.\nPan the map to see results.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + color: isDark + ? Colors.grey[500] + : Colors.grey[600], + ), + ), + ], + ), + ), + ), + ] + : [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final r = results[index]; + final dist = userPos != null + ? _distanceMeters(userPos.$1, userPos.$2, + r.latitude, r.longitude) + : null; + final distLabel = dist == null + ? null + : dist < 1000 + ? '${dist.round()} m away' + : '${(dist / 1000).toStringAsFixed(1)} km away'; + + return Column( + children: [ + ListTile( + splashColor: Colors.transparent, + leading: Icon( + _iconForResult(r), + size: 20, + color: isDark + ? Colors.grey[400] + : Colors.grey[600], + ), + title: Text( + r.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 16), + ), + subtitle: distLabel != null + ? Text(distLabel, + style: TextStyle( + fontSize: 13, + color: Colors.grey[500], + )) + : (r.subtitle.isNotEmpty + ? Text(r.subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: + const TextStyle(fontSize: 13)) + : null), + dense: true, + onTap: () => _selectResultFromPin(r), + ), + if (index < results.length - 1) + const Divider(height: 1), + ], + ); + }, + childCount: results.length, + ), + ), + ], + ); + }, + ); + } + + // --------------------------------------------------------------------------- + // Detail slide-over + // --------------------------------------------------------------------------- + + Widget _buildDetailSlideOver(BuildContext context, MapSearchResult result) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final isRouting = _showRouteFields || _hasRoute || _routeFailed; + + final detailText = result.source == MapSearchSource.building + ? result.address + : result.description; + final categoryLabel = result.source == MapSearchSource.building + ? 'Building' + : result.subtitle; + + // Content-aware sizing: estimate how tall the detail content is + final screenHeight = MediaQuery.of(context).size.height; + // header ~80 + name ~30 + detail ~20 + buttons ~48 + padding ~40 + double contentEst = _slideOverHeaderHeight + 30 + 48 + 40; + if (detailText.isNotEmpty) contentEst += 60; + if (_hasRoute && _routeManeuvers.isNotEmpty) contentEst += _routeManeuvers.length * 52.0; + final contentFraction = (contentEst / screenHeight).clamp(0.15, 0.80); + final initialSize = + _hasRoute ? 0.35 : (contentFraction < 0.30 ? contentFraction : 0.30); + final maxSize = _hasRoute + ? 0.80 + : (contentFraction < 0.80 ? contentFraction.clamp(0.30, 0.80) : 0.80); + // Snap sizes must be strictly increasing and within [min, max] + final snaps = [0.15]; + if (initialSize > 0.15 + 0.01) snaps.add(initialSize); + + return DraggableScrollableSheet( + controller: _detailSheetController, + initialChildSize: initialSize, + minChildSize: 0.15, + maxChildSize: maxSize, + snap: true, + snapSizes: snaps, + builder: (context, scrollController) { + return _buildSlideOverContent( + context: context, + scrollController: scrollController, + headerTitle: isRouting + ? 'Directions to ${result.name}' + : result.name, + headerIcon: _iconForResult(result), + onClose: isRouting ? _clearRoute : _closeDetail, + sliverBody: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 8, 20, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Category label (hidden in routing mode) + if (!isRouting) + Text( + categoryLabel, + style: TextStyle( + fontSize: 14, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + + // Detail text (hidden in routing mode) + if (!isRouting && detailText.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + detailText, + style: TextStyle( + fontSize: 16, + color: + isDark ? Colors.grey[400] : Colors.grey[600], + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + if (!isRouting) const SizedBox(height: 16), + + // Action buttons + if (_routeFailed) ...[ + Text( + 'No route available from your current location.', + style: TextStyle( + fontSize: 16, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + ), + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: () => _launchWebsite( + 'https://www.google.com/maps/dir/?api=1' + '&destination=${_routeDestination?.latitude ?? result.latitude},${_routeDestination?.longitude ?? result.longitude}' + '&travelmode=walking' + '${_fromLatLng != null ? '&origin=${_fromLatLng!.$1},${_fromLatLng!.$2}' : ''}', + ), + icon: const Icon(Icons.map_outlined, size: 18), + label: const Text('Navigate in Google Maps'), + style: FilledButton.styleFrom( + padding: + const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ] else if (_hasRoute) ...[ + // Travel time + Row( + children: [ + Icon( + Icons.schedule, + size: 18, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + const SizedBox(width: 6), + Text( + _routeTravelTimeMinutes < 1 + ? '< 1 min' + : '${_routeTravelTimeMinutes.ceil()} min', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isDark ? Colors.white : Colors.grey[900], + ), + ), + ], + ), + const SizedBox(height: 12), + + // Travel mode toggle chips + Row( + children: [ + for (final mode in ['Walking', 'Accessible']) ...[ + if (mode != 'Walking') const SizedBox(width: 8), + GestureDetector( + onTap: _isRouting || mode == _travelMode + ? null + : () => _solveRoute(result, travelMode: mode, originLatLng: _fromLatLng), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: mode == _travelMode + ? (isDark + ? Colors.white + : Colors.grey[900]) + : (isDark + ? Colors.grey[800] + : Colors.grey[100]), + borderRadius: BorderRadius.circular(16), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + mode == 'Walking' + ? Icons.directions_walk + : Icons.accessible, + size: 16, + color: mode == _travelMode + ? (isDark + ? Colors.grey[900] + : Colors.white) + : (isDark + ? Colors.white70 + : Colors.grey[700]), + ), + const SizedBox(width: 6), + Text( + mode, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: mode == _travelMode + ? (isDark + ? Colors.grey[900] + : Colors.white) + : (isDark + ? Colors.white70 + : Colors.grey[700]), + ), + ), + ], + ), + ), + ), + ], + ], + ), + const SizedBox(height: 12), + + // Navigate in Google Maps button + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: () => _launchWebsite( + 'https://www.google.com/maps/dir/?api=1' + '&destination=${_routeDestination?.latitude ?? result.latitude},${_routeDestination?.longitude ?? result.longitude}' + '&travelmode=walking' + '${_fromLatLng != null ? '&origin=${_fromLatLng!.$1},${_fromLatLng!.$2}' : ''}', + ), + icon: const Icon(Icons.map_outlined, size: 18), + label: const Text('Navigate in Google Maps'), + style: OutlinedButton.styleFrom( + foregroundColor: isDark + ? const Color(0xFFFFCD00) + : null, + side: isDark + ? const BorderSide( + color: Color(0xFFFFCD00)) + : null, + padding: + const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ] else + Row( + children: [ + if (result.websiteUrl != null) ...[ + Expanded( + child: OutlinedButton.icon( + onPressed: () => + _launchWebsite(result.websiteUrl!), + icon: const Icon(Icons.language, size: 18), + label: const Text('View website'), + style: OutlinedButton.styleFrom( + foregroundColor: isDark + ? const Color(0xFFFFCD00) + : null, + side: isDark + ? const BorderSide( + color: Color(0xFFFFCD00)) + : null, + padding: const EdgeInsets.symmetric( + vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + const SizedBox(width: 12), + ], + Expanded( + child: FilledButton.icon( + onPressed: _isRouting + ? null + : () { + final gps = _getUserLatLng(); + _fromController.text = + gps != null ? 'My Location' : ''; + _toController.text = result.name; + _routeDestination = result; + _graphicsOverlay.graphics.clear(); + setState(() { + _showRouteFields = true; + _selectedResult = null; + _mappedResults = []; + _allCategoryResults = []; + _showCategoryList = false; + _activeCategory = null; + }); + _solveRoute(result); + }, + icon: _isRouting + ? const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.directions, size: 18), + label: Text( + _isRouting ? 'Routing...' : 'Get Directions'), + style: FilledButton.styleFrom( + padding: + const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ), + // Direction maneuver steps (when route is active) + if (_hasRoute && _routeManeuvers.isNotEmpty) + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final step = _routeManeuvers[index]; + final distMeters = step.length; + final distLabel = distMeters < 1000 + ? '${distMeters.round()} m' + : '${(distMeters / 1000).toStringAsFixed(1)} km'; + return Column( + children: [ + if (index == 0) const Divider(height: 1), + ListTile( + leading: Icon( + Icons.subdirectory_arrow_right, + size: 20, + color: isDark ? Colors.grey[400] : Colors.grey[600], + ), + title: Text( + step.directionText, + style: const TextStyle(fontSize: 14), + ), + trailing: distMeters > 0 + ? Text( + distLabel, + style: TextStyle( + fontSize: 13, + color: isDark + ? Colors.grey[500] + : Colors.grey[500], + ), + ) + : null, + dense: true, + ), + if (index < _routeManeuvers.length - 1) + const Divider(height: 1), + ], + ); + }, + childCount: _routeManeuvers.length, + ), + ), + ], + ); + }, + ); + } + + // --------------------------------------------------------------------------- + // Build + // --------------------------------------------------------------------------- + + @override + Widget build(BuildContext context) { + super.build(context); + final isDark = Theme.of(context).brightness == Brightness.dark; + final keyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; + + return Scaffold( + body: Stack( + children: [ + // Map — swaps between 2D ArcGISMapView and 3D ArcGISSceneView + Column( + children: [ + Expanded( + child: IndexedStack( + index: _sceneMode == 'building3d' ? 1 + : _sceneMode == 'droneView' ? 2 + : 0, + children: [ + Listener( + onPointerDown: _onMapPointerDown, + child: ArcGISMapView( + controllerProvider: () => _mapViewController, + onMapViewReady: _onMapViewReady, + onTap: _onMapTap, + ), + ), + _scene3DWidget ?? const SizedBox.shrink(), + _sceneDroneWidget ?? const SizedBox.shrink(), + ], + ), + ), + ], + ), + + // Floating search bar + dropdown — hidden in 3D/Drone View modes + if (_sceneMode == 'default') + Positioned( + top: 8, + left: 12, + right: 12, + child: Column( + children: [ + // Search bar / route fields + if (_showRouteFields) + Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + color: isDark ? Colors.grey[850] : Colors.white, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Left dot column + Padding( + padding: const EdgeInsets.only(left: 14), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.circle_outlined, + size: 12, + color: Colors.blue), + Container( + width: 1.5, + height: 24, + color: isDark + ? Colors.grey[600] + : Colors.grey[300], + ), + Icon(Icons.circle, + size: 12, + color: Colors.red), + ], + ), + ), + // Text fields + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + Expanded( + child: TextField( + controller: _fromController, + focusNode: _fromFocusNode, + style: const TextStyle(fontSize: 16), + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: + EdgeInsets.symmetric( + horizontal: 10, + vertical: 14), + hintText: 'From', + isDense: true, + ), + onTap: () { + setState(() { + _activeRouteField = 'from'; + _showSuggestions = true; + _showResults = false; + }); + }, + onChanged: (text) { + _fromLatLng = null; + if (text.length >= 3) { + _performSearch(text); + } else if (text.isEmpty) { + setState(() { + _showResults = false; + _showSuggestions = true; + }); + } else { + setState(() { + _showResults = false; + _showSuggestions = false; + }); + } + }, + ), + ), + if (_fromController.text.isNotEmpty) + SizedBox( + width: 28, + height: 28, + child: IconButton( + padding: EdgeInsets.zero, + icon: Icon(Icons.close, + size: 16, + color: isDark + ? Colors.white70 + : Colors.grey[600]), + onPressed: () { + _fromController.clear(); + _fromLatLng = null; + setState(() => + _showSuggestions = true); + }, + ), + ), + ], + ), + Divider(height: 1, indent: 10, endIndent: 10), + Row( + children: [ + Expanded( + child: TextField( + controller: _toController, + focusNode: _toFocusNode, + style: const TextStyle(fontSize: 16), + decoration: const InputDecoration( + border: InputBorder.none, + contentPadding: + EdgeInsets.symmetric( + horizontal: 10, + vertical: 14), + hintText: 'To', + isDense: true, + ), + onTap: () { + setState(() { + _activeRouteField = 'to'; + _showSuggestions = true; + _showResults = false; + }); + }, + onChanged: (text) { + if (text.length >= 3) { + _performSearch(text); + } else if (text.isEmpty) { + setState(() { + _showResults = false; + _showSuggestions = true; + }); + } else { + setState(() { + _showResults = false; + _showSuggestions = false; + }); + } + }, + ), + ), + if (_toController.text.isNotEmpty) + SizedBox( + width: 28, + height: 28, + child: IconButton( + padding: EdgeInsets.zero, + icon: Icon(Icons.close, + size: 16, + color: isDark + ? Colors.white70 + : Colors.grey[600]), + onPressed: () { + _toController.clear(); + setState(() => + _showSuggestions = true); + }, + ), + ), + ], + ), + ], + ), + ), + // Close + Swap buttons + Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 36, + height: 36, + child: IconButton( + padding: EdgeInsets.zero, + icon: Icon(Icons.close, + size: 20, + color: Colors.redAccent), + onPressed: _clearRoute, + ), + ), + SizedBox( + width: 36, + height: 36, + child: IconButton( + padding: EdgeInsets.zero, + icon: Icon(Icons.swap_vert, + size: 20, + color: isDark + ? Colors.white70 + : Colors.grey[600]), + onPressed: () { + final tmpText = _fromController.text; + final tmpLatLng = _fromLatLng; + _fromController.text = _toController.text; + _toController.text = tmpText; + setState(() { + _fromLatLng = _routeDestination != null + ? (_routeDestination!.latitude, _routeDestination!.longitude) + : null; + _routeDestination = tmpLatLng != null + ? MapSearchResult( + name: tmpText, + subtitle: '', + latitude: tmpLatLng.$1, + longitude: tmpLatLng.$2, + source: MapSearchSource.building, + ) + : null; + }); + if (_routeDestination != null && _fromLatLng != null) { + _solveRoute(_routeDestination!, originLatLng: _fromLatLng); + } + }, + ), + ), + ], + ), + const SizedBox(width: 4), + ], + ), + ), + ) + else + Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + color: isDark ? Colors.grey[850] : Colors.white, + child: Row( + children: [ + Padding( + padding: EdgeInsets.only(left: 12), + child: Icon( + Icons.search, + color: isDark ? Colors.white70 : Colors.grey[600], + ), + ), + Expanded( + child: TextField( + controller: _searchController, + focusNode: _focusNode, + textInputAction: TextInputAction.search, + onSubmitted: _performSearch, + onChanged: (text) { + if (text.isEmpty) { + setState(() { + _showResults = false; + _searchResults = []; + _matchingPoiClasses = []; + _showSuggestions = _focusNode.hasFocus; + }); + } else { + final q = text.toLowerCase(); + // Match against server-fetched classes + final matched = _allPoiClasses + .where((c) => c.toLowerCase().contains(q)) + .toList(); + + // Always include hardcoded category class values as a fallback + // so chips appear even if the server fetch is still in flight + for (final cat in _categories) { + if (cat.poiClassValue.toLowerCase().contains(q) && + !matched.contains(cat.poiClassValue)) { + matched.insert(0, cat.poiClassValue); + } + } + + setState(() { + _showSuggestions = false; + _matchingPoiClasses = matched; + }); + } + }, + onTap: () { + // Close detail panel when user taps search bar + if (_selectedResult != null) { + setState(() { + _selectedResult = null; + }); + } + // Show suggestions if text is empty + if (_searchController.text.isEmpty) { + setState(() { + _showSuggestions = true; + _showResults = false; + }); + } + }, + style: TextStyle(fontSize: 16), + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric( + horizontal: 12, + vertical: 16, + ), + hintText: 'Search buildings, places...', + ), + ), + ), + if (_searchController.text.isNotEmpty) + IconButton( + icon: Icon(Icons.clear), + onPressed: _clearSearch, + ), + if (_config?.features.aiSearch ?? false) + IconButton( + icon: Icon( + Icons.auto_awesome, + color: isDark + ? Colors.amber[300] + : Theme.of(context).colorScheme.primary, + size: 20, + ), + onPressed: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => EsriAiSearchSheet( + userLat: _getUserLatLng()?.$1, + userLon: _getUserLatLng()?.$2, + onLocationSelected: _onAiLocationSelected, + onRouteRequested: _onAiRouteRequested, + ), + ), + ), + ], + ), + ), + + SizedBox(height: 4), + + // Suggestions panel (categories + recents) + if (_showSuggestions && !_showResults) + _buildSuggestionsPanel(context), + + // Search results dropdown + if (_showResults || (_matchingPoiClasses.isNotEmpty && _searchController.text.isNotEmpty && !_showSuggestions)) + Material( + elevation: 4, + borderRadius: BorderRadius.circular(8), + color: isDark ? Colors.grey[850] : Colors.white, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ── Category matches (always at top) ────────────────────────── + if (_matchingPoiClasses.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.fromLTRB(16, 10, 16, 4), + child: Row( + children: [ + Icon(Icons.category_outlined, + size: 13, + color: isDark ? Colors.grey[500] : Colors.grey[500]), + const SizedBox(width: 6), + Text( + 'CATEGORIES', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + letterSpacing: 0.6, + color: isDark ? Colors.grey[500] : Colors.grey[500], + ), + ), + ], + ), + ), + ..._matchingPoiClasses.map( + (classValue) => ListTile( + splashColor: Colors.transparent, + leading: Icon( + _iconForClass(classValue), + size: 20, + color: isDark ? Colors.white70 : Colors.grey[700], + ), + title: Text(_labelForClass(classValue)), + subtitle: Text( + classValue, + style: TextStyle( + fontSize: 13, + color: isDark ? Colors.grey[500] : Colors.grey[500]), + ), + dense: true, + onTap: () => _performClassSearch(classValue), + ), + ), + if (_showResults && (_isSearching || _searchResults.isNotEmpty)) + const Divider(height: 1), + ], + + // ── POI / building results ───────────────────────────────────── + if (_showResults) + _isSearching + ? const Padding( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), + ) + : _searchResults.isEmpty + // Only show "no results" if there are also no category matches + ? (_matchingPoiClasses.isEmpty + ? const Padding( + padding: EdgeInsets.all(16), + child: Text('No results found'), + ) + : const SizedBox.shrink()) + : ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.4, + ), + child: ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: _searchResults.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final result = _searchResults[index]; + return ListTile( + splashColor: Colors.transparent, + leading: Icon(_iconForResult(result)), + title: Text(result.name, + maxLines: 1, + overflow: TextOverflow.ellipsis), + subtitle: result.subtitle.isNotEmpty + ? Text(result.subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis) + : null, + dense: true, + onTap: () => _selectResult(result), + ); + }, + ), + ), + + ], + ), + ), + ], + ), + ), + + + // Category list panel — shown when list button is toggled + if (_showCategoryList && _selectedResult == null && _allCategoryResults.isNotEmpty) + _buildCategoryListPanel(context), + + // Detail slide-over + if (_selectedResult != null) + _buildDetailSlideOver(context, _selectedResult!), + + // Top-right FAB cluster — hidden when keyboard or layers panel is active + if (!keyboardVisible && !_focusNode.hasFocus && !_fromFocusNode.hasFocus && !_toFocusNode.hasFocus && !_showLayersPanel) + Positioned( + right: 12, + top: _showRouteFields ? 120 : 68, + child: EsriMapFabCluster( + isDark: isDark, + is3D: _sceneMode != 'default', + mapRotation: _mapRotation, + isLocationActive: _isLocationActive, + isRecenterActive: _isRecenterActive, + onShowLayersPanel: () => setState(() => _showLayersPanel = true), + onRecenterOnView: _recenterOnView, + onRecenterOnUser: _recenterOnUser, + onSnapToNorth: _snapToNorth, + ), + ), + + // Layers/display panel + if (_showLayersPanel && _config != null) + EsriMapLayersPanel( + config: _config!, + currentBasemapType: _currentBasemapType, + currentSceneKey: _sceneMode, + layerVisible: _layerVisible, + layerLoading: _layerLoading, + hideSceneSwitcher: _selectedResult != null, + onSwitchBasemap: _switchBasemap, + onSetSceneMode: _setSceneMode, + onToggleLayer: _toggleLayer, + onClose: () => setState(() => _showLayersPanel = false), + ), + ], + ), + ); + } +} + +class _AgeAuthChallengeHandler implements ArcGISAuthenticationChallengeHandler { + final String tokensUrl; + + String? _cachedAgeToken; + DateTime? _ageTokenExpiry; + + String? _cachedAgoToken; + DateTime? _agoTokenExpiry; + + _AgeAuthChallengeHandler(this.tokensUrl); + + Future<(String?, DateTime?)> _getTokenForHost(String host) async { + final isAgo = host.contains('arcgis.com'); + + if (isAgo) { + if (_cachedAgoToken != null && + _agoTokenExpiry != null && + DateTime.now().isBefore(_agoTokenExpiry!.subtract(const Duration(minutes: 5)))) { + return (_cachedAgoToken, _agoTokenExpiry); + } + } else { + if (_cachedAgeToken != null && + _ageTokenExpiry != null && + DateTime.now().isBefore(_ageTokenExpiry!.subtract(const Duration(minutes: 5)))) { + return (_cachedAgeToken, _ageTokenExpiry); + } + } + + final response = await http.get(Uri.parse(tokensUrl)); + if (response.statusCode != 200) throw Exception('Token fetch failed: ${response.statusCode}'); + final data = jsonDecode(response.body) as Map; + + + _cachedAgeToken = data['age']?['token'] as String?; + final ageExpiresIn = data['age']?['expires_in'] as int? ?? 7200; + _ageTokenExpiry = DateTime.now().add(Duration(seconds: ageExpiresIn)); + + _cachedAgoToken = data['ago']?['token'] as String?; + final agoExpiresIn = data['ago']?['expires_in'] as int? ?? 7200; + _agoTokenExpiry = DateTime.now().add(Duration(seconds: agoExpiresIn)); + + return isAgo ? (_cachedAgoToken, _agoTokenExpiry) : (_cachedAgeToken, _ageTokenExpiry); + } + + @override + Future handleArcGISAuthenticationChallenge( + ArcGISAuthenticationChallenge challenge, + ) async { + try { + final host = challenge.requestUri.host; + final (token, expiry) = await _getTokenForHost(host); + if (token == null || expiry == null) { + challenge.continueAndFail(); + return; + } + final tokenInfo = TokenInfo.create( + accessToken: token, + expirationDate: expiry, + isSslRequired: true, + ); + if (tokenInfo == null) { + challenge.continueAndFail(); + return; + } + final credential = PregeneratedTokenCredential( + uri: challenge.requestUri, + tokenInfo: tokenInfo, + referer: '', + ); + challenge.continueWithCredential(credential); + } catch (e) { + debugPrint('Auth challenge failed: $e'); + challenge.continueAndFail(); + } + } +} \ No newline at end of file diff --git a/lib/ui/esrimap/esrimap_ai_search.dart b/lib/ui/esrimap/esrimap_ai_search.dart new file mode 100644 index 000000000..0f40ec052 --- /dev/null +++ b/lib/ui/esrimap/esrimap_ai_search.dart @@ -0,0 +1,317 @@ +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'esrimap_config.dart'; + +class AiSearchResult { + final String name; + final String subtitle; + final String address; + final double latitude; + final double longitude; + final int? distanceFeet; + final String? distanceFormatted; + + const AiSearchResult({ + required this.name, + required this.subtitle, + required this.address, + required this.latitude, + required this.longitude, + this.distanceFeet, + this.distanceFormatted, + }); + + factory AiSearchResult.fromJson(Map j) => AiSearchResult( + name: j['name'] as String, + subtitle: j['subtitle'] as String? ?? '', + address: j['address'] as String? ?? '', + latitude: (j['latitude'] as num).toDouble(), + longitude: (j['longitude'] as num).toDouble(), + distanceFeet: j['distanceFeet'] as int?, + distanceFormatted: j['distanceFormatted'] as String?, + ); +} + +class AiSearchRouteStop { + final double lat; + final double lon; + const AiSearchRouteStop({required this.lat, required this.lon}); + factory AiSearchRouteStop.fromJson(Map j) => + AiSearchRouteStop(lat: (j['lat'] as num).toDouble(), lon: (j['lon'] as num).toDouble()); +} + +class AiSearchResponse { + final String message; + final List results; + final List? routeStopNames; + final List? routeStopCoords; + + const AiSearchResponse({ + required this.message, + required this.results, + this.routeStopNames, + this.routeStopCoords, + }); + + factory AiSearchResponse.fromJson(Map j) { + List? stopNames; + List? stopCoords; + final route = j['suggestedRoute']; + if (route != null) { + stopNames = (route['stops'] as List).cast(); + stopCoords = (route['stopCoords'] as List) + .map((s) => AiSearchRouteStop.fromJson(s as Map)) + .toList(); + } + return AiSearchResponse( + message: j['message'] as String, + results: (j['results'] as List) + .map((r) => AiSearchResult.fromJson(r as Map)) + .toList(), + routeStopNames: stopNames, + routeStopCoords: stopCoords, + ); + } +} + +enum _AiSearchState { idle, loading, results, error } + +class EsriAiSearchSheet extends StatefulWidget { + final double? userLat; + final double? userLon; + final void Function(AiSearchResult result) onLocationSelected; + final void Function(List stops) onRouteRequested; + + const EsriAiSearchSheet({ + Key? key, + this.userLat, + this.userLon, + required this.onLocationSelected, + required this.onRouteRequested, + }) : super(key: key); + + @override + State createState() => _EsriAiSearchSheetState(); +} + +class _EsriAiSearchSheetState extends State { + final _controller = TextEditingController(); + _AiSearchState _state = _AiSearchState.idle; + AiSearchResponse? _response; + String? _errorMessage; + + static const _baseUrl = + 'https://appzxi70zi.execute-api.us-west-2.amazonaws.com/test/ArcGIS-Map'; + + Future _submit() async { + final query = _controller.text.trim(); + if (query.isEmpty) return; + setState(() { + _state = _AiSearchState.loading; + _response = null; + _errorMessage = null; + }); + try { + final body = {'query': query}; + if (widget.userLat != null) body['lat'] = widget.userLat; + if (widget.userLon != null) body['lon'] = widget.userLon; + + final res = await http.post( + Uri.parse('$_baseUrl/ai-search'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(body), + ); + if (res.statusCode != 200) throw Exception('Request failed: ${res.statusCode}'); + final data = jsonDecode(res.body) as Map; + setState(() { + _response = AiSearchResponse.fromJson(data); + _state = _AiSearchState.results; + }); + } catch (e) { + setState(() { + _errorMessage = e.toString(); + _state = _AiSearchState.error; + }); + } + } + + IconData _iconForSubtitle(String subtitle) { + final s = subtitle.toLowerCase(); + if (s.contains('dining') || s.contains('food') || s.contains('coffee') || s.contains('cafe')) { + return Icons.restaurant; + } + if (s.contains('library') || s.contains('academic')) return Icons.menu_book; + if (s.contains('parking')) return Icons.local_parking; + if (s.contains('recreation') || s.contains('gym') || s.contains('fitness')) return Icons.fitness_center; + if (s.contains('transit') || s.contains('shuttle')) return Icons.directions_bus; + return Icons.place; + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final bgColor = isDark ? Colors.grey[900]! : Colors.white; + final textColor = isDark ? Colors.white : Colors.grey[900]!; + final subtitleColor = isDark ? Colors.grey[400]! : Colors.grey[600]!; + final accent = Theme.of(context).colorScheme.primary; + final bottomPad = MediaQuery.of(context).viewInsets.bottom; + + return Container( + decoration: BoxDecoration( + color: bgColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), + ), + padding: EdgeInsets.only(bottom: bottomPad), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Handle + Center( + child: Container( + margin: const EdgeInsets.only(top: 10, bottom: 6), + width: 36, + height: 4, + decoration: BoxDecoration( + color: isDark ? Colors.grey[700] : Colors.grey[300], + borderRadius: BorderRadius.circular(2), + ), + ), + ), + + // Header + Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 12), + child: Row( + children: [ + Icon(Icons.auto_awesome, size: 18, color: accent), + const SizedBox(width: 8), + Text('Ask the Map', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: textColor)), + ], + ), + ), + + // Search input + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _controller, + autofocus: true, + textInputAction: TextInputAction.search, + onSubmitted: (_) => _submit(), + style: TextStyle(color: textColor), + decoration: InputDecoration( + hintText: 'e.g. "good lunch spots near Price Center"', + hintStyle: TextStyle(color: subtitleColor, fontSize: 13), + filled: true, + fillColor: isDark ? Colors.grey[800] : Colors.grey[100], + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + ), + ), + ), + const SizedBox(width: 8), + FilledButton( + onPressed: _state == _AiSearchState.loading ? null : _submit, + style: FilledButton.styleFrom( + minimumSize: const Size(48, 48), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + ), + child: _state == _AiSearchState.loading + ? const SizedBox(width: 18, height: 18, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white)) + : const Icon(Icons.search), + ), + ], + ), + ), + + // Results + if (_state == _AiSearchState.results && _response != null) ...[ + if (_response!.message.isNotEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: Text(_response!.message, + style: TextStyle(fontSize: 13, color: subtitleColor)), + ), + + if (_response!.results.isEmpty) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Text('No results found. Try a different query.', + style: TextStyle(fontSize: 13, color: subtitleColor)), + ), + + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 280), + child: ListView.separated( + shrinkWrap: true, + itemCount: _response!.results.length, + separatorBuilder: (_, __) => + Divider(height: 1, indent: 56, color: isDark ? Colors.grey[800] : Colors.grey[200]), + itemBuilder: (context, i) { + final r = _response!.results[i]; + return ListTile( + leading: CircleAvatar( + backgroundColor: accent.withOpacity(0.12), + child: Icon(_iconForSubtitle(r.subtitle), color: accent, size: 18), + ), + title: Text(r.name, + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: textColor)), + subtitle: Text(r.distanceFormatted != null ? '${r.subtitle} · ${r.distanceFormatted}' : r.subtitle, + style: TextStyle(fontSize: 12, color: subtitleColor)), + onTap: () { + Navigator.pop(context); + widget.onLocationSelected(r); + }, + ); + }, + ), + ), + + // Multi-stop route button + if (_response!.routeStopNames != null) ...[ + const Divider(height: 1), + Padding( + padding: const EdgeInsets.all(12), + child: SizedBox( + width: double.infinity, + child: FilledButton.icon( + icon: const Icon(Icons.directions), + label: Text('Route through ${_response!.routeStopNames!.length} stops'), + onPressed: () { + Navigator.pop(context); + widget.onRouteRequested(_response!.results); + }, + ), + ), + ), + ] else + const SizedBox(height: 16), + ], + + if (_state == _AiSearchState.error) + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + child: Text('Something went wrong. Please try again.', + style: TextStyle(fontSize: 13, color: Colors.redAccent)), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/esrimap/esrimap_basemaps.dart b/lib/ui/esrimap/esrimap_basemaps.dart new file mode 100644 index 000000000..6c373c6ad --- /dev/null +++ b/lib/ui/esrimap/esrimap_basemaps.dart @@ -0,0 +1,68 @@ +import 'package:arcgis_maps/arcgis_maps.dart'; +import 'esrimap_config.dart'; + +enum BasemapType { defaultMap, light, dark, satellite } + +String basemapKey(BasemapType type) { + switch (type) { + case BasemapType.defaultMap: return 'defaultMap'; + case BasemapType.light: return 'light'; + case BasemapType.dark: return 'dark'; + case BasemapType.satellite: return 'satellite'; + } +} + +BasemapType? basemapTypeFromKey(String key) { + switch (key) { + case 'defaultMap': return BasemapType.defaultMap; + case 'light': return BasemapType.light; + case 'dark': return BasemapType.dark; + case 'satellite': return BasemapType.satellite; + default: return null; + } +} + +Basemap buildBasemap(BasemapType type, EsriMapConfig config) { + final entry = config.basemaps[basemapKey(type)]; + if (entry == null) return Basemap(); + + final basemap = Basemap(); + for (final layer in entry.baseLayers) { + switch (layer.type) { + case 'arcgisTiled': + final url = layer.serviceKey != null + ? config.serviceUrls[layer.serviceKey!] + : null; + if (url != null) { + basemap.baseLayers.add(ArcGISTiledLayer.withUri(Uri.parse(url))); + } + break; + + case 'arcgisVectorTiled': + final portalUrl = layer.portalKey != null + ? config.portals[layer.portalKey!] + : null; + if (portalUrl != null && layer.itemId != null) { + basemap.baseLayers.add( + ArcGISVectorTiledLayer.withItem( + PortalItem.withPortalAndItemId( + portal: Portal(Uri.parse(portalUrl)), + itemId: layer.itemId!, + ), + ), + ); + } + break; + + case 'arcgisMapImage': + final url = layer.serviceKey != null + ? config.serviceUrls[layer.serviceKey!] + : null; + if (url != null) { + basemap.baseLayers.add(ArcGISMapImageLayer.withUri(Uri.parse(url))); + } + break; + } + } + return basemap; +} \ No newline at end of file diff --git a/lib/ui/esrimap/esrimap_config.dart b/lib/ui/esrimap/esrimap_config.dart new file mode 100644 index 000000000..a63a01656 --- /dev/null +++ b/lib/ui/esrimap/esrimap_config.dart @@ -0,0 +1,260 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; + +// Base layer spec + +class BaseLayerSpec { + final String type; // 'arcgisTiled' | 'arcgisVectorTiled' | 'arcgisMapImage' + final String? serviceKey; // resolves via EsriMapConfig.serviceUrls + final String? portalKey; // resolves via EsriMapConfig.portals + final String? itemId; + + const BaseLayerSpec({ + required this.type, + this.serviceKey, + this.portalKey, + this.itemId, + }); + + factory BaseLayerSpec.fromJson(Map json) => BaseLayerSpec( + type: json['type'] as String, + serviceKey: json['serviceKey'] as String?, + portalKey: json['portalKey'] as String?, + itemId: json['itemId'] as String?, + ); +} + +// Basemaps + +class BasemapConfig { + final String label; + final String thumbnailAsset; + final List baseLayers; + + const BasemapConfig({ + required this.label, + required this.thumbnailAsset, + required this.baseLayers, + }); + + factory BasemapConfig.fromJson(Map json) => BasemapConfig( + label: json['label'] as String, + thumbnailAsset: json['thumbnailAsset'] as String, + baseLayers: (json['baseLayers'] as List) + .map((e) => BaseLayerSpec.fromJson(e as Map)) + .toList(), + ); +} + +// Operational layers + +class SublayerEntry { + final String name; + final String source; + final String? url; + final String? portalKey; + final String? itemId; + final int refreshInterval; + final bool popup; + final String? popupTemplate; + + const SublayerEntry({ + required this.name, + required this.source, + this.url, + this.portalKey, + this.itemId, + required this.refreshInterval, + required this.popup, + this.popupTemplate, + }); + + factory SublayerEntry.fromJson(Map json) => SublayerEntry( + name: json['name'] as String, + source: json['source'] as String, + url: json['url'] as String?, + portalKey: json['portalKey'] as String?, + itemId: json['itemId'] as String?, + refreshInterval: (json['refreshInterval'] as num?)?.toInt() ?? 0, + popup: json['popup'] as bool? ?? false, + popupTemplate: json['popupTemplate'] as String?, + ); +} + +class LayerEntry { + final String label; + final String thumbnailAsset; + final String? source; + final String? url; + final String? portalKey; + final String? itemId; + final int refreshInterval; + final bool popup; + final String? popupTemplate; + final List? sublayers; + + const LayerEntry({ + required this.label, + required this.thumbnailAsset, + this.source, + this.url, + this.portalKey, + this.itemId, + this.refreshInterval = 0, + this.popup = false, + this.popupTemplate, + this.sublayers, + }); + + bool get hasSublayers => sublayers != null && sublayers!.isNotEmpty; + + factory LayerEntry.fromJson(Map json) => LayerEntry( + label: json['label'] as String, + thumbnailAsset: json['thumbnailAsset'] as String, + source: json['source'] as String?, + url: json['url'] as String?, + portalKey: json['portalKey'] as String?, + itemId: json['itemId'] as String?, + refreshInterval: (json['refreshInterval'] as num?)?.toInt() ?? 0, + popup: json['popup'] as bool? ?? false, + popupTemplate: json['popupTemplate'] as String?, + sublayers: json['sublayers'] == null + ? null + : (json['sublayers'] as List) + .map((e) => SublayerEntry.fromJson(e as Map)) + .toList(), + ); +} + +// Scenes + +class SceneConfig { + final String label; + final String type; // '2d' | 'scene' + final String? portalKey; + final String? itemId; + + const SceneConfig({ + required this.label, + required this.type, + this.portalKey, + this.itemId, + }); + + factory SceneConfig.fromJson(Map json) => SceneConfig( + label: json['label'] as String, + type: json['type'] as String, + portalKey: json['portalKey'] as String?, + itemId: json['itemId'] as String?, + ); +} + +// Features +// Allows remote enabling/disabling of app features without redeploying the app. + +class FeaturesConfig { + final bool aiSearch; + final bool scenes; + + const FeaturesConfig({required this.aiSearch, required this.scenes}); + + factory FeaturesConfig.fromJson(Map json) => FeaturesConfig( + aiSearch: json['aiSearch'] as bool? ?? false, + scenes: json['scenes'] as bool? ?? false, + ); +} + +// Search categories + +class SearchCategoryConfig { + final String label; + final String poiClass; + final String icon; + final String? color; + + const SearchCategoryConfig({ + required this.label, + required this.poiClass, + required this.icon, + this.color, + }); + + factory SearchCategoryConfig.fromJson(Map json) => + SearchCategoryConfig( + label: json['label'] as String, + poiClass: json['poiClass'] as String, + icon: json['icon'] as String, + color: json['color'] as String?, + ); +} + +// Root config + +class EsriMapConfig { + final Map portals; + final Map serviceUrls; + final Map basemaps; + final Map layers; + final Map scenes; + final FeaturesConfig features; + final List searchCategories; + + const EsriMapConfig({ + required this.portals, + required this.serviceUrls, + required this.basemaps, + required this.layers, + required this.scenes, + required this.features, + required this.searchCategories, + }); + + factory EsriMapConfig.fromJson(Map json) => EsriMapConfig( + portals: (json['portals'] as Map).cast(), + serviceUrls: (json['serviceUrls'] as Map).cast(), + basemaps: (json['basemaps'] as Map).map( + (k, v) => MapEntry(k, BasemapConfig.fromJson(v as Map)), + ), + layers: (json['layers'] as Map).map( + (k, v) => MapEntry(k, LayerEntry.fromJson(v as Map)), + ), + scenes: (json['scenes'] as Map).map( + (k, v) => MapEntry(k, SceneConfig.fromJson(v as Map)), + ), + features: FeaturesConfig.fromJson(json['features'] as Map), + searchCategories: (json['searchCategories'] as List) + .map((e) => SearchCategoryConfig.fromJson(e as Map)) + .toList(), + ); +} + +// Service +// Fetches and caches the EsriMapConfig from the backend API. + +class EsriMapConfigService { + static final EsriMapConfigService instance = EsriMapConfigService._(); + EsriMapConfigService._(); + + static const _baseUrl = + 'https://appzxi70zi.execute-api.us-west-2.amazonaws.com/test/ArcGIS-Map'; + + EsriMapConfig? _config; + Future? _pending; + + Future fetch() => _pending ??= _doFetch(); + + EsriMapConfig? get cached => _config; + + String get tokensUrl => '$_baseUrl/tokens'; + + Future _doFetch() async { + final response = await http.get(Uri.parse('$_baseUrl/config')); + if (response.statusCode != 200) { + throw Exception('Config fetch failed: ${response.statusCode}'); + } + _config = EsriMapConfig.fromJson( + jsonDecode(response.body) as Map, + ); + return _config!; + } +} \ No newline at end of file diff --git a/lib/ui/esrimap/esrimap_fab.dart b/lib/ui/esrimap/esrimap_fab.dart new file mode 100644 index 000000000..a2187190a --- /dev/null +++ b/lib/ui/esrimap/esrimap_fab.dart @@ -0,0 +1,163 @@ +import 'dart:math' as math; +import 'package:flutter/material.dart'; + +class EsriMapFabCluster extends StatelessWidget { + final bool isDark; + final bool is3D; + final double mapRotation; + final bool isLocationActive; + final bool isRecenterActive; + final VoidCallback onShowLayersPanel; + final VoidCallback onRecenterOnView; + final VoidCallback onRecenterOnUser; + final VoidCallback onSnapToNorth; + + const EsriMapFabCluster({ + Key? key, + required this.isDark, + this.is3D = false, + required this.mapRotation, + this.isLocationActive = false, + this.isRecenterActive = false, + required this.onShowLayersPanel, + required this.onRecenterOnView, + required this.onRecenterOnUser, + required this.onSnapToNorth, + }) : super(key: key); + + static const _activeColor = Color(0xFFC69214); + static const _size = 48.0; + static const _btnHeight = 44.0; + + @override + Widget build(BuildContext context) { + final bgColor = isDark ? Colors.grey[800]! : Colors.white; + final fgColor = isDark ? Colors.white : Colors.grey[800]!; + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Compass — matches pill width + Material( + elevation: 4, + color: bgColor, + shape: const CircleBorder(), + clipBehavior: Clip.antiAlias, + child: GestureDetector( + onTap: onSnapToNorth, + behavior: HitTestBehavior.opaque, + child: SizedBox( + width: _size, + height: _size, + child: Center( + child: CustomPaint( + size: const Size(24, 24), + painter: _CompassNeedlePainter( + rotationDegrees: mapRotation, + southColor: fgColor.withValues(alpha: 0.35), + ), + ), + ), + ), + ), + ), + const SizedBox(height: 10), + // Pill — my location, recenter, layers + Material( + elevation: 4, + color: bgColor, + borderRadius: BorderRadius.circular(_size / 2), + clipBehavior: Clip.antiAlias, + child: SizedBox( + width: _size, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!is3D) + _pillButton( + icon: Icons.my_location, + color: isLocationActive ? _activeColor : fgColor, + onTap: onRecenterOnUser, + ), + _pillButton( + icon: Icons.center_focus_strong, + color: isRecenterActive ? _activeColor : fgColor, + onTap: onRecenterOnView, + ), + _pillButton( + icon: Icons.layers_outlined, + color: fgColor, + onTap: onShowLayersPanel, + ), + ], + ), + ), + ), + ), + ], + ); + } + + Widget _pillButton({ + required IconData icon, + required Color color, + required VoidCallback onTap, + }) { + return GestureDetector( + onTap: onTap, + behavior: HitTestBehavior.opaque, + child: SizedBox( + height: _btnHeight, + child: Center( + child: Icon(icon, size: 22, color: color), + ), + ), + ); + } +} + +class _CompassNeedlePainter extends CustomPainter { + final double rotationDegrees; + final Color southColor; + + const _CompassNeedlePainter({ + required this.rotationDegrees, + required this.southColor, + }); + + @override + void paint(Canvas canvas, Size size) { + final cx = size.width / 2; + final cy = size.height / 2; + final tipDist = size.height * 0.46; + final halfWidth = size.width * 0.18; + + canvas.save(); + canvas.translate(cx, cy); + canvas.rotate(-rotationDegrees * math.pi / 180); + + final northPath = Path() + ..moveTo(0, -tipDist) + ..lineTo(halfWidth, 0) + ..lineTo(-halfWidth, 0) + ..close(); + + final southPath = Path() + ..moveTo(0, tipDist) + ..lineTo(halfWidth, 0) + ..lineTo(-halfWidth, 0) + ..close(); + + canvas.drawPath(northPath, Paint()..color = Colors.red); + canvas.drawPath(southPath, Paint()..color = southColor); + + canvas.restore(); + } + + @override + bool shouldRepaint(_CompassNeedlePainter old) => + old.rotationDegrees != rotationDegrees || old.southColor != southColor; +} diff --git a/lib/ui/esrimap/esrimap_layers_panel.dart b/lib/ui/esrimap/esrimap_layers_panel.dart new file mode 100644 index 000000000..7f25c7466 --- /dev/null +++ b/lib/ui/esrimap/esrimap_layers_panel.dart @@ -0,0 +1,281 @@ +import 'package:flutter/material.dart'; +import 'esrimap_basemaps.dart'; +import 'esrimap_config.dart'; + +class EsriMapLayersPanel extends StatelessWidget { + final EsriMapConfig config; + final BasemapType currentBasemapType; + final String currentSceneKey; + final Map layerVisible; + final Map layerLoading; + final void Function(BasemapType) onSwitchBasemap; + final void Function(String) onSetSceneMode; + final void Function(String) onToggleLayer; + final VoidCallback onClose; + final bool hideSceneSwitcher; + + const EsriMapLayersPanel({ + Key? key, + required this.config, + required this.currentBasemapType, + required this.currentSceneKey, + required this.layerVisible, + required this.layerLoading, + required this.onSwitchBasemap, + required this.onSetSceneMode, + required this.onToggleLayer, + required this.onClose, + this.hideSceneSwitcher = false, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final bgColor = isDark ? Colors.grey[900]! : Colors.white; + final textColor = isDark ? Colors.white : Colors.grey[900]!; + final subtitleColor = isDark ? Colors.grey[400]! : Colors.grey[600]!; + final accent = isDark + ? Colors.lightBlue[300]! + : Theme.of(context).colorScheme.primary; + + Widget sectionLabel(String text) => Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Text( + text, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + letterSpacing: 0.8, + color: subtitleColor, + ), + ), + ); + + Widget imageTile({ + required String label, + required bool selected, + required VoidCallback onTap, + Widget? imageWidget, + bool loading = false, + }) { + return GestureDetector( + onTap: onTap, + child: SizedBox( + width: 72, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 56, + height: 56, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: selected + ? Border.all(color: accent, width: 2.5) + : Border.all( + color: isDark ? Colors.grey[700]! : Colors.grey[300]!, + width: 1, + ), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(6.5), + child: loading + ? const Center( + child: SizedBox( + width: 22, + height: 22, + child: CircularProgressIndicator(strokeWidth: 2), + ), + ) + : (imageWidget ?? + Container( + color: isDark ? Colors.grey[700] : Colors.grey[300], + )), + ), + ), + const SizedBox(height: 5), + Text( + label, + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 11, + fontWeight: selected ? FontWeight.w700 : FontWeight.w400, + color: selected ? accent : textColor, + ), + ), + ], + ), + ), + ); + } + + Widget networkImage(String url) => Image.network( + url, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Container( + color: isDark ? Colors.grey[700] : Colors.grey[300], + ), + ); + + Widget sceneChip(String key, String label, bool selected) => + GestureDetector( + onTap: () => onSetSceneMode(key), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + border: selected + ? Border.all(color: accent, width: 2) + : Border.all( + color: isDark ? Colors.grey[700]! : Colors.grey[300]!, + width: 1, + ), + color: selected + ? accent.withOpacity(0.12) + : (isDark ? Colors.grey[850] : Colors.grey[100]), + ), + child: Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: selected ? FontWeight.w700 : FontWeight.w400, + color: selected ? accent : textColor, + ), + ), + ), + ); + + final bottomPad = MediaQuery.of(context).padding.bottom; + final isDefault = currentSceneKey == 'default'; + + return Positioned( + left: 12, + right: 12, + bottom: bottomPad + 12, + child: GestureDetector( + onVerticalDragEnd: (details) { + if (details.velocity.pixelsPerSecond.dy > 200) onClose(); + }, + child: Material( + elevation: 10, + color: bgColor, + borderRadius: BorderRadius.circular(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + Padding( + padding: const EdgeInsets.fromLTRB(16, 14, 8, 0), + child: Row( + children: [ + Expanded( + child: Text( + 'Map Display', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + ), + IconButton( + icon: Icon(Icons.close, size: 20, color: subtitleColor), + onPressed: onClose, + ), + ], + ), + ), + + // Basemap + Layers — grayed out when not in default scene + Opacity( + opacity: isDefault ? 1.0 : 0.35, + child: IgnorePointer( + ignoring: !isDefault, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + sectionLabel('BASEMAP'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (final entry in config.basemaps.entries) ...[ + if (basemapTypeFromKey(entry.key) != null) ...[ + imageTile( + label: entry.value.label, + selected: currentBasemapType == + basemapTypeFromKey(entry.key), + onTap: () { + final t = basemapTypeFromKey(entry.key); + if (t != null) onSwitchBasemap(t); + }, + imageWidget: networkImage(entry.value.thumbnailAsset), + ), + const SizedBox(width: 8), + ], + ], + ], + ), + ), + ), + + const Divider(height: 24, indent: 16, endIndent: 16), + + sectionLabel('LAYERS'), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + for (final entry in config.layers.entries) ...[ + imageTile( + label: entry.value.label, + selected: layerVisible[entry.key] ?? false, + loading: layerLoading[entry.key] ?? false, + onTap: () => onToggleLayer(entry.key), + imageWidget: networkImage(entry.value.thumbnailAsset), + ), + const SizedBox(width: 8), + ], + ], + ), + ), + ), + ], + ), + ), + ), + + // Scene chips — gated by features flag, hidden when slide-over is active + if (config.features.scenes && !hideSceneSwitcher) ...[ + const Divider(height: 24, indent: 16, endIndent: 16), + sectionLabel('SCENE'), + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 20), + child: Wrap( + spacing: 8, + children: [ + for (final entry in config.scenes.entries) + sceneChip( + entry.key, + entry.value.label, + currentSceneKey == entry.key, + ), + ], + ), + ), + ] else + const SizedBox(height: 20), + ], + ), + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/ui/esrimap/esrimap_scene.dart b/lib/ui/esrimap/esrimap_scene.dart new file mode 100644 index 000000000..5b4515b4c --- /dev/null +++ b/lib/ui/esrimap/esrimap_scene.dart @@ -0,0 +1,151 @@ +import 'dart:async'; +import 'package:arcgis_maps/arcgis_maps.dart'; +import 'package:flutter/material.dart'; + +class EsriSceneWidget extends StatefulWidget { + final String portalUri; + final String itemId; + final void Function(double heading)? onHeadingChanged; + + const EsriSceneWidget({ + super.key, + required this.portalUri, + required this.itemId, + this.onHeadingChanged, + }); + + @override + State createState() => EsriSceneWidgetState(); +} + +class EsriSceneWidgetState extends State { + late final ArcGISSceneViewController _sceneViewController; + Camera? _initialCamera; + double _camToTargetLatOffset = 0.0; + StreamSubscription? _viewpointSubscription; + + @override + void initState() { + super.initState(); + _sceneViewController = ArcGISSceneView.createController(); + _sceneViewController.interactionOptions.rotateEnabled = true; + _sceneViewController.interactionOptions.panEnabled = true; + _sceneViewController.interactionOptions.flingEnabled = true; + _sceneViewController.interactionOptions.zoomFactor = 3.0; + _sceneViewController.atmosphereEffect = AtmosphereEffect.none; + _sceneViewController.spaceEffect = SpaceEffect.transparent; + } + + void _onSceneViewReady() { + final portal = Portal(Uri.parse(widget.portalUri)); + final portalItem = PortalItem.withPortalAndItemId( + portal: portal, + itemId: widget.itemId, + ); + final scene = ArcGISScene.withItem(portalItem); + _sceneViewController.arcGISScene = scene; + + _initialCamera = Camera.withLatLong( + latitude: 32.86872066, + longitude: -117.23732235, + altitude: 1062.871, + heading: 0, + pitch: 53.261, + roll: 0, + ); + _sceneViewController.setViewpointCamera(_initialCamera!); + + // Capture the lat offset between camera and look-at target once the + // viewpoint stabilizes — used by snapToNorth to rotate around target. + Future.delayed(const Duration(milliseconds: 500), () { + if (!mounted) return; + final vp = _sceneViewController.getCurrentViewpoint( + ViewpointType.centerAndScale, + ); + final pt = vp?.targetGeometry; + if (pt is ArcGISPoint) { + _camToTargetLatOffset = pt.y - _initialCamera!.location.y; + } + }); + + _viewpointSubscription = _sceneViewController.onViewpointChanged.listen((_) { + if (!mounted) return; + final vp = _sceneViewController.getCurrentViewpoint( + ViewpointType.centerAndScale, + ); + if (vp != null) { + widget.onHeadingChanged?.call(vp.rotation); + } + }); + + _applyLabelScales(scene); + } + + // Restrict building labels to only appear when zoomed in close (scale <= 4000) + Future _applyLabelScales(ArcGISScene scene) async { + await scene.load(); + _applyLabelScalesToLayers(scene.operationalLayers); + } + + void _applyLabelScalesToLayers(List layers) { + for (final layer in layers) { + if (layer is FeatureLayer) { + for (final labelDef in layer.labelDefinitions) { + labelDef.minScale = 4000; + } + } else if (layer is GroupLayer) { + _applyLabelScalesToLayers(layer.layers); + } + } + } + + void resetCamera() { + if (_initialCamera == null) return; + _sceneViewController.setViewpointCamera(_initialCamera!); + } + + /// Snaps heading to north by rotating the camera around the current + /// look-at target so the building/point stays centered on screen. + void snapToNorth() { + if (_initialCamera == null) return; + final vp = _sceneViewController.getCurrentViewpoint( + ViewpointType.centerAndScale, + ); + final target = vp?.targetGeometry; + if (target is! ArcGISPoint) { + resetCamera(); + return; + } + + // Position camera due south of the current target at the same distance + // as the initial camera-to-target offset + final pivotLat = target.y; + final pivotLon = target.x; + final camAlt = _initialCamera!.location.z ?? 1062.871; + + _sceneViewController.setViewpointCamera( + Camera.withLatLong( + latitude: pivotLat - _camToTargetLatOffset, + longitude: pivotLon, + altitude: camAlt, + heading: 0, + pitch: _initialCamera!.pitch, + roll: 0, + ), + ); + } + + @override + void dispose() { + _viewpointSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ArcGISSceneView( + controllerProvider: () => _sceneViewController, + onSceneViewReady: _onSceneViewReady, + ); + } +} diff --git a/lib/ui/esrimap/mapConfig.json b/lib/ui/esrimap/mapConfig.json new file mode 100644 index 000000000..7eda26b77 --- /dev/null +++ b/lib/ui/esrimap/mapConfig.json @@ -0,0 +1,166 @@ +{ + "portals": { + "age": "https://admin-enterprise-gis.ucsd.edu/portal", + "ago": "https://ucsd-admin.maps.arcgis.com" + }, + + "serviceUrls": { + "hillshade": "https://services.arcgisonline.com/arcgis/rest/services/Elevation/World_Hillshade/MapServer", + "lightGrayBase": "https://services.arcgisonline.com/arcgis/rest/services/Canvas/World_Light_Gray_Base/MapServer", + "darkGrayBase": "https://services.arcgisonline.com/arcgis/rest/services/Canvas/World_Dark_Gray_Base/MapServer", + "nearmap": "https://admin-enterprise-gis.ucsd.edu/server/rest/services/Campus_Imagery_Nearmap_9in_2025_0423_0514/MapServer" + }, + + "basemaps": { + "defaultMap": { + "label": "Default", + "thumbnailAsset": "https://dvsbke0idzv4u.cloudfront.net/thumbnails/default-thumbnail.png", + "baseLayers": [ + { + "type": "arcgisTiled", + "serviceKey": "hillshade" + }, + { + "type": "arcgisTiled", + "serviceKey": "lightGrayBase" + }, + { + "type": "arcgisVectorTiled", + "portalKey": "age", + "itemId": "e19f33d2c1f44967aef673306c483913" + } + ] + }, + "light": { + "label": "Light", + "thumbnailAsset": "https://dvsbke0idzv4u.cloudfront.net/thumbnails/light-thumbnail.png", + "baseLayers": [ + { + "type": "arcgisTiled", + "serviceKey": "hillshade" + }, + { + "type": "arcgisTiled", + "serviceKey": "lightGrayBase" + }, + { + "type": "arcgisVectorTiled", + "portalKey": "age", + "itemId": "6643ee62af494f5bafe7dfdb8eb3f857" + } + ] + }, + "dark": { + "label": "Dark", + "thumbnailAsset": "https://dvsbke0idzv4u.cloudfront.net/thumbnails/dark-thumbnail.png", + "baseLayers": [ + { + "type": "arcgisTiled", + "serviceKey": "hillshade" + }, + { + "type": "arcgisTiled", + "serviceKey": "darkGrayBase" + }, + { + "type": "arcgisVectorTiled", + "portalKey": "age", + "itemId": "09d7b3934b6c4c2cad8380c04e08c1b1" + } + ] + }, + "satellite": { + "label": "Satellite", + "thumbnailAsset": "https://dvsbke0idzv4u.cloudfront.net/thumbnails/satellite-thumbnail.png", + "baseLayers": [ + { + "type": "arcgisTiled", + "serviceKey": "lightGrayBase" + }, + { + "type": "arcgisMapImage", + "serviceKey": "nearmap" + } + ] + } + }, + + "layers": { + "tritonTransit": { + "label": "Transit", + "thumbnailAsset": "https://dvsbke0idzv4u.cloudfront.net/thumbnails/shuttles-thumbnail.png", + "source": "portalItem", + "portalKey": "ago", + "itemId": "ea5cdd8987414942b62178726120d5c7", + "refreshInterval": 0, + "popup": false + }, + "campusDistricts": { + "label": "Districts", + "thumbnailAsset": "https://dvsbke0idzv4u.cloudfront.net/thumbnails/districts-thumbnail.png", + "source": "url", + "url": "https://admin-enterprise-gis.ucsd.edu/server/rest/services/AdministrationServices/Areas_and_Boundaries/MapServer", + "refreshInterval": 0, + "popup": false + }, + "construction": { + "label": "Construction", + "thumbnailAsset": "https://dvsbke0idzv4u.cloudfront.net/thumbnails/construction-thumbnail.png", + "source": "url", + "url": "https://admin-enterprise-gis.ucsd.edu/server/rest/services/Construction/Construction_Alert_Approved/MapServer", + "refreshInterval": 0, + "popup": false + } + }, + + "scenes": { + "default": { + "label": "Default", + "type": "2d" + }, + "building3d": { + "label": "3D Building", + "type": "scene", + "portalKey": "ago", + "itemId": "b6931d8c429c4138afd9dd936ee7bcd1" + }, + "droneView": { + "label": "Drone View", + "type": "scene", + "portalKey": "age", + "itemId": "0ffe293479844ce49ff5c30ffc0a0b67" + } + }, + + "features": { + "aiSearch": true, + "scenes": true + }, + + "searchCategories": [ + { + "label": "Dining", + "poiClass": "Dining and Beverage", + "icon": "restaurant", + "color": "#00C6D7" + }, + { + "label": "Library", + "poiClass": "Library", + "icon": "menu_book", + "color": "#D462AD" + }, + { + "label": "Parking", + "poiClass": "Parking", + "icon": "local_parking", + "color": "#6E963B" + }, + { + "label": "Recreation", + "poiClass": "Recreation Facilities", + "icon": "fitness_center", + "color": "#FC8900" + } + ] +} \ No newline at end of file diff --git a/lib/ui/esrimap/temp_assets/construction-thumbnail.png b/lib/ui/esrimap/temp_assets/construction-thumbnail.png new file mode 100644 index 000000000..61830070a Binary files /dev/null and b/lib/ui/esrimap/temp_assets/construction-thumbnail.png differ diff --git a/lib/ui/esrimap/temp_assets/dark-thumbnail.png b/lib/ui/esrimap/temp_assets/dark-thumbnail.png new file mode 100644 index 000000000..a075c0646 Binary files /dev/null and b/lib/ui/esrimap/temp_assets/dark-thumbnail.png differ diff --git a/lib/ui/esrimap/temp_assets/default-thumbnail.png b/lib/ui/esrimap/temp_assets/default-thumbnail.png new file mode 100644 index 000000000..d0f788b91 Binary files /dev/null and b/lib/ui/esrimap/temp_assets/default-thumbnail.png differ diff --git a/lib/ui/esrimap/temp_assets/districts-thumbnail.png b/lib/ui/esrimap/temp_assets/districts-thumbnail.png new file mode 100644 index 000000000..148916efa Binary files /dev/null and b/lib/ui/esrimap/temp_assets/districts-thumbnail.png differ diff --git a/lib/ui/esrimap/temp_assets/light-thumbnail.png b/lib/ui/esrimap/temp_assets/light-thumbnail.png new file mode 100644 index 000000000..36b5e8643 Binary files /dev/null and b/lib/ui/esrimap/temp_assets/light-thumbnail.png differ diff --git a/lib/ui/esrimap/temp_assets/satellite-thumbnail.png b/lib/ui/esrimap/temp_assets/satellite-thumbnail.png new file mode 100644 index 000000000..d66c7b683 Binary files /dev/null and b/lib/ui/esrimap/temp_assets/satellite-thumbnail.png differ diff --git a/lib/ui/esrimap/temp_assets/shuttles-thumbnail.png b/lib/ui/esrimap/temp_assets/shuttles-thumbnail.png new file mode 100644 index 000000000..977d2bb51 Binary files /dev/null and b/lib/ui/esrimap/temp_assets/shuttles-thumbnail.png differ diff --git a/lib/ui/esrimap/temp_assets/tgpt-dark.svg b/lib/ui/esrimap/temp_assets/tgpt-dark.svg new file mode 100644 index 000000000..59a576767 --- /dev/null +++ b/lib/ui/esrimap/temp_assets/tgpt-dark.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/lib/ui/esrimap/temp_assets/tgpt-light.svg b/lib/ui/esrimap/temp_assets/tgpt-light.svg new file mode 100644 index 000000000..10d9304a8 --- /dev/null +++ b/lib/ui/esrimap/temp_assets/tgpt-light.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/lib/ui/navigator/bottom.dart b/lib/ui/navigator/bottom.dart index b176481e4..c0229ac4e 100644 --- a/lib/ui/navigator/bottom.dart +++ b/lib/ui/navigator/bottom.dart @@ -4,7 +4,8 @@ import 'package:campus_mobile_experimental/core/providers/bottom_nav.dart'; import 'package:campus_mobile_experimental/core/wrappers/push_notifications.dart'; import 'package:campus_mobile_experimental/ui/ai_assistant/ai_assistant.dart'; import 'package:campus_mobile_experimental/ui/home/home.dart'; -import 'package:campus_mobile_experimental/ui/map/map.dart' as prefix0; +// import 'package:campus_mobile_experimental/ui/map/map.dart' as prefix0; +import 'package:campus_mobile_experimental/ui/esrimap/esrimap.dart'; import 'package:campus_mobile_experimental/ui/navigator/top.dart'; import 'package:campus_mobile_experimental/ui/notifications/notifications_list_view.dart'; import 'package:campus_mobile_experimental/ui/profile/profile.dart'; @@ -32,7 +33,7 @@ class _BottomTabBarState extends State { var currentTab = [ Home(), - prefix0.Maps(), + EsriMap(), AIAssistantTab(), NotificationsListView(), Profile(), @@ -48,14 +49,15 @@ class _BottomTabBarState extends State { drawerScrimColor: Colors.transparent, backgroundColor: provider.currentIndex == 0 ? lightPrimaryColor : theme.scaffoldBackgroundColor, appBar: isAssistantTab - ? null - : PreferredSize(preferredSize: Size.fromHeight(57), child: Provider.of(context).appBar), + ? null + : PreferredSize(preferredSize: Size.fromHeight(50), child: Provider.of(context).appBar), body: PushNotificationWrapper( child: IndexedStack( index: provider.currentIndex, children: currentTab, ), ), + bottomNavigationBar: Container( decoration: BoxDecoration( color: theme.bottomNavigationBarTheme.backgroundColor, diff --git a/pubspec.lock b/pubspec.lock index d536f8129..5276b22dc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: _flutterfire_internals - sha256: e4a1b612fd2955908e26116075b3a4baf10c353418ca645b4deae231c82bf144 + sha256: bda3b7b55958bfd867addc40d067b4b11f7b8846d57671f5b5a6e7f9a56fe3ad url: "https://pub.dev" source: hosted - version: "1.3.65" + version: "1.3.69" analyzer: dependency: transitive description: @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.5.1" + arcgis_maps: + dependency: "direct main" + description: + name: arcgis_maps + sha256: "6eda8e19a5580b40c58c39134edfa6e90b649d97298460b31f006457f6fbeaf5" + url: "https://pub.dev" + source: hosted + version: "300.0.0+4935" args: dependency: transitive description: @@ -53,10 +61,10 @@ packages: dependency: transitive description: name: async - sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.13.1" barcode: dependency: transitive description: @@ -101,10 +109,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "409002f1adeea601018715d613115cfaf0e31f512cb80ae4534c79867ae2363d" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.1.1" build_resolvers: dependency: transitive description: @@ -141,18 +149,18 @@ packages: dependency: transitive description: name: built_value - sha256: a30f0a0e38671e89a492c44d005b5545b830a961575bbd8336d42869ff71066d + sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" url: "https://pub.dev" source: hosted - version: "8.12.0" + version: "8.12.5" characters: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -169,14 +177,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: name: code_builder - sha256: "11654819532ba94c34de52ff5feb52bd81cba1de00ef2ed622fd50295f9d4243" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.11.1" collection: dependency: transitive description: @@ -197,10 +213,10 @@ packages: dependency: transitive description: name: connectivity_plus_platform_interface - sha256: "42657c1715d48b167930d5f34d00222ac100475f73d10162ddf43e714932f204" + sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.0" convert: dependency: transitive description: @@ -213,10 +229,10 @@ packages: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" csslib: dependency: transitive description: @@ -237,18 +253,26 @@ packages: dependency: transitive description: name: dbus - sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.12" + desktop_webview_window: + dependency: transitive + description: + name: desktop_webview_window + sha256: "57cf20d81689d5cbb1adfd0017e96b669398a669d927906073b0e42fc64111c0" + url: "https://pub.dev" + source: hosted + version: "0.2.3" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: "98f28b42168cc509abc92f88518882fd58061ea372d7999aecc424345c7bff6a" + sha256: b4fed1b2835da9d670d7bed7db79ae2a94b0f5ad6312268158a9b5479abbacdd url: "https://pub.dev" source: hosted - version: "11.5.0" + version: "12.4.0" device_info_plus_platform_interface: dependency: transitive description: @@ -261,18 +285,26 @@ packages: dependency: "direct main" description: name: dio - sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_cache_interceptor: + dependency: transitive + description: + name: dio_cache_interceptor + sha256: "3644ce3e0c9ba21885cbb0578b3d2ffe022c8badc6f6f4042cb56ecdbe90a7ad" url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "4.0.6" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" dots_indicator: dependency: "direct main" description: @@ -281,6 +313,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.1" + drift: + dependency: transitive + description: + name: drift + sha256: "055c249d1f91be5a47fe447f88afc24c4ca6f4cd6c5ed66767b4797d48acc2e5" + url: "https://pub.dev" + source: hosted + version: "2.32.1" encrypt: dependency: "direct main" description: @@ -301,10 +341,10 @@ packages: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" file: dependency: transitive description: @@ -317,90 +357,90 @@ packages: dependency: "direct main" description: name: firebase_analytics - sha256: "8ca4832c7a6d145ce987fd07d6dfbb8c91d9058178342f20de6305fb77b1b40d" + sha256: "6993e54441e96b4de1dd85a159b236bbcd2c2487215c9d02b12c1685ddfded73" url: "https://pub.dev" source: hosted - version: "12.1.0" + version: "12.3.0" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: d00234716f415f89eb5c2cefb1238d7fd2f3120275d71414b84ae434dcdb7a19 + sha256: ac2a17484daf380b2247b9874e675ff9e4ac611d839a10b91b7445f764756760 url: "https://pub.dev" source: hosted - version: "5.0.5" + version: "5.1.1" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: e42b294e51aedb4bd4b761a886c8d6b473c44b44aa4c0b47cab06b2c66ac3fba + sha256: "167a3115e71501ba6706235f00e370072920ee8d26b44fbfb9dc8e63c98a8d9b" url: "https://pub.dev" source: hosted - version: "0.6.1+1" + version: "0.6.1+5" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: "29cfa93c771d8105484acac340b5ea0835be371672c91405a300303986f4eba9" + sha256: d5a94b884dcb1e6d3430298e94bfe002238094cdfd5e29202d536ee2120f9158 url: "https://pub.dev" source: hosted - version: "4.3.0" + version: "4.7.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: cccb4f572325dc14904c02fcc7db6323ad62ba02536833dddb5c02cac7341c64 + sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.0.3" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: a631bbfbfa26963d68046aed949df80b228964020e9155b086eff94f462bbf1f + sha256: dc5096257cd67292d34d78ceeb90836f02a4be921b5f3934311a02bb2376118c url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.6.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "8d52022ee6fdd224e92c042f297d1fd0ec277195c49f39fa61b8cc500a639f00" + sha256: "43a311b280d9391389a690d10e1ac0d458b965154a57de5be2f0857225aa2016" url: "https://pub.dev" source: hosted - version: "5.0.6" + version: "5.2.0" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "97c6a97b35e3d3dafe38fb053a65086a1efb125022d292161405848527cc25a4" + sha256: "1b6a921ad6f0d08203ecc1310437a88cec357bc3cad27e1138f1e2c16dd71db9" url: "https://pub.dev" source: hosted - version: "3.8.16" + version: "3.8.20" firebase_messaging: dependency: "direct main" description: name: firebase_messaging - sha256: "1ad663fbb6758acec09d7e84a2e6478265f0a517f40ef77c573efd5e0089f400" + sha256: e5c93e8e7a9b0513f94bb684d2cf100e32e7dcdf2949574386b1955fc9a9b96a url: "https://pub.dev" source: hosted - version: "16.1.0" + version: "16.2.0" firebase_messaging_platform_interface: dependency: transitive description: name: firebase_messaging_platform_interface - sha256: ea620e841fbcec62a96984295fc628f53ef5a8da4f53238159719ed0af7db834 + sha256: "8cbb7d842e5071bba836452aff262f7db4b14bb3a0d00c1896cf176df886d65a" url: "https://pub.dev" source: hosted - version: "4.7.5" + version: "4.7.9" firebase_messaging_web: dependency: transitive description: name: firebase_messaging_web - sha256: "7d0fb6256202515bba8489a3d69c6bc9d52d69a4999bad789053b486c8e7323e" + sha256: "8750bacf50573c0383535fc3f9c58c6a2f9dff5320a16a82c30631b9dad894f1" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.5" fixnum: dependency: transitive description: @@ -475,58 +515,58 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476 + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" url: "https://pub.dev" source: hosted - version: "2.0.31" + version: "2.0.34" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + sha256: da922f2aab2d733db7e011a6bcc4a825b844892d4edd6df83ff156b09a9b2e40 url: "https://pub.dev" source: hosted - version: "9.2.4" - flutter_secure_storage_linux: + version: "10.0.0" + flutter_secure_storage_darwin: dependency: transitive description: - name: flutter_secure_storage_linux - sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + name: flutter_secure_storage_darwin + sha256: "8878c25136a79def1668c75985e8e193d9d7d095453ec28730da0315dc69aee3" url: "https://pub.dev" source: hosted - version: "1.2.3" - flutter_secure_storage_macos: + version: "0.2.0" + flutter_secure_storage_linux: dependency: transitive description: - name: flutter_secure_storage_macos - sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + name: flutter_secure_storage_linux + sha256: "2b5c76dce569ab752d55a1cee6a2242bcc11fdba927078fb88c503f150767cda" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.0.0" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + sha256: "8ceea1223bee3c6ac1a22dabd8feefc550e4729b3675de4b5900f55afcb435d6" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.0.1" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + sha256: "6a1137df62b84b54261dca582c1c09ea72f4f9a4b2fcee21b025964132d5d0c3" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "2.1.0" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + sha256: "3b7c8e068875dfd46719ff57c90d8c459c87f2302ed6b00ff006b3c9fcad1613" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "4.1.0" flutter_sticky_header: dependency: "direct main" description: @@ -539,15 +579,31 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "87fbd7c534435b6c5d9d98b01e1fd527812b82e68ddd8bd35fc45ed0fa8f0a95" + sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.2.4" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_auth_2: + dependency: transitive + description: + name: flutter_web_auth_2 + sha256: d354998934ddc338e69b999b2abaeb33c6fd09999d3a5f92ead1a6b49b49712e + url: "https://pub.dev" + source: hosted + version: "5.0.2" + flutter_web_auth_2_platform_interface: + dependency: transitive + description: + name: flutter_web_auth_2_platform_interface + sha256: ba0fbba55bffb47242025f96852ad1ffba34bc451568f56ef36e613612baffab + url: "https://pub.dev" + source: hosted + version: "5.0.0" flutter_web_plugins: dependency: transitive description: flutter @@ -561,22 +617,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" geolocator: dependency: "direct main" description: name: geolocator - sha256: f62bcd90459e63210bbf9c35deb6a51c521f992a78de19a1fe5c11704f9530e2 + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" url: "https://pub.dev" source: hosted - version: "13.0.4" + version: "14.0.2" geolocator_android: dependency: transitive description: name: geolocator_android - sha256: fcb1760a50d7500deca37c9a666785c047139b5f9ee15aa5469fae7dbbe3170d + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" url: "https://pub.dev" source: hosted - version: "4.6.2" + version: "5.0.2" geolocator_apple: dependency: transitive description: @@ -585,6 +649,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: d64112a205931926f4363bb6bd48f14cb38e7326833041d170615586cd143797 + url: "https://pub.dev" + source: hosted + version: "0.2.4" geolocator_platform_interface: dependency: transitive description: @@ -637,18 +709,18 @@ packages: dependency: "direct main" description: name: google_maps_flutter - sha256: "819985697596a42e1054b5feb2f407ba1ac92262e02844a40168e742b9f36dca" + sha256: fc714bf8072e2c121d4277cb6dca23bbfae954b6c7b5d6dd73f1bc8d09762921 url: "https://pub.dev" source: hosted - version: "2.14.0" + version: "2.17.0" google_maps_flutter_android: dependency: transitive description: name: google_maps_flutter_android - sha256: "7c7ff5b883b27bfdd0d52d91d89faf00858a6c1b33aeca0dc80faca64f389983" + sha256: f1eb5ffa34ba41f8591e53ce439f78af179a506e8386a1297d0ecd202e05c734 url: "https://pub.dev" source: hosted - version: "2.18.3" + version: "2.19.8" google_maps_flutter_ios: dependency: "direct overridden" description: @@ -661,18 +733,18 @@ packages: dependency: transitive description: name: google_maps_flutter_platform_interface - sha256: f4b9b44f7b12a1f6707ffc79d082738e0b7e194bf728ee61d2b3cdf5fdf16081 + sha256: ddbe34435dfb34e83fca295c6a8dcc53c3b51487e9eec3c737ce4ae605574347 url: "https://pub.dev" source: hosted - version: "2.14.0" + version: "2.15.0" google_maps_flutter_web: dependency: transitive description: name: google_maps_flutter_web - sha256: "53e5dbf73ff04153acc55a038248706967c21d5b6ef6657a57fce2be73c2895a" + sha256: "6cefe4ef4cc61dc0dfba4c413dec4bd105cb6b9461bfbe1465ddd09f80af377d" url: "https://pub.dev" source: hosted - version: "0.5.14+2" + version: "0.6.2" graphs: dependency: transitive description: @@ -681,6 +753,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" gtk: dependency: transitive description: @@ -689,6 +769,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + haptic_feedback: + dependency: transitive + description: + name: haptic_feedback + sha256: dcc2494994c41428823f8f2082fd17a4e89e372bd142e07681420cbfaf99dcad + url: "https://pub.dev" + source: hosted + version: "0.6.4+3" hive: dependency: "direct main" description: @@ -713,6 +801,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.3" + hooks: + dependency: transitive + description: + name: hooks + sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + url: "https://pub.dev" + source: hosted + version: "1.0.3" html: dependency: transitive description: @@ -725,10 +821,26 @@ packages: dependency: transitive description: name: http - sha256: bb2ce4590bc2667c96f318d68cac1b5a7987ec819351d32b1c987239a815e007 + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.6.0" + http_cache_core: + dependency: transitive + description: + name: http_cache_core + sha256: ff0b6e6c3766d774d59b806f928b39e6a48f7b2c47ae1fe27410bfd792bee511 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + http_cache_drift_store: + dependency: transitive + description: + name: http_cache_drift_store + sha256: "369497975bb21b62410da601907e94bdf748bf9d0f189e14449ebf0592748ddb" + url: "https://pub.dev" + source: hosted + version: "7.0.0" http_multi_server: dependency: transitive description: @@ -761,46 +873,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" js: dependency: transitive description: name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" url: "https://pub.dev" source: hosted - version: "0.6.7" + version: "0.7.2" json_annotation: dependency: transitive description: name: json_annotation - sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "4.9.0" + version: "4.11.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" linkify: dependency: transitive description: @@ -817,6 +945,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + logger: + dependency: transitive + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.dev" + source: hosted + version: "2.7.0" logging: dependency: transitive description: @@ -829,26 +965,26 @@ packages: dependency: transitive description: name: markdown - sha256: "935e23e1ff3bc02d390bad4d4be001208ee92cc217cb5b5a6c19bc14aaa318c1" + sha256: ee85086ad7698b42522c6ad42fe195f1b9898e4d974a1af4576c1a3a176cada9 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.1" matcher: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" measured_size: dependency: "direct main" description: @@ -861,10 +997,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -873,6 +1009,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mutex: + dependency: transitive + description: + name: mutex + sha256: "8827da25de792088eb33e572115a5eb0d61d61a3c01acbc8bcbe76ed78f1a1f2" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" + url: "https://pub.dev" + source: hosted + version: "0.17.6" nested: dependency: transitive description: @@ -889,6 +1041,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" package_config: dependency: transitive description: @@ -901,10 +1061,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" url: "https://pub.dev" source: hosted - version: "8.3.1" + version: "9.0.1" package_info_plus_platform_interface: dependency: transitive description: @@ -941,18 +1101,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37" + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" url: "https://pub.dev" source: hosted - version: "2.2.19" + version: "2.3.1" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -1037,10 +1197,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" platform: dependency: transitive description: @@ -1061,10 +1221,10 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "4.0.0" pool: dependency: transitive description: @@ -1105,6 +1265,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" sanitize_html: dependency: transitive description: @@ -1117,26 +1285,26 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.5" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.23" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.6" shared_preferences_linux: dependency: transitive description: @@ -1149,10 +1317,10 @@ packages: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" shared_preferences_web: dependency: transitive description: @@ -1210,10 +1378,18 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "56da3e13ed7d28a66f930aa2b2b29db6736a233f08283326e96321dd812030f5" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "3.3.1" stack_trace: dependency: transitive description: @@ -1258,10 +1434,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.9" timezone: dependency: transitive description: @@ -1298,34 +1474,34 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e" + sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" url: "https://pub.dev" source: hosted - version: "6.3.20" + version: "6.3.29" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0" url: "https://pub.dev" source: hosted - version: "6.3.4" + version: "6.4.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + sha256: d5e14138b3bc193a0f63c10a53c94b91d399df0512b1f29b94a043db7482384a url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18" url: "https://pub.dev" source: hosted - version: "3.2.3" + version: "3.2.5" url_launcher_platform_interface: dependency: transitive description: @@ -1338,26 +1514,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + sha256: "712c70ab1b99744ff066053cbe3e80c73332b38d46e5e945c98689b2e66fc15f" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" uuid: dependency: transitive description: name: uuid - sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.2" + version: "4.5.3" value_layout_builder: dependency: "direct overridden" description: @@ -1370,10 +1546,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 + sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373" url: "https://pub.dev" source: hosted - version: "1.1.19" + version: "1.1.21" vector_graphics_codec: dependency: transitive description: @@ -1386,18 +1562,18 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" url: "https://pub.dev" source: hosted - version: "1.1.19" + version: "1.2.0" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" visibility_detector: dependency: "direct main" description: @@ -1410,18 +1586,18 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.1.0" watcher: dependency: transitive description: name: watcher - sha256: "592ab6e2892f67760543fb712ff0177f4ec76c031f02f5b4ff8d3fc5eb9fb61a" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.1" web: dependency: transitive description: @@ -1450,34 +1626,34 @@ packages: dependency: "direct main" description: name: webview_flutter - sha256: c3e4fe614b1c814950ad07186007eff2f2e5dd2935eba7b9a9a1af8e5885f1ba + sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 url: "https://pub.dev" source: hosted - version: "4.13.0" + version: "4.13.1" webview_flutter_android: dependency: transitive description: name: webview_flutter_android - sha256: "9a25f6b4313978ba1c2cda03a242eea17848174912cfb4d2d8ee84a556f248e3" + sha256: f560f57d0f529c1dcdaf4edc3a3217b099560622f9f4a10b6bdbb566553c61ea url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.11.0" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" + sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" url: "https://pub.dev" source: hosted - version: "2.14.0" + version: "2.15.1" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: fb46db8216131a3e55bcf44040ca808423539bc6732e7ed34fb6d8044e3d512f + sha256: a68868ac4828a5f012bf81e8bd25d879f3cec5bd5301575466caafbf9a320a65 url: "https://pub.dev" source: hosted - version: "3.23.0" + version: "3.24.5" wifi_connection: dependency: "direct main" description: @@ -1503,6 +1679,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + window_to_front: + dependency: transitive + description: + name: window_to_front + sha256: "7aef379752b7190c10479e12b5fd7c0b9d92adc96817d9e96c59937929512aee" + url: "https://pub.dev" + source: hosted + version: "0.0.3" xdg_directories: dependency: transitive description: @@ -1528,5 +1712,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.8.0 <4.0.0" - flutter: ">=3.32.0" + dart: ">=3.11.1 <4.0.0" + flutter: ">=3.41.3" diff --git a/pubspec.yaml b/pubspec.yaml index 42dfdde32..789232d86 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: ref: master flutter_linkify: 6.0.0 flutter_local_notifications: ^19.5.0 - flutter_secure_storage: ^9.2.4 + flutter_secure_storage: ^10.0.0 flutter_sticky_header: ^0.8.0 get: 4.7.2 google_maps_flutter: ^2.14.0 @@ -31,13 +31,13 @@ dependencies: intl: ^0.20.2 liquid_progress_indicator_v2: 0.5.0 measured_size: 1.0.0 - geolocator: ^13.0.4 - package_info_plus: ^8.3.1 + geolocator: ^14.0.0 + package_info_plus: ^9.0.0 flutter_markdown: ^0.6.18 flutter_svg: ^2.2.0 percent_indicator: ^4.2.5 permission_handler: ^12.0.1 - pointycastle: ^3.9.0 + pointycastle: ^4.0.0 provider: ^6.1.5+1 shared_preferences: ^2.5.3 app_links: ^3.0.0 @@ -48,8 +48,10 @@ dependencies: git: url: https://github.com/UCSD/wifi_connection.git ref: e0fb416f81daedeabb1eedef22041e67f30f792b - device_info_plus: ^11.5.0 + device_info_plus: ^12.1.0 + arcgis_maps: ^300.0.0 dependency_overrides: + pointycastle: ^4.0.0 google_maps_flutter_ios: 2.15.7 value_layout_builder: ^0.5.0 # Fix Flutter 3.32.0 compatibility dev_dependencies: @@ -63,6 +65,7 @@ flutter: - assets/images/ - assets/images/onboarding/ - assets/images/tgpt/ + - lib/ui/esrimap/temp_assets/ - .env fonts: - family: Refrigerator Deluxe