Skip to content

Add ZoneAvoidance sample #221

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Navigation-Examples.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
C5F130A01FE9B44600463E86 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F1309F1FE9B44600463E86 /* Constants.swift */; };
C5F130A61FEB2D7800463E86 /* Advanced.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5F130A51FEB2D7800463E86 /* Advanced.swift */; };
CC6D97FED3D0CBEF7CA47168 /* Pods_Navigation_Examples.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8592DC8F0A49F0C2214E1AC /* Pods_Navigation_Examples.framework */; };
DFC7EE142C0A22DC00266857 /* Zone-Avoidance.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC7EE132C0A22DC00266857 /* Zone-Avoidance.swift */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
Expand Down Expand Up @@ -114,6 +115,7 @@
C5F1309F1FE9B44600463E86 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
C5F130A51FEB2D7800463E86 /* Advanced.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advanced.swift; sourceTree = "<group>"; };
C8592DC8F0A49F0C2214E1AC /* Pods_Navigation_Examples.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Navigation_Examples.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DFC7EE132C0A22DC00266857 /* Zone-Avoidance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Zone-Avoidance.swift"; sourceTree = "<group>"; };
E4F99A65CCB2B9CB623270EE /* Pods-Navigation-Examples.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Navigation-Examples.debug.xcconfig"; path = "Target Support Files/Pods-Navigation-Examples/Pods-Navigation-Examples.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */

Expand Down Expand Up @@ -280,6 +282,7 @@
11B6E81626176F3600872E4D /* Upcoming-Intersection.swift */,
B4F805ED26791AD900D5F1C8 /* Custom-User-Location.swift */,
3B39AC1028CB6F7F0052A80E /* History-Recording.swift */,
DFC7EE132C0A22DC00266857 /* Zone-Avoidance.swift */,
8AFBF217265E851700468551 /* CustomSegue */,
8A2DFA52261650600034A87E /* NavigationCamera */,
8AC9129E2494118400B6941E /* TestData */,
Expand Down Expand Up @@ -622,6 +625,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DFC7EE142C0A22DC00266857 /* Zone-Avoidance.swift in Sources */,
C5F130A01FE9B44600463E86 /* Constants.swift in Sources */,
8A2DFA6C261651600034A87E /* Custom-Navigation-Camera.swift in Sources */,
11DC36F226161DAF0042CD4A /* Location-Snapping.swift in Sources */,
Expand Down
7 changes: 7 additions & 0 deletions Navigation-Examples/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,12 @@ let listOfExamples: [NamedController] = [
controller: HistoryRecordingViewController.self,
storyboard: nil,
pushExampleToViewController: true
),
(
name: "Zone Avoidance",
description: "Demonstrates how to build navigation routes with zone avoidance.",
controller: ZoneAvoidanceViewController.self,
storyboard: nil,
pushExampleToViewController: true
)
]
223 changes: 223 additions & 0 deletions Navigation-Examples/Examples/Zone-Avoidance.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
/*
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-examples
To learn more about each example in this app, including descriptions and links
to documentation, see our docs: https://docs.mapbox.com/ios/navigation/examples
*/

import UIKit
import MapboxCoreNavigation
import MapboxNavigation
import MapboxDirections
import MapboxMaps

class ZoneAvoidanceViewController: UIViewController, NavigationMapViewDelegate, NavigationViewControllerDelegate {

var navigationMapView: NavigationMapView!

var routeResponse: RouteResponse? {
didSet {
guard let routes = routeResponse?.routes, let currentRoute = routes.first else {
navigationMapView.removeRoutes()
return
}
navigationMapView.show(routes)
navigationMapView.showWaypoints(on: currentRoute)
}
}

var startButton: UIButton!
var recipeNameTextField: UITextField!

// MARK: - UIViewController lifecycle methods

override func viewDidLoad() {
super.viewDidLoad()

navigationMapView = NavigationMapView(frame: view.bounds)
navigationMapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
navigationMapView.delegate = self
navigationMapView.userLocationStyle = .puck2D()

let navigationViewportDataSource = NavigationViewportDataSource(navigationMapView.mapView, viewportDataSourceType: .raw)
navigationViewportDataSource.options.followingCameraOptions.zoomUpdatesAllowed = false
navigationViewportDataSource.followingMobileCamera.zoom = 13.0
navigationMapView.navigationCamera.viewportDataSource = navigationViewportDataSource

view.addSubview(navigationMapView)

startButton = UIButton()
startButton.setTitle("Start Navigation", for: .normal)
startButton.translatesAutoresizingMaskIntoConstraints = false
startButton.backgroundColor = .blue
startButton.contentEdgeInsets = UIEdgeInsets(top: 10, left: 20, bottom: 10, right: 20)
startButton.addTarget(self, action: #selector(tappedStartButton(sender:)), for: .touchUpInside)
startButton.isHidden = true
view.addSubview(startButton)

startButton.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, constant: -20).isActive = true
startButton.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
view.setNeedsLayout()

let gesture = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))
navigationMapView.addGestureRecognizer(gesture)

setupRecipeTextField()
}

func setupRecipeTextField() {
recipeNameTextField = UITextField(frame: CGRect(x: 75, y: 100, width: 300, height: 35))
recipeNameTextField.placeholder = "Recipe name"
recipeNameTextField.borderStyle = .roundedRect
recipeNameTextField.center.x = view.center.x
recipeNameTextField.isHidden = false
view.addSubview(recipeNameTextField)
}

// Override layout lifecycle callback to be able to style the start button.
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()

startButton.layer.cornerRadius = startButton.bounds.midY
startButton.clipsToBounds = true
startButton.setNeedsDisplay()
}

@objc func tappedStartButton(sender: UIButton) {
guard let routeResponse = routeResponse else { return }

// For demonstration purposes, simulate locations if the Simulate Navigation option is on.
let indexedRouteResponse = IndexedRouteResponse(routeResponse: routeResponse, routeIndex: 0)
let navigationService = MapboxNavigationService(indexedRouteResponse: indexedRouteResponse,
customRoutingProvider: NavigationSettings.shared.directions,
credentials: NavigationSettings.shared.directions.credentials,
simulating: simulationIsEnabled ? .always : .onPoorGPS)
let navigationOptions = NavigationOptions(navigationService: navigationService)
let navigationViewController = NavigationViewController(for: indexedRouteResponse,
navigationOptions: navigationOptions)
navigationViewController.delegate = self

present(navigationViewController, animated: true, completion: nil)
}

@objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {
guard gesture.state == .ended else { return }
let location = navigationMapView.mapView.mapboxMap.coordinate(for: gesture.location(in: navigationMapView.mapView))

requestRoute(destination: location)
}

func requestRoute(destination: CLLocationCoordinate2D) {
guard let userLocation = navigationMapView.mapView.location.latestLocation else { return }
guard let recipeName = recipeNameTextField.text, !recipeName.isEmpty else {
let alert = UIAlertController(
title: "Error",
message: "Please type BYOND recipe name first",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
return
}

let location = CLLocation(latitude: userLocation.coordinate.latitude,
longitude: userLocation.coordinate.longitude)

let userWaypoint = Waypoint(location: location,
heading: userLocation.heading,
name: "user")

let destinationWaypoint = Waypoint(coordinate: destination)
let navigationRouteOptions = ZoneAvoidanceRouteOptions(
waypoints: [userWaypoint, destinationWaypoint],
byondRecipeName: recipeName
)

Directions.shared.calculate(navigationRouteOptions) { [weak self] (_, result) in
switch result {
case .failure(let error):
print(error.localizedDescription)
case .success(let response):
guard let routes = response.routes,
let currentRoute = routes.first,
let self = self else { return }

// As an example we are cheking only the first leg on the main route
checkLegForViolations(currentRoute.legs.first!)
self.routeResponse = response
self.startButton?.isHidden = false
self.recipeNameTextField?.isHidden = true
self.navigationMapView.show(routes)
self.navigationMapView.showWaypoints(on: currentRoute)
}
}
}

func checkLegForViolations(_ leg: RouteLeg) {
// Documentation on notifications: https://docs.mapbox.com/api/navigation/directions/#notification-object
// BYOND violation has subtype "byondExcludeRouting"

guard let notificationsJsonObj = leg.foreignMembers["notifications"] else { return }
guard case let .array(notifications) = notificationsJsonObj else { return }

let notificationSubtypes = notifications
.compactMap { $0 }
.compactMap { (notification: JSONValue) -> JSONValue?? in
if case let .object(properties) = notification {
return properties["subtype"]
} else {
return nil
}
}
.compactMap { $0 }

guard notificationSubtypes.contains(where: { subtypeJSONValue in
if case let .string(subtype) = subtypeJSONValue {
return subtype == "byondExcludeRouting"
} else {
return false
}
}) else { return }

let alert = UIAlertController(
title: "Violation",
message: "The built route contains a BYOND violation",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "OK", style: .default))
present(alert, animated: true)
}

func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) {
dismiss(animated: true, completion: nil)
}
}

class ZoneAvoidanceRouteOptions: NavigationRouteOptions {
var byondRecipeName: String!

// add byond_recipe_name to URLQueryItems
override var urlQueryItems: [URLQueryItem] {
var items = super.urlQueryItems
items.append(URLQueryItem(name: "byond_recipe_name", value: byondRecipeName))
return items
}

// create initializer to take in the byond_recipe_name
public init(waypoints: [Waypoint], byondRecipeName: String) {
self.byondRecipeName = byondRecipeName
super.init(waypoints: waypoints)
}

required init(from decoder: Decoder) throws {
fatalError("init(from:) has not been implemented")
}

required init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic) {
fatalError("init(waypoints:profileIdentifier:) has not been implemented")
}

required init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = .automobileAvoidingTraffic, queryItems: [URLQueryItem]? = nil) {
fatalError("init(waypoints:profileIdentifier:queryItems:) has not been implemented")
}
}