Electronic horizon
Predict user's most probable path and show upcoming intersections.
/*This code example is part of the Mapbox Navigation SDK for iOS demo app,which you can build and run: https://github.com/mapbox/mapbox-navigation-ios-examplesTo learn more about each example in this app, including descriptions and linksto documentation, see our docs: https://docs.mapbox.com/ios/navigation/examples/electronic-horizon*/ import UIKitimport MapboxNavigationimport MapboxCoreNavigationimport MapboxMapsimport Turf class ElectronicHorizonEventsViewController: UIViewController { private lazy var navigationMapView = NavigationMapView(frame: view.bounds) private let upcomingIntersectionLabel = UILabel()private let passiveLocationManager = PassiveLocationManager()private lazy var passiveLocationProvider = PassiveLocationProvider(locationManager: passiveLocationManager)private let routeLineColor: UIColor = .green.withAlphaComponent(0.9)private let traversedRouteColor: UIColor = .clearprivate var totalDistance: CLLocationDistance = 0.0 override func viewDidLoad() {super.viewDidLoad() setupNavigationMapView()setupUpcomingIntersectionLabel()setupElectronicHorizonUpdates()} func setupNavigationMapView() {navigationMapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]navigationMapView.mapView.location.overrideLocationProvider(with: passiveLocationProvider)navigationMapView.userLocationStyle = .puck2D()navigationMapView.mapView.mapboxMap.onNext(event: .styleLoaded, handler: { [weak self] _ inself?.setupMostProbablePathStyle()}) view.addSubview(navigationMapView)} func setupElectronicHorizonUpdates() {// Customize the `ElectronicHorizonOptions` for `PassiveLocationManager` to start Electronic Horizon updates.let options = ElectronicHorizonOptions(length: 500, expansionLevel: 1, branchLength: 50, minTimeDeltaBetweenUpdates: nil)passiveLocationManager.startUpdatingElectronicHorizon(with: options)subscribeToElectronicHorizonUpdates()} private func setupUpcomingIntersectionLabel() {upcomingIntersectionLabel.translatesAutoresizingMaskIntoConstraints = falseview.addSubview(upcomingIntersectionLabel) let safeAreaWidthAnchor = view.safeAreaLayoutGuide.widthAnchorNSLayoutConstraint.activate([upcomingIntersectionLabel.widthAnchor.constraint(lessThanOrEqualTo: safeAreaWidthAnchor, multiplier: 0.9),upcomingIntersectionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),upcomingIntersectionLabel.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor)])upcomingIntersectionLabel.backgroundColor = #colorLiteral(red: 0.9568627477, green: 0.6588235497, blue: 0.5450980663, alpha: 1)upcomingIntersectionLabel.layer.cornerRadius = 5upcomingIntersectionLabel.numberOfLines = 0} private func subscribeToElectronicHorizonUpdates() {NotificationCenter.default.addObserver(self,selector: #selector(didUpdateElectronicHorizonPosition),name: .electronicHorizonDidUpdatePosition,object: nil)} @objc private func didUpdateElectronicHorizonPosition(_ notification: Notification) {let horizonTreeKey = RoadGraph.NotificationUserInfoKey.treeKeyguard let horizonTree = notification.userInfo?[horizonTreeKey] as? RoadGraph.Edge,let position = notification.userInfo?[RoadGraph.NotificationUserInfoKey.positionKey] as? RoadGraph.Position,let updatesMostProbablePath = notification.userInfo?[RoadGraph.NotificationUserInfoKey.updatesMostProbablePathKey] as? Bool else {return} let currentStreetName = streetName(for: horizonTree)let upcomingCrossStreet = nearestCrossStreetName(from: horizonTree)updateLabel(currentStreetName: currentStreetName, predictedCrossStreet: upcomingCrossStreet) // Update the most probable path when the position update indicates a new most probable path (MPP).if updatesMostProbablePath {let mostProbablePath = routeLine(from: horizonTree, roadGraph: passiveLocationManager.roadGraph)updateMostProbablePath(with: mostProbablePath)} // Update the most probable path layer when the position update indicates// a change of the fraction of the point traveled distance to the current edge’s length.updateMostProbablePathLayer(fractionFromStart: position.fractionFromStart,roadGraph: passiveLocationManager.roadGraph,currentEdge: horizonTree.identifier)} private func streetName(for edge: RoadGraph.Edge) -> String? {let edgeMetadata = passiveLocationManager.roadGraph.edgeMetadata(edgeIdentifier: edge.identifier)return edgeMetadata?.names.first.map { roadName inswitch roadName {case .name(let name):return namecase .code(let code):return "\(code)"}}} private func nearestCrossStreetName(from edge: RoadGraph.Edge) -> String? {let initialStreetName = streetName(for: edge)var currentEdge: RoadGraph.Edge? = edgewhile let nextEdge = currentEdge?.outletEdges.max(by: { $0.probability < $1.probability }) {if let nextStreetName = streetName(for: nextEdge), nextStreetName != initialStreetName {return nextStreetName}currentEdge = nextEdge}return nil} private func updateLabel(currentStreetName: String?, predictedCrossStreet: String?) {var statusString = ""if let currentStreetName = currentStreetName {statusString = "Currently on:\n\(currentStreetName)"if let predictedCrossStreet = predictedCrossStreet {statusString += "\nUpcoming intersection with:\n\(predictedCrossStreet)"} else {statusString += "\nNo upcoming intersections"}} DispatchQueue.main.async {self.upcomingIntersectionLabel.text = statusStringself.upcomingIntersectionLabel.sizeToFit()}} // MARK: - Drawing the most probable pathprivate let sourceIdentifier = "mpp-source"private let layerIdentifier = "mpp-layer" private func routeLine(from edge: RoadGraph.Edge, roadGraph: RoadGraph) -> [LocationCoordinate2D] {var coordinates = [LocationCoordinate2D]()var edge: RoadGraph.Edge? = edgetotalDistance = 0.0 // Update the route line shape and total distance of the most probable path.while let currentEdge = edge {if let shape = roadGraph.edgeShape(edgeIdentifier: currentEdge.identifier) {coordinates.append(contentsOf: shape.coordinates.dropFirst(coordinates.isEmpty ? 0 : 1))}if let distance = roadGraph.edgeMetadata(edgeIdentifier: currentEdge.identifier)?.length {totalDistance += distance}edge = currentEdge.outletEdges.max(by: { $0.probability < $1.probability })}return coordinates} private func updateMostProbablePath(with mostProbablePath: [CLLocationCoordinate2D]) {let feature = Feature(geometry: .lineString(LineString(mostProbablePath)))try? navigationMapView.mapView.mapboxMap.style.updateGeoJSONSource(withId: sourceIdentifier,geoJSON: .feature(feature))} private func updateMostProbablePathLayer(fractionFromStart: Double,roadGraph: RoadGraph,currentEdge: RoadGraph.Edge.Identifier) {// Based on the length of current edge and the total distance of the most probable path (MPP),// calculate the fraction of the point traveled distance to the whole most probable path (MPP).if totalDistance > 0.0,let currentLength = roadGraph.edgeMetadata(edgeIdentifier: currentEdge)?.length {let fraction = fractionFromStart * currentLength / totalDistanceupdateMostProbablePathLayerFraction(fraction)}} private func setupMostProbablePathStyle() {var source = GeoJSONSource()source.data = .geometry(Geometry.lineString(LineString([])))source.lineMetrics = truetry? navigationMapView.mapView.mapboxMap.style.addSource(source, id: sourceIdentifier) var layer = LineLayer(id: layerIdentifier)layer.source = sourceIdentifierlayer.lineWidth = .expression(Exp(.interpolate) {Exp(.linear)Exp(.zoom)RouteLineWidthByZoomLevel.mapValues { $0 * 0.5 }})layer.lineColor = .constant(.init(routeLineColor))layer.lineCap = .constant(.round)layer.lineJoin = .constant(.miter)layer.minZoom = 9try? navigationMapView.mapView.mapboxMap.style.addLayer(layer)} // Update the line gradient property of the most probable path line layer,// so the part of the most probable path that has been traversed will be rendered with full transparency.private func updateMostProbablePathLayerFraction(_ fraction: Double) {let nextDown = max(fraction.nextDown, 0.0)let exp = Exp(.step) {Exp(.lineProgress)traversedRouteColornextDowntraversedRouteColorfractionrouteLineColor} if let data = try? JSONEncoder().encode(exp.self),let jsonObject = try? JSONSerialization.jsonObject(with: data, options: []) {try? navigationMapView.mapView.mapboxMap.style.setLayerProperty(for: layerIdentifier,property: "line-gradient",value: jsonObject)}}}