Skip to content

Commit d649164

Browse files
committed
Add the intersection signals along current route.
1 parent 643bd18 commit d649164

File tree

16 files changed

+242
-0
lines changed

16 files changed

+242
-0
lines changed

Sources/MapboxNavigation/CarPlayMapViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ extension CarPlayMapViewController: StyleManagerDelegate {
405405
if mapboxMapStyle.uri?.rawValue != style.mapStyleURL.absoluteString {
406406
let styleURI = StyleURI(url: style.mapStyleURL)
407407
mapboxMapStyle.uri = styleURI
408+
navigationMapView.styleType = style.styleType
408409
// Update the sprite repository of wayNameView when map style changes.
409410
wayNameView?.label.updateStyle(styleURI: styleURI, idiom: .carPlay)
410411
}

Sources/MapboxNavigation/CarPlayNavigationViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1035,6 +1035,7 @@ extension CarPlayNavigationViewController: StyleManagerDelegate {
10351035
if mapboxMapStyle?.uri?.rawValue != style.mapStyleURL.absoluteString {
10361036
let styleURI = StyleURI(url: style.mapStyleURL)
10371037
mapboxMapStyle?.uri = styleURI
1038+
navigationMapView?.styleType = style.styleType
10381039
// Update the sprite repository of wayNameView when map style changes.
10391040
wayNameView?.label.updateStyle(styleURI: styleURI, idiom: .carPlay)
10401041
}

Sources/MapboxNavigation/NavigationMapView.swift

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ open class NavigationMapView: UIView {
264264
customRouteLineLayerPosition = layerPosition
265265

266266
applyRoutesDisplay(layerPosition: layerPosition)
267+
updateIntersectionSignalsAlongRouteOnMap(styleType: styleType)
267268
}
268269

269270
func applyRoutesDisplay(layerPosition: MapboxMaps.LayerPosition? = nil) {
@@ -351,6 +352,7 @@ open class NavigationMapView: UIView {
351352

352353
routes = nil
353354
removeLineGradientStops()
355+
removeIntersectionSignals()
354356
}
355357

356358
/**
@@ -1125,8 +1127,107 @@ open class NavigationMapView: UIView {
11251127
}
11261128
}
11271129

1130+
/**
1131+
Shows the intersection signals on current route.
1132+
*/
1133+
public var showsIntersectionSignalsOnRoutes: Bool = false {
1134+
didSet {
1135+
updateIntersectionSignalsAlongRouteOnMap(styleType: styleType)
1136+
}
1137+
}
1138+
1139+
/**
1140+
The style type of `NavigationMapView` during active navigation.
1141+
*/
1142+
var styleType: StyleType = .day {
1143+
didSet {
1144+
updateIntersectionSignalsAlongRouteOnMap(styleType: styleType)
1145+
}
1146+
}
1147+
11281148
private let continuousAlternativeDurationAnnotationOffset: LocationDistance = 75
11291149

1150+
func updateIntersectionSignalsAlongRouteOnMap(styleType: StyleType = .day) {
1151+
guard showsIntersectionSignalsOnRoutes, let route = routes?.first else {
1152+
removeIntersectionSignals()
1153+
return
1154+
}
1155+
1156+
do {
1157+
try updateIntersectionSymbolImages()
1158+
} catch {
1159+
Log.error("Error occured while updating intersection signal images: \(error.localizedDescription).",
1160+
category: .navigationUI)
1161+
}
1162+
1163+
updateIntersectionSignals(along: route, styleType: styleType)
1164+
}
1165+
1166+
private func signalFeature(from intersection: Intersection, styleType: StyleType) -> Feature? {
1167+
var properties: JSONObject? = nil
1168+
let styleTypeString = (styleType == .day) ? "Day" : "Night"
1169+
if intersection.railroadCrossing == true {
1170+
properties = ["imageName": .string("RailroadCrossing" + styleTypeString)]
1171+
}
1172+
if intersection.trafficSignal == true {
1173+
properties = ["imageName": .string("TrafficSignal" + styleTypeString)]
1174+
}
1175+
1176+
guard let properties = properties else { return nil }
1177+
1178+
var feature = Feature(geometry: .point(Point(intersection.location)))
1179+
feature.properties = properties
1180+
return feature
1181+
1182+
}
1183+
1184+
private func updateIntersectionSignals(along route: Route, styleType: StyleType) {
1185+
var featureCollection = FeatureCollection(features: [])
1186+
for leg in route.legs {
1187+
for step in leg.steps {
1188+
guard let intersections = step.intersections else { continue }
1189+
for intersection in intersections {
1190+
guard let feature = signalFeature(from: intersection, styleType: styleType) else { continue }
1191+
featureCollection.features.append(feature)
1192+
}
1193+
}
1194+
}
1195+
1196+
let style = mapView.mapboxMap.style
1197+
1198+
do {
1199+
let sourceIdentifier = NavigationMapView.SourceIdentifier.intersectionSignalSource
1200+
if style.sourceExists(withId: sourceIdentifier) {
1201+
try style.updateGeoJSONSource(withId: sourceIdentifier, geoJSON: .featureCollection(featureCollection))
1202+
} else {
1203+
var source = GeoJSONSource()
1204+
source.data = .featureCollection(featureCollection)
1205+
try style.addSource(source, id: sourceIdentifier)
1206+
}
1207+
1208+
let layerIdentifier = NavigationMapView.LayerIdentifier.intersectionSignalLayer
1209+
var shapeLayer: SymbolLayer
1210+
if style.layerExists(withId: layerIdentifier),
1211+
let symbolLayer = try style.layer(withId: layerIdentifier) as? SymbolLayer {
1212+
shapeLayer = symbolLayer
1213+
} else {
1214+
shapeLayer = SymbolLayer(id: layerIdentifier)
1215+
}
1216+
1217+
shapeLayer.source = sourceIdentifier
1218+
shapeLayer.iconAllowOverlap = .constant(false)
1219+
shapeLayer.iconImage = .expression(Exp(.get) {
1220+
"imageName"
1221+
})
1222+
1223+
let layerPosition = layerPosition(for: layerIdentifier)
1224+
try style.addPersistentLayer(shapeLayer, layerPosition: layerPosition)
1225+
} catch {
1226+
Log.error("Failed to perform operation while adding intersection signals with error: \(error.localizedDescription).",
1227+
category: .navigationUI)
1228+
}
1229+
}
1230+
11301231
func showContinuousAlternativeRoutesDurations() {
11311232
// Remove any existing route annotation.
11321233
removeContinuousAlternativeRoutesDurations()
@@ -1338,6 +1439,15 @@ open class NavigationMapView: UIView {
13381439
style.removeSources([NavigationMapView.SourceIdentifier.routeDurationAnnotationsSource])
13391440
}
13401441

1442+
/**
1443+
Removes all intersection signals on current route.
1444+
*/
1445+
func removeIntersectionSignals() {
1446+
let style = mapView.mapboxMap.style
1447+
style.removeLayers([NavigationMapView.LayerIdentifier.intersectionSignalLayer])
1448+
style.removeSources([NavigationMapView.SourceIdentifier.intersectionSignalSource])
1449+
}
1450+
13411451
/**
13421452
Removes all visible continuous alternative routes duration callouts.
13431453
*/
@@ -1628,6 +1738,33 @@ open class NavigationMapView: UIView {
16281738
return pointAnnotationManager?.annotations.filter({ $0.id == identifier }) ?? []
16291739
}
16301740

1741+
/**
1742+
Updates the image assets in the map style for the route intersection signals.
1743+
*/
1744+
private func updateIntersectionSymbolImages() throws {
1745+
let style = mapView.mapboxMap.style
1746+
1747+
if style.image(withId: ImageIdentifier.trafficSignalDay) == nil,
1748+
let trafficSignlaDay = Bundle.mapboxNavigation.image(named: "TrafficSignalDay") {
1749+
try style.addImage(trafficSignlaDay, id: ImageIdentifier.trafficSignalDay)
1750+
}
1751+
1752+
if style.image(withId: ImageIdentifier.trafficSignalNight) == nil,
1753+
let trafficSignalNight = Bundle.mapboxNavigation.image(named: "TrafficSignalNight") {
1754+
try style.addImage(trafficSignalNight, id: ImageIdentifier.trafficSignalNight)
1755+
}
1756+
1757+
if style.image(withId: ImageIdentifier.railroadCrossingDay) == nil,
1758+
let railroadCrossingDay = Bundle.mapboxNavigation.image(named: "RailroadCrossingDay") {
1759+
try style.addImage(railroadCrossingDay, id: ImageIdentifier.railroadCrossingDay)
1760+
}
1761+
1762+
if style.image(withId: ImageIdentifier.railroadCrossingNight) == nil,
1763+
let railroadCrossingNight = Bundle.mapboxNavigation.image(named: "RailroadCrossingNight") {
1764+
try style.addImage(railroadCrossingNight, id: ImageIdentifier.railroadCrossingNight)
1765+
}
1766+
}
1767+
16311768
/**
16321769
Updates the image assets in the map style for the route duration annotations. Useful when the
16331770
desired callout colors change, such as when transitioning between light and dark mode on iOS 13 and later.
@@ -1827,6 +1964,7 @@ open class NavigationMapView: UIView {
18271964
route?.identifier(.restrictedRouteAreaRoute)
18281965
].compactMap{ $0 }
18291966
let arrowLayers: [String] = [
1967+
LayerIdentifier.intersectionSignalLayer,
18301968
LayerIdentifier.arrowStrokeLayer,
18311969
LayerIdentifier.arrowLayer,
18321970
LayerIdentifier.arrowSymbolCasingLayer,

Sources/MapboxNavigation/NavigationMapViewIdentifiers.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ extension NavigationMapView {
1111
static let arrowSymbolCasingLayer = "\(identifier)_arrowSymbolCasingLayer"
1212
static let voiceInstructionLabelLayer = "\(identifier)_voiceInstructionLabelLayer"
1313
static let voiceInstructionCircleLayer = "\(identifier)_voiceInstructionCircleLayer"
14+
static let intersectionSignalLayer = "\(identifier)_trafficLayer"
1415
static let waypointCircleLayer = "\(identifier)_waypointCircleLayer"
1516
static let waypointSymbolLayer = "\(identifier)_waypointSymbolLayer"
1617
static let buildingExtrusionLayer = "\(identifier)_buildingExtrusionLayer"
@@ -25,6 +26,7 @@ extension NavigationMapView {
2526
static let arrowStrokeSource = "\(identifier)_arrowStrokeSource"
2627
static let arrowSymbolSource = "\(identifier)_arrowSymbolSource"
2728
static let voiceInstructionSource = "\(identifier)_instructionSource"
29+
static let intersectionSignalSource = "\(identifier)_trafficSource"
2830
static let waypointSource = "\(identifier)_waypointSource"
2931
static let routeDurationAnnotationsSource: String = "\(identifier)_routeDurationAnnotationsSource"
3032
static let continuousAlternativeRoutesDurationAnnotationsSource: String = "\(identifier)_continuousAlternativeRoutesDurationAnnotationsSource"
@@ -36,6 +38,10 @@ extension NavigationMapView {
3638
static let markerImage = "default_marker"
3739
static let routeAnnotationLeftHanded = "RouteInfoAnnotationLeftHanded"
3840
static let routeAnnotationRightHanded = "RouteInfoAnnotationRightHanded"
41+
static let trafficSignalDay = "TrafficSignalDay"
42+
static let trafficSignalNight = "TrafficSignalNight"
43+
static let railroadCrossingDay = "RailroadCrossingDay"
44+
static let railroadCrossingNight = "RailroadCrossingNight"
3945
}
4046

4147
struct ModelKeyIdentifier {

Sources/MapboxNavigation/NavigationViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,7 @@ extension NavigationViewController: StyleManagerDelegate {
11581158
if navigationMapView?.mapView.mapboxMap.style.uri?.rawValue != style.mapStyleURL.absoluteString {
11591159
let styleURI = StyleURI(url: style.mapStyleURL)
11601160
navigationMapView?.mapView.mapboxMap.style.uri = styleURI
1161+
navigationMapView?.styleType = style.styleType
11611162
// Update the sprite repository of wayNameView when map style changes.
11621163
ornamentsController?.updateStyle(styleURI: styleURI)
11631164
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"info" : {
3+
"version" : 1,
4+
"author" : "xcode"
5+
}
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "universal",
5+
"filename" : "RailroadCrossingDay.pdf"
6+
}
7+
],
8+
"info" : {
9+
"version" : 1,
10+
"author" : "xcode"
11+
},
12+
"properties" : {
13+
"template-rendering-intent" : "template",
14+
"preserves-vector-representation" : true
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "universal",
5+
"filename" : "RailroadCrossingNight.pdf"
6+
}
7+
],
8+
"info" : {
9+
"version" : 1,
10+
"author" : "xcode"
11+
},
12+
"properties" : {
13+
"template-rendering-intent" : "template",
14+
"preserves-vector-representation" : true
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "universal",
5+
"filename" : "TrafficSignalDay.pdf"
6+
}
7+
],
8+
"info" : {
9+
"version" : 1,
10+
"author" : "xcode"
11+
},
12+
"properties" : {
13+
"template-rendering-intent" : "template",
14+
"preserves-vector-representation" : true
15+
}
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"images" : [
3+
{
4+
"idiom" : "universal",
5+
"filename" : "TrafficSignalNight.pdf"
6+
}
7+
],
8+
"info" : {
9+
"version" : 1,
10+
"author" : "xcode"
11+
},
12+
"properties" : {
13+
"template-rendering-intent" : "template",
14+
"preserves-vector-representation" : true
15+
}
16+
}

Tests/MapboxNavigationTests/NavigationMapViewTests.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,4 +487,26 @@ class NavigationMapViewTests: TestCase {
487487
NavigationMapView.AnnotationIdentifier.finalDestinationAnnotation,
488488
"Point annotation identifiers should be equal.")
489489
}
490+
491+
func testUpdateIntersectionSignalsAlongRouteOnMap() {
492+
let navigationMapView = NavigationMapView(frame: CGRect(origin: .zero, size: .iPhone6Plus))
493+
let imageIdentifier = NavigationMapView.ImageIdentifier.trafficSignalDay
494+
let layerIdentifier = NavigationMapView.LayerIdentifier.intersectionSignalLayer
495+
let style = navigationMapView.mapView.mapboxMap.style
496+
497+
XCTAssertFalse(navigationMapView.showsIntersectionSignalsOnRoutes)
498+
499+
let route = loadRoute(from: "route-with-mixed-road-classes")
500+
navigationMapView.show([route])
501+
XCTAssertFalse(style.imageExists(withId: imageIdentifier))
502+
XCTAssertFalse(style.layerExists(withId: layerIdentifier))
503+
504+
navigationMapView.showsIntersectionSignalsOnRoutes = true
505+
XCTAssertTrue(style.imageExists(withId: imageIdentifier))
506+
XCTAssertTrue(style.layerExists(withId: layerIdentifier))
507+
508+
navigationMapView.removeRoutes()
509+
XCTAssertTrue(style.imageExists(withId: imageIdentifier))
510+
XCTAssertFalse(style.layerExists(withId: layerIdentifier))
511+
}
490512
}

Tests/MapboxNavigationTests/RouteLineLayerPositionTests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ class RouteLineLayerPositionTests: TestCase {
239239
navigationMapView.removeWaypoints()
240240
navigationMapView.showsRestrictedAreasOnRoute = false
241241
navigationMapView.routeLineTracksTraversal = true
242+
navigationMapView.showsIntersectionSignalsOnRoutes = true
242243

243244
expectedLayerSequence = [
244245
buildingLayer["id"]!,
@@ -247,6 +248,7 @@ class RouteLineLayerPositionTests: TestCase {
247248
multilegRoute.identifier(.route(isMainRoute: true)),
248249
roadTrafficLayer["id"]!,
249250
roadLabelLayer["id"]!,
251+
NavigationMapView.LayerIdentifier.intersectionSignalLayer,
250252
NavigationMapView.LayerIdentifier.arrowStrokeLayer,
251253
NavigationMapView.LayerIdentifier.arrowLayer,
252254
NavigationMapView.LayerIdentifier.arrowSymbolCasingLayer,
@@ -307,6 +309,7 @@ class RouteLineLayerPositionTests: TestCase {
307309
multilegRoute.identifier(.route(isMainRoute: true)),
308310
multilegRoute.identifier(.restrictedRouteAreaRoute),
309311
roadLabelLayer["id"]!,
312+
NavigationMapView.LayerIdentifier.intersectionSignalLayer,
310313
NavigationMapView.LayerIdentifier.arrowStrokeLayer,
311314
NavigationMapView.LayerIdentifier.arrowLayer,
312315
NavigationMapView.LayerIdentifier.arrowSymbolCasingLayer,

0 commit comments

Comments
 (0)