diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f88918fc9..5c7ed62164 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * Since pure Swift protocols cannot have optional methods, various delegate protocols now provide default no-op implementations for all their methods and conform to the `UnimplementedLogging` protocol, which can inform you at runtime when a delegate method is called but has not been implemented. Messages are sent through Apple Unified Logging and can be disabled globally through [Unifed Logging](https://developer.apple.com/documentation/os/logging#2878594), or by overriding the delegate function with a no-op implementation. ([#2230](https://github.com/mapbox/mapbox-navigation-ios/pull/2230)) * Renamed `RouteProgress.nearbyCoordinates` to `RouteProgress.nearbyShape`. ([#2275](https://github.com/mapbox/mapbox-navigation-ios/pull/2275)) * Removed `RouteLegProgress.nearbyCoordinates`. ([#2275](https://github.com/mapbox/mapbox-navigation-ios/pull/2275)) +* Added the `RouteLegProgress.currentSpeedLimit` property. ([#2114](https://github.com/mapbox/mapbox-navigation-ios/pull/2114)) ## v0.38.1 diff --git a/Cartfile.resolved b/Cartfile.resolved index 5100a8b13b..aeca5e0022 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -5,7 +5,7 @@ github "CedarBDD/Cedar" "v1.0" github "Quick/Nimble" "v8.0.4" github "Quick/Quick" "v2.2.0" github "ceeK/Solar" "2.1.0" -github "mapbox/MapboxDirections.swift" "472cbced199ef2d9c5c2fa49a21e8f6b249e9bf8" +github "mapbox/MapboxDirections.swift" "93a790ab18168c61a99f72b94e984b00e5bc7476" github "mapbox/MapboxGeocoder.swift" "v0.10.2" github "mapbox/mapbox-events-ios" "v0.9.5" github "mapbox/mapbox-speech-swift" "v0.1.1" diff --git a/MapboxCoreNavigation/NavigationRouteOptions.swift b/MapboxCoreNavigation/NavigationRouteOptions.swift index a9a01b7a73..05bca77d20 100644 --- a/MapboxCoreNavigation/NavigationRouteOptions.swift +++ b/MapboxCoreNavigation/NavigationRouteOptions.swift @@ -22,7 +22,11 @@ open class NavigationRouteOptions: RouteOptions { shapeFormat = .polyline6 includesSteps = true routeShapeResolution = .full - attributeOptions = [.congestionLevel, .expectedTravelTime] + if profileIdentifier == .walking { + attributeOptions = [.congestionLevel, .expectedTravelTime] + } else { + attributeOptions = [.congestionLevel, .expectedTravelTime, .maximumSpeedLimit] + } includesSpokenInstructions = true locale = Locale.nationalizedCurrent distanceMeasurementSystem = Locale.current.usesMetricSystem ? .metric : .imperial @@ -75,6 +79,9 @@ open class NavigationMatchOptions: MatchOptions { routeShapeResolution = .full shapeFormat = .polyline6 attributeOptions = [.congestionLevel, .expectedTravelTime] + if profileIdentifier == .automobile || profileIdentifier == .automobileAvoidingTraffic { + attributeOptions.insert(.maximumSpeedLimit) + } includesSpokenInstructions = true locale = Locale.nationalizedCurrent distanceMeasurementSystem = Locale.current.usesMetricSystem ? .metric : .imperial diff --git a/MapboxCoreNavigation/RouteProgress.swift b/MapboxCoreNavigation/RouteProgress.swift index 7082bc9a42..4a776d0eb3 100644 --- a/MapboxCoreNavigation/RouteProgress.swift +++ b/MapboxCoreNavigation/RouteProgress.swift @@ -474,6 +474,27 @@ open class RouteLegProgress: NSObject { return accumulatedCoordinates <= userCoordinateIndex }) } + + /** + Returns the SpeedLimit for the current position along the route. Returns SpeedLimit.invalid if the speed limit is unknown or missing. + + The maximum speed may be an advisory speed limit for segments where legal limits are not posted, such as highway entrance and exit ramps. If the speed limit along a particular segment is unknown, it is set to `nil`. If the speed is unregulated along the segment, such as on the German _Autobahn_ system, it is represented by a measurement whose value is `Double.infinity`. + + Speed limit data is available in [a number of countries and territories worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/). + */ + public var currentSpeedLimit: Measurement? { + let distanceTraveled = currentStepProgress.distanceTraveled + guard let index = currentStep.shape?.indexedCoordinateFromStart(distance: distanceTraveled)?.index else { + return nil + } + let range = leg.segmentRangesByStep[stepIndex] + let speedLimit = leg.segmentMaximumSpeedLimits?[range][index] + if let speedUnit = currentStep.speedLimitUnit { + return speedLimit?.converted(to: speedUnit) + } else { + return speedLimit + } + } } /** diff --git a/MapboxCoreNavigationTests/OptionsTests.swift b/MapboxCoreNavigationTests/OptionsTests.swift index 2f8ffbc754..b785a26b31 100644 --- a/MapboxCoreNavigationTests/OptionsTests.swift +++ b/MapboxCoreNavigationTests/OptionsTests.swift @@ -20,7 +20,7 @@ class OptionsTests: XCTestCase { XCTAssertEqual(options.profileIdentifier, .automobileAvoidingTraffic) XCTAssertEqual(options.routeShapeResolution, .full) XCTAssertEqual(options.shapeFormat, .polyline6) - XCTAssertEqual(options.attributeOptions, [.congestionLevel, .expectedTravelTime]) + XCTAssertEqual(options.attributeOptions, [.congestionLevel, .expectedTravelTime, .maximumSpeedLimit]) XCTAssertTrue(options.includesVisualInstructions) XCTAssertTrue(options.includesSpokenInstructions) } diff --git a/MapboxCoreNavigationTests/RouteProgressTests.swift b/MapboxCoreNavigationTests/RouteProgressTests.swift index 5b1f192a84..ef77fc4b9b 100644 --- a/MapboxCoreNavigationTests/RouteProgressTests.swift +++ b/MapboxCoreNavigationTests/RouteProgressTests.swift @@ -88,7 +88,7 @@ class RouteProgressTests: XCTestCase { XCTAssertEqual(waypoints.1.map { $0.coordinate }, [coordinates.last!]) } - func routeLegProgress(options: RouteOptions, routeCoordinates: [CLLocationCoordinate2D]) -> RouteLegProgress { + func routeLeg(options: RouteOptions, routeCoordinates: [CLLocationCoordinate2D]) -> RouteLeg { let source = options.waypoints.first! let destination = options.waypoints.last! options.shapeFormat = .polyline @@ -97,7 +97,11 @@ class RouteProgressTests: XCTestCase { RouteStep(transportType: .automobile, maneuverLocation: destination.coordinate, maneuverType: .arrive, maneuverDirection: nil, instructions: "", initialHeading: nil, finalHeading: nil, drivingSide: .right, exitCodes: nil, exitNames: nil, phoneticExitNames: nil, distance: 0, expectedTravelTime: 0, names: nil, phoneticNames: nil, codes: nil, destinationCodes: nil, destinations: nil, intersections: nil, instructionsSpokenAlongStep: nil, instructionsDisplayedAlongStep: nil), ] steps[0].shape = LineString(routeCoordinates) - let leg = RouteLeg(steps: steps, name: "", distance: 0, expectedTravelTime: 0, profileIdentifier: .automobile) + return RouteLeg(steps: steps, name: "", distance: 0, expectedTravelTime: 0, profileIdentifier: .automobile) + } + + func routeLegProgress(options: RouteOptions, routeCoordinates: [CLLocationCoordinate2D]) -> RouteLegProgress { + let leg = routeLeg(options: options, routeCoordinates: routeCoordinates) return RouteLegProgress(leg: leg) } @@ -212,4 +216,59 @@ class RouteProgressTests: XCTestCase { XCTAssertEqual(remainingWaypoints.count, 0, "At the last via point after backtracking, nothing should remain") } + + func testSpeedLimits() { + let coordinates = [ + CLLocationCoordinate2D(latitude: 0, longitude: 0), + CLLocationCoordinate2D(latitude: 2, longitude: 3), + CLLocationCoordinate2D(latitude: 4, longitude: 6), + CLLocationCoordinate2D(latitude: 6, longitude: 9), + CLLocationCoordinate2D(latitude: 8, longitude: 12), + CLLocationCoordinate2D(latitude: 10, longitude: 15), + CLLocationCoordinate2D(latitude: 12, longitude: 18), + ] + let lineString = LineString(coordinates) + + let options = RouteOptions(coordinates: [coordinates.first!, coordinates.last!]) + let leg = routeLeg(options: options, routeCoordinates: coordinates) + leg.segmentMaximumSpeedLimits = [ + .init(value: 10, unit: .kilometersPerHour), + .init(value: 20, unit: .milesPerHour), + nil, + .init(value: 40, unit: .milesPerHour), + .init(value: 50, unit: .kilometersPerHour), + .init(value: .infinity, unit: .kilometersPerHour), + ] + let legProgress = RouteLegProgress(leg: leg) + + XCTAssertEqual(legProgress.distanceTraveled, 0) + XCTAssertEqual(legProgress.currentSpeedLimit, Measurement(value: 10, unit: UnitSpeed.kilometersPerHour)) + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[1]) / 2.0 + XCTAssertEqual(legProgress.currentSpeedLimit, Measurement(value: 10, unit: UnitSpeed.kilometersPerHour)) + + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[1]) + XCTAssertEqual(legProgress.currentSpeedLimit, Measurement(value: 20, unit: UnitSpeed.milesPerHour)) + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[1]) + lineString.distance(from: coordinates[1], to: coordinates[2]) / 2.0 + XCTAssertEqual(legProgress.currentSpeedLimit, Measurement(value: 20, unit: UnitSpeed.milesPerHour)) + + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[2]) + XCTAssertNil(legProgress.currentSpeedLimit) + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[2]) + lineString.distance(from: coordinates[2], to: coordinates[3]) / 2.0 + XCTAssertNil(legProgress.currentSpeedLimit) + + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[3]) + XCTAssertEqual(legProgress.currentSpeedLimit, Measurement(value: 40, unit: UnitSpeed.milesPerHour)) + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[3]) + lineString.distance(from: coordinates[3], to: coordinates[4]) / 2.0 + XCTAssertEqual(legProgress.currentSpeedLimit, Measurement(value: 40, unit: UnitSpeed.milesPerHour)) + + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[4]) + XCTAssertEqual(legProgress.currentSpeedLimit, Measurement(value: 50, unit: UnitSpeed.kilometersPerHour)) + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[4]) + lineString.distance(from: coordinates[4], to: coordinates[5]) / 2.0 + XCTAssertEqual(legProgress.currentSpeedLimit, Measurement(value: 50, unit: UnitSpeed.kilometersPerHour)) + + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[5]) + XCTAssertTrue(legProgress.currentSpeedLimit?.value.isInfinite ?? false) + legProgress.currentStepProgress.distanceTraveled = lineString.distance(to: coordinates[5]) + (lineString.distance() - lineString.distance(to: coordinates[5])) / 2.0 + XCTAssertTrue(legProgress.currentSpeedLimit?.value.isInfinite ?? false) + } }