From 8f584e5439ee2db72639e8d2cc16fdb69f3c915d Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 25 Sep 2024 19:20:20 +0900 Subject: [PATCH 01/31] Support arbitrary Valhalla options (formerly we only merged costing options) --- Package.swift | 2 +- android/build.gradle | 2 +- .../ferrostar/core/ValhallaCoreTest.kt | 2 +- .../ferrostar/core/FerrostarCore.kt | 4 +- .../com/stadiamaps/ferrostar/AppModule.kt | 2 +- .../Sources/FerrostarCore/FerrostarCore.swift | 8 +- apple/Sources/UniFFI/ferrostar.swift | 12 +-- .../FerrostarCoreTests.swift | 2 +- common/Cargo.lock | 2 +- common/ferrostar/Cargo.toml | 2 +- common/ferrostar/src/lib.rs | 14 ++- common/ferrostar/src/routing_adapters/mod.rs | 6 +- .../src/routing_adapters/valhalla.rs | 89 +++++++++++++++---- web/package-lock.json | 4 +- web/package.json | 2 +- 15 files changed, 105 insertions(+), 48 deletions(-) diff --git a/Package.swift b/Package.swift index 9610ba34d..7255e0709 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,7 @@ if useLocalFramework { path: "./common/target/ios/libferrostar-rs.xcframework" ) } else { - let releaseTag = "0.14.0" + let releaseTag = "0.15.0" let releaseChecksum = "f8503dc1657b99c83489bac78dd2f4c014ae3d20e9d83f3e779260d255b6cbbb" binaryTarget = .binaryTarget( name: "FerrostarCoreRS", diff --git a/android/build.gradle b/android/build.gradle index be704e9bb..609801f92 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -12,5 +12,5 @@ plugins { allprojects { group = "com.stadiamaps.ferrostar" - version = "0.14.0" + version = "0.15.0" } diff --git a/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt b/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt index 201e5a4f8..3a68c8bf8 100644 --- a/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt +++ b/android/core/src/androidTest/java/com/stadiamaps/ferrostar/core/ValhallaCoreTest.kt @@ -303,7 +303,7 @@ class ValhallaCoreTest { navigationControllerConfig = NavigationControllerConfig( StepAdvanceMode.Manual, RouteDeviationTracking.None, CourseFiltering.RAW), - costingOptions = mapOf("auto" to mapOf("useTolls" to 0))) + options = mapOf("costing_options" to mapOf("auto" to mapOf("useTolls" to 0)))) return runTest { val routes = diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt index 59f4d1875..53571f4b9 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/FerrostarCore.kt @@ -136,11 +136,11 @@ class FerrostarCore( locationProvider: LocationProvider, navigationControllerConfig: NavigationControllerConfig, foregroundServiceManager: ForegroundServiceManager? = null, - costingOptions: Map = emptyMap(), + options: Map = emptyMap(), ) : this( RouteProvider.RouteAdapter( RouteAdapter.newValhallaHttp( - valhallaEndpointURL.toString(), profile, jsonAdapter.toJson(costingOptions))), + valhallaEndpointURL.toString(), profile, jsonAdapter.toJson(options))), httpClient, locationProvider, foregroundServiceManager, diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt index 39d511945..ae59b6a2a 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt @@ -61,7 +61,7 @@ object AppModule { minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U), RouteDeviationTracking.StaticThreshold(25U, 10.0), CourseFiltering.SNAP_TO_ROUTE), - costingOptions = mapOf("bicycle" to mapOf("use_roads" to 0.2))) + options = mapOf("units" to "miles")) // Not all navigation apps will require this sort of extra configuration. // In fact, we hope that most don't! diff --git a/apple/Sources/FerrostarCore/FerrostarCore.swift b/apple/Sources/FerrostarCore/FerrostarCore.swift index 4ccd9f697..aef0f5c9f 100644 --- a/apple/Sources/FerrostarCore/FerrostarCore.swift +++ b/apple/Sources/FerrostarCore/FerrostarCore.swift @@ -115,11 +115,11 @@ public protocol FerrostarCoreDelegate: AnyObject { profile: String, locationProvider: LocationProviding, navigationControllerConfig: SwiftNavigationControllerConfig, - costingOptions: [String: Any] = [:], + options: [String: Any] = [:], networkSession: URLRequestLoading = URLSession.shared ) throws { - guard let jsonCostingOptions = try String( - data: JSONSerialization.data(withJSONObject: costingOptions), + guard let jsonOptions = try String( + data: JSONSerialization.data(withJSONObject: options), encoding: .utf8 ) else { throw InstantiationError.JsonError @@ -128,7 +128,7 @@ public protocol FerrostarCoreDelegate: AnyObject { let adapter = try RouteAdapter.newValhallaHttp( endpointUrl: valhallaEndpointUrl.absoluteString, profile: profile, - costingOptionsJson: jsonCostingOptions + optionsJson: jsonOptions ) self.init( routeProvider: .routeAdapter(adapter), diff --git a/apple/Sources/UniFFI/ferrostar.swift b/apple/Sources/UniFFI/ferrostar.swift index 1ce53f75d..b0b7c21b7 100644 --- a/apple/Sources/UniFFI/ferrostar.swift +++ b/apple/Sources/UniFFI/ferrostar.swift @@ -809,13 +809,13 @@ open class RouteAdapter: } public static func newValhallaHttp(endpointUrl: String, profile: String, - costingOptionsJson: String?) throws -> RouteAdapter + optionsJson: String?) throws -> RouteAdapter { try FfiConverterTypeRouteAdapter.lift(rustCallWithError(FfiConverterTypeInstantiationError.lift) { uniffi_ferrostar_fn_constructor_routeadapter_new_valhalla_http( FfiConverterString.lower(endpointUrl), FfiConverterString.lower(profile), - FfiConverterOptionString.lower(costingOptionsJson), $0 + FfiConverterOptionString.lower(optionsJson), $0 ) }) } @@ -4075,13 +4075,13 @@ public func createRouteFromOsrm(routeData: Data, waypointData: Data, polylinePre * This is provided as a convenience for use from foreign code when creating your own [`routing_adapters::RouteAdapter`]. */ public func createValhallaRequestGenerator(endpointUrl: String, profile: String, - costingOptionsJson: String?) throws -> RouteRequestGenerator + optionsJson: String?) throws -> RouteRequestGenerator { try FfiConverterTypeRouteRequestGenerator.lift(rustCallWithError(FfiConverterTypeInstantiationError.lift) { uniffi_ferrostar_fn_func_create_valhalla_request_generator( FfiConverterString.lower(endpointUrl), FfiConverterString.lower(profile), - FfiConverterOptionString.lower(costingOptionsJson), $0 + FfiConverterOptionString.lower(optionsJson), $0 ) }) } @@ -4172,7 +4172,7 @@ private var initializationResult: InitializationResult = { if uniffi_ferrostar_checksum_func_create_route_from_osrm() != 42270 { return InitializationResult.apiChecksumMismatch } - if uniffi_ferrostar_checksum_func_create_valhalla_request_generator() != 62919 { + if uniffi_ferrostar_checksum_func_create_valhalla_request_generator() != 16275 { return InitializationResult.apiChecksumMismatch } if uniffi_ferrostar_checksum_func_get_route_polyline() != 31480 { @@ -4217,7 +4217,7 @@ private var initializationResult: InitializationResult = { if uniffi_ferrostar_checksum_constructor_routeadapter_new() != 32290 { return InitializationResult.apiChecksumMismatch } - if uniffi_ferrostar_checksum_constructor_routeadapter_new_valhalla_http() != 63624 { + if uniffi_ferrostar_checksum_constructor_routeadapter_new_valhalla_http() != 3524 { return InitializationResult.apiChecksumMismatch } diff --git a/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift b/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift index 9e18292e5..cb0a9913e 100644 --- a/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift +++ b/apple/Tests/FerrostarCoreTests/FerrostarCoreTests.swift @@ -237,7 +237,7 @@ final class FerrostarCoreTests: XCTestCase { routeDeviationTracking: .none, snappedLocationCourseFiltering: .raw ), - costingOptions: ["low_speed_vehicle": ["vehicle_type": "golf_cart"]], + options: ["costing_options": ["low_speed_vehicle": ["vehicle_type": "golf_cart"]]], networkSession: mockSession ) diff --git a/common/Cargo.lock b/common/Cargo.lock index c806da7e9..63ec804c8 100644 --- a/common/Cargo.lock +++ b/common/Cargo.lock @@ -337,7 +337,7 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "ferrostar" -version = "0.14.0" +version = "0.15.0" dependencies = [ "assert-json-diff", "geo", diff --git a/common/ferrostar/Cargo.toml b/common/ferrostar/Cargo.toml index 95af02363..1b9ba2099 100644 --- a/common/ferrostar/Cargo.toml +++ b/common/ferrostar/Cargo.toml @@ -2,7 +2,7 @@ lints.workspace = true [package] name = "ferrostar" -version = "0.14.0" +version = "0.15.0" readme = "README.md" description = "The core of modern turn-by-turn navigation." keywords = ["navigation", "routing", "valhalla", "osrm"] diff --git a/common/ferrostar/src/lib.rs b/common/ferrostar/src/lib.rs index fb5e4bcdc..5e3ba139f 100644 --- a/common/ferrostar/src/lib.rs +++ b/common/ferrostar/src/lib.rs @@ -82,15 +82,13 @@ impl UniffiCustomTypeConverter for Uuid { fn create_valhalla_request_generator( endpoint_url: String, profile: String, - costing_options_json: Option, + options_json: Option, ) -> Result, InstantiationError> { - Ok(Arc::new( - ValhallaHttpRequestGenerator::with_costing_options_json( - endpoint_url, - profile, - costing_options_json, - )?, - )) + Ok(Arc::new(ValhallaHttpRequestGenerator::with_options_json( + endpoint_url, + profile, + options_json, + )?)) } /// Creates a [`RouteResponseParser`] capable of parsing OSRM responses. diff --git a/common/ferrostar/src/routing_adapters/mod.rs b/common/ferrostar/src/routing_adapters/mod.rs index ecf2fff92..1c30ea99f 100644 --- a/common/ferrostar/src/routing_adapters/mod.rs +++ b/common/ferrostar/src/routing_adapters/mod.rs @@ -152,12 +152,12 @@ impl RouteAdapter { pub fn new_valhalla_http( endpoint_url: String, profile: String, - costing_options_json: Option, + options_json: Option, ) -> Result { - let request_generator = Arc::new(ValhallaHttpRequestGenerator::with_costing_options_json( + let request_generator = Arc::new(ValhallaHttpRequestGenerator::with_options_json( endpoint_url, profile, - costing_options_json, + options_json, )?); let response_parser = Arc::new(OsrmResponseParser::new(6)); Ok(Self::new(request_generator, response_parser)) diff --git a/common/ferrostar/src/routing_adapters/valhalla.rs b/common/ferrostar/src/routing_adapters/valhalla.rs index b19dbd692..c69247789 100644 --- a/common/ferrostar/src/routing_adapters/valhalla.rs +++ b/common/ferrostar/src/routing_adapters/valhalla.rs @@ -20,6 +20,19 @@ use alloc::{ /// Valhalla supports the [`WaypointKind`] field of [`Waypoint`]s. Variants have the same meaning as their /// [`type` strings in Valhalla API](https://valhalla.github.io/valhalla/api/turn-by-turn/api-reference/#locations) /// having the same name. +/// +/// ``` +/// use serde_json::json; +/// use ferrostar::routing_adapters::valhalla::ValhallaHttpRequestGenerator; +/// let options = json!({ +/// "costing_options": { +/// "low_speed_vehicle": { +/// "vehicle_type": "golf_cart" +/// } +/// } +/// }); +/// let request_generator = ValhallaHttpRequestGenerator::new("https://api.stadiamaps.com/route/v1?api_key=YOUR-API-KEY".to_string(), "low_speed_vehicle".to_string(), Some(options)); +/// ``` #[derive(Debug)] pub struct ValhallaHttpRequestGenerator { /// The full URL of the Valhalla endpoint to access. This will normally be the route endpoint, @@ -30,32 +43,41 @@ pub struct ValhallaHttpRequestGenerator { /// The Valhalla costing model to use. profile: String, // TODO: Language, units, and other top-level parameters - /// JSON costing options to pass through. - costing_options: JsonValue, + /// JSON arbitrary key/values which override the defaults. + /// + /// These may contain nested keys. + options: JsonValue, } impl ValhallaHttpRequestGenerator { - pub fn new(endpoint_url: String, profile: String, costing_options: Option) -> Self { + /// Creates a new Valhalla request generator given an endpoint URL, a profile name, + /// and options (which will update the minimal defaults set internally). + /// + /// NOTE: If options is `None` or a non-object, it will be interpreted as an empty object. + pub fn new(endpoint_url: String, profile: String, options: Option) -> Self { Self { endpoint_url, profile, - costing_options: costing_options.unwrap_or(json!({})), + options: options.unwrap_or(json!({})), } } - pub fn with_costing_options_json( + /// Creates a new Valhalla request generator given an endpoint URL, a profile name, + /// and options (which will update the minimal defaults set internally). + /// NOTE: If options is `None` or a non-object, it will be interpreted as an empty object. + pub fn with_options_json( endpoint_url: String, profile: String, costing_options_json: Option, ) -> Result { - let parsed_costing_options: JsonValue = match costing_options_json.as_deref() { + let parsed_options: JsonValue = match costing_options_json.as_deref() { Some(options) => serde_json::from_str(options)?, None => json!({}), }; Ok(Self { endpoint_url, profile, - costing_options: parsed_costing_options, + options: parsed_options, }) } } @@ -99,7 +121,7 @@ impl RouteRequestGenerator for ValhallaHttpRequestGenerator { // Though it would be nice to use PBF if we can get the required data. // However, certain info (like banners) are only available in the OSRM format. // TODO: Trace attributes as we go rather than pulling a fat payload upfront that we might ditch later? - let args = json!({ + let mut args = json!({ "format": "osrm", "filters": { "action": "include", @@ -114,8 +136,14 @@ impl RouteRequestGenerator for ValhallaHttpRequestGenerator { "voice_instructions": true, "costing": &self.profile, "locations": locations, - "costing_options": &self.costing_options, }); + + if let Some(options) = self.options.as_object() { + for (k, v) in options.iter() { + args[k] = v.clone(); + } + } + let body = serde_json::to_vec(&args)?; Ok(RouteRequest::HttpPost { url: self.endpoint_url.clone(), @@ -183,12 +211,12 @@ mod tests { fn generate_body( user_location: UserLocation, waypoints: Vec, - costing_options_json: Option, + options_json: Option, ) -> JsonValue { - let generator = ValhallaHttpRequestGenerator::with_costing_options_json( + let generator = ValhallaHttpRequestGenerator::with_options_json( ENDPOINT_URL.to_string(), COSTING.to_string(), - costing_options_json, + options_json, ) .expect("Unable to create request generator"); @@ -271,20 +299,50 @@ mod tests { fn request_body_without_costing_options() { let body_json = generate_body(USER_LOCATION, WAYPOINTS.to_vec(), None); + assert!(body_json["costing_options"].is_null()); + } + + #[test] + fn request_body_invalid_costing_options() { + // Valid JSON, but it's not an object. + let body_json = generate_body( + USER_LOCATION, + WAYPOINTS.to_vec(), + Some(r#"["costing_options"]"#.to_string()), + ); + + assert!(body_json["costing_options"].is_null()); + } + + #[test] + fn request_body_with_costing_options() { + let body_json = generate_body( + USER_LOCATION, + WAYPOINTS.to_vec(), + Some(r#"{"costing_options": {"bicycle": {"bicycle_type": "Road"}}}"#.to_string()), + ); + assert_json_include!( actual: body_json, expected: json!({ - "costing_options": {}, + "costing_options": { + "bicycle": { + "bicycle_type": "Road", + }, + }, }) ); } #[test] - fn request_body_with_costing_options() { + fn request_body_with_multiple_options() { let body_json = generate_body( USER_LOCATION, WAYPOINTS.to_vec(), - Some(r#"{"bicycle": {"bicycle_type": "Road"}}"#.to_string()), + Some( + r#"{"units": "mi", "costing_options": {"bicycle": {"bicycle_type": "Road"}}}"# + .to_string(), + ), ); assert_json_include!( @@ -295,6 +353,7 @@ mod tests { "bicycle_type": "Road", }, }, + "units": "mi" }) ); } diff --git a/web/package-lock.json b/web/package-lock.json index 5eef0de35..c5585ffab 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "@stadiamaps/ferrostar-webcomponents", - "version": "0.14.0", + "version": "0.15.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@stadiamaps/ferrostar-webcomponents", - "version": "0.14.0", + "version": "0.15.0", "license": "BSD-3-Clause", "dependencies": { "@stadiamaps/ferrostar": "file:../common/ferrostar/pkg", diff --git a/web/package.json b/web/package.json index d68b59cc9..1f451624c 100644 --- a/web/package.json +++ b/web/package.json @@ -6,7 +6,7 @@ "CatMe0w (https://github.com/CatMe0w)", "Luke Seelenbinder " ], - "version": "0.14.0", + "version": "0.15.0", "license": "BSD-3-Clause", "type": "module", "main": "./dist/ferrostar-webcomponents.js", From 4d7fda9c6649930f88ad63c8d67c8208d8a98c33 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 25 Sep 2024 22:27:55 +0900 Subject: [PATCH 02/31] Fix argument --- apple/DemoApp/Demo/DemoNavigationView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apple/DemoApp/Demo/DemoNavigationView.swift b/apple/DemoApp/Demo/DemoNavigationView.swift index fa0e62e60..4c78e2208 100644 --- a/apple/DemoApp/Demo/DemoNavigationView.swift +++ b/apple/DemoApp/Demo/DemoNavigationView.swift @@ -55,7 +55,7 @@ struct DemoNavigationView: View { profile: "bicycle", locationProvider: locationProvider, navigationControllerConfig: config, - costingOptions: ["bicycle": ["use_roads": 0.2]] + options: ["costing_options": ["bicycle": ["use_roads": 0.2]]] ) // NOTE: Not all applications will need a delegate. Read the NavigationDelegate documentation for details. ferrostarCore.delegate = navigationDelegate From 616b5e4d8ad60e70b7298fde20f952985c6730fe Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 25 Sep 2024 22:28:11 +0900 Subject: [PATCH 03/31] Improve docs of some convenience initializers --- apple/Sources/FerrostarCore/FerrostarCore.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apple/Sources/FerrostarCore/FerrostarCore.swift b/apple/Sources/FerrostarCore/FerrostarCore.swift index aef0f5c9f..4eafff73f 100644 --- a/apple/Sources/FerrostarCore/FerrostarCore.swift +++ b/apple/Sources/FerrostarCore/FerrostarCore.swift @@ -93,6 +93,10 @@ public protocol FerrostarCoreDelegate: AnyObject { private var config: SwiftNavigationControllerConfig + /// Initializes a core instance with the given parameters. + /// + /// This designated initializer is the most flexible, but the convenience ones may be easier to use. + /// for common configuraitons. public init( routeProvider: RouteProvider, locationProvider: LocationProviding, @@ -110,6 +114,14 @@ public protocol FerrostarCoreDelegate: AnyObject { locationProvider.delegate = self } + /// Initializes a core instance for a Valhalla API accessed over HTTP. + /// + /// - Parameters + /// - valhallaEndpointUrl: The URL of the Valhalla endpoint you're trying to hit for route requests. If necessary, include your API key here. + /// - profile: The Valhalla costing model to use for route requests. + /// - navigationControllerConfig: Configuration of the navigation session. + /// - options: A dictionary of options to include in the request. The Valhalla request generator sets several automatically (like `format`), but this lets you add arbitrary options so you can access the full API. + /// - networkSession: The network session to use. Don't set this unless you need to replace the networking stack (ex: for testing). public convenience init( valhallaEndpointUrl: URL, profile: String, From 02dd283851622d315254f9f3486f874273ea00f4 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 25 Sep 2024 22:40:55 +0900 Subject: [PATCH 04/31] Align Android with iOS costing options --- .../src/main/java/com/stadiamaps/ferrostar/AppModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt index ae59b6a2a..a9878e459 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt @@ -61,7 +61,7 @@ object AppModule { minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U), RouteDeviationTracking.StaticThreshold(25U, 10.0), CourseFiltering.SNAP_TO_ROUTE), - options = mapOf("units" to "miles")) + options = mapOf("costingOptions" to mapOf("bicycle" to mapOf("use_roads" to 0.2)))) // Not all navigation apps will require this sort of extra configuration. // In fact, we hope that most don't! From 306a121022515fd853ec15a376422342732801d6 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 25 Sep 2024 22:41:51 +0900 Subject: [PATCH 05/31] swiftformat --- apple/Sources/FerrostarCore/FerrostarCore.swift | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apple/Sources/FerrostarCore/FerrostarCore.swift b/apple/Sources/FerrostarCore/FerrostarCore.swift index 4eafff73f..f90dbaa92 100644 --- a/apple/Sources/FerrostarCore/FerrostarCore.swift +++ b/apple/Sources/FerrostarCore/FerrostarCore.swift @@ -117,11 +117,14 @@ public protocol FerrostarCoreDelegate: AnyObject { /// Initializes a core instance for a Valhalla API accessed over HTTP. /// /// - Parameters - /// - valhallaEndpointUrl: The URL of the Valhalla endpoint you're trying to hit for route requests. If necessary, include your API key here. + /// - valhallaEndpointUrl: The URL of the Valhalla endpoint you're trying to hit for route requests. If necessary, + /// include your API key here. /// - profile: The Valhalla costing model to use for route requests. /// - navigationControllerConfig: Configuration of the navigation session. - /// - options: A dictionary of options to include in the request. The Valhalla request generator sets several automatically (like `format`), but this lets you add arbitrary options so you can access the full API. - /// - networkSession: The network session to use. Don't set this unless you need to replace the networking stack (ex: for testing). + /// - options: A dictionary of options to include in the request. The Valhalla request generator sets several + /// automatically (like `format`), but this lets you add arbitrary options so you can access the full API. + /// - networkSession: The network session to use. Don't set this unless you need to replace the networking stack + /// (ex: for testing). public convenience init( valhallaEndpointUrl: URL, profile: String, From 02bb61b7ced181f4611683acffab93b1418fde7c Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 25 Sep 2024 23:38:29 +0900 Subject: [PATCH 06/31] Don't forget the web ;) --- web/index.html | 2 +- web/src/ferrostar-map.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/index.html b/web/index.html index 02b1434e9..80d2aba8d 100644 --- a/web/index.html +++ b/web/index.html @@ -96,7 +96,7 @@ ferrostar.center = {lng: -122.42, lat: 37.81}; ferrostar.zoom = 18; - ferrostar.costingOptions = { bicycle: { use_roads: 0.2 } }; + ferrostar.options = {costing_options: { bicycle: { use_roads: 0.2 } } }; ferrostar.customStyles = searchBoxStyle; ferrostar.geolocateOnLoad = false; diff --git a/web/src/ferrostar-map.ts b/web/src/ferrostar-map.ts index f8f1af947..63050cdd8 100644 --- a/web/src/ferrostar-map.ts +++ b/web/src/ferrostar-map.ts @@ -45,7 +45,7 @@ export class FerrostarMap extends LitElement { // TODO: type @property({ type: Object, attribute: false }) - costingOptions: object = {}; + options: object = {}; // TODO: type @state() @@ -216,7 +216,7 @@ export class FerrostarMap extends LitElement { async getRoutes(initialLocation: any, waypoints: any) { // Initialize the route adapter // (NOTE: currently only supports Valhalla, but working toward expansion) - this.routeAdapter = new RouteAdapter(this.valhallaEndpointUrl, this.profile, JSON.stringify(this.costingOptions)); + this.routeAdapter = new RouteAdapter(this.valhallaEndpointUrl, this.profile, JSON.stringify(this.options)); // Generate the request body const routeRequest = this.routeAdapter.generateRequest(initialLocation, waypoints); From eb5a9e7a50d93fafb34ee9d11545c285b7f1d742 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 25 Sep 2024 12:49:59 +0900 Subject: [PATCH 07/31] Add Android-specific README --- android/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 android/README.md diff --git a/android/README.md b/android/README.md new file mode 100644 index 000000000..0a9b95e87 --- /dev/null +++ b/android/README.md @@ -0,0 +1,9 @@ +# Ferrostar Android + +This directory tree contains the Gradle workspace for Ferrostar on Android. + +* `composeui` - Jetpack Compose UI elements which are not tightly coupled to any particular map renderer. +* `core` - The core module is where all the "business logic", location management, and other core functionality lives. +* `demo-app` - A minimal demonstration app. +* `google-play-services` - Optional functionality that depends on Google Play Services (like the a fused location client wrapper). This is a separate module so that apps are able to "de-Google" if necessary. +* `maplibreui` - Map-related user interface components built with MapLibre. \ No newline at end of file From 5100febe5f41639f29e279150f32a10c84c42cb2 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 25 Sep 2024 16:04:55 +0900 Subject: [PATCH 08/31] Set up Android to use API keys properly via local.properties --- Package.resolved | 4 +-- android/README.md | 14 +++++++- android/demo-app/build.gradle | 1 + android/demo-app/src/main/AndroidManifest.xml | 6 ++++ .../com/stadiamaps/ferrostar/AppModule.kt | 34 ++++++++++++++----- android/gradle/libs.versions.toml | 2 ++ 6 files changed, 50 insertions(+), 11 deletions(-) diff --git a/Package.resolved b/Package.resolved index 5ea0cc538..f0a248a76 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git", "state" : { - "revision" : "def156895a9ce38ea9bf9632c1e2272280ce0ae3", - "version" : "6.6.0" + "revision" : "e409318144091c3ee9ad551b202e1c36695f8086", + "version" : "6.7.0" } }, { diff --git a/android/README.md b/android/README.md index 0a9b95e87..6c71ca503 100644 --- a/android/README.md +++ b/android/README.md @@ -6,4 +6,16 @@ This directory tree contains the Gradle workspace for Ferrostar on Android. * `core` - The core module is where all the "business logic", location management, and other core functionality lives. * `demo-app` - A minimal demonstration app. * `google-play-services` - Optional functionality that depends on Google Play Services (like the a fused location client wrapper). This is a separate module so that apps are able to "de-Google" if necessary. -* `maplibreui` - Map-related user interface components built with MapLibre. \ No newline at end of file +* `maplibreui` - Map-related user interface components built with MapLibre. + +## Running the demo app + +To run the demo app, you'll need a Stadia Maps API key +(free for development and evaluation use; no credit card required; get one at https://client.stadiamaps.com/). +You can also modify it to work with your preferred maps and routing vendor by editing `AppModule.kt`. + +Set your API key in `local.properties` to run the demo app: + +```properties +stadiaApiKey=YOUR-API-KEY +``` diff --git a/android/demo-app/build.gradle b/android/demo-app/build.gradle index 066a278f3..230da289c 100644 --- a/android/demo-app/build.gradle +++ b/android/demo-app/build.gradle @@ -3,6 +3,7 @@ plugins { alias libs.plugins.jetbrainsKotlinAndroid alias libs.plugins.ktfmt alias libs.plugins.compose.compiler + alias libs.plugins.mapsplatform.secrets.plugin } android { diff --git a/android/demo-app/src/main/AndroidManifest.xml b/android/demo-app/src/main/AndroidManifest.xml index dcdc85d76..9821568bc 100644 --- a/android/demo-app/src/main/AndroidManifest.xml +++ b/android/demo-app/src/main/AndroidManifest.xml @@ -21,6 +21,12 @@ android:theme="@style/Theme.Ferrostar" tools:targetApi="31"> + + + Log.i(TAG, "Received alternate route(s): $routes") if (routes.isNotEmpty()) { - // NB: Use `replaceRoute` for cases like this!! + // NB: Use `replaceRoute` for cases like this! it.replaceRoute( routes.first(), NavigationControllerConfig( diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index b53a26c5d..fa61fc69b 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -22,6 +22,7 @@ junitCompose = "1.7.2" espressoCore = "3.6.1" okhttp-mock = "2.0.0" mavenPublish = "0.29.0" +secretsGradlePlugin = "2.0.1" material = "1.12.0" [libraries] @@ -73,3 +74,4 @@ cargo-ndk = { id = "com.github.willir.rust.cargo-ndk-android", version.ref = "ca ktfmt = { id = "com.ncorti.ktfmt.gradle", version.ref = "ktfmt" } paparazzi = { id = "app.cash.paparazzi", version.ref = "paparazzi" } mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } +mapsplatform-secrets-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "secretsGradlePlugin" } \ No newline at end of file From b97d5fa6ddae0c5955ef6028cd76f83f59ee5bde Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 25 Sep 2024 19:27:05 +0900 Subject: [PATCH 09/31] Fix CI --- .github/workflows/android.yml | 16 ++++++++-------- .github/workflows/gradle-publish.yml | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index d44f31225..aff191f5b 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -36,8 +36,8 @@ jobs: - name: Install cargo-ndk run: cargo install cargo-ndk - - name: Touch local.properties (required for cargo-ndk) - run: touch local.properties + - name: Touch local.properties (required for build) + run: echo 'stadiaApiKey=' > local.properties working-directory: android - name: Build with Gradle @@ -77,7 +77,7 @@ jobs: run: cargo install cargo-ndk - name: Touch local.properties (required for cargo-ndk) - run: touch local.properties + run: echo 'stadiaApiKey=' > local.properties working-directory: android - name: Verify Kotlin formatting @@ -117,7 +117,7 @@ jobs: run: cargo install cargo-ndk - name: Touch local.properties (required for cargo-ndk) - run: touch local.properties + run: echo 'stadiaApiKey=' > local.properties working-directory: android - name: Unit test @@ -166,7 +166,7 @@ jobs: run: cargo install cargo-ndk - name: Touch local.properties (required for cargo-ndk) - run: touch local.properties + run: echo 'stadiaApiKey=' > local.properties working-directory: android - name: Verify snapshots @@ -215,8 +215,8 @@ jobs: - name: Install cargo-ndk run: cargo install cargo-ndk - - name: Touch local.properties (required for cargo-ndk) - run: touch local.properties + - name: Touch local.properties (required for build) + run: echo 'stadiaApiKey=' > local.properties working-directory: android - name: Run Connected Checks @@ -239,4 +239,4 @@ jobs: name: connected-reports path: | android/**/build/reports - retention-days: 5 \ No newline at end of file + retention-days: 5 diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index 28f963f67..dac95e6b1 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -32,8 +32,8 @@ jobs: - name: Install cargo-ndk run: cargo install cargo-ndk - - name: Touch local.properties (required for cargo-ndk) - run: touch local.properties + - name: Creat local.properties (required for cargo-ndk and the demo app) + run: echo 'stadiaApiKey=' > local.properties working-directory: android - name: Publish to Maven Central From 8ff64b40012fa99e3220874288c1f331bb2f5668 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Wed, 25 Sep 2024 23:37:00 +0900 Subject: [PATCH 10/31] Fix subtle bug where Android location providers never updated lastLocation --- .../com/stadiamaps/ferrostar/core/Location.kt | 10 +++++++++- .../FusedLocationProvider.kt | 18 +++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt index 2f98cf342..3d0495665 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt @@ -78,13 +78,21 @@ class AndroidSystemLocationProvider(context: Context) : LocationProvider { android.util.Log.d(TAG, "Already registered; skipping") return } - val androidListener = LocationListener { listener.onLocationUpdated(it.toUserLocation()) } + val androidListener = LocationListener { + val userLocation = it.toUserLocation() + lastLocation = userLocation + listener.onLocationUpdated(userLocation) + } listeners[listener] = androidListener val handler = Handler(Looper.getMainLooper()) executor.execute { handler.post { + val last = locationManager.getLastKnownLocation(getBestProvider())?.toUserLocation() + if (last != null) { + androidListener.onLocationChanged(last.toAndroidLocation()) + } locationManager.requestLocationUpdates(getBestProvider(), 100L, 5.0f, androidListener) } } diff --git a/android/google-play-services/src/main/java/com/stadiamaps/ferrostar/googleplayservices/FusedLocationProvider.kt b/android/google-play-services/src/main/java/com/stadiamaps/ferrostar/googleplayservices/FusedLocationProvider.kt index 566f9212b..15ad509cb 100644 --- a/android/google-play-services/src/main/java/com/stadiamaps/ferrostar/googleplayservices/FusedLocationProvider.kt +++ b/android/google-play-services/src/main/java/com/stadiamaps/ferrostar/googleplayservices/FusedLocationProvider.kt @@ -2,7 +2,6 @@ package com.stadiamaps.ferrostar.googleplayservices import android.annotation.SuppressLint import android.content.Context -import android.os.Looper import android.util.Log import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationListener @@ -19,7 +18,8 @@ import uniffi.ferrostar.UserLocation class FusedLocationProvider( context: Context, private val fusedLocationProviderClient: FusedLocationProviderClient = - LocationServices.getFusedLocationProviderClient(context) + LocationServices.getFusedLocationProviderClient(context), + private val priority: Int = Priority.PRIORITY_HIGH_ACCURACY ) : LocationProvider { companion object { @@ -42,16 +42,20 @@ class FusedLocationProvider( return } - val locationListener = LocationListener { newLocation -> - listener.onLocationUpdated(newLocation.toUserLocation()) + val locationListener = LocationListener { + val userLocation = it.toUserLocation() + lastLocation = userLocation + listener.onLocationUpdated(userLocation) } listeners[listener] = locationListener val locationRequest = - LocationRequest.Builder(1000L).setPriority(Priority.PRIORITY_HIGH_ACCURACY).build() + LocationRequest.Builder(priority, 1000L) + .setMinUpdateDistanceMeters(5.0f) + .setWaitForAccurateLocation(false) + .build() - fusedLocationProviderClient.requestLocationUpdates( - locationRequest, locationListener, Looper.getMainLooper()) + fusedLocationProviderClient.requestLocationUpdates(locationRequest, executor, locationListener) } override fun removeListener(listener: LocationUpdateListener) { From 10f0c722851f71fc626c73757aa188f4e86243fe Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Thu, 26 Sep 2024 00:56:47 +0900 Subject: [PATCH 11/31] Rename U-Turn images to match how Kotlin stringifies the maneuvers --- .../composeui/views/maneuver/ManeuverImage.kt | 13 ++++++++++++- ...inue_uturn.xml => direction_continue_u_turn.xml} | 0 ...valid_uturn.xml => direction_invalid_u_turn.xml} | 0 .../{direction_uturn.xml => direction_u_turn.xml} | 0 .../java/com/stadiamaps/ferrostar/core/Location.kt | 4 ++-- common/ferrostar/src/models.rs | 1 + 6 files changed, 15 insertions(+), 3 deletions(-) rename android/composeui/src/main/res/drawable/{direction_continue_uturn.xml => direction_continue_u_turn.xml} (100%) rename android/composeui/src/main/res/drawable/{direction_invalid_uturn.xml => direction_invalid_u_turn.xml} (100%) rename android/composeui/src/main/res/drawable/{direction_uturn.xml => direction_u_turn.xml} (100%) diff --git a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/maneuver/ManeuverImage.kt b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/maneuver/ManeuverImage.kt index 84dca6eef..01b9838f6 100644 --- a/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/maneuver/ManeuverImage.kt +++ b/android/composeui/src/main/java/com/stadiamaps/ferrostar/composeui/views/maneuver/ManeuverImage.kt @@ -47,7 +47,7 @@ fun ManeuverImage(content: VisualInstructionContent, tint: Color = LocalContentC @Preview @Composable -fun ManeuverImagePreview() { +fun ManeuverImageLeftTurnPreview() { ManeuverImage( VisualInstructionContent( text = "", @@ -55,3 +55,14 @@ fun ManeuverImagePreview() { maneuverModifier = ManeuverModifier.LEFT, roundaboutExitDegrees = null)) } + +@Preview +@Composable +fun ManeuverImageContinueUturnPreview() { + ManeuverImage( + VisualInstructionContent( + text = "", + maneuverType = ManeuverType.CONTINUE, + maneuverModifier = ManeuverModifier.U_TURN, + roundaboutExitDegrees = null)) +} diff --git a/android/composeui/src/main/res/drawable/direction_continue_uturn.xml b/android/composeui/src/main/res/drawable/direction_continue_u_turn.xml similarity index 100% rename from android/composeui/src/main/res/drawable/direction_continue_uturn.xml rename to android/composeui/src/main/res/drawable/direction_continue_u_turn.xml diff --git a/android/composeui/src/main/res/drawable/direction_invalid_uturn.xml b/android/composeui/src/main/res/drawable/direction_invalid_u_turn.xml similarity index 100% rename from android/composeui/src/main/res/drawable/direction_invalid_uturn.xml rename to android/composeui/src/main/res/drawable/direction_invalid_u_turn.xml diff --git a/android/composeui/src/main/res/drawable/direction_uturn.xml b/android/composeui/src/main/res/drawable/direction_u_turn.xml similarity index 100% rename from android/composeui/src/main/res/drawable/direction_uturn.xml rename to android/composeui/src/main/res/drawable/direction_u_turn.xml diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt index 3d0495665..98e0e53fe 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt @@ -89,9 +89,9 @@ class AndroidSystemLocationProvider(context: Context) : LocationProvider { executor.execute { handler.post { - val last = locationManager.getLastKnownLocation(getBestProvider())?.toUserLocation() + val last = locationManager.getLastKnownLocation(getBestProvider()) if (last != null) { - androidListener.onLocationChanged(last.toAndroidLocation()) + androidListener.onLocationChanged(last) } locationManager.requestLocationUpdates(getBestProvider(), 100L, 5.0f, androidListener) } diff --git a/common/ferrostar/src/models.rs b/common/ferrostar/src/models.rs index bb1504a4d..2fb765b40 100644 --- a/common/ferrostar/src/models.rs +++ b/common/ferrostar/src/models.rs @@ -404,6 +404,7 @@ pub enum ManeuverType { #[cfg_attr(any(test, feature = "wasm-bindgen"), derive(Serialize))] #[serde(rename_all = "lowercase")] pub enum ManeuverModifier { + #[serde(rename = "uturn")] UTurn, #[serde(rename = "sharp right")] SharpRight, From db4c02e186e1c82d63896bf4a98dcec67b5e52ac Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Thu, 26 Sep 2024 00:59:19 +0900 Subject: [PATCH 12/31] Allow the fused location provider to report an initial fix faster --- .../googleplayservices/FusedLocationProvider.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/android/google-play-services/src/main/java/com/stadiamaps/ferrostar/googleplayservices/FusedLocationProvider.kt b/android/google-play-services/src/main/java/com/stadiamaps/ferrostar/googleplayservices/FusedLocationProvider.kt index 15ad509cb..654161cbb 100644 --- a/android/google-play-services/src/main/java/com/stadiamaps/ferrostar/googleplayservices/FusedLocationProvider.kt +++ b/android/google-play-services/src/main/java/com/stadiamaps/ferrostar/googleplayservices/FusedLocationProvider.kt @@ -42,12 +42,12 @@ class FusedLocationProvider( return } - val locationListener = LocationListener { + val androidListener = LocationListener { val userLocation = it.toUserLocation() lastLocation = userLocation listener.onLocationUpdated(userLocation) } - listeners[listener] = locationListener + listeners[listener] = androidListener val locationRequest = LocationRequest.Builder(priority, 1000L) @@ -55,7 +55,14 @@ class FusedLocationProvider( .setWaitForAccurateLocation(false) .build() - fusedLocationProviderClient.requestLocationUpdates(locationRequest, executor, locationListener) + if (lastLocation == null) { + fusedLocationProviderClient.lastLocation.addOnSuccessListener { location -> + if (location != null) { + androidListener.onLocationChanged(location) + } + } + } + fusedLocationProviderClient.requestLocationUpdates(locationRequest, executor, androidListener) } override fun removeListener(listener: LocationUpdateListener) { From afce8ff294bdac646ee12bf374663fba38eed6f8 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Thu, 26 Sep 2024 01:45:33 +0900 Subject: [PATCH 13/31] First pass at an actually usable demo app --- android/demo-app/build.gradle | 9 +- .../com/stadiamaps/ferrostar/AppModule.kt | 30 ++-- .../ferrostar/DemoNavigationScene.kt | 157 ++++++++++++------ .../com/stadiamaps/ferrostar/MainActivity.kt | 5 +- android/gradle/libs.versions.toml | 2 + 5 files changed, 134 insertions(+), 69 deletions(-) diff --git a/android/demo-app/build.gradle b/android/demo-app/build.gradle index 230da289c..270a25471 100644 --- a/android/demo-app/build.gradle +++ b/android/demo-app/build.gradle @@ -64,11 +64,16 @@ dependencies { implementation project(':core') implementation project(':composeui') implementation project(':maplibreui') + implementation project(':google-play-services') implementation libs.maplibre.compose - implementation(platform(libs.okhttp.bom)) - implementation(libs.okhttp.core) + implementation platform(libs.okhttp.bom) + implementation libs.okhttp.core + + implementation libs.play.services.location + + implementation libs.stadiamaps.autocomplete.search testImplementation libs.junit androidTestImplementation libs.androidx.test.junit diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt index 1c7a4dc44..25ca34451 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt @@ -8,10 +8,11 @@ import com.stadiamaps.ferrostar.core.AlternativeRouteProcessor import com.stadiamaps.ferrostar.core.AndroidTtsObserver import com.stadiamaps.ferrostar.core.CorrectiveAction import com.stadiamaps.ferrostar.core.FerrostarCore +import com.stadiamaps.ferrostar.core.LocationProvider import com.stadiamaps.ferrostar.core.RouteDeviationHandler -import com.stadiamaps.ferrostar.core.SimulatedLocationProvider import com.stadiamaps.ferrostar.core.service.FerrostarForegroundServiceManager import com.stadiamaps.ferrostar.core.service.ForegroundServiceManager +import com.stadiamaps.ferrostar.googleplayservices.FusedLocationProvider import java.net.URL import java.time.Duration import okhttp3.OkHttpClient @@ -56,7 +57,10 @@ object AppModule { appContext = context } - val locationProvider: SimulatedLocationProvider by lazy { SimulatedLocationProvider() } + val locationProvider: LocationProvider by lazy { + // TODO: Make this configurable? + FusedLocationProvider(appContext) + } private val httpClient: OkHttpClient by lazy { OkHttpClient.Builder().callTimeout(Duration.ofSeconds(15)).build() } @@ -65,11 +69,12 @@ object AppModule { FerrostarForegroundServiceManager(appContext, DefaultForegroundNotificationBuilder(appContext)) } + // TODO: This is hard-coded for golf cart routing; change to something else before merging val ferrostarCore: FerrostarCore by lazy { val core = FerrostarCore( valhallaEndpointURL = valhallaEndpointUrl, - profile = "bicycle", + profile = "low_speed_vehicle", httpClient = httpClient, locationProvider = locationProvider, foregroundServiceManager = foregroundServiceManager, @@ -79,7 +84,16 @@ object AppModule { minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U), RouteDeviationTracking.StaticThreshold(15U, 25.0), CourseFiltering.SNAP_TO_ROUTE), - options = mapOf("costingOptions" to mapOf("bicycle" to mapOf("use_roads" to 0.2)))) + options = + mapOf( + "costingOptions" to + mapOf( + "low_speed_vehicle" to + mapOf( + "vehicle_type" to "golf_cart", + "top_speed" to 32 // 24kph ~= 15mph + )), + "units" to "miles")) // Not all navigation apps will require this sort of extra configuration. // In fact, we hope that most don't! @@ -93,13 +107,7 @@ object AppModule { Log.i(TAG, "Received alternate route(s): $routes") if (routes.isNotEmpty()) { // NB: Use `replaceRoute` for cases like this! - it.replaceRoute( - routes.first(), - NavigationControllerConfig( - StepAdvanceMode.RelativeLineStringDistance( - minimumHorizontalAccuracy = 25U, automaticAdvanceDistance = 10U), - RouteDeviationTracking.StaticThreshold(25U, 10.0), - CourseFiltering.SNAP_TO_ROUTE)) + it.replaceRoute(routes.first()) } } diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt index b089e6d22..a572d87d1 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt @@ -5,45 +5,57 @@ import android.os.Build import android.os.Bundle import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.width -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.mapbox.mapboxsdk.geometry.LatLng import com.maplibre.compose.symbols.Circle +import com.stadiamaps.autocomplete.AutocompleteSearch +import com.stadiamaps.autocomplete.center import com.stadiamaps.ferrostar.composeui.runtime.KeepScreenOnDisposableEffect +import com.stadiamaps.ferrostar.core.AndroidSystemLocationProvider import com.stadiamaps.ferrostar.core.FerrostarCore +import com.stadiamaps.ferrostar.core.LocationProvider +import com.stadiamaps.ferrostar.core.LocationUpdateListener import com.stadiamaps.ferrostar.core.NavigationViewModel import com.stadiamaps.ferrostar.core.SimulatedLocationProvider +import com.stadiamaps.ferrostar.core.toAndroidLocation +import com.stadiamaps.ferrostar.googleplayservices.FusedLocationProvider import com.stadiamaps.ferrostar.maplibreui.views.DynamicallyOrientingNavigationView -import com.stadiamaps.ferrostar.support.initialSimulatedLocation +import java.util.concurrent.Executors +import kotlin.math.min import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import uniffi.ferrostar.GeographicCoordinate +import uniffi.ferrostar.Heading +import uniffi.ferrostar.UserLocation import uniffi.ferrostar.Waypoint import uniffi.ferrostar.WaypointKind @Composable fun DemoNavigationScene( savedInstanceState: Bundle?, - locationProvider: SimulatedLocationProvider = AppModule.locationProvider, - core: FerrostarCore = AppModule.ferrostarCore + locationProvider: LocationProvider = AppModule.locationProvider, ) { + val executor = remember { Executors.newSingleThreadScheduledExecutor() } + // Keeps the screen on at consistent brightness while this Composable is in the view hierarchy. KeepScreenOnDisposableEffect() var viewModel by remember { mutableStateOf(null) } + val scope = rememberCoroutineScope() // Get location permissions. // NOTE: This is NOT a robust suggestion for how to get permissions in a production app. @@ -60,18 +72,38 @@ fun DemoNavigationScene( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) } + val locationUpdateListener = remember { + object : LocationUpdateListener { + private var _lastLocation: MutableStateFlow = MutableStateFlow(null) + val userLocation = _lastLocation.asStateFlow() + + override fun onLocationUpdated(location: UserLocation) { + _lastLocation.value = location + } + + override fun onHeadingUpdated(heading: Heading) { + // Not relevant yet... + } + } + } + + val lastLocation = locationUpdateListener.userLocation.collectAsState(scope.coroutineContext) + val permissionsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> when { permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { - // TODO - // onAccess() + if (locationProvider is AndroidSystemLocationProvider || + locationProvider is FusedLocationProvider) { + locationProvider.addListener(locationUpdateListener, executor) + } } permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { - // TODO + // TODO: Probably alert the user that this is unusable for navigation // onAccess() } + // TODO: Foreground service permissions; we should block access until approved on API 34+ else -> { // TODO // onFailed() @@ -83,62 +115,81 @@ fun DemoNavigationScene( LaunchedEffect(savedInstanceState) { // Request all permissions permissionsLauncher.launch(allPermissions) + } - // Fetch a route in the background - launch(Dispatchers.IO) { - val routes = - core.getRoutes( - initialSimulatedLocation, - listOf( - Waypoint( - coordinate = GeographicCoordinate(37.807587, -122.428411), - kind = WaypointKind.BREAK), - )) - - val route = routes.first() - viewModel = core.startNavigation(route = route) - - locationProvider.setSimulatedRoute(route) + // For smart casting + val loc = lastLocation.value + if (loc == null) { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Text("Waiting to acquire your GPS location...", modifier = Modifier.padding(innerPadding)) } + return } - if (viewModel != null) { - // Demo tiles illustrate a basic integration without any API key required, - // but you can replace the styleURL with any valid MapLibre style URL. - // See https://stadiamaps.github.io/ferrostar/vendors.html for some vendors. - // Most vendors offer free API keys for development use. + // Capture for smart casts + val vm = viewModel + if (vm == null) { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + AutocompleteSearch( + modifier = Modifier.padding(innerPadding), + apiKey = AppModule.stadiaApiKey, + userLocation = lastLocation.value?.toAndroidLocation()) { feature -> + // Fetch a route in the background + scope.launch(Dispatchers.IO) { + // TODO: Fail gracefully + val center = feature.center()!! + val routes = + AppModule.ferrostarCore.getRoutes( + loc, + listOf( + Waypoint( + coordinate = GeographicCoordinate(center.latitude, center.longitude), + kind = WaypointKind.BREAK), + )) + + val route = routes.first() + viewModel = AppModule.ferrostarCore.startNavigation(route = route) + + if (locationProvider is SimulatedLocationProvider) { + locationProvider.setSimulatedRoute(route) + } + } + } + } + } else { DynamicallyOrientingNavigationView( modifier = Modifier.fillMaxSize(), - // These are demo tiles and not very useful. - // Check https://stadiamaps.github.io/ferrostar/vendors.html for some vendors of vector - // tiles. - // Most vendors offer free API keys for development use. - styleUrl = "https://demotiles.maplibre.org/style.json", - viewModel = viewModel!!, - // This is the default value, which works well for motor vehicle navigation. + styleUrl = AppModule.mapStyleUrl, + // TODO: Make this nullable! + viewModel = vm, + // This is the default value, which works well for most motor vehicle navigation. // Other travel modes though, such as walking, may not want snapping. - snapUserLocationToRoute = true, - onTapExit = { viewModel!!.stopNavigation() }) { uiState -> + snapUserLocationToRoute = false, + onTapExit = { + viewModel?.stopNavigation() + viewModel = null + }) { uiState -> // Trivial, if silly example of how to add your own overlay layers. // (Also incidentally highlights the lag inherent in MapLibre location tracking // as-is.) - uiState.value.snappedLocation?.let { + uiState.value.location?.let { location -> Circle( - center = LatLng(it.coordinates.lat, it.coordinates.lng), + center = LatLng(location.coordinates.lat, location.coordinates.lng), radius = 10f, color = "Blue", - zIndex = 2, + zIndex = 3, ) + + if (location.horizontalAccuracy > 15) { + Circle( + center = LatLng(location.coordinates.lat, location.coordinates.lng), + radius = min(location.horizontalAccuracy.toFloat(), 150f), + color = "Blue", + opacity = 0.2f, + zIndex = 2, + ) + } } } - } else { - // Loading indicator - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally) { - Text(text = "Calculating route...") - CircularProgressIndicator(modifier = Modifier.width(64.dp)) - } } } diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt index 24dc163a7..a568b21d8 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt @@ -10,7 +10,6 @@ import androidx.compose.material3.Surface import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import com.stadiamaps.ferrostar.core.AndroidTtsStatusListener -import com.stadiamaps.ferrostar.support.initialSimulatedLocation import com.stadiamaps.ferrostar.ui.theme.FerrostarTheme import java.util.Locale @@ -44,8 +43,8 @@ class MainActivity : ComponentActivity(), AndroidTtsStatusListener { AppModule.ferrostarCore.spokenInstructionObserver = AppModule.ttsObserver // Set up the location provider - AppModule.locationProvider.lastLocation = initialSimulatedLocation - AppModule.locationProvider.warpFactor = 2u + // AppModule.locationProvider.lastLocation = initialSimulatedLocation + // AppModule.locationProvider.warpFactor = 2u // Edge to edge (this will be default in Android 15) // See https://developer.android.com/codelabs/edge-to-edge#0 diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index fa61fc69b..0134a38da 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -24,6 +24,7 @@ okhttp-mock = "2.0.0" mavenPublish = "0.29.0" secretsGradlePlugin = "2.0.1" material = "1.12.0" +stadiaAutocompleteSearch = "0.0.5" [libraries] desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } @@ -64,6 +65,7 @@ androidx-test-junit = { group = "androidx.test.ext", name = "junit", version.ref androidx-test-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4", version.ref = "junitCompose" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } +stadiamaps-autocomplete-search = { group = "com.stadiamaps", name = "jetpack-compose-autocomplete", version.ref = "stadiaAutocompleteSearch" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" } From 8f5bec434838a8c13c0b27415fa5ce5bb2982c0c Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 7 Oct 2024 11:28:01 +0900 Subject: [PATCH 14/31] Minor build script improvement --- common/build-ios.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/build-ios.sh b/common/build-ios.sh index df1356546..8c6d53dd0 100755 --- a/common/build-ios.sh +++ b/common/build-ios.sh @@ -57,7 +57,7 @@ build_xcframework() { echo "Building xcframework archive" ditto -c -k --sequesterRsrc --keepParent target/ios/lib$1-rs.xcframework target/ios/lib$1-rs.xcframework.zip checksum=$(swift package compute-checksum target/ios/lib$1-rs.xcframework.zip) - version=$(cargo metadata --format-version 1 | jq -r '.packages[] | select(.name=="ferrostar") .version') + version=$(cargo metadata --format-version 1 | jq -r --arg pkg_name "$1" '.packages[] | select(.name==$pkg_name) .version') sed -i "" -E "s/(let releaseTag = \")[^\"]+(\")/\1$version\2/g" ../Package.swift sed -i "" -E "s/(let releaseChecksum = \")[^\"]+(\")/\1$checksum\2/g" ../Package.swift fi From 37a45116f30f1a50d629ddc299d4aab3d4443282 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Thu, 10 Oct 2024 18:00:07 +0900 Subject: [PATCH 15/31] Checkpoint commit phase 1 --- .../ferrostar/core/NavigationViewModel.kt | 59 +++++++- android/demo-app/build.gradle | 2 +- .../ferrostar/DemoNavigationScene.kt | 142 +++++++++--------- android/gradle/libs.versions.toml | 6 +- .../ferrostar/maplibreui/NavigationMapView.kt | 26 ++-- .../DynamicallyOrientingNavigationView.kt | 56 ++++--- 6 files changed, 188 insertions(+), 103 deletions(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index 7759356d0..eb14bdba9 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -6,11 +6,15 @@ import androidx.lifecycle.viewModelScope import com.stadiamaps.ferrostar.core.extensions.deviation import com.stadiamaps.ferrostar.core.extensions.progress import com.stadiamaps.ferrostar.core.extensions.visualInstruction +import java.util.concurrent.Executors +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update import uniffi.ferrostar.GeographicCoordinate +import uniffi.ferrostar.Heading import uniffi.ferrostar.RouteDeviation import uniffi.ferrostar.SpokenInstruction import uniffi.ferrostar.TripProgress @@ -31,7 +35,7 @@ data class NavigationUiState( */ val heading: Float?, /** The geometry of the full route. */ - val routeGeometry: List, + val routeGeometry: List?, /** Visual instructions which should be displayed based on the user's current progress. */ val visualInstruction: VisualInstruction?, /** @@ -76,6 +80,59 @@ interface NavigationViewModel { fun toggleMute() fun stopNavigation() + + fun isNavigating(): Boolean = uiState.value.progress != null +} + +class IdleNavigationViewModel(locationProvider: LocationProvider) : + ViewModel(), NavigationViewModel { + private val locationStateFlow = MutableStateFlow(locationProvider.lastLocation) + private val executor = Executors.newSingleThreadScheduledExecutor() + + init { + locationProvider.addListener( + object : LocationUpdateListener { + override fun onLocationUpdated(location: UserLocation) { + locationStateFlow.update { location } + } + + override fun onHeadingUpdated(heading: Heading) { + // TODO: Heading + } + }, + executor) + } + + override val uiState = + locationStateFlow + .map { userLocation -> + // TODO: Heading + NavigationUiState(userLocation, null, null, null, null, null, null, false, null, null) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + // TODO: Heading + initialValue = + NavigationUiState( + locationProvider.lastLocation, + null, + null, + null, + null, + null, + null, + false, + null, + null)) + + override fun toggleMute() { + // Do nothing + } + + override fun stopNavigation() { + // Do nothing + } } class DefaultNavigationViewModel( diff --git a/android/demo-app/build.gradle b/android/demo-app/build.gradle index 270a25471..eb588330e 100644 --- a/android/demo-app/build.gradle +++ b/android/demo-app/build.gradle @@ -13,7 +13,7 @@ android { defaultConfig { applicationId "com.stadiamaps.ferrostar.demo" minSdk 26 - targetSdk 34 + targetSdk 35 versionCode 1 versionName "1.0" diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt index a572d87d1..38121fb04 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt @@ -18,15 +18,18 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import com.mapbox.mapboxsdk.geometry.LatLng +import com.maplibre.compose.camera.MapViewCamera +import com.maplibre.compose.rememberSaveableMapViewCamera import com.maplibre.compose.symbols.Circle import com.stadiamaps.autocomplete.AutocompleteSearch import com.stadiamaps.autocomplete.center import com.stadiamaps.ferrostar.composeui.runtime.KeepScreenOnDisposableEffect +import com.stadiamaps.ferrostar.composeui.views.gridviews.InnerGridView import com.stadiamaps.ferrostar.core.AndroidSystemLocationProvider -import com.stadiamaps.ferrostar.core.FerrostarCore +import com.stadiamaps.ferrostar.core.IdleNavigationViewModel import com.stadiamaps.ferrostar.core.LocationProvider -import com.stadiamaps.ferrostar.core.LocationUpdateListener import com.stadiamaps.ferrostar.core.NavigationViewModel import com.stadiamaps.ferrostar.core.SimulatedLocationProvider import com.stadiamaps.ferrostar.core.toAndroidLocation @@ -35,12 +38,8 @@ import com.stadiamaps.ferrostar.maplibreui.views.DynamicallyOrientingNavigationV import java.util.concurrent.Executors import kotlin.math.min import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import uniffi.ferrostar.GeographicCoordinate -import uniffi.ferrostar.Heading -import uniffi.ferrostar.UserLocation import uniffi.ferrostar.Waypoint import uniffi.ferrostar.WaypointKind @@ -54,7 +53,9 @@ fun DemoNavigationScene( // Keeps the screen on at consistent brightness while this Composable is in the view hierarchy. KeepScreenOnDisposableEffect() - var viewModel by remember { mutableStateOf(null) } + var viewModel by remember { + mutableStateOf(IdleNavigationViewModel(locationProvider)) + } val scope = rememberCoroutineScope() // Get location permissions. @@ -118,7 +119,7 @@ fun DemoNavigationScene( } // For smart casting - val loc = lastLocation.value + val loc = vmState.value.location if (loc == null) { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Text("Waiting to acquire your GPS location...", modifier = Modifier.padding(innerPadding)) @@ -126,70 +127,75 @@ fun DemoNavigationScene( return } - // Capture for smart casts - val vm = viewModel - if (vm == null) { - Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - AutocompleteSearch( - modifier = Modifier.padding(innerPadding), - apiKey = AppModule.stadiaApiKey, - userLocation = lastLocation.value?.toAndroidLocation()) { feature -> - // Fetch a route in the background - scope.launch(Dispatchers.IO) { - // TODO: Fail gracefully - val center = feature.center()!! - val routes = - AppModule.ferrostarCore.getRoutes( - loc, - listOf( - Waypoint( - coordinate = GeographicCoordinate(center.latitude, center.longitude), - kind = WaypointKind.BREAK), - )) - - val route = routes.first() - viewModel = AppModule.ferrostarCore.startNavigation(route = route) - - if (locationProvider is SimulatedLocationProvider) { - locationProvider.setSimulatedRoute(route) - } - } - } - } - } else { - DynamicallyOrientingNavigationView( - modifier = Modifier.fillMaxSize(), - styleUrl = AppModule.mapStyleUrl, - // TODO: Make this nullable! - viewModel = vm, - // This is the default value, which works well for most motor vehicle navigation. - // Other travel modes though, such as walking, may not want snapping. - snapUserLocationToRoute = false, - onTapExit = { - viewModel?.stopNavigation() - viewModel = null - }) { uiState -> - // Trivial, if silly example of how to add your own overlay layers. - // (Also incidentally highlights the lag inherent in MapLibre location tracking - // as-is.) - uiState.value.location?.let { location -> + // Set up the map! + val camera = rememberSaveableMapViewCamera(MapViewCamera.TrackingUserLocation()) + DynamicallyOrientingNavigationView( + modifier = Modifier.fillMaxSize(), + styleUrl = AppModule.mapStyleUrl, + camera = camera, + viewModel = viewModel, + // Snapping works well for most motor vehicle navigation. + // Other travel modes though, such as walking, may not want snapping. + snapUserLocationToRoute = false, + onTapExit = { + viewModel.stopNavigation() + viewModel = IdleNavigationViewModel(locationProvider) + }, + userContent = { modifier -> + if (!viewModel.isNavigating()) { + InnerGridView( + modifier = modifier.fillMaxSize().padding(bottom = 16.dp, top = 16.dp), + topCenter = { + AutocompleteSearch( + apiKey = AppModule.stadiaApiKey, + userLocation = loc.toAndroidLocation()) { feature -> + feature.center()?.let { center -> + // Fetch a route in the background + scope.launch(Dispatchers.IO) { + // TODO: Fail gracefully + val routes = + AppModule.ferrostarCore.getRoutes( + loc, + listOf( + Waypoint( + coordinate = + GeographicCoordinate( + center.latitude, center.longitude), + kind = WaypointKind.BREAK), + )) + + val route = routes.first() + viewModel = AppModule.ferrostarCore.startNavigation(route = route) + + if (locationProvider is SimulatedLocationProvider) { + locationProvider.setSimulatedRoute(route) + } + } + } + } + }) + } + }) { uiState -> + // Trivial, if silly example of how to add your own overlay layers. + // (Also incidentally highlights the lag inherent in MapLibre location tracking + // as-is.) + uiState.value.location?.let { location -> + Circle( + center = LatLng(location.coordinates.lat, location.coordinates.lng), + radius = 10f, + color = "Blue", + zIndex = 3, + ) + + if (location.horizontalAccuracy > 15) { Circle( center = LatLng(location.coordinates.lat, location.coordinates.lng), - radius = 10f, + radius = min(location.horizontalAccuracy.toFloat(), 150f), color = "Blue", - zIndex = 3, + opacity = 0.2f, + zIndex = 2, ) - - if (location.horizontalAccuracy > 15) { - Circle( - center = LatLng(location.coordinates.lat, location.coordinates.lng), - radius = min(location.horizontalAccuracy.toFloat(), 150f), - color = "Blue", - opacity = 0.2f, - zIndex = 2, - ) - } } } - } + } } diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 9d58d8268..4aacc7901 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -11,20 +11,20 @@ kotlinx-coroutines = "1.9.0" kotlinx-datetime = "0.6.1" androidx-appcompat = "1.7.0" androidx-activity-compose = "1.9.2" -compose = "2024.09.02" +compose = "2024.09.03" okhttp = "4.12.0" moshi = "1.15.1" maplibre-compose = "0.2.0" playServicesLocation = "21.3.0" junit = "4.13.2" junitVersion = "1.2.1" -junitCompose = "1.7.2" +junitCompose = "1.7.3" espressoCore = "3.6.1" okhttp-mock = "2.0.0" mavenPublish = "0.29.0" secretsGradlePlugin = "2.0.1" material = "1.12.0" -stadiaAutocompleteSearch = "0.0.5" +stadiaAutocompleteSearch = "1.0.0" [libraries] desugar_jdk_libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt index b258aa0e1..fdf238d00 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt @@ -25,7 +25,10 @@ import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera * The base MapLibre MapView configured for navigation with a polyline representing the route. * * @param styleUrl The MapLibre style URL to use for the map. - * @param camera The bi-directional camera state to use for the map. + * @param camera The bi-directional camera state to use for the map. Note: this is a bit + * non-standard as far as normal compose patterns go, but we independently came up with this + * approach and later verified that Google Maps does the same thing in their compose SDK. + * @param navigationCamera The default camera settings to use when navigation starts. * @param viewModel The navigation view model provided by Ferrostar Core. * @param locationRequestProperties The location request properties to use for the map's location * engine. @@ -45,17 +48,21 @@ fun NavigationMapView( locationRequestProperties: LocationRequestProperties = LocationRequestProperties.NavigationDefault(), snapUserLocationToRoute: Boolean = true, - onMapReadyCallback: (Style) -> Unit = { camera.value = navigationCamera }, - content: @Composable @MapLibreComposable() ((State) -> Unit)? = null + onMapReadyCallback: (Style) -> Unit = { + if (viewModel.isNavigating()) camera.value = navigationCamera + }, + content: @Composable @MapLibreComposable ((State) -> Unit)? = null ) { val uiState = viewModel.uiState.collectAsState() val locationEngine = remember { StaticLocationEngine() } locationEngine.lastLocation = - if (snapUserLocationToRoute) { - uiState.value.snappedLocation?.toAndroidLocation() - } else { - uiState.value.location?.toAndroidLocation() + uiState.value.let { state -> + if (snapUserLocationToRoute) { + state.snappedLocation?.toAndroidLocation() + } else { + state.location?.toAndroidLocation() + } } MapView( @@ -67,8 +74,9 @@ fun NavigationMapView( locationEngine = locationEngine, onMapReadyCallback = onMapReadyCallback, ) { - BorderedPolyline( - points = uiState.value.routeGeometry.map { LatLng(it.lat, it.lng) }, zIndex = 0) + val geometry = uiState.value.routeGeometry + if (geometry != null) + BorderedPolyline(points = geometry.map { LatLng(it.lat, it.lng) }, zIndex = 0) if (content != null) { content(uiState) diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt index 9ea20dd18..c9e37abf2 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt @@ -2,6 +2,7 @@ package com.stadiamaps.ferrostar.maplibreui.views import android.content.res.Configuration import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars @@ -46,7 +47,8 @@ import com.stadiamaps.ferrostar.maplibreui.views.overlays.PortraitNavigationOver * route line. * @param config The configuration for the navigation view. * @param onTapExit The callback to invoke when the exit button is tapped. - * @param content Any additional composable map symbol content to render. + * @param userContent TODO docs + * @param mapContent Any additional composable map symbol content to render. */ @Composable fun DynamicallyOrientingNavigationView( @@ -60,7 +62,8 @@ fun DynamicallyOrientingNavigationView( snapUserLocationToRoute: Boolean = true, config: VisualNavigationViewConfig = VisualNavigationViewConfig.Default(), onTapExit: (() -> Unit)? = null, - content: @Composable @MapLibreComposable() ((State) -> Unit)? = null, + userContent: @Composable (BoxScope.(Modifier) -> Unit)? = null, + mapContent: @Composable @MapLibreComposable ((State) -> Unit)? = null, ) { val orientation = LocalConfiguration.current.orientation @@ -83,27 +86,38 @@ fun DynamicallyOrientingNavigationView( mapControls, locationRequestProperties, snapUserLocationToRoute, - onMapReadyCallback = { camera.value = navigationCamera }, - content) + onMapReadyCallback = { + if (viewModel.isNavigating()) { + camera.value = navigationCamera + } + }, + mapContent) - when (orientation) { - Configuration.ORIENTATION_LANDSCAPE -> { - LandscapeNavigationOverlayView( - modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), - camera = camera, - viewModel = viewModel, - config = config, - onTapExit = onTapExit) - } - else -> { - PortraitNavigationOverlayView( - modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), - camera = camera, - viewModel = viewModel, - config = config, - arrivalViewSize = rememberArrivalViewSize, - onTapExit = onTapExit) + if (viewModel.isNavigating()) { + when (orientation) { + Configuration.ORIENTATION_LANDSCAPE -> { + LandscapeNavigationOverlayView( + modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), + camera = camera, + viewModel = viewModel, + config = config, + onTapExit = onTapExit) + } + + else -> { + PortraitNavigationOverlayView( + modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding), + camera = camera, + viewModel = viewModel, + config = config, + arrivalViewSize = rememberArrivalViewSize, + onTapExit = onTapExit) + } } } + + if (userContent != null) { + userContent(Modifier.windowInsetsPadding(WindowInsets.systemBars).padding(gridPadding)) + } } } From f81cb8a854e65818c9d5832c47e2cf85f00b8fcd Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Thu, 10 Oct 2024 18:00:30 +0900 Subject: [PATCH 16/31] Checkpoint commit phase 2 highlighting some bad stuff to clean up --- .../ferrostar/DemoNavigationScene.kt | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt index 38121fb04..5d224f53b 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt @@ -73,22 +73,23 @@ fun DemoNavigationScene( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) } - val locationUpdateListener = remember { - object : LocationUpdateListener { - private var _lastLocation: MutableStateFlow = MutableStateFlow(null) - val userLocation = _lastLocation.asStateFlow() - - override fun onLocationUpdated(location: UserLocation) { - _lastLocation.value = location - } - - override fun onHeadingUpdated(heading: Heading) { - // Not relevant yet... - } - } - } - - val lastLocation = locationUpdateListener.userLocation.collectAsState(scope.coroutineContext) + // val locationUpdateListener = remember { + // object : LocationUpdateListener { + // private var _lastLocation: MutableStateFlow = MutableStateFlow(null) + // val userLocation = _lastLocation.asStateFlow() + // + // override fun onLocationUpdated(location: UserLocation) { + // _lastLocation.value = location + // } + // + // override fun onHeadingUpdated(heading: Heading) { + // // TODO + // } + // } + // } + + // val lastLocation = locationUpdateListener.userLocation.collectAsState(scope.coroutineContext) + val vmState = viewModel.uiState.collectAsState(scope.coroutineContext) val permissionsLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { @@ -97,7 +98,8 @@ fun DemoNavigationScene( permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { if (locationProvider is AndroidSystemLocationProvider || locationProvider is FusedLocationProvider) { - locationProvider.addListener(locationUpdateListener, executor) + // FIXME + // locationProvider.addListener(locationUpdateListener, executor) } } permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { From 9e887d6e2b38eb8471afa73d06c29234488faa66 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Thu, 10 Oct 2024 18:00:56 +0900 Subject: [PATCH 17/31] Add docs on the StaticLocationEngine --- guide/src/location-providers.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/guide/src/location-providers.md b/guide/src/location-providers.md index f4730c814..12d02e3fc 100644 --- a/guide/src/location-providers.md +++ b/guide/src/location-providers.md @@ -44,7 +44,21 @@ and unsubscribes itself (Android) or stops location updates automatically (iOS) Ferrostar includes the following live location providers: * iOS - - `CoreLocationProvider` - Location backed by a `CLLocationManager`. See the [iOS tutorial](./ios-getting-started.md) for a usage example. + - `CoreLocationProvider` - Location backed by a `CLLocationManager`. See the [iOS tutorial](./ios-getting-started.md#corelocationprovider) for a usage example. * Android - - [`AndroidSystemLocationProvider`] - Location backed by a android.location.LocationManger` (the class that is included in AOSP). See the [Android tutorial](./android-getting-started.md) for a usage example. - - TODO: Provider backed by the Google Fused Location Client + - [`AndroidSystemLocationProvider`] - Location backed by an `android.location.LocationManger` (the class that is included in AOSP). See the [Android tutorial](./android-getting-started.md#androidsystemlocationprovider) for a usage example. + - [`FusedLocationProvider`] - Location backed by a Google Play Services `FusedLocationClient`, which is proprietary but often provides better location updates. See the [Android tutorial](./android-getting-started.md#google-play-fused-location-client) for a usage example. + +## Implementation note: `StaticLocationEngine` + +If you dig around the FerrostarMapLibreUI modules, you may come across as `StaticLocationEngine`. + +The static location engine exists to bridge between Ferrostar location providers and MapLibre. +MapLibre uses `LocationEngine` objects, not platform-native location clients, as its first line. +This is smart, since it makes MapLibre generic enough to support location from other sources. +For Ferrostar, it enables us to account for things like snapping, simulated routes, etc. +The easiest way to hide all that complexity from `LocationProvider` implementors +is to introduce the `StaticLocationEngine` with a simple interface to set location. + +This is mostly transparent to developers using Ferrostar, +but in case you come across it, hopefully this note explains the purpose. From 784832a41b6a219b3b1d7b8919c39dffe6714dfc Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Sun, 13 Oct 2024 17:19:57 +0900 Subject: [PATCH 18/31] Fix formatting + doc test --- Package.resolved | 4 ++-- .../java/com/stadiamaps/ferrostar/DemoNavigationScene.kt | 4 ++-- common/ferrostar/src/routing_adapters/valhalla.rs | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Package.resolved b/Package.resolved index 74475d8e1..2940eb9f6 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/maplibre/maplibre-gl-native-distribution.git", "state" : { - "revision" : "e409318144091c3ee9ad551b202e1c36695f8086", - "version" : "6.7.0" + "revision" : "f23db791d7b6f0329e3c6788d8e4152c24c52b6b", + "version" : "6.7.1" } }, { diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt index 5d224f53b..7821c47f2 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt @@ -149,8 +149,8 @@ fun DemoNavigationScene( modifier = modifier.fillMaxSize().padding(bottom = 16.dp, top = 16.dp), topCenter = { AutocompleteSearch( - apiKey = AppModule.stadiaApiKey, - userLocation = loc.toAndroidLocation()) { feature -> + apiKey = AppModule.stadiaApiKey, userLocation = loc.toAndroidLocation()) { + feature -> feature.center()?.let { center -> // Fetch a route in the background scope.launch(Dispatchers.IO) { diff --git a/common/ferrostar/src/routing_adapters/valhalla.rs b/common/ferrostar/src/routing_adapters/valhalla.rs index 78512dd04..9e95131df 100644 --- a/common/ferrostar/src/routing_adapters/valhalla.rs +++ b/common/ferrostar/src/routing_adapters/valhalla.rs @@ -23,16 +23,16 @@ use alloc::{ /// having the same name. /// /// ``` -/// use serde_json::json; +/// use serde_json::{json, Map, Value}; /// use ferrostar::routing_adapters::valhalla::ValhallaHttpRequestGenerator; -/// let options = json!({ +/// let options: Map = json!({ /// "costing_options": { /// "low_speed_vehicle": { /// "vehicle_type": "golf_cart" /// } /// } -/// }); -/// let request_generator = ValhallaHttpRequestGenerator::new("https://api.stadiamaps.com/route/v1?api_key=YOUR-API-KEY".to_string(), "low_speed_vehicle".to_string(), Some(options)); +/// }).as_object().unwrap().to_owned();; +/// let request_generator = ValhallaHttpRequestGenerator::new("https://api.stadiamaps.com/route/v1?api_key=YOUR-API-KEY".to_string(), "low_speed_vehicle".to_string(), options); /// ``` #[derive(Debug)] pub struct ValhallaHttpRequestGenerator { From 454309ec3bab91d17a43247dc7329294640eb6af Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 14 Oct 2024 09:41:50 +0900 Subject: [PATCH 19/31] Fix typo in Android readme Co-authored-by: Jacob Fielding --- android/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/README.md b/android/README.md index 6c71ca503..356bb4ce8 100644 --- a/android/README.md +++ b/android/README.md @@ -5,7 +5,7 @@ This directory tree contains the Gradle workspace for Ferrostar on Android. * `composeui` - Jetpack Compose UI elements which are not tightly coupled to any particular map renderer. * `core` - The core module is where all the "business logic", location management, and other core functionality lives. * `demo-app` - A minimal demonstration app. -* `google-play-services` - Optional functionality that depends on Google Play Services (like the a fused location client wrapper). This is a separate module so that apps are able to "de-Google" if necessary. +* `google-play-services` - Optional functionality that depends on Google Play Services (like the fused location client wrapper). This is a separate module so that apps are able to "de-Google" if necessary. * `maplibreui` - Map-related user interface components built with MapLibre. ## Running the demo app From 821fd8aa6c10d25e336247165f32d175126d3b51 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 14 Oct 2024 09:42:28 +0900 Subject: [PATCH 20/31] Kotlin let closure idiom --- .../src/main/java/com/stadiamaps/ferrostar/core/Location.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt index 98e0e53fe..a1acdfde4 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt @@ -90,7 +90,7 @@ class AndroidSystemLocationProvider(context: Context) : LocationProvider { executor.execute { handler.post { val last = locationManager.getLastKnownLocation(getBestProvider()) - if (last != null) { + last?.let { androidListener.onLocationChanged(last) } locationManager.requestLocationUpdates(getBestProvider(), 100L, 5.0f, androidListener) From ea883b106807e5ddda131afd4641c8ece6701ed6 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 14 Oct 2024 10:15:46 +0900 Subject: [PATCH 21/31] ktfmtFormat + auto-commit --- .github/workflows/android.yml | 48 +++++------------------------------ 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index aff191f5b..de7944e7d 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -40,52 +40,18 @@ jobs: run: echo 'stadiaApiKey=' > local.properties working-directory: android + - name: Run ktfmtFormat + run: ./gradlew ktfmtFormat + working-directory: android + - name: Build with Gradle - env: - GITHUB_ACTOR: ${{ github.actor }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: ./gradlew build working-directory: android - check-ktfmt: - - runs-on: macos-13 - concurrency: - group: ${{ github.workflow }}-${{ github.ref }}-android-ktfmt - cancel-in-progress: true - permissions: - contents: read - packages: read - - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - - name: set up JDK 17 - uses: actions/setup-java@v4 + - name: Commit changed Kotlin source (it is easy to forget ktfmtFormat) + uses: stefanzweifel/git-auto-commit-action@v5 with: - java-version: '17' - distribution: 'temurin' - cache: gradle - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - working-directory: android - - - name: Install cargo-ndk - run: cargo install cargo-ndk - - - name: Touch local.properties (required for cargo-ndk) - run: echo 'stadiaApiKey=' > local.properties - working-directory: android - - - name: Verify Kotlin formatting - env: - GITHUB_ACTOR: ${{ github.actor }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew ktfmtCheck - working-directory: android + file_pattern: 'android/**/*.kt' test: From b8fc5909a505ddd1d55684ace983e1cc6afaf63e Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 14 Oct 2024 10:16:02 +0900 Subject: [PATCH 22/31] Remove extraneous env? --- .github/workflows/android.yml | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index de7944e7d..90a2a497f 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -87,9 +87,6 @@ jobs: working-directory: android - name: Unit test - env: - GITHUB_ACTOR: ${{ github.actor }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: ./gradlew test working-directory: android @@ -136,9 +133,6 @@ jobs: working-directory: android - name: Verify snapshots - env: - GITHUB_ACTOR: ${{ github.actor }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: ./gradlew verifyPaparazziDebug working-directory: android @@ -186,9 +180,6 @@ jobs: working-directory: android - name: Run Connected Checks - env: - GITHUB_ACTOR: ${{ github.actor }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: reactivecircus/android-emulator-runner@v2 with: api-level: 30 From d5f527414d6e24540009a110eb125f94bbfb0e02 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 14 Oct 2024 10:22:23 +0900 Subject: [PATCH 23/31] Try switching to a mix of Apple Silicon Ubuntu runners for Android --- .github/workflows/android.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 90a2a497f..6e30fd045 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -9,7 +9,7 @@ on: jobs: build: - runs-on: macos-13 + runs-on: macos-14 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-android-build cancel-in-progress: true @@ -55,7 +55,7 @@ jobs: test: - runs-on: macos-13 + runs-on: macos-14 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-android-test cancel-in-progress: true @@ -101,7 +101,7 @@ jobs: verify-snapshots: - runs-on: macos-13 + runs-on: macos-14 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-android-snapshots cancel-in-progress: true @@ -148,7 +148,7 @@ jobs: connected-check: - runs-on: macos-13 + runs-on: ubuntu-latest concurrency: group: ${{ github.workflow }}-${{ github.ref }}-android-connected-check cancel-in-progress: true @@ -179,12 +179,18 @@ jobs: run: echo 'stadiaApiKey=' > local.properties working-directory: android + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Run Connected Checks uses: reactivecircus/android-emulator-runner@v2 with: api-level: 30 - avd-name: macOS-13-x86-aosp-atd-30 - arch: x86 + avd-name: ubuntu-latest-x86_64-aosp-atd-30 + arch: x86_64 target: aosp_atd script: ./gradlew connectedCheck working-directory: android From 75019913942a251bc29ebe245e9ad1e91caee0fa Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Mon, 14 Oct 2024 10:41:10 +0900 Subject: [PATCH 24/31] Fix runner permissions (I think) --- .github/workflows/android.yml | 12 +++++------- .github/workflows/ios.yml | 3 +++ 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 6e30fd045..ebd4a150a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -8,14 +8,12 @@ on: jobs: build: - runs-on: macos-14 concurrency: group: ${{ github.workflow }}-${{ github.ref }}-android-build cancel-in-progress: true permissions: - contents: read - packages: read + contents: write # To auto-commit ktfmtFormat changes steps: - uses: actions/checkout@v4 @@ -43,15 +41,15 @@ jobs: - name: Run ktfmtFormat run: ./gradlew ktfmtFormat working-directory: android - - - name: Build with Gradle - run: ./gradlew build - working-directory: android - name: Commit changed Kotlin source (it is easy to forget ktfmtFormat) uses: stefanzweifel/git-auto-commit-action@v5 with: file_pattern: 'android/**/*.kt' + + - name: Build with Gradle + run: ./gradlew build + working-directory: android test: diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index f21174d52..4ae138a1e 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -25,6 +25,9 @@ jobs: build-ferrostar: runs-on: macos-14 + permissions: + contents: write # To auto-commit Package.swift and binding changes + concurrency: group: ${{ github.workflow }}-${{ github.ref }}-ios-build-lib cancel-in-progress: true From ff295fcbe04713c021277963f4c61919dce7a345 Mon Sep 17 00:00:00 2001 From: ianthetechie Date: Mon, 14 Oct 2024 01:54:30 +0000 Subject: [PATCH 25/31] Apply automatic changes --- .../src/main/java/com/stadiamaps/ferrostar/core/Location.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt index a1acdfde4..827eaa97b 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/Location.kt @@ -90,9 +90,7 @@ class AndroidSystemLocationProvider(context: Context) : LocationProvider { executor.execute { handler.post { val last = locationManager.getLastKnownLocation(getBestProvider()) - last?.let { - androidListener.onLocationChanged(last) - } + last?.let { androidListener.onLocationChanged(last) } locationManager.requestLocationUpdates(getBestProvider(), 100L, 5.0f, androidListener) } } From 84745b0b5dc2663e1d1157880797957d05f5a325 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Sat, 19 Oct 2024 17:44:16 +0900 Subject: [PATCH 26/31] Address open thread re: iOS uturn image name --- .github/workflows/gradle-publish.yml | 2 +- Package.resolved | 12 ++++++------ .../Contents.json | 0 .../uturn.svg | 0 .../Maneuver/ManeuverInstructionView.swift | 11 +++++++++++ .../Views/ManeuverImageTests.swift | 10 ++++++++++ .../testManeuverImageDefaultTheme.3.png | Bin 0 -> 81223 bytes .../testManeuverImageDefaultTheme.4.png | Bin 0 -> 81329 bytes 8 files changed, 28 insertions(+), 7 deletions(-) rename apple/Sources/FerrostarSwiftUI/Resources/Directions.xcassets/{uturn.imageset => turn_uturn.imageset}/Contents.json (100%) rename apple/Sources/FerrostarSwiftUI/Resources/Directions.xcassets/{uturn.imageset => turn_uturn.imageset}/uturn.svg (100%) create mode 100644 apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverImageTests/testManeuverImageDefaultTheme.3.png create mode 100644 apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverImageTests/testManeuverImageDefaultTheme.4.png diff --git a/.github/workflows/gradle-publish.yml b/.github/workflows/gradle-publish.yml index dac95e6b1..0f6f2bd80 100644 --- a/.github/workflows/gradle-publish.yml +++ b/.github/workflows/gradle-publish.yml @@ -32,7 +32,7 @@ jobs: - name: Install cargo-ndk run: cargo install cargo-ndk - - name: Creat local.properties (required for cargo-ndk and the demo app) + - name: Create local.properties (required for cargo-ndk and the demo app) run: echo 'stadiaApiKey=' > local.properties working-directory: android diff --git a/Package.resolved b/Package.resolved index 2940eb9f6..b44164b29 100644 --- a/Package.resolved +++ b/Package.resolved @@ -14,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/stadiamaps/maplibre-swift-macros.git", "state" : { - "revision" : "236215c13bff962009e0f0257d6d8349be33442f", - "version" : "0.0.4" + "revision" : "9e27e62dff7fd727aebd0a7c8aa74e7635a5583e", + "version" : "0.0.5" } }, { @@ -41,8 +41,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax.git", "state" : { - "revision" : "64889f0c732f210a935a0ad7cda38f77f876262d", - "version" : "509.1.1" + "revision" : "2bc86522d115234d1f588efe2bcb4ce4be8f8b82", + "version" : "510.0.3" } }, { @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/maplibre/swiftui-dsl", "state" : { - "revision" : "5ba75ef1e4382fcc7ee71e274eb9c2a50906b14e", - "version" : "0.1.0" + "revision" : "c39688db3aac50b523ea062b286c584123022093", + "version" : "0.3.0" } } ], diff --git a/apple/Sources/FerrostarSwiftUI/Resources/Directions.xcassets/uturn.imageset/Contents.json b/apple/Sources/FerrostarSwiftUI/Resources/Directions.xcassets/turn_uturn.imageset/Contents.json similarity index 100% rename from apple/Sources/FerrostarSwiftUI/Resources/Directions.xcassets/uturn.imageset/Contents.json rename to apple/Sources/FerrostarSwiftUI/Resources/Directions.xcassets/turn_uturn.imageset/Contents.json diff --git a/apple/Sources/FerrostarSwiftUI/Resources/Directions.xcassets/uturn.imageset/uturn.svg b/apple/Sources/FerrostarSwiftUI/Resources/Directions.xcassets/turn_uturn.imageset/uturn.svg similarity index 100% rename from apple/Sources/FerrostarSwiftUI/Resources/Directions.xcassets/uturn.imageset/uturn.svg rename to apple/Sources/FerrostarSwiftUI/Resources/Directions.xcassets/turn_uturn.imageset/uturn.svg diff --git a/apple/Sources/FerrostarSwiftUI/Views/Maneuver/ManeuverInstructionView.swift b/apple/Sources/FerrostarSwiftUI/Views/Maneuver/ManeuverInstructionView.swift index 69c541cf2..8389413e7 100644 --- a/apple/Sources/FerrostarSwiftUI/Views/Maneuver/ManeuverInstructionView.swift +++ b/apple/Sources/FerrostarSwiftUI/Views/Maneuver/ManeuverInstructionView.swift @@ -94,6 +94,17 @@ public struct ManeuverInstructionView: View { .font(.body) .foregroundColor(.blue) + ManeuverInstructionView( + text: "Make a legal u-turn", + distanceFormatter: MKDistanceFormatter(), + distanceToNextManeuver: 152.4 + ) { + ManeuverImage(maneuverType: .turn, maneuverModifier: .uTurn) + .frame(width: 24) + } + .font(.body) + .foregroundColor(.blue) + // Demonstrate a Right to Left ManeuverInstructionView( text: "ادمج يسارًا", diff --git a/apple/Tests/FerrostarSwiftUITests/Views/ManeuverImageTests.swift b/apple/Tests/FerrostarSwiftUITests/Views/ManeuverImageTests.swift index b3dcbbbf7..6efda9f64 100644 --- a/apple/Tests/FerrostarSwiftUITests/Views/ManeuverImageTests.swift +++ b/apple/Tests/FerrostarSwiftUITests/Views/ManeuverImageTests.swift @@ -14,6 +14,16 @@ final class ManeuverImageTests: XCTestCase { ManeuverImage(maneuverType: .fork, maneuverModifier: .left) .frame(width: 32) } + + assertView { + ManeuverImage(maneuverType: .turn, maneuverModifier: .uTurn) + .frame(width: 32) + } + + assertView { + ManeuverImage(maneuverType: .continue, maneuverModifier: .uTurn) + .frame(width: 32) + } } func testManeuverImageLarge() { diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverImageTests/testManeuverImageDefaultTheme.3.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverImageTests/testManeuverImageDefaultTheme.3.png new file mode 100644 index 0000000000000000000000000000000000000000..af60e69680d7936950fcee1164570777dfeedec4 GIT binary patch literal 81223 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 literal 0 HcmV?d00001 diff --git a/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverImageTests/testManeuverImageDefaultTheme.4.png b/apple/Tests/FerrostarSwiftUITests/Views/__Snapshots__/ManeuverImageTests/testManeuverImageDefaultTheme.4.png new file mode 100644 index 0000000000000000000000000000000000000000..a16ebacede5e0558df721b277dccc7300d69daa0 GIT binary patch literal 81329 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 YxApWK--B@s4D#bSd(o^^4@&fZ0hYv`*Z=?k literal 0 HcmV?d00001 From 71622fdbaa236d7125a06d71a18ce9dd53f10c6d Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Sat, 19 Oct 2024 17:52:36 +0900 Subject: [PATCH 27/31] Add TODO docs --- .../maplibreui/views/DynamicallyOrientingNavigationView.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt index c9e37abf2..9beb41b27 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/views/DynamicallyOrientingNavigationView.kt @@ -47,7 +47,9 @@ import com.stadiamaps.ferrostar.maplibreui.views.overlays.PortraitNavigationOver * route line. * @param config The configuration for the navigation view. * @param onTapExit The callback to invoke when the exit button is tapped. - * @param userContent TODO docs + * @param userContent Any composable with additional content to render. The most common use of this + * parameter is to display custom UI when there is no navigation in progress. See the demo app for + * an example that adds a search box. * @param mapContent Any additional composable map symbol content to render. */ @Composable From 7510aa88af8b6a03f55721fae7c697dcf9004998 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Sat, 19 Oct 2024 18:32:09 +0900 Subject: [PATCH 28/31] Fix the navigation camera (re-)application logic --- .../ferrostar/core/NavigationViewModel.kt | 3 +++ .../ferrostar/maplibreui/NavigationMapView.kt | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index eb14bdba9..7893c9da8 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -82,6 +82,9 @@ interface NavigationViewModel { fun stopNavigation() fun isNavigating(): Boolean = uiState.value.progress != null + + // TODO: We think the camera may eventually need to be owned by the view model, but that's going + // to be a very big refactor (maybe even crossing into the MapLibre Compose project) } class IdleNavigationViewModel(locationProvider: LocationProvider) : diff --git a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt index fdf238d00..9954d4cd3 100644 --- a/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt +++ b/android/maplibreui/src/main/java/com/stadiamaps/ferrostar/maplibreui/NavigationMapView.kt @@ -28,7 +28,8 @@ import com.stadiamaps.ferrostar.maplibreui.runtime.navigationMapViewCamera * @param camera The bi-directional camera state to use for the map. Note: this is a bit * non-standard as far as normal compose patterns go, but we independently came up with this * approach and later verified that Google Maps does the same thing in their compose SDK. - * @param navigationCamera The default camera settings to use when navigation starts. + * @param navigationCamera The default camera settings to use when navigation starts. This will be + * re-applied to the camera any time that navigation is started. * @param viewModel The navigation view model provided by Ferrostar Core. * @param locationRequestProperties The location request properties to use for the map's location * engine. @@ -55,6 +56,17 @@ fun NavigationMapView( ) { val uiState = viewModel.uiState.collectAsState() + // TODO: This works for now, but in the end, the view model may need to "own" the camera. + // We can move this code if we do such a refactor. + var isNavigating = remember { viewModel.isNavigating() } + if (viewModel.isNavigating() != isNavigating) { + isNavigating = viewModel.isNavigating() + + if (isNavigating) { + camera.value = navigationCamera + } + } + val locationEngine = remember { StaticLocationEngine() } locationEngine.lastLocation = uiState.value.let { state -> From 56cb38f03fd12c1fdee8f49f56bae8274cd83c72 Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Tue, 22 Oct 2024 15:16:14 +0900 Subject: [PATCH 29/31] Cleanup --- .../ferrostar/core/NavigationViewModel.kt | 55 ----------------- .../com/stadiamaps/ferrostar/AppModule.kt | 6 +- .../ferrostar/DemoNavigationScene.kt | 40 ++++-------- .../ferrostar/DemoNavigationViewModel.kt | 61 +++++++++++++++++++ .../com/stadiamaps/ferrostar/MainActivity.kt | 1 + 5 files changed, 80 insertions(+), 83 deletions(-) create mode 100644 android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt index 7893c9da8..25c7883a4 100644 --- a/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/NavigationViewModel.kt @@ -6,15 +6,11 @@ import androidx.lifecycle.viewModelScope import com.stadiamaps.ferrostar.core.extensions.deviation import com.stadiamaps.ferrostar.core.extensions.progress import com.stadiamaps.ferrostar.core.extensions.visualInstruction -import java.util.concurrent.Executors -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update import uniffi.ferrostar.GeographicCoordinate -import uniffi.ferrostar.Heading import uniffi.ferrostar.RouteDeviation import uniffi.ferrostar.SpokenInstruction import uniffi.ferrostar.TripProgress @@ -87,57 +83,6 @@ interface NavigationViewModel { // to be a very big refactor (maybe even crossing into the MapLibre Compose project) } -class IdleNavigationViewModel(locationProvider: LocationProvider) : - ViewModel(), NavigationViewModel { - private val locationStateFlow = MutableStateFlow(locationProvider.lastLocation) - private val executor = Executors.newSingleThreadScheduledExecutor() - - init { - locationProvider.addListener( - object : LocationUpdateListener { - override fun onLocationUpdated(location: UserLocation) { - locationStateFlow.update { location } - } - - override fun onHeadingUpdated(heading: Heading) { - // TODO: Heading - } - }, - executor) - } - - override val uiState = - locationStateFlow - .map { userLocation -> - // TODO: Heading - NavigationUiState(userLocation, null, null, null, null, null, null, false, null, null) - } - .stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(), - // TODO: Heading - initialValue = - NavigationUiState( - locationProvider.lastLocation, - null, - null, - null, - null, - null, - null, - false, - null, - null)) - - override fun toggleMute() { - // Do nothing - } - - override fun stopNavigation() { - // Do nothing - } -} - class DefaultNavigationViewModel( private val ferrostarCore: FerrostarCore, private val spokenInstructionObserver: SpokenInstructionObserver? = null, diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt index 25ca34451..b3b3278e6 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt @@ -74,7 +74,7 @@ object AppModule { val core = FerrostarCore( valhallaEndpointURL = valhallaEndpointUrl, - profile = "low_speed_vehicle", + profile = "auto", httpClient = httpClient, locationProvider = locationProvider, foregroundServiceManager = foregroundServiceManager, @@ -87,6 +87,10 @@ object AppModule { options = mapOf( "costingOptions" to + // Just an example... You can set multiple costing options for any profile + // in Valhalla. + // If your app uses multiple routing modes, you can have a master settings + // map, or construct a new one each time. mapOf( "low_speed_vehicle" to mapOf( diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt index 7821c47f2..cfa857352 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationScene.kt @@ -28,7 +28,6 @@ import com.stadiamaps.autocomplete.center import com.stadiamaps.ferrostar.composeui.runtime.KeepScreenOnDisposableEffect import com.stadiamaps.ferrostar.composeui.views.gridviews.InnerGridView import com.stadiamaps.ferrostar.core.AndroidSystemLocationProvider -import com.stadiamaps.ferrostar.core.IdleNavigationViewModel import com.stadiamaps.ferrostar.core.LocationProvider import com.stadiamaps.ferrostar.core.NavigationViewModel import com.stadiamaps.ferrostar.core.SimulatedLocationProvider @@ -53,9 +52,10 @@ fun DemoNavigationScene( // Keeps the screen on at consistent brightness while this Composable is in the view hierarchy. KeepScreenOnDisposableEffect() - var viewModel by remember { - mutableStateOf(IdleNavigationViewModel(locationProvider)) - } + // NOTE: We are aware that this is not a particularly great pattern. + // We are working on improving this. See the discussion on + // https://github.com/stadiamaps/ferrostar/pull/295. + var viewModel by remember { mutableStateOf(DemoNavigationViewModel()) } val scope = rememberCoroutineScope() // Get location permissions. @@ -73,22 +73,6 @@ fun DemoNavigationScene( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION) } - // val locationUpdateListener = remember { - // object : LocationUpdateListener { - // private var _lastLocation: MutableStateFlow = MutableStateFlow(null) - // val userLocation = _lastLocation.asStateFlow() - // - // override fun onLocationUpdated(location: UserLocation) { - // _lastLocation.value = location - // } - // - // override fun onHeadingUpdated(heading: Heading) { - // // TODO - // } - // } - // } - - // val lastLocation = locationUpdateListener.userLocation.collectAsState(scope.coroutineContext) val vmState = viewModel.uiState.collectAsState(scope.coroutineContext) val permissionsLauncher = @@ -96,20 +80,19 @@ fun DemoNavigationScene( permissions -> when { permissions.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false) -> { - if (locationProvider is AndroidSystemLocationProvider || - locationProvider is FusedLocationProvider) { - // FIXME - // locationProvider.addListener(locationUpdateListener, executor) + val vm = viewModel + if ((locationProvider is AndroidSystemLocationProvider || + locationProvider is FusedLocationProvider) && vm is DemoNavigationViewModel) { + // Activate location updates in the view model + vm.startLocationUpdates(locationProvider) } } permissions.getOrDefault(Manifest.permission.ACCESS_COARSE_LOCATION, false) -> { // TODO: Probably alert the user that this is unusable for navigation - // onAccess() } // TODO: Foreground service permissions; we should block access until approved on API 34+ else -> { // TODO - // onFailed() } } } @@ -141,7 +124,10 @@ fun DemoNavigationScene( snapUserLocationToRoute = false, onTapExit = { viewModel.stopNavigation() - viewModel = IdleNavigationViewModel(locationProvider) + val vm = DemoNavigationViewModel() + viewModel = vm + + vm.startLocationUpdates(locationProvider) }, userContent = { modifier -> if (!viewModel.isNavigating()) { diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt new file mode 100644 index 000000000..558382c7e --- /dev/null +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt @@ -0,0 +1,61 @@ +package com.stadiamaps.ferrostar + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.stadiamaps.ferrostar.core.LocationProvider +import com.stadiamaps.ferrostar.core.LocationUpdateListener +import com.stadiamaps.ferrostar.core.NavigationUiState +import com.stadiamaps.ferrostar.core.NavigationViewModel +import java.util.concurrent.Executors +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import uniffi.ferrostar.Heading +import uniffi.ferrostar.UserLocation + +// NOTE: We are aware that this is not a particularly great ViewModel. +// We are working on improving this. See the discussion on +// https://github.com/stadiamaps/ferrostar/pull/295. +class DemoNavigationViewModel : ViewModel(), NavigationViewModel { + private val locationStateFlow = MutableStateFlow(null) + private val executor = Executors.newSingleThreadScheduledExecutor() + + fun startLocationUpdates(locationProvider: LocationProvider) { + locationStateFlow.update { locationProvider.lastLocation } + locationProvider.addListener( + object : LocationUpdateListener { + override fun onLocationUpdated(location: UserLocation) { + locationStateFlow.update { location } + } + + override fun onHeadingUpdated(heading: Heading) { + // TODO: Heading + } + }, + executor) + } + + override val uiState = + locationStateFlow + .map { userLocation -> + // TODO: Heading + NavigationUiState(userLocation, null, null, null, null, null, null, false, null, null) + } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(), + // TODO: Heading + initialValue = + NavigationUiState(null, null, null, null, null, null, null, false, null, null)) + + override fun toggleMute() { + // Do nothing + } + + override fun stopNavigation() { + // Do nothing + } +} + diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt index a568b21d8..1d039aa48 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/MainActivity.kt @@ -66,6 +66,7 @@ class MainActivity : ComponentActivity(), AndroidTtsStatusListener { // TTS listener methods override fun onTtsInitialized(tts: TextToSpeech?, status: Int) { + // Set this up as appropriate for your app if (tts != null) { tts.setLanguage(Locale.US) android.util.Log.i(TAG, "setLanguage status: $status") From b9211cbaa4ad9399d3e2f098647bc75a5406ffed Mon Sep 17 00:00:00 2001 From: Ian Wagner Date: Tue, 22 Oct 2024 15:40:52 +0900 Subject: [PATCH 30/31] Remove old comment --- .../demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt index b3b3278e6..2bcd2ded6 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/AppModule.kt @@ -69,7 +69,6 @@ object AppModule { FerrostarForegroundServiceManager(appContext, DefaultForegroundNotificationBuilder(appContext)) } - // TODO: This is hard-coded for golf cart routing; change to something else before merging val ferrostarCore: FerrostarCore by lazy { val core = FerrostarCore( From f80c4945aff317e1b6e499af7d5df3590cf1a583 Mon Sep 17 00:00:00 2001 From: ianthetechie Date: Tue, 22 Oct 2024 06:45:08 +0000 Subject: [PATCH 31/31] Apply automatic changes --- .../java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt index 558382c7e..7d10ac2af 100644 --- a/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt +++ b/android/demo-app/src/main/java/com/stadiamaps/ferrostar/DemoNavigationViewModel.kt @@ -58,4 +58,3 @@ class DemoNavigationViewModel : ViewModel(), NavigationViewModel { // Do nothing } } -