diff --git a/apple/Sources/FerrostarCore/Extensions/CoreLocation Extensions.swift b/apple/Sources/FerrostarCore/Extensions/CoreLocation Extensions.swift index ddf637fc..7ff1ae18 100644 --- a/apple/Sources/FerrostarCore/Extensions/CoreLocation Extensions.swift +++ b/apple/Sources/FerrostarCore/Extensions/CoreLocation Extensions.swift @@ -61,12 +61,14 @@ extension CourseOverGround { /// Intialize a Course Over Ground object from the relevant Core Location types. These can be found on a /// CLLocation object. /// + /// This returns nil if the course or courseAccuracy are invalid as defined by Apple (negative) + /// /// - Parameters: /// - course: The direction the device is travelling, measured in degrees relative to true north. /// - courseAccuracy: The accuracy of the direction public init?(course: CLLocationDirection, courseAccuracy: CLLocationDirectionAccuracy) { - guard course > 0 && courseAccuracy > 0 else { + guard course >= 0 && courseAccuracy >= 0 else { return nil } @@ -78,12 +80,12 @@ extension Heading { /// Initialize a Heading if it can be represented as integers /// - /// This will return nil for invalid headings. + /// This returns nil if the heading or accuracy are invalid as defined by Apple (negative) /// /// - Parameter clHeading: The CoreLocation heading provided by the location manager. public init?(clHeading: CLHeading) { - guard clHeading.trueHeading > 0 - && clHeading.headingAccuracy > 0 else { + guard clHeading.trueHeading >= 0 + && clHeading.headingAccuracy >= 0 else { return nil } @@ -95,8 +97,43 @@ extension Heading { extension UserLocation { + /// Initialize a UserLocation from values. + /// + /// - Parameters: + /// - latitude: The latitude in decimal degrees + /// - longitude: The longitude in decimal degrees + /// - horizontalAccuracy: The horizontal accuracy + /// - course: The direction of travel measured in degrees clockwise from north. + /// - courseAccuracy: The course accuracy measured in degrees. + /// - timestamp: The timestamp of the location record. + public init(latitude: CLLocationDegrees, + longitude: CLLocationDegrees, + horizontalAccuracy: CLLocationDistance, + course: CLLocationDirection, + courseAccuracy: CLLocationDirectionAccuracy, + timestamp: Date) { + + self.init(coordinates: GeographicCoordinate(lat: latitude, lng: longitude), + horizontalAccuracy: horizontalAccuracy, + courseOverGround: CourseOverGround(course: course, courseAccuracy: courseAccuracy), + timestamp: timestamp) + } + + /// Initialize a UserLocation with a coordinate only. + /// + /// - Parameter clCoordinateLocation2D: A core location coordinate. + public init(clCoordinateLocation2D: CLLocationCoordinate2D) { + // This behavior matches how CLLocation initializes with a coordinate (setting accuracy to 0 & date to now) + self.init(coordinates: GeographicCoordinate(cl: clCoordinateLocation2D), + horizontalAccuracy: 0, + courseOverGround: nil, + timestamp: Date()) + } + /// Initialize a UserLocation from an Apple CoreLocation CLLocation /// + /// Unlike CourseOverGround & Heading, this value will init with invalid values. + /// /// - Parameter clLocation: The location. public init(clLocation: CLLocation) { self.init( diff --git a/apple/Sources/FerrostarCore/FerrostarCore.swift b/apple/Sources/FerrostarCore/FerrostarCore.swift index 1b92f1f3..5e9ab6fb 100644 --- a/apple/Sources/FerrostarCore/FerrostarCore.swift +++ b/apple/Sources/FerrostarCore/FerrostarCore.swift @@ -237,7 +237,7 @@ public protocol FerrostarCoreDelegate: AnyObject { case .complete: // TODO: "You have arrived"? self.state?.visualInstructions = nil - self.state?.snappedLocation = UserLocation(clLocation: location) // We arrived; no more snapping needed + self.state?.snappedLocation = UserLocation(clLocation: location)! // TODO: Handle error? // We arrived; no more snapping needed self.state?.courseOverGround = CourseOverGround(course: location.course, courseAccuracy: location.courseAccuracy) self.state?.spokenInstruction = nil } diff --git a/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift b/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift new file mode 100644 index 00000000..8e1e057c --- /dev/null +++ b/apple/Sources/FerrostarCore/Mock/MockNavigationState.swift @@ -0,0 +1,94 @@ +import Foundation +import CoreLocation +import UniFFI + +extension NavigationState { + public static let pedestrianExample = NavigationState( + snappedLocation: CLLocation(latitude: samplePedestrianWaypoints.first!.latitude, + longitude: samplePedestrianWaypoints.first!.longitude), + fullRoute: samplePedestrianWaypoints, + steps: [] + ) + + public static func modifiedPedestrianExample(droppingNWaypoints n: Int) -> NavigationState { + let remainingLocations = Array(samplePedestrianWaypoints.dropFirst(n)) + let lastUserLocation = remainingLocations.first! + + var result = NavigationState( + snappedLocation: CLLocation(latitude: samplePedestrianWaypoints.first!.latitude, + longitude: samplePedestrianWaypoints.first!.longitude), + fullRoute: samplePedestrianWaypoints, + steps: [UniFFI.RouteStep( + geometry: [lastUserLocation.geographicCoordinates], + distance: 100, roadName: "Jefferson St.", + instruction: "Walk west on Jefferson St.", + visualInstructions: [ + UniFFI.VisualInstruction( + primaryContent: VisualInstructionContent(text: "Hyde Street", maneuverType: .turn, maneuverModifier: .left, roundaboutExitDegrees: nil), + secondaryContent: nil, triggerDistanceBeforeManeuver: 42.0) + ], + spokenInstructions: []) + ]) + + result.snappedLocation = UserLocation( + coordinates: GeographicCoordinate(lat: samplePedestrianWaypoints.first!.latitude, + lng: samplePedestrianWaypoints.first!.longitude), + horizontalAccuracy: 10, + courseOverGround: CourseOverGround(degrees: 0, accuracy: 10), + timestamp: Date() + ) + + return result + } +} + +// Derived from the Stadia Maps map matching example +private let samplePedestrianWaypoints = [ + CLLocationCoordinate2D(latitude: 37.807770999999995, longitude: -122.41970699999999), + CLLocationCoordinate2D(latitude: 37.807680999999995, longitude: -122.42041599999999), + CLLocationCoordinate2D(latitude: 37.807623, longitude: -122.42040399999999), + CLLocationCoordinate2D(latitude: 37.807587, longitude: -122.420678), + CLLocationCoordinate2D(latitude: 37.807527, longitude: -122.420666), + CLLocationCoordinate2D(latitude: 37.807514, longitude: -122.420766), + CLLocationCoordinate2D(latitude: 37.807475, longitude: -122.420757), + CLLocationCoordinate2D(latitude: 37.807438, longitude: -122.42073599999999), + CLLocationCoordinate2D(latitude: 37.807403, longitude: -122.420721), + CLLocationCoordinate2D(latitude: 37.806951999999995, longitude: -122.420633), + CLLocationCoordinate2D(latitude: 37.806779999999996, longitude: -122.4206), + CLLocationCoordinate2D(latitude: 37.806806, longitude: -122.42069599999999), + CLLocationCoordinate2D(latitude: 37.806781, longitude: -122.42071999999999), + CLLocationCoordinate2D(latitude: 37.806754999999995, longitude: -122.420746), + CLLocationCoordinate2D(latitude: 37.806739, longitude: -122.420761), + CLLocationCoordinate2D(latitude: 37.806701, longitude: -122.42105699999999), + CLLocationCoordinate2D(latitude: 37.806616999999996, longitude: -122.42171599999999), + CLLocationCoordinate2D(latitude: 37.806562, longitude: -122.42214299999999), + CLLocationCoordinate2D(latitude: 37.806464999999996, longitude: -122.422123), + CLLocationCoordinate2D(latitude: 37.806453, longitude: -122.42221699999999), + CLLocationCoordinate2D(latitude: 37.806439999999995, longitude: -122.42231), + CLLocationCoordinate2D(latitude: 37.806394999999995, longitude: -122.422585), + CLLocationCoordinate2D(latitude: 37.806305, longitude: -122.423289), + CLLocationCoordinate2D(latitude: 37.806242999999995, longitude: -122.423773), + CLLocationCoordinate2D(latitude: 37.806232, longitude: -122.423862), + CLLocationCoordinate2D(latitude: 37.806152999999995, longitude: -122.423846), + CLLocationCoordinate2D(latitude: 37.805687999999996, longitude: -122.423755), + CLLocationCoordinate2D(latitude: 37.805385, longitude: -122.42369), + CLLocationCoordinate2D(latitude: 37.805371, longitude: -122.423797), + CLLocationCoordinate2D(latitude: 37.805306, longitude: -122.42426999999999), + CLLocationCoordinate2D(latitude: 37.805259, longitude: -122.42463699999999), + CLLocationCoordinate2D(latitude: 37.805192, longitude: -122.425147), + CLLocationCoordinate2D(latitude: 37.805184, longitude: -122.42521199999999), + CLLocationCoordinate2D(latitude: 37.805096999999996, longitude: -122.425218), + CLLocationCoordinate2D(latitude: 37.805074999999995, longitude: -122.42539699999999), + CLLocationCoordinate2D(latitude: 37.804992, longitude: -122.425373), + CLLocationCoordinate2D(latitude: 37.804852, longitude: -122.425345), + CLLocationCoordinate2D(latitude: 37.804657, longitude: -122.42530599999999), + CLLocationCoordinate2D(latitude: 37.804259, longitude: -122.425224), + CLLocationCoordinate2D(latitude: 37.804249, longitude: -122.425339), + CLLocationCoordinate2D(latitude: 37.804128, longitude: -122.425314), + CLLocationCoordinate2D(latitude: 37.804109, longitude: -122.425461), + CLLocationCoordinate2D(latitude: 37.803956, longitude: -122.426678), + CLLocationCoordinate2D(latitude: 37.803944, longitude: -122.42677599999999), + CLLocationCoordinate2D(latitude: 37.803931, longitude: -122.42687699999999), + CLLocationCoordinate2D(latitude: 37.803736, longitude: -122.42841899999999), + CLLocationCoordinate2D(latitude: 37.803695, longitude: -122.428411), +] diff --git a/apple/Sources/FerrostarCore/NavigationState.swift b/apple/Sources/FerrostarCore/NavigationState.swift index f7be48c3..073d6f0c 100644 --- a/apple/Sources/FerrostarCore/NavigationState.swift +++ b/apple/Sources/FerrostarCore/NavigationState.swift @@ -19,102 +19,12 @@ public struct NavigationState: Hashable { public internal(set) var isCalculatingNewRoute: Bool = false init(snappedLocation: CLLocation, heading: CLHeading? = nil, fullRoute: [CLLocationCoordinate2D], steps: [RouteStep]) { - self.snappedLocation = UserLocation(clLocation: snappedLocation) + self.snappedLocation = UserLocation(clLocation: snappedLocation)! // TODO: Handle an error here if UserLocation is invalid? if let heading { self.heading = Heading(clHeading: heading) } - self.courseOverGround = CourseOverGround(course: snappedLocation.course, - courseAccuracy: snappedLocation.courseAccuracy) - self.fullRouteShape = fullRoute.map { GeographicCoordinate(lat: $0.latitude, lng: $0.longitude) } + self.courseOverGround = self.snappedLocation.courseOverGround + self.fullRouteShape = fullRoute.map { GeographicCoordinate(cl: $0) } self.currentStep = steps.first! } - - public static let pedestrianExample = NavigationState( - snappedLocation: CLLocation(latitude: samplePedestrianWaypoints.first!.latitude, - longitude: samplePedestrianWaypoints.first!.longitude), - fullRoute: samplePedestrianWaypoints, - steps: [] - ) - - public static func modifiedPedestrianExample(droppingNWaypoints n: Int) -> NavigationState { - let remainingLocations = Array(samplePedestrianWaypoints.dropFirst(n)) - let lastUserLocation = remainingLocations.first! - - var result = NavigationState( - snappedLocation: CLLocation(latitude: samplePedestrianWaypoints.first!.latitude, - longitude: samplePedestrianWaypoints.first!.longitude), - fullRoute: samplePedestrianWaypoints, - steps: [UniFFI.RouteStep( - geometry: [lastUserLocation.geographicCoordinates], - distance: 100, roadName: "Jefferson St.", - instruction: "Walk west on Jefferson St.", - visualInstructions: [ - UniFFI.VisualInstruction( - primaryContent: VisualInstructionContent(text: "Hyde Street", maneuverType: .turn, maneuverModifier: .left, roundaboutExitDegrees: nil), - secondaryContent: nil, triggerDistanceBeforeManeuver: 42.0) - ], - spokenInstructions: []) - ]) - - result.snappedLocation = UserLocation( - coordinates: GeographicCoordinate(lat: samplePedestrianWaypoints.first!.latitude, - lng: samplePedestrianWaypoints.first!.longitude), - horizontalAccuracy: 10, - courseOverGround: CourseOverGround(degrees: 0, accuracy: 10), - timestamp: Date() - ) - - return result - } } - -// Derived from the Stadia Maps map matching example -private let samplePedestrianWaypoints = [ - CLLocationCoordinate2D(latitude: 37.807770999999995, longitude: -122.41970699999999), - CLLocationCoordinate2D(latitude: 37.807680999999995, longitude: -122.42041599999999), - CLLocationCoordinate2D(latitude: 37.807623, longitude: -122.42040399999999), - CLLocationCoordinate2D(latitude: 37.807587, longitude: -122.420678), - CLLocationCoordinate2D(latitude: 37.807527, longitude: -122.420666), - CLLocationCoordinate2D(latitude: 37.807514, longitude: -122.420766), - CLLocationCoordinate2D(latitude: 37.807475, longitude: -122.420757), - CLLocationCoordinate2D(latitude: 37.807438, longitude: -122.42073599999999), - CLLocationCoordinate2D(latitude: 37.807403, longitude: -122.420721), - CLLocationCoordinate2D(latitude: 37.806951999999995, longitude: -122.420633), - CLLocationCoordinate2D(latitude: 37.806779999999996, longitude: -122.4206), - CLLocationCoordinate2D(latitude: 37.806806, longitude: -122.42069599999999), - CLLocationCoordinate2D(latitude: 37.806781, longitude: -122.42071999999999), - CLLocationCoordinate2D(latitude: 37.806754999999995, longitude: -122.420746), - CLLocationCoordinate2D(latitude: 37.806739, longitude: -122.420761), - CLLocationCoordinate2D(latitude: 37.806701, longitude: -122.42105699999999), - CLLocationCoordinate2D(latitude: 37.806616999999996, longitude: -122.42171599999999), - CLLocationCoordinate2D(latitude: 37.806562, longitude: -122.42214299999999), - CLLocationCoordinate2D(latitude: 37.806464999999996, longitude: -122.422123), - CLLocationCoordinate2D(latitude: 37.806453, longitude: -122.42221699999999), - CLLocationCoordinate2D(latitude: 37.806439999999995, longitude: -122.42231), - CLLocationCoordinate2D(latitude: 37.806394999999995, longitude: -122.422585), - CLLocationCoordinate2D(latitude: 37.806305, longitude: -122.423289), - CLLocationCoordinate2D(latitude: 37.806242999999995, longitude: -122.423773), - CLLocationCoordinate2D(latitude: 37.806232, longitude: -122.423862), - CLLocationCoordinate2D(latitude: 37.806152999999995, longitude: -122.423846), - CLLocationCoordinate2D(latitude: 37.805687999999996, longitude: -122.423755), - CLLocationCoordinate2D(latitude: 37.805385, longitude: -122.42369), - CLLocationCoordinate2D(latitude: 37.805371, longitude: -122.423797), - CLLocationCoordinate2D(latitude: 37.805306, longitude: -122.42426999999999), - CLLocationCoordinate2D(latitude: 37.805259, longitude: -122.42463699999999), - CLLocationCoordinate2D(latitude: 37.805192, longitude: -122.425147), - CLLocationCoordinate2D(latitude: 37.805184, longitude: -122.42521199999999), - CLLocationCoordinate2D(latitude: 37.805096999999996, longitude: -122.425218), - CLLocationCoordinate2D(latitude: 37.805074999999995, longitude: -122.42539699999999), - CLLocationCoordinate2D(latitude: 37.804992, longitude: -122.425373), - CLLocationCoordinate2D(latitude: 37.804852, longitude: -122.425345), - CLLocationCoordinate2D(latitude: 37.804657, longitude: -122.42530599999999), - CLLocationCoordinate2D(latitude: 37.804259, longitude: -122.425224), - CLLocationCoordinate2D(latitude: 37.804249, longitude: -122.425339), - CLLocationCoordinate2D(latitude: 37.804128, longitude: -122.425314), - CLLocationCoordinate2D(latitude: 37.804109, longitude: -122.425461), - CLLocationCoordinate2D(latitude: 37.803956, longitude: -122.426678), - CLLocationCoordinate2D(latitude: 37.803944, longitude: -122.42677599999999), - CLLocationCoordinate2D(latitude: 37.803931, longitude: -122.42687699999999), - CLLocationCoordinate2D(latitude: 37.803736, longitude: -122.42841899999999), - CLLocationCoordinate2D(latitude: 37.803695, longitude: -122.428411), -] diff --git a/apple/Sources/FerrostarMapLibreUI/MapLibre Extensions.swift b/apple/Sources/FerrostarMapLibreUI/MapLibre Extensions.swift index 6bea748e..4aef616b 100644 --- a/apple/Sources/FerrostarMapLibreUI/MapLibre Extensions.swift +++ b/apple/Sources/FerrostarMapLibreUI/MapLibre Extensions.swift @@ -4,11 +4,11 @@ import MapLibre extension NavigationState { var routePolyline: MLNPolyline { - return MLNPolylineFeature(coordinates: fullRouteShape.map { CLLocationCoordinate2D(latitude: $0.lat, longitude: $0.lng) }) + return MLNPolylineFeature(coordinates: fullRouteShape.map { $0.clLocationCoordinate2D }) } var remainingRoutePolyline: MLNPolyline { // FIXME - return MLNPolylineFeature(coordinates: fullRouteShape.map { CLLocationCoordinate2D(latitude: $0.lat, longitude: $0.lng) }) + return MLNPolylineFeature(coordinates: fullRouteShape.map { $0.clLocationCoordinate2D }) } } diff --git a/apple/Tests/FerrostarCoreTests/FerrostarCoreModelTests.swift b/apple/Tests/FerrostarCoreTests/FerrostarCoreModelTests.swift deleted file mode 100644 index e69de29b..00000000 diff --git a/apple/Tests/FerrostarCoreTests/Models/CoreLocationModelTests.swift b/apple/Tests/FerrostarCoreTests/Models/CoreLocationModelTests.swift index 57b44885..3a519bbe 100644 --- a/apple/Tests/FerrostarCoreTests/Models/CoreLocationModelTests.swift +++ b/apple/Tests/FerrostarCoreTests/Models/CoreLocationModelTests.swift @@ -62,7 +62,23 @@ final class CoreLocationModelTests: XCTestCase { XCTAssertEqual(userLocation.timestamp, timestamp) } - // TODO: Decide methodology for invalid CLLocation data (negatives) + func testInvalidInitUserLocation() { + let timestamp = Date() + let coordinate = CLLocationCoordinate2D(latitude: -77.846323, longitude: 166.668235) + let location = CLLocation(coordinate: coordinate, + altitude: 10, + horizontalAccuracy: -1.1, + verticalAccuracy: 6.6, + course: 45.5, + courseAccuracy: 3.3, + speed: 4.4, + speedAccuracy: 1.1, + timestamp: timestamp) + + let invalid = UserLocation(clLocation: location) + + XCTAssertNil(invalid) + } // MARK: CourseOverGround diff --git a/common/ferrostar/src/models.rs b/common/ferrostar/src/models.rs index cb909d7a..df51aaf0 100644 --- a/common/ferrostar/src/models.rs +++ b/common/ferrostar/src/models.rs @@ -72,29 +72,6 @@ pub struct Heading { pub timestamp: SystemTime, } -impl Heading { - - /// Create a new heading. - /// - /// # Arguments - /// - /// * `true_heading` - The heading relative to true north, measured in clockwise degrees from - /// true north (N = 0, E = 90, S = 180, W = 270). - /// * `accuracy` - The maximum deviation in degrees between the reported heading and the true geomagnetic heading. - /// * `timestamp` - The time at which the heading was recorded. - pub fn new( - true_heading: u16, - accuracy: u16, - timestamp: SystemTime, - ) -> Self { - Self { - true_heading, - accuracy, - timestamp, - } - } -} - /// The direction in which the user/device is observed to be traveling. #[derive(Clone, Copy, PartialEq, PartialOrd, Debug, uniffi::Record)] pub struct CourseOverGround { @@ -124,26 +101,6 @@ pub struct UserLocation { pub timestamp: SystemTime, } -impl UserLocation { - pub fn new( - latitude: f64, - longitude: f64, - horizontal_accuracy: f64, - course_over_ground: Option, - timestamp: SystemTime, - ) -> Self { - Self { - coordinates: GeographicCoordinate { - lat: latitude, - lng: longitude - }, - horizontal_accuracy, - course_over_ground, - timestamp, - } - } -} - impl From for Point { fn from(val: UserLocation) -> Point { Point::new(val.coordinates.lng, val.coordinates.lat)