-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Feat: annotation parsing on iOS * Feat: annotation parsing on iOS * Feat: annotation parsing on iOS * Finalized annotation processing for speed limit * Finalized annotation processing for speed limit * Finalized annotation processing for speed limit * Finalized annotation processing for speed limit * Applied swiftformat * removed no longer needed cargo maxspeed mps tests * removed no longer needed cargo maxspeed mps tests * removed no longer needed cargo maxspeed mps tests * removed no longer needed cargo maxspeed mps tests * removed no longer needed cargo maxspeed mps tests * removed no longer needed cargo maxspeed mps tests * Apply suggestions from code review Co-authored-by: Ian Wagner <ian.wagner@stadiamaps.com> * Improvements and testing from PR reivew * Improvements and testing from PR reivew * Improvements and testing from PR reivew --------- Co-authored-by: Ian Wagner <ian.wagner@stadiamaps.com>
- Loading branch information
1 parent
f434b17
commit 14b41be
Showing
27 changed files
with
623 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,5 +17,5 @@ ext { | |
|
||
allprojects { | ||
group = "com.stadiamaps.ferrostar" | ||
version = "0.19.0" | ||
version = "0.20.0" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
77 changes: 77 additions & 0 deletions
77
apple/Sources/FerrostarCore/Annotations/AnnotationPublisher.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import Combine | ||
import Foundation | ||
|
||
/// A generic implementation of the annotation publisher. | ||
/// To allow dynamic specialization in the core ``FerrostarCore/FerrostarCore/init(routeProvider:locationProvider:navigationControllerConfig:networkSession:annotation:)`` | ||
public protocol AnnotationPublishing { | ||
associatedtype Annotation: Decodable | ||
|
||
var currentValue: Annotation? { get } | ||
var speedLimit: Measurement<UnitSpeed>? { get } | ||
|
||
func configure(_ navigationState: Published<NavigationState?>.Publisher) | ||
} | ||
|
||
/// A class that publishes the decoded annotation object off of ``FerrostarCore``'s | ||
/// ``NavigationState`` publisher. | ||
public class AnnotationPublisher<Annotation: Decodable>: ObservableObject, AnnotationPublishing { | ||
@Published public var currentValue: Annotation? | ||
@Published public var speedLimit: Measurement<UnitSpeed>? | ||
|
||
private let mapSpeedLimit: ((Annotation?) -> Measurement<UnitSpeed>?)? | ||
private let decoder: JSONDecoder | ||
private let onError: (Error) -> Void | ||
private var cancellables = Set<AnyCancellable>() | ||
|
||
/// Create a new annotation publisher with an instance of ``FerrostarCore`` | ||
/// | ||
/// - Parameters: | ||
/// - mapSpeedLimit: Extract and convert the annotation types speed limit (if one exists). | ||
/// - onError: A closure to run any time a `DecoderError` occurs. | ||
/// - decoder: Specify a custom JSONDecoder if desired. | ||
public init( | ||
mapSpeedLimit: ((Annotation?) -> Measurement<UnitSpeed>?)? = nil, | ||
onError: @escaping (Error) -> Void = { _ in }, | ||
decoder: JSONDecoder = JSONDecoder() | ||
) { | ||
self.mapSpeedLimit = mapSpeedLimit | ||
self.onError = onError | ||
self.decoder = decoder | ||
} | ||
|
||
/// Configure the AnnotationPublisher to run off of a specific navigation state published value. | ||
/// | ||
/// - Parameter navigationState: Ferrostar's current navigation state. | ||
public func configure(_ navigationState: Published<NavigationState?>.Publisher) { | ||
// Important quote from Apple's Combine Docs @ | ||
// https://developer.apple.com/documentation/combine/just/assign(to:)#discussion: | ||
// | ||
// "The assign(to:) operator manages the life cycle of the subscription, canceling the subscription | ||
// automatically when the Published instance deinitializes. Because of this, the assign(to:) operator | ||
// doesn’t return an AnyCancellable that you’re responsible for like assign(to:on:) does." | ||
|
||
navigationState | ||
.map(decodeAnnotation) | ||
.receive(on: DispatchQueue.main) | ||
.assign(to: &$currentValue) | ||
|
||
if let mapSpeedLimit { | ||
$currentValue | ||
.map(mapSpeedLimit) | ||
.assign(to: &$speedLimit) | ||
} | ||
} | ||
|
||
func decodeAnnotation(_ state: NavigationState?) -> Annotation? { | ||
guard let data = state?.currentAnnotationJSON?.data(using: .utf8) else { | ||
return nil | ||
} | ||
|
||
do { | ||
return try decoder.decode(Annotation.self, from: data) | ||
} catch { | ||
onError(error) | ||
return nil | ||
} | ||
} | ||
} |
43 changes: 43 additions & 0 deletions
43
apple/Sources/FerrostarCore/Annotations/Models/ValhallaOSRMAnnotation.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import Foundation | ||
|
||
/// A Valhalla extended OSRM annotation object. | ||
/// | ||
/// Describes attributes about a segment of an edge between two points | ||
/// in a route step. | ||
public struct ValhallaExtendedOSRMAnnotation: Codable, Equatable, Hashable { | ||
enum CodingKeys: String, CodingKey { | ||
case speedLimit = "maxspeed" | ||
case speed | ||
case distance | ||
case duration | ||
} | ||
|
||
/// The speed limit of the segment. | ||
public let speedLimit: MaxSpeed? | ||
|
||
/// The estimated speed of travel for the segment, in meters per second. | ||
public let speed: Double? | ||
|
||
/// The distance in meters of the segment. | ||
public let distance: Double? | ||
|
||
/// The estimated time to traverse the segment, in seconds. | ||
public let duration: Double? | ||
} | ||
|
||
public extension AnnotationPublisher { | ||
/// Create a Valhalla extended OSRM annotation publisher | ||
/// | ||
/// - Parameter onError: An optional error closure (runs when a `DecoderError` occurs) | ||
/// - Returns: The annotation publisher. | ||
static func valhallaExtendedOSRM( | ||
onError: @escaping (Error) -> Void = { _ in } | ||
) -> AnnotationPublisher<ValhallaExtendedOSRMAnnotation> { | ||
AnnotationPublisher<ValhallaExtendedOSRMAnnotation>( | ||
mapSpeedLimit: { | ||
$0?.speedLimit?.measurementValue | ||
}, | ||
onError: onError | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import Foundation | ||
|
||
/// The OSRM formatted MaxSpeed. This is a custom field used by some API's like Mapbox, | ||
/// Valhalla with OSRM json output, etc. | ||
/// | ||
/// For more information see: | ||
/// - https://wiki.openstreetmap.org/wiki/Key:maxspeed | ||
/// - https://docs.mapbox.com/api/navigation/directions/#route-leg-object (search for `max_speed`) | ||
/// - https://valhalla.github.io/valhalla/speeds/#assignment-of-speeds-to-roadways | ||
public enum MaxSpeed: Codable, Equatable, Hashable { | ||
public enum Units: String, Codable { | ||
case kilometersPerHour = "km/h" | ||
case milesPerHour = "mph" | ||
case knots // "knots" are an option in core OSRM docs, though unsure if they're ever used in this context. | ||
} | ||
|
||
/// There is no speed limit (it's unlimited, e.g. German Autobahn) | ||
case noLimit | ||
|
||
/// The speed limit is not known. | ||
case unknown | ||
|
||
/// The speed limit is a known value and unit (this may be localized depending on the API). | ||
case speed(Double, unit: Units) | ||
|
||
enum CodingKeys: CodingKey { | ||
case none | ||
case unknown | ||
case speed | ||
case unit | ||
} | ||
|
||
public init(from decoder: any Decoder) throws { | ||
let container = try decoder.container(keyedBy: CodingKeys.self) | ||
|
||
if let none = try container.decodeIfPresent(Bool.self, forKey: .none), | ||
none == true | ||
{ | ||
// The speed configuration is `{none: true}` for unlimited. | ||
self = .noLimit | ||
} else if let unknown = try container.decodeIfPresent(Bool.self, forKey: .unknown), | ||
unknown == true | ||
{ | ||
// The speed configuration is `{unknown: true}` for unknown. | ||
self = .unknown | ||
} else if let value = try container.decodeIfPresent(Double.self, forKey: .speed), | ||
let unit = try container.decodeIfPresent(Units.self, forKey: .unit) | ||
{ | ||
// The speed is a known value with units. Some API's may localize, others only support a single unit. | ||
self = .speed(value, unit: unit) | ||
} else { | ||
throw DecodingError.dataCorrupted(.init( | ||
codingPath: decoder.codingPath, | ||
debugDescription: "Invalid MaxSpeed, see docstrings for reference links" | ||
)) | ||
} | ||
} | ||
|
||
public func encode(to encoder: any Encoder) throws { | ||
var container = encoder.container(keyedBy: CodingKeys.self) | ||
|
||
switch self { | ||
case .noLimit: | ||
try container.encode(true, forKey: .none) | ||
case .unknown: | ||
try container.encode(true, forKey: .unknown) | ||
case let .speed(value, unit: unit): | ||
try container.encode(value, forKey: .speed) | ||
try container.encode(unit, forKey: .unit) | ||
} | ||
} | ||
|
||
/// The MaxSpeed as a measurement | ||
public var measurementValue: Measurement<UnitSpeed>? { | ||
switch self { | ||
case .noLimit: .init(value: .infinity, unit: .kilometersPerHour) | ||
case .unknown: nil | ||
case let .speed(value, unit): | ||
switch unit { | ||
case .kilometersPerHour: | ||
.init(value: value, unit: .kilometersPerHour) | ||
case .milesPerHour: | ||
.init(value: value, unit: .milesPerHour) | ||
case .knots: | ||
.init(value: value, unit: .knots) | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.