Skip to content

Commit 0a0a7cc

Browse files
authored
Continuous Alternatives CarPlay selection and callouts (#3956)
* vk-3927-alternatives-selection: added CarPlay list view for picking an alternative route; localizable string added; added alternative routes duration delta callouts; refactored duration callouts a bit; CHANGELOG updated; factored out DateComponentsFormatter.travelDurationStyle
1 parent 8c4105a commit 0a0a7cc

10 files changed

+368
-69
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@
3535
* Added the `layerPosition` parameter to the `NavigationMapView.show(_:layerPosition:legIndex:)` method for controlling the position of the main route layer while presenting routes. ([#3897](https://github.com/mapbox/mapbox-navigation-ios/pull/3897))
3636
* Fixed an issue where camera kept receiving updates even after stopping `MapboxNavigationService`. ([#3928](https://github.com/mapbox/mapbox-navigation-ios/pull/3928))
3737
* Added `NavigationViewController.showsContinuousAlternatives` and `CarPlayNavigationViewController.showsContinuousAlternatives` flags to toggle displaying alternative route's lines during navigation session. Use `NavigationMapView.show(continuousAlternatives:)` and `NavigationMapView.removeContinuousAlternativesRoutes()` methods for fine control over displayed routes. ([#3850](https://github.com/mapbox/mapbox-navigation-ios/pull/3850))
38+
* Each alternative route shown during turn-by-turn navigation is now annotated with the amount of time it saves or adds to the trip. To hide these annotations, set the `NavigationMapView.showsRelativeDurationOnContinuousAlternativeRoutes` property to `false`. ([#3956](https://github.com/mapbox/mapbox-navigation-ios/pull/3956))
3839

3940
### CarPlay
4041

4142
* Fixed an issue where route shields disappeared when the user enters a tunnel. ([#3882](https://github.com/mapbox/mapbox-navigation-ios/pull/3882))
4243
* The map automatically chooses the night style when "Always Show Dark Maps" is enabled in the Appearance section of Settings. ([#3882](https://github.com/mapbox/mapbox-navigation-ios/pull/3882))
4344
* Renamed the `VisualInstruction.carPlayManeuverLabelAttributedText(bounds:shieldHeight:window:)` to the `VisualInstruction.carPlayManeuverLabelAttributedText(bounds:shieldHeight:window:traitCollection:instructionLabelType:)` to have the ability to change color of the shield icons depending on provided trait collection. ([#3882](https://github.com/mapbox/mapbox-navigation-ios/pull/3882))
4445
* Fixed an issue where a `StyleManager` for CarPlay would update the appearance on both CarPlay and the iOS device simultaneously. ([#3914](https://github.com/mapbox/mapbox-navigation-ios/pull/3914))
46+
* During the navigating activity, the user can tap the "Alternatives" button in the navigation bar to switch to an alternative route. ([#3956](https://github.com/mapbox/mapbox-navigation-ios/pull/3956))
4547

4648
### Other changes
4749

Sources/MapboxCoreNavigation/AlternativeRoute.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ public struct AlternativeRoute: Identifiable {
3838
public let infoFromDeviationPoint: RouteInfo
3939
/// Alternative route statistics, counting from it's origin.
4040
public let infoFromOrigin: RouteInfo
41-
41+
/// The difference of distances between alternative and the main routes
42+
public let distanceDelta: LocationDistance
43+
/// The difference of expected travel time between alternative and the main routes
44+
public let expectedTravelTimeDelta: TimeInterval
45+
4246
init?(mainRoute: Route, alternativeRoute nativeRouteAlternative: RouteAlternative) {
4347
self.id = nativeRouteAlternative.id
4448
guard let decoded = RerouteController.decode(routeRequest: nativeRouteAlternative.route.getRequestUri(),
@@ -70,6 +74,9 @@ public struct AlternativeRoute: Identifiable {
7074
duration: nativeRouteAlternative.infoFromFork.duration)
7175
self.infoFromOrigin = .init(distance: nativeRouteAlternative.infoFromStart.distance,
7276
duration: nativeRouteAlternative.infoFromStart.duration)
77+
78+
self.distanceDelta = infoFromOrigin.distance - mainRoute.distance
79+
self.expectedTravelTimeDelta = infoFromOrigin.duration - mainRoute.expectedTravelTime
7380
}
7481

7582
/**
@@ -83,13 +90,17 @@ public struct AlternativeRoute: Identifiable {
8390
mainRouteIntersection: Intersection,
8491
alternativeRouteIntersection: Intersection,
8592
infoFromDeviationPoint: RouteInfo,
86-
infoFromOrigin: RouteInfo) {
93+
infoFromOrigin: RouteInfo,
94+
distanceDelta: LocationDistance,
95+
expectedTravelTimeDelta: TimeInterval) {
8796
self.id = id
8897
self.indexedRouteResponse = indexedRouteResponse
8998
self.mainRouteIntersection = mainRouteIntersection
9099
self.alternativeRouteIntersection = alternativeRouteIntersection
91100
self.infoFromDeviationPoint = infoFromDeviationPoint
92101
self.infoFromOrigin = infoFromOrigin
102+
self.distanceDelta = distanceDelta
103+
self.expectedTravelTimeDelta = expectedTravelTimeDelta
93104
}
94105
}
95106

Sources/MapboxNavigation/BottomBannerViewController.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ open class BottomBannerViewController: UIViewController, NavigationComponent {
206206
distanceRemainingLabel.text = distanceFormatter.string(from: routeProgress.distanceRemaining)
207207
}
208208

209-
dateComponentsFormatter.unitsStyle = routeProgress.durationRemaining < 3600 ? .short : .abbreviated
209+
dateComponentsFormatter.unitsStyle = DateComponentsFormatter.travelDurationUnitStyle(interval: routeProgress.durationRemaining)
210210

211211
if let hardcodedTime = dateComponentsFormatter.string(from: 61), routeProgress.durationRemaining < 60 {
212212
timeRemainingLabel.text = String.localizedStringWithFormat(NSLocalizedString("LESS_THAN", bundle: .mapboxNavigation, value: "<%@", comment: "Format string for a short distance or time less than a minimum threshold; 1 = duration remaining"), hardcodedTime)

Sources/MapboxNavigation/CarPlayManager.swift

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,26 @@ public class CarPlayManager: NSObject {
300300
return muteButton
301301
}()
302302

303+
/**
304+
The bar button that brings alternative routes selection during navigation.
305+
*/
306+
public lazy var alternativeRoutesButton: CPBarButton = {
307+
let altsButton = CPBarButton(type: .text) { [weak self] (button: CPBarButton) in
308+
guard let template = self?.carPlayNavigationViewController?.alternativesListTemplate() else {
309+
return
310+
}
311+
self?.interfaceController?.pushTemplate(template,
312+
animated: true)
313+
}
314+
315+
altsButton.title = NSLocalizedString("CARPLAY_ALTERNATIVES",
316+
bundle: .mapboxNavigation,
317+
value: "Alternatives",
318+
comment: "Title for alternatives selection list button")
319+
320+
return altsButton
321+
}()
322+
303323
/**
304324
The bar button that prompts the presented navigation view controller to display the feedback screen.
305325
*/
@@ -989,7 +1009,7 @@ extension CarPlayManager: CPMapTemplateDelegate {
9891009
for: currentActivity) {
9901010
mapTemplate.leadingNavigationBarButtons = leadingButtons
9911011
} else {
992-
mapTemplate.leadingNavigationBarButtons = [muteButton]
1012+
mapTemplate.leadingNavigationBarButtons = [muteButton, alternativeRoutesButton]
9931013
}
9941014

9951015
if let trailingButtons = delegate?.carPlayManager(self,

Sources/MapboxNavigation/CarPlayNavigationViewController.swift

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import MapboxCoreNavigation
66
#if canImport(CarPlay)
77
import CarPlay
88

9+
let CarPlayAlternativeIDKey: String = "MBCarPlayAlternativeID"
10+
911
/**
1012
`CarPlayNavigationViewController` is a fully-featured turn-by-turn navigation UI for CarPlay.
1113

@@ -88,6 +90,86 @@ open class CarPlayNavigationViewController: UIViewController, BuildingHighlighti
8890
navigationService.router.continuousAlternatives
8991
}
9092

93+
private func format(value: LocationDistance,
94+
labels: (decreasing: String, increasing: String, equal: String)) -> String {
95+
switch value {
96+
case ..<0:
97+
return labels.decreasing
98+
case 0:
99+
return labels.equal
100+
default:
101+
return labels.increasing
102+
}
103+
}
104+
105+
func alternativesListTemplate() -> CPListTemplate {
106+
var variants: [CPListSection] = []
107+
let distanceFormatter = DistanceFormatter()
108+
self.continuousAlternatives.forEach { alternative in
109+
guard let title = alternative.indexedRouteResponse.currentRoute?.description else {
110+
return
111+
}
112+
distanceFormatter.measurementFormatter.numberFormatter.negativePrefix = ""
113+
let distanceDeltaText = distanceFormatter.string(from: alternative.distanceDelta)
114+
let distanceDelta = format(value: alternative.distanceDelta,
115+
labels: (decreasing: String.localizedStringWithFormat(NSLocalizedString("SHORTER_ALTERNATIVE",
116+
bundle: .mapboxNavigation,
117+
value: "%@ shorter",
118+
comment: "Alternatives selection note about a shorter route distance in any unit."),
119+
distanceDeltaText),
120+
increasing: String.localizedStringWithFormat(NSLocalizedString("LONGER_ALTERNATIVE",
121+
bundle: .mapboxNavigation,
122+
value: "%@ longer",
123+
comment: "Alternatives selection note about a longer route distance in any unit."),
124+
distanceDeltaText),
125+
equal: NSLocalizedString("SAME_DISTANCE",
126+
bundle: .mapboxNavigation,
127+
value: "Same distance",
128+
comment: "Alternatives selection note about equal travel distance.")))
129+
let timeDeltaText = DateComponentsFormatter.travelTimeString(alternative.expectedTravelTimeDelta, signed: false, unitStyle: .full)
130+
let timeDelta = format(value: alternative.expectedTravelTimeDelta,
131+
labels: (decreasing: String.localizedStringWithFormat(NSLocalizedString("FASTER_ALTERNATIVE",
132+
bundle: .mapboxNavigation,
133+
value: "%@ faster",
134+
comment: "Alternatives selection note about a faster route time interval in any unit."),
135+
timeDeltaText),
136+
increasing: String.localizedStringWithFormat(NSLocalizedString("SLOWER_ALTERNATIVE",
137+
bundle: .mapboxNavigation,
138+
value: "%@ slower",
139+
comment: "Alternatives selection note about a slower route time interval in any unit."),
140+
timeDeltaText),
141+
equal: NSLocalizedString("SAME_TIME",
142+
bundle: .mapboxNavigation,
143+
value: "Same time",
144+
comment: "Alternatives selection note about equal travel time.")))
145+
146+
let items: [CPListItem] = [CPListItem(text: String.localizedStringWithFormat(NSLocalizedString("ALTERNATIVE_NOTES",
147+
bundle: .mapboxNavigation,
148+
value: "%1$@ / %2$@",
149+
comment: "Combined alternatives selection notes about duration (first slot position) and distance (second slot position) delta."),
150+
timeDelta,
151+
distanceDelta),
152+
detailText: nil)]
153+
items.forEach { (item: CPListItem) -> Void in
154+
item.userInfo = [CarPlayAlternativeIDKey: alternative.id]
155+
}
156+
let section = CPListSection(items: items,
157+
header: title,
158+
sectionIndexTitle: nil)
159+
variants.append(section)
160+
}
161+
162+
let alternativesTitle = NSLocalizedString("CARPLAY_ALTERNATIVES",
163+
bundle: .mapboxNavigation,
164+
value: "Alternatives",
165+
comment: "Title for alternatives selection list button")
166+
167+
let template = CPListTemplate(title: alternativesTitle,
168+
sections: variants)
169+
template.delegate = self
170+
return template
171+
}
172+
91173
/**
92174
Controls whether night style will be used whenever traversing through a tunnel. Defaults to `true`.
93175
*/
@@ -1019,4 +1101,28 @@ extension CarPlayNavigationViewController: CPSessionConfigurationDelegate {
10191101
applyStyleIfNeeded(contentStyle)
10201102
}
10211103
}
1104+
1105+
@available(iOS 12.0, *)
1106+
extension CarPlayNavigationViewController: CPListTemplateDelegate {
1107+
1108+
public func listTemplate(_ listTemplate: CPListTemplate,
1109+
didSelect item: CPListItem,
1110+
completionHandler: @escaping () -> Void) {
1111+
// Selected a list item for switching to alternative route.
1112+
guard let userInfo = item.userInfo as? CarPlayUserInfo,
1113+
let alternativeId = userInfo[CarPlayAlternativeIDKey] as? AlternativeRoute.ID,
1114+
let alternativeRoute = continuousAlternatives.first(where: { $0.id == alternativeId})?.indexedRouteResponse else {
1115+
completionHandler()
1116+
return
1117+
}
1118+
1119+
navigationService.router.updateRoute(with: alternativeRoute,
1120+
routeOptions: nil,
1121+
completion: { _ in
1122+
self.carInterfaceController.popTemplate(animated: true)
1123+
completionHandler()
1124+
})
1125+
}
1126+
}
1127+
10221128
#endif

Sources/MapboxNavigation/DateComponentsFormatter+NavigationAdditions.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,26 @@ extension DateComponentsFormatter {
2121
formatter.allowedUnits = [.day, .hour, .minute]
2222
return formatter
2323
}()
24+
25+
public static func travelDurationUnitStyle(interval: TimeInterval) -> DateComponentsFormatter.UnitsStyle {
26+
return interval < 3600 ? .short : .abbreviated
27+
}
28+
29+
public static func travelTimeString(_ interval: TimeInterval,
30+
signed: Bool,
31+
unitStyle: DateComponentsFormatter.UnitsStyle?) -> String {
32+
let formatter = DateComponentsFormatter()
33+
formatter.unitsStyle = unitStyle ?? travelDurationUnitStyle(interval: interval)
34+
let timeString = formatter.string(from: signed ? interval : abs(interval)) ?? ""
35+
36+
if signed && interval >= 0 {
37+
return String.localizedStringWithFormat(NSLocalizedString("EXPLICITLY_POSITIVE_NUMBER",
38+
bundle: .mapboxNavigation,
39+
value: "+%@",
40+
comment: "Number string with an explicit '+' sign."),
41+
timeString)
42+
} else {
43+
return timeString
44+
}
45+
}
2446
}

Sources/MapboxNavigation/NavigationMapView+ContinuousAlternatives.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ extension NavigationMapView {
2020

2121
self.continuousAlternatives = continuousAlternatives
2222

23+
showContinuousAlternativeRoutesDurations()
24+
2325
guard let routes = self.routes,
2426
!routes.isEmpty else { return }
2527

@@ -52,6 +54,8 @@ extension NavigationMapView {
5254
removeContinuousAlternativesRoutesLayers()
5355

5456
continuousAlternatives = nil
57+
58+
showContinuousAlternativeRoutesDurations()
5559
}
5660

5761
/**

0 commit comments

Comments
 (0)