diff --git a/android/build.gradle b/android/build.gradle index af43afe9..bd71aee5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -25,5 +25,5 @@ publishing { allprojects { group = "com.stadiamaps.ferrostar" - version = "0.6.1" + version = "0.7.0" } diff --git a/android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/RouteExtensions.kt b/android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/RouteExtensions.kt new file mode 100644 index 00000000..b5124cb6 --- /dev/null +++ b/android/core/src/main/java/com/stadiamaps/ferrostar/core/extensions/RouteExtensions.kt @@ -0,0 +1,23 @@ +package com.stadiamaps.ferrostar.core.extensions + +import uniffi.ferrostar.Route +import uniffi.ferrostar.createRouteFromOsrm + +/** + * Create a [Route] from OSRM route and waypoint data. + * + * This behavior uses the same internal decoders as the OsrmResponseParser. This function will + * automatically map & combine via and break waypoints from the route and waypoint data objects. + * + * @param route The encoded JSON data for the OSRM route. + * @param waypoints The encoded JSON data for the OSRM waypoints. + * @param polylinePrecision The polyline precision. + * @return The navigation [Route] + */ +fun Route.Companion.fromOsrm( + route: ByteArray, + waypoints: ByteArray, + polylinePrecision: UInt +): Route { + return createRouteFromOsrm(routeData = route, waypointData = waypoints, polylinePrecision) +} diff --git a/apple/Sources/FerrostarCore/Extensions/CoreLocation Extensions.swift b/apple/Sources/FerrostarCore/Extensions/CoreLocationExtensions.swift similarity index 100% rename from apple/Sources/FerrostarCore/Extensions/CoreLocation Extensions.swift rename to apple/Sources/FerrostarCore/Extensions/CoreLocationExtensions.swift diff --git a/apple/Sources/FerrostarCore/Extensions/RouteExtensions.swift b/apple/Sources/FerrostarCore/Extensions/RouteExtensions.swift new file mode 100644 index 00000000..5ab9daf5 --- /dev/null +++ b/apple/Sources/FerrostarCore/Extensions/RouteExtensions.swift @@ -0,0 +1,21 @@ +import FerrostarCoreFFI +import Foundation + +public extension Route { + /// Create a new Route directly from an OSRM route. + /// + /// This behavior uses the same internal decoders as the OsrmResponseParser. This function will + /// automatically map & combine via and break waypoints from the route and waypoint data objects. + /// + /// - Parameters: + /// - route: The encoded JSON data for the OSRM route. + /// - waypoints: The encoded JSON data for the OSRM waypoints. + /// - precision: The polyline precision. + static func initFromOsrm(route: Data, waypoints: Data, polylinePrecision: UInt32) throws -> Route { + try createRouteFromOsrm(routeData: route, waypointData: waypoints, polylinePrecision: polylinePrecision) + } + + func getPolyline(precision: UInt32) throws -> String { + try getRoutePolyline(route: self, precision: precision) + } +} diff --git a/apple/Sources/FerrostarCore/Models/ModelWrappers.swift b/apple/Sources/FerrostarCore/Models/ModelWrappers.swift index bc536fee..d998a1ae 100644 --- a/apple/Sources/FerrostarCore/Models/ModelWrappers.swift +++ b/apple/Sources/FerrostarCore/Models/ModelWrappers.swift @@ -2,12 +2,6 @@ import CoreLocation import FerrostarCoreFFI import Foundation -public extension Route { - func getPolyline(precision: UInt32) throws -> String { - try getRoutePolyline(route: self, precision: precision) - } -} - private class DetectorImpl: RouteDeviationDetector { let detectorFunc: (UserLocation, Route, RouteStep) -> RouteDeviation diff --git a/apple/Sources/UniFFI/ferrostar.swift b/apple/Sources/UniFFI/ferrostar.swift index 3596601c..e577a522 100644 --- a/apple/Sources/UniFFI/ferrostar.swift +++ b/apple/Sources/UniFFI/ferrostar.swift @@ -799,7 +799,7 @@ open class RouteAdapter: } open func parseResponse(response: Data) throws -> [Route] { - try FfiConverterSequenceTypeRoute.lift(rustCallWithError(FfiConverterTypeRoutingResponseParseError.lift) { + try FfiConverterSequenceTypeRoute.lift(rustCallWithError(FfiConverterTypeParsingError.lift) { uniffi_ferrostar_fn_method_routeadapter_parse_response(self.uniffiClonePointer(), FfiConverterData.lower(response), $0) }) @@ -1284,7 +1284,7 @@ open class RouteResponseParserImpl: * as this works for all currently conceivable formats (JSON, PBF, etc.). */ open func parseResponse(response: Data) throws -> [Route] { - try FfiConverterSequenceTypeRoute.lift(rustCallWithError(FfiConverterTypeRoutingResponseParseError.lift) { + try FfiConverterSequenceTypeRoute.lift(rustCallWithError(FfiConverterTypeParsingError.lift) { uniffi_ferrostar_fn_method_routeresponseparser_parse_response(self.uniffiClonePointer(), FfiConverterData.lower(response), $0) }) @@ -1318,7 +1318,7 @@ private enum UniffiCallbackInterfaceRouteResponseParser { callStatus: uniffiCallStatus, makeCall: makeCall, writeReturn: writeReturn, - lowerError: FfiConverterTypeRoutingResponseParseError.lower + lowerError: FfiConverterTypeParsingError.lower ) }, uniffiFree: { (uniffiHandle: UInt64) in @@ -2875,6 +2875,47 @@ extension ModelError: Foundation.LocalizedError { } } +public enum ParsingError { + case ParseError(error: String) + case UnknownError +} + +public struct FfiConverterTypeParsingError: FfiConverterRustBuffer { + typealias SwiftType = ParsingError + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ParsingError { + let variant: Int32 = try readInt(&buf) + switch variant { + case 1: return try .ParseError( + error: FfiConverterString.read(from: &buf) + ) + + case 2: return .UnknownError + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: ParsingError, into buf: inout [UInt8]) { + switch value { + case let .ParseError(error): + writeInt(&buf, Int32(1)) + FfiConverterString.write(error, into: &buf) + + case .UnknownError: + writeInt(&buf, Int32(2)) + } + } +} + +extension ParsingError: Equatable, Hashable {} + +extension ParsingError: Foundation.LocalizedError { + public var errorDescription: String? { + String(reflecting: self) + } +} + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. /** @@ -3100,47 +3141,6 @@ extension RoutingRequestGenerationError: Foundation.LocalizedError { } } -public enum RoutingResponseParseError { - case ParseError(error: String) - case UnknownError -} - -public struct FfiConverterTypeRoutingResponseParseError: FfiConverterRustBuffer { - typealias SwiftType = RoutingResponseParseError - - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RoutingResponseParseError { - let variant: Int32 = try readInt(&buf) - switch variant { - case 1: return try .ParseError( - error: FfiConverterString.read(from: &buf) - ) - - case 2: return .UnknownError - - default: throw UniffiInternalError.unexpectedEnumCase - } - } - - public static func write(_ value: RoutingResponseParseError, into buf: inout [UInt8]) { - switch value { - case let .ParseError(error): - writeInt(&buf, Int32(1)) - FfiConverterString.write(error, into: &buf) - - case .UnknownError: - writeInt(&buf, Int32(2)) - } - } -} - -extension RoutingResponseParseError: Equatable, Hashable {} - -extension RoutingResponseParseError: Foundation.LocalizedError { - public var errorDescription: String? { - String(reflecting: self) - } -} - public enum SimulationError { /** * Errors decoding the polyline string. @@ -3879,6 +3879,23 @@ public func createOsrmResponseParser(polylinePrecision: UInt32) -> RouteResponse }) } +/** + * Creates a [`Route`] from OSRM data. + * + * This uses the same logic as the [`OsrmResponseParser`] and is designed to be fairly flexible, + * supporting both vanilla OSRM and enhanced Valhalla (ex: from Stadia Maps and Mapbox) outputs + * which contain richer information like banners and voice instructions for navigation. + */ +public func createRouteFromOsrm(routeData: Data, waypointData: Data, polylinePrecision: UInt32) throws -> Route { + try FfiConverterTypeRoute.lift(rustCallWithError(FfiConverterTypeParsingError.lift) { + uniffi_ferrostar_fn_func_create_route_from_osrm( + FfiConverterData.lower(routeData), + FfiConverterData.lower(waypointData), + FfiConverterUInt32.lower(polylinePrecision), $0 + ) + }) +} + /** * Creates a [`RouteRequestGenerator`] * which generates requests to an arbitrary Valhalla server (using the OSRM response format). @@ -3980,6 +3997,9 @@ private var initializationResult: InitializationResult = { if uniffi_ferrostar_checksum_func_create_osrm_response_parser() != 16550 { return InitializationResult.apiChecksumMismatch } + if uniffi_ferrostar_checksum_func_create_route_from_osrm() != 42270 { + return InitializationResult.apiChecksumMismatch + } if uniffi_ferrostar_checksum_func_create_valhalla_request_generator() != 62919 { return InitializationResult.apiChecksumMismatch } @@ -4007,7 +4027,7 @@ private var initializationResult: InitializationResult = { if uniffi_ferrostar_checksum_method_routeadapter_generate_request() != 59034 { return InitializationResult.apiChecksumMismatch } - if uniffi_ferrostar_checksum_method_routeadapter_parse_response() != 47311 { + if uniffi_ferrostar_checksum_method_routeadapter_parse_response() != 34481 { return InitializationResult.apiChecksumMismatch } if uniffi_ferrostar_checksum_method_routedeviationdetector_check_route_deviation() != 50476 { @@ -4016,7 +4036,7 @@ private var initializationResult: InitializationResult = { if uniffi_ferrostar_checksum_method_routerequestgenerator_generate_request() != 63458 { return InitializationResult.apiChecksumMismatch } - if uniffi_ferrostar_checksum_method_routeresponseparser_parse_response() != 38851 { + if uniffi_ferrostar_checksum_method_routeresponseparser_parse_response() != 44735 { return InitializationResult.apiChecksumMismatch } if uniffi_ferrostar_checksum_constructor_navigationcontroller_new() != 60881 { diff --git a/common/Cargo.lock b/common/Cargo.lock index 6c4df148..67f07b9c 100644 --- a/common/Cargo.lock +++ b/common/Cargo.lock @@ -328,7 +328,7 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "ferrostar" -version = "0.6.1" +version = "0.7.0" dependencies = [ "assert-json-diff", "geo", diff --git a/common/ferrostar/Cargo.toml b/common/ferrostar/Cargo.toml index f388eefc..8b0bdb2b 100644 --- a/common/ferrostar/Cargo.toml +++ b/common/ferrostar/Cargo.toml @@ -2,7 +2,7 @@ lints.workspace = true [package] name = "ferrostar" -version = "0.6.1" +version = "0.7.0" readme = "README.md" description = "The core of modern turn-by-turn navigation." keywords = ["navigation", "routing", "valhalla", "osrm"] @@ -19,14 +19,20 @@ rust-version.workspace = true alloc = [] std = ["alloc", "serde_json/std", "proptest/std"] default = ["std", "uniffi"] -wasm_js = ["std", "getrandom/js", "serde-wasm-bindgen", "wasm-bindgen", "web-time"] +wasm_js = [ + "std", + "getrandom/js", + "serde-wasm-bindgen", + "wasm-bindgen", + "web-time", +] [dependencies] geo = "0.28.0" polyline = "0.11.0" serde = { version = "1.0.162", features = ["derive"] } serde_json = { version = "1.0.117", default-features = false } -serde-wasm-bindgen = { version = "0.6.5", optional = true} +serde-wasm-bindgen = { version = "0.6.5", optional = true } thiserror = "1.0.40" uniffi = { workspace = true, optional = true } uuid = { version = "1.8.0", features = ["v4", "serde"] } diff --git a/common/ferrostar/src/lib.rs b/common/ferrostar/src/lib.rs index e415bf30..4680ebec 100644 --- a/common/ferrostar/src/lib.rs +++ b/common/ferrostar/src/lib.rs @@ -25,9 +25,15 @@ pub mod navigation_controller; pub mod routing_adapters; pub mod simulation; +use models::Route; #[cfg(feature = "uniffi")] use routing_adapters::{ - error::InstantiationError, osrm::OsrmResponseParser, valhalla::ValhallaHttpRequestGenerator, + error::{InstantiationError, ParsingError}, + osrm::{ + models::{Route as OsrmRoute, Waypoint as OsrmWaypoint}, + OsrmResponseParser, + }, + valhalla::ValhallaHttpRequestGenerator, RouteRequestGenerator, RouteResponseParser, }; #[cfg(feature = "uniffi")] @@ -92,3 +98,22 @@ fn create_valhalla_request_generator( fn create_osrm_response_parser(polyline_precision: u32) -> Arc { Arc::new(OsrmResponseParser::new(polyline_precision)) } + +// MARK: OSRM Route Conversion + +/// Creates a [`Route`] from OSRM data. +/// +/// This uses the same logic as the [`OsrmResponseParser`] and is designed to be fairly flexible, +/// supporting both vanilla OSRM and enhanced Valhalla (ex: from Stadia Maps and Mapbox) outputs +/// which contain richer information like banners and voice instructions for navigation. +#[cfg(feature = "uniffi")] +#[uniffi::export] +fn create_route_from_osrm( + route_data: Vec, + waypoint_data: Vec, + polyline_precision: u32, +) -> Result { + let route: OsrmRoute = serde_json::from_slice(&route_data)?; + let waypoints: Vec = serde_json::from_slice(&waypoint_data)?; + return Route::from_osrm(&route, &waypoints, polyline_precision); +} diff --git a/common/ferrostar/src/routing_adapters/error.rs b/common/ferrostar/src/routing_adapters/error.rs index a31a084a..5266dd6a 100644 --- a/common/ferrostar/src/routing_adapters/error.rs +++ b/common/ferrostar/src/routing_adapters/error.rs @@ -53,7 +53,7 @@ impl From for RoutingRequestGenerationError { #[derive(Debug)] #[cfg_attr(feature = "std", derive(thiserror::Error))] #[cfg_attr(feature = "uniffi", derive(uniffi::Error))] -pub enum RoutingResponseParseError { +pub enum ParsingError { // TODO: Unable to find route and other common errors #[cfg_attr(feature = "std", error("Failed to parse route response: {error}."))] ParseError { error: String }, @@ -65,15 +65,15 @@ pub enum RoutingResponseParseError { } #[cfg(feature = "uniffi")] -impl From for RoutingResponseParseError { - fn from(_: uniffi::UnexpectedUniFFICallbackError) -> RoutingResponseParseError { - RoutingResponseParseError::UnknownError +impl From for ParsingError { + fn from(_: uniffi::UnexpectedUniFFICallbackError) -> ParsingError { + ParsingError::UnknownError } } -impl From for RoutingResponseParseError { +impl From for ParsingError { fn from(e: serde_json::Error) -> Self { - RoutingResponseParseError::ParseError { + ParsingError::ParseError { error: e.to_string(), } } diff --git a/common/ferrostar/src/routing_adapters/mod.rs b/common/ferrostar/src/routing_adapters/mod.rs index 521b31bf..ccdd7402 100644 --- a/common/ferrostar/src/routing_adapters/mod.rs +++ b/common/ferrostar/src/routing_adapters/mod.rs @@ -35,7 +35,7 @@ use crate::models::Waypoint; use crate::models::{Route, UserLocation}; use crate::routing_adapters::error::InstantiationError; -use error::{RoutingRequestGenerationError, RoutingResponseParseError}; +use error::{ParsingError, RoutingRequestGenerationError}; #[cfg(all(not(feature = "std"), feature = "alloc"))] use alloc::collections::BTreeMap as HashMap; @@ -102,7 +102,7 @@ pub trait RouteResponseParser: Send + Sync { /// /// We use a sequence of octets as a common interchange format. /// as this works for all currently conceivable formats (JSON, PBF, etc.). - fn parse_response(&self, response: Vec) -> Result, RoutingResponseParseError>; + fn parse_response(&self, response: Vec) -> Result, ParsingError>; } /// The route adapter bridges between the common core and a routing backend where interaction takes place @@ -172,10 +172,7 @@ impl RouteAdapter { .generate_request(user_location, waypoints) } - pub fn parse_response( - &self, - response: Vec, - ) -> Result, RoutingResponseParseError> { + pub fn parse_response(&self, response: Vec) -> Result, ParsingError> { self.response_parser.parse_response(response) } } diff --git a/common/ferrostar/src/routing_adapters/osrm/mod.rs b/common/ferrostar/src/routing_adapters/osrm/mod.rs index bee04cee..06b63895 100644 --- a/common/ferrostar/src/routing_adapters/osrm/mod.rs +++ b/common/ferrostar/src/routing_adapters/osrm/mod.rs @@ -8,8 +8,10 @@ use crate::models::{ VisualInstructionContent, Waypoint, WaypointKind, }; use crate::routing_adapters::{ - osrm::models::{RouteResponse, RouteStep as OsrmRouteStep}, - Route, RoutingResponseParseError, + osrm::models::{ + Route as OsrmRoute, RouteResponse, RouteStep as OsrmRouteStep, Waypoint as OsrmWaypoint, + }, + ParsingError, Route, }; #[cfg(all(not(feature = "std"), feature = "alloc"))] use alloc::{collections::BTreeSet as HashSet, string::ToString, vec, vec::Vec}; @@ -35,21 +37,30 @@ impl OsrmResponseParser { } impl RouteResponseParser for OsrmResponseParser { - fn parse_response(&self, response: Vec) -> Result, RoutingResponseParseError> { + fn parse_response(&self, response: Vec) -> Result, ParsingError> { let res: RouteResponse = serde_json::from_slice(&response)?; - let via_waypoint_indices: HashSet<_> = res + + return res .routes .iter() - .flat_map(|route| { - route - .legs - .iter() - .flat_map(|leg| leg.via_waypoints.iter().map(|via| via.waypoint_index)) - }) + .map(|route| Route::from_osrm(route, &res.waypoints, self.polyline_precision)) + .collect::, _>>(); + } +} + +impl Route { + pub fn from_osrm( + route: &OsrmRoute, + waypoints: &Vec, + polyline_precision: u32, + ) -> Result { + let via_waypoint_indices: Vec<_> = route + .legs + .iter() + .flat_map(|leg| leg.via_waypoints.iter().map(|via| via.waypoint_index)) .collect(); - let waypoints: Vec<_> = res - .waypoints + let waypoints: Vec<_> = waypoints .iter() .enumerate() .map(|(idx, waypoint)| Waypoint { @@ -65,50 +76,45 @@ impl RouteResponseParser for OsrmResponseParser { }) .collect(); - // This isn't the most functional in style, but it's a bit difficult to construct a pipeline - // today. Stabilization of try_collect may help. - let mut routes = vec![]; - for route in res.routes { - let linestring = - decode_polyline(&route.geometry, self.polyline_precision).map_err(|error| { - RoutingResponseParseError::ParseError { - error: error.to_string(), - } - })?; - if let Some(bbox) = linestring.bounding_rect() { - let geometry = linestring - .coords() - .map(|coord| GeographicCoordinate::from(*coord)) - .collect(); - - let mut steps = vec![]; - for leg in route.legs { - for step in leg.steps { - steps.push(RouteStep::from_osrm(&step, self.polyline_precision)?); - } - } - - routes.push(Route { - geometry, - bbox: bbox.into(), - distance: route.distance, - waypoints: waypoints.clone(), - steps, - }); + let linestring = decode_polyline(&route.geometry, polyline_precision).map_err(|error| { + ParsingError::ParseError { + error: error.to_string(), } + })?; + if let Some(bbox) = linestring.bounding_rect() { + let geometry = linestring + .coords() + .map(|coord| GeographicCoordinate::from(*coord)) + .collect(); + + let steps = route + .legs + .iter() + .flat_map(|leg| { + leg.steps + .iter() + .map(|step| RouteStep::from_osrm(step, polyline_precision)) + }) + .collect::, _>>()?; + Ok(Route { + geometry, + bbox: bbox.into(), + distance: route.distance, + waypoints: waypoints.clone(), + steps, + }) + } else { + Err(ParsingError::ParseError { + error: "Bounding box could not be calculated".to_string(), + }) } - - Ok(routes) } } impl RouteStep { - fn from_osrm( - value: &OsrmRouteStep, - polyline_precision: u32, - ) -> Result { + fn from_osrm(value: &OsrmRouteStep, polyline_precision: u32) -> Result { let linestring = decode_polyline(&value.geometry, polyline_precision).map_err(|error| { - RoutingResponseParseError::ParseError { + ParsingError::ParseError { error: error.to_string(), } })?;