Skip to content

Commit

Permalink
Improvements and testing from PR reivew
Browse files Browse the repository at this point in the history
  • Loading branch information
Archdoog committed Oct 28, 2024
1 parent 694923f commit de5a702
Show file tree
Hide file tree
Showing 17 changed files with 191 additions and 121 deletions.
2 changes: 1 addition & 1 deletion apple/DemoApp/Demo/DemoNavigationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct DemoNavigationView: View {
locationProvider: locationProvider,
navigationControllerConfig: config,
options: ["costing_options": ["bicycle": ["use_roads": 0.2]]],
annotation: AnnotationPublisher<ValhallaOSRMAnnotation>.valhallaOSRM()
annotation: AnnotationPublisher<ValhallaExtendedOSRMAnnotation>.valhallaExtendedOSRM()
)
// NOTE: Not all applications will need a delegate. Read the NavigationDelegate documentation for details.
ferrostarCore.delegate = navigationDelegate
Expand Down
14 changes: 10 additions & 4 deletions apple/Sources/FerrostarCore/Annotations/AnnotationPublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation
public protocol AnnotationPublishing {
associatedtype Annotation: Decodable

var value: Annotation? { get }
var currentValue: Annotation? { get }
var speedLimit: Measurement<UnitSpeed>? { get }

func configure(_ navigationState: Published<NavigationState?>.Publisher)
Expand All @@ -15,7 +15,7 @@ public protocol AnnotationPublishing {
/// A class that publishes the decoded annotation object off of ``FerrostarCore``'s
/// ``NavigationState`` publisher.
public class AnnotationPublisher<Annotation: Decodable>: ObservableObject, AnnotationPublishing {
@Published public var value: Annotation?
@Published public var currentValue: Annotation?
@Published public var speedLimit: Measurement<UnitSpeed>?

private let mapSpeedLimit: ((Annotation?) -> Measurement<UnitSpeed>?)?
Expand Down Expand Up @@ -43,13 +43,19 @@ public class AnnotationPublisher<Annotation: Decodable>: ObservableObject, Annot
///
/// - 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: &$value)
.assign(to: &$currentValue)

if let mapSpeedLimit {
$value
$currentValue
.map(mapSpeedLimit)
.assign(to: &$speedLimit)
}
Expand Down
6 changes: 3 additions & 3 deletions apple/Sources/FerrostarCore/Models/MaxSpeed.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public enum MaxSpeed: Codable, Equatable, Hashable {
none == true
{
// The speed configuration is `{none: true}` for unlimited.
self = .none
self = .noLimit
} else if let unknown = try container.decodeIfPresent(Bool.self, forKey: .unknown),
unknown == true
{
Expand All @@ -60,7 +60,7 @@ public enum MaxSpeed: Codable, Equatable, Hashable {
var container = encoder.container(keyedBy: CodingKeys.self)

switch self {
case .none:
case .noLimit:
try container.encode(true, forKey: .none)
case .unknown:
try container.encode(true, forKey: .unknown)
Expand All @@ -73,7 +73,7 @@ public enum MaxSpeed: Codable, Equatable, Hashable {
/// The MaxSpeed as a measurement
public var measurementValue: Measurement<UnitSpeed>? {
switch self {
case .none: .init(value: .infinity, unit: .kilometersPerHour)
case .noLimit: .init(value: .infinity, unit: .kilometersPerHour)
case .unknown: nil
case let .speed(value, unit):
switch unit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public struct DynamicallyOrientingNavigationView: View, CustomizableNavigatingIn
private let userLayers: [StyleLayerDefinition]

public var speedLimit: Measurement<UnitSpeed>?
public var speedLimitStyle: SpeedLimitView.SignageStyle = .viennaConvention
public var speedLimitStyle: SpeedLimitView.SignageStyle?

public var topCenter: (() -> AnyView)?
public var topTrailing: (() -> AnyView)?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public struct LandscapeNavigationView: View, CustomizableNavigatingInnerGridView
private let userLayers: [StyleLayerDefinition]

public var speedLimit: Measurement<UnitSpeed>?
public var speedLimitStyle: SpeedLimitView.SignageStyle = .viennaConvention
public var speedLimitStyle: SpeedLimitView.SignageStyle?

public var topCenter: (() -> AnyView)?
public var topTrailing: (() -> AnyView)?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView
var bottomTrailing: (() -> AnyView)?

var speedLimit: Measurement<UnitSpeed>?
var speedLimitStyle: SpeedLimitView.SignageStyle
var speedLimitStyle: SpeedLimitView.SignageStyle?
var showZoom: Bool
var onZoomIn: () -> Void
var onZoomOut: () -> Void
Expand All @@ -30,7 +30,7 @@ struct LandscapeNavigationOverlayView: View, CustomizableNavigatingInnerGridView
init(
navigationState: NavigationState?,
speedLimit: Measurement<UnitSpeed>? = nil,
speedLimitStyle: SpeedLimitView.SignageStyle = .viennaConvention,
speedLimitStyle: SpeedLimitView.SignageStyle? = nil,
showZoom: Bool = false,
onZoomIn: @escaping () -> Void = {},
onZoomOut: @escaping () -> Void = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct PortraitNavigationOverlayView: View, CustomizableNavigatingInnerGridView
var bottomTrailing: (() -> AnyView)?

var speedLimit: Measurement<UnitSpeed>?
var speedLimitStyle: SpeedLimitView.SignageStyle
var speedLimitStyle: SpeedLimitView.SignageStyle?
var showZoom: Bool
var onZoomIn: () -> Void
var onZoomOut: () -> Void
Expand All @@ -31,7 +31,7 @@ struct PortraitNavigationOverlayView: View, CustomizableNavigatingInnerGridView
init(
navigationState: NavigationState?,
speedLimit: Measurement<UnitSpeed>? = nil,
speedLimitStyle: SpeedLimitView.SignageStyle = .viennaConvention,
speedLimitStyle: SpeedLimitView.SignageStyle? = nil,
showZoom: Bool = false,
onZoomIn: @escaping () -> Void = {},
onZoomOut: @escaping () -> Void = {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public struct PortraitNavigationView: View, CustomizableNavigatingInnerGridView,
private let userLayers: [StyleLayerDefinition]

public var speedLimit: Measurement<UnitSpeed>?
public var speedLimitStyle: SpeedLimitView.SignageStyle = .viennaConvention
public var speedLimitStyle: SpeedLimitView.SignageStyle?

public var topCenter: (() -> AnyView)?
public var topTrailing: (() -> AnyView)?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import SwiftUI
/// An extension for a NavigationView that can host a SpeedLimitView.
public protocol SpeedLimitViewHost where Self: View {
var speedLimit: Measurement<UnitSpeed>? { get set }
var speedLimitStyle: SpeedLimitView.SignageStyle { get set }
var speedLimitStyle: SpeedLimitView.SignageStyle? { get set }
}

public extension SpeedLimitViewHost {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public struct NavigatingInnerGridView: View, CustomizableNavigatingInnerGridView
@Environment(\.navigationFormatterCollection) var formatterCollection: any FormatterCollection

var speedLimit: Measurement<UnitSpeed>?
var speedLimitStyle: SpeedLimitView.SignageStyle
var speedLimitStyle: SpeedLimitView.SignageStyle?

var showZoom: Bool
var onZoomIn: () -> Void
Expand Down Expand Up @@ -41,7 +41,7 @@ public struct NavigatingInnerGridView: View, CustomizableNavigatingInnerGridView
/// map on the user).
public init(
speedLimit: Measurement<UnitSpeed>? = nil,
speedLimitStyle: SpeedLimitView.SignageStyle = .viennaConvention,
speedLimitStyle: SpeedLimitView.SignageStyle? = nil,
showZoom: Bool = false,
onZoomIn: @escaping () -> Void = {},
onZoomOut: @escaping () -> Void = {},
Expand All @@ -60,7 +60,7 @@ public struct NavigatingInnerGridView: View, CustomizableNavigatingInnerGridView
public var body: some View {
InnerGridView(
topLeading: {
if let speedLimit {
if let speedLimit, let speedLimitStyle {
SpeedLimitView(
speedLimit: speedLimit,
signageStyle: speedLimitStyle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ final class AnnotationPublisherTests: XCTestCase {
var cancellables = Set<AnyCancellable>()

func testSpeedLimitConversion() throws {
let annotation = AnnotationPublisher<ValhallaOSRMAnnotation>.valhallaOSRM()
let annotation = AnnotationPublisher<ValhallaExtendedOSRMAnnotation>.valhallaExtendedOSRM()
annotation.configure($fakeNavigationState)

let exp = expectation(description: "speed limit converted")
Expand All @@ -25,7 +25,7 @@ final class AnnotationPublisherTests: XCTestCase {
.store(in: &cancellables)

let annotationJson = try encode(
ValhallaOSRMAnnotation(
ValhallaExtendedOSRMAnnotation(
speedLimit: .speed(15, unit: .milesPerHour),
speed: nil,
distance: nil,
Expand All @@ -40,7 +40,7 @@ final class AnnotationPublisherTests: XCTestCase {
func testInvalidJSON() throws {
let exp = expectation(description: "json decoder error")

let annotation = AnnotationPublisher<ValhallaOSRMAnnotation>.valhallaOSRM { error in
let annotation = AnnotationPublisher<ValhallaExtendedOSRMAnnotation>.valhallaExtendedOSRM { error in
XCTAssert(error is DecodingError)
exp.fulfill()
}
Expand All @@ -58,7 +58,7 @@ final class AnnotationPublisherTests: XCTestCase {
}

func testUnhandledException() throws {
let annotation = AnnotationPublisher<ValhallaOSRMAnnotation>.valhallaOSRM()
let annotation = AnnotationPublisher<ValhallaExtendedOSRMAnnotation>.valhallaExtendedOSRM()
annotation.configure($fakeNavigationState)

let exp = expectation(description: "speed limit converted")
Expand All @@ -75,7 +75,7 @@ final class AnnotationPublisherTests: XCTestCase {
fakeNavigationState = makeNavigationState("broken-json")

let annotationJson = try encode(
ValhallaOSRMAnnotation(
ValhallaExtendedOSRMAnnotation(
speedLimit: .unknown,
speed: nil,
distance: nil,
Expand All @@ -86,7 +86,7 @@ final class AnnotationPublisherTests: XCTestCase {
fakeNavigationState = makeNavigationState(annotationJson)

let annotationJsonTwo = try encode(
ValhallaOSRMAnnotation(
ValhallaExtendedOSRMAnnotation(
speedLimit: .speed(15, unit: .kilometersPerHour),
speed: nil,
distance: nil,
Expand Down Expand Up @@ -125,7 +125,7 @@ final class AnnotationPublisherTests: XCTestCase {
)
}

func encode(_ annotation: ValhallaOSRMAnnotation) throws -> String {
func encode(_ annotation: ValhallaExtendedOSRMAnnotation) throws -> String {
let data = try JSONEncoder().encode(annotation)
return String(data: data, encoding: .utf8)!
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import XCTest
@testable import FerrostarCore

final class ValhallaOSRMAnnotationTests: XCTestCase {

let encoder = JSONEncoder()
let decoder = JSONDecoder()

func testEmptyAnnotations() throws {
try assertAnnotations {
ValhallaExtendedOSRMAnnotation(
speedLimit: nil,
speed: nil,
distance: nil,
duration: nil
)
}
}

func testMilesPerHour() throws {
try assertAnnotations {
ValhallaExtendedOSRMAnnotation(
speedLimit: .speed(15, unit: .milesPerHour),
speed: 11.0,
distance: 12.0,
duration: 13.0
)
}
}

func testKilometersPerHour() throws {
try assertAnnotations {
ValhallaExtendedOSRMAnnotation(
speedLimit: .speed(15, unit: .kilometersPerHour),
speed: 11.0,
distance: 12.0,
duration: 13.0
)
}
}

func testKnots() throws {
try assertAnnotations {
ValhallaExtendedOSRMAnnotation(
speedLimit: .speed(15, unit: .knots),
speed: 11.0,
distance: 12.0,
duration: 13.0
)
}
}

func testNoLimit() throws {
try assertAnnotations {
ValhallaExtendedOSRMAnnotation(
speedLimit: MaxSpeed.noLimit,
speed: 11.0,
distance: 12.0,
duration: 13.0
)
}
}

func testUnknown() throws {
try assertAnnotations {
ValhallaExtendedOSRMAnnotation(
speedLimit: .unknown,
speed: 11.0,
distance: 12.0,
duration: 13.0
)
}
}

func test_decodeFromString_withMaxSpeed() throws {
// Unsupported/incomplete congestion is in the json, but ignored by our `ValhallaExtendedOSRMAnnotation` Codable.
let jsonString = "{\"distance\":4.294596842089401,\"duration\":1,\"speed\":4.2,\"congestion\":\"low\",\"maxspeed\":{\"speed\":56,\"unit\":\"km/h\"}}"
guard let jsonData = jsonString.data(using: .utf8) else {
XCTFail("Could not convert string to data")
return
}

let result = try decoder.decode(ValhallaExtendedOSRMAnnotation.self, from: jsonData)

XCTAssertEqual(result.distance, 4.294596842089401)
XCTAssertEqual(result.duration, 1.0)
XCTAssertEqual(result.speed, 4.2)
XCTAssertEqual(result.speedLimit, .speed(56.0, unit: .kilometersPerHour))
}

func test_decodeFromString_incompleteModel_unlimitedMaxSpeed() throws {
let jsonString = "{\"maxspeed\":{\"none\":true}}"
guard let jsonData = jsonString.data(using: .utf8) else {
XCTFail("Could not convert string to data")
return
}

let result = try decoder.decode(ValhallaExtendedOSRMAnnotation.self, from: jsonData)

XCTAssertNil(result.distance)
XCTAssertNil(result.duration)
XCTAssertNil(result.speed)
XCTAssertEqual(result.speedLimit, .noLimit)
}

func test_decodeFromString_unknownSpeed() throws {
let jsonString = "{\"distance\":2,\"duration\":1,\"speed\":3,\"maxspeed\":{\"unknown\":true}}"
guard let jsonData = jsonString.data(using: .utf8) else {
XCTFail("Could not convert string to data")
return
}

let result = try decoder.decode(ValhallaExtendedOSRMAnnotation.self, from: jsonData)

XCTAssertEqual(result.distance, 2.0)
XCTAssertEqual(result.duration, 1.0)
XCTAssertEqual(result.speed, 3.0)
XCTAssertEqual(result.speedLimit, .unknown)
}

func test_decodeFromString_nilSpeed() throws {
let jsonString = "{\"distance\":2,\"duration\":1,\"speed\":3}"
guard let jsonData = jsonString.data(using: .utf8) else {
XCTFail("Could not convert string to data")
return
}

let result = try decoder.decode(ValhallaExtendedOSRMAnnotation.self, from: jsonData)

XCTAssertEqual(result.distance, 2.0)
XCTAssertEqual(result.duration, 1.0)
XCTAssertEqual(result.speed, 3.0)
XCTAssertNil(result.speedLimit)
}

func assertAnnotations(_ makeAnnotations: () -> ValhallaExtendedOSRMAnnotation) throws {
let annotations = makeAnnotations()

let encoded = try encoder.encode(annotations)
let result = try decoder.decode(ValhallaExtendedOSRMAnnotation.self, from: encoded)

XCTAssertEqual(annotations, result)
}
}
Loading

0 comments on commit de5a702

Please sign in to comment.