Offline regions
Download offline regions for navigation.
/*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*/ import UIKitimport MapboxCoreNavigationimport MapboxNavigationNativeimport MapboxMapsimport MapboxNavigationimport MapboxDirections class OfflineRegionsViewController: UIViewController {// MARK: Setup variables for Tile Managementlet styleURI: StyleURI = .streetsvar region: Region?let zoomMin: UInt8 = 0let zoomMax: UInt8 = 16let offlineManager = OfflineManager(resourceOptions: .init(accessToken: NavigationSettings.shared.directions.credentials.accessToken ?? ""))let tileStoreConfiguration: TileStoreConfiguration = .defaultlet tileStoreLocation: TileStoreConfiguration.Location = .defaultvar tileStore: TileStore {tileStoreLocation.tileStore} var downloadButton = UIButton()var startButton = UIButton()var navigationMapView: NavigationMapView?var passiveLocationManager: PassiveLocationManager?var options: NavigationRouteOptions? var routeResponse: RouteResponse? {didSet {showRoutes()showStartNavigationAlert()}} var routeIndex: Int = 0 {didSet {showRoutes()}} struct Region {var bbox: [CLLocationCoordinate2D]var identifier: String} // MARK: Life cycle override func viewDidLoad() {super.viewDidLoad()setupNavigationMapView()addDownloadButton()addStartButton()} func setupNavigationMapView() {navigationMapView = NavigationMapView(frame: view.bounds)navigationMapView?.autoresizingMask = [.flexibleWidth, .flexibleHeight]navigationMapView?.userLocationStyle = .puck2D()navigationMapView?.delegate = selfview.addSubview(navigationMapView!) setupGestureRecognizers() passiveLocationManager = PassiveLocationManager()let passiveLocationProvider = PassiveLocationProvider(locationManager: passiveLocationManager!)navigationMapView?.mapView.location.overrideLocationProvider(with: passiveLocationProvider) navigationMapView?.mapView.mapboxMap.onNext(event: .styleLoaded) { _ inself.createRegion()}} func addDownloadButton() {downloadButton.setTitle("Download Offline Region", for: .normal)downloadButton.backgroundColor = .bluedownloadButton.layer.cornerRadius = 5downloadButton.translatesAutoresizingMaskIntoConstraints = falsedownloadButton.addTarget(self, action: #selector(tappedDownloadButton(sender:)), for: .touchUpInside)view.addSubview(downloadButton) downloadButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50).isActive = truedownloadButton.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = truedownloadButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)downloadButton.sizeToFit()downloadButton.titleLabel?.font = UIFont.systemFont(ofSize: 25)} func addStartButton() {startButton.setTitle("Start Offline Navigation", for: .normal)startButton.backgroundColor = .bluestartButton.layer.cornerRadius = 5startButton.translatesAutoresizingMaskIntoConstraints = falsestartButton.addTarget(self, action: #selector(tappedStartButton(sender:)), for: .touchUpInside)showStartButton(false)view.addSubview(startButton) startButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -50).isActive = truestartButton.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor).isActive = truestartButton.contentEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5)startButton.sizeToFit()startButton.titleLabel?.font = UIFont.systemFont(ofSize: 25)} func showStartButton(_ show: Bool = true) {startButton.isHidden = !showstartButton.isEnabled = show} func setupGestureRecognizers() {let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(handleLongPress(_:)))navigationMapView?.gestureRecognizers?.filter({ $0 is UILongPressGestureRecognizer }).forEach(longPressGestureRecognizer.require(toFail:))navigationMapView?.addGestureRecognizer(longPressGestureRecognizer)} @objc func handleLongPress(_ gesture: UILongPressGestureRecognizer) {let gestureLocation = gesture.location(in: navigationMapView)guard gesture.state == .began,downloadButton.isHidden == true,let currentCoordinate = passiveLocationManager?.location?.coordinate,let destinationCoordinate = navigationMapView?.mapView.mapboxMap.coordinate(for: gestureLocation) else { return } options = NavigationRouteOptions(coordinates: [currentCoordinate, destinationCoordinate])requestRoute()} @objc func tappedDownloadButton(sender: UIButton) {downloadButton.isHidden = truedownloadTileRegion()} @objc func tappedStartButton(sender: UIButton) {showStartButton(false)startNavigation()} // MARK: Offline navigation func showRoutes() {guard var routes = routeResponse?.routes, !routes.isEmpty else { return }routes.insert(routes.remove(at: routeIndex), at: 0)navigationMapView?.showsCongestionForAlternativeRoutes = truenavigationMapView?.showsRestrictedAreasOnRoute = truenavigationMapView?.showcase(routes)navigationMapView?.showRouteDurations(along: routes)} func showStartNavigationAlert() {let alertController = UIAlertController(title: "Start navigation",message: "Turn off network access to start active navigation",preferredStyle: .alert)let approveAction = UIAlertAction(title: "OK", style: .default, handler: {_ in self.showStartButton()})alertController.addAction(approveAction)self.present(alertController, animated: true, completion: nil)} func requestRoute() {guard let options = options else { return }Directions.shared.calculate(options) { [weak self] (_, result) inswitch result {case .failure(let error):print("Failed to request route with error: \(error.localizedDescription)")case .success(let response):guard let strongSelf = self else { return }strongSelf.routeResponse = response}}} func startNavigation() {guard let response = routeResponse else { return }let indexedRouteResponse = IndexedRouteResponse(routeResponse: response, routeIndex: routeIndex)let navigationService = MapboxNavigationService(indexedRouteResponse: indexedRouteResponse,customRoutingProvider: NavigationSettings.shared.directions,credentials: NavigationSettings.shared.directions.credentials,simulating: .always)let navigationOptions = NavigationOptions(navigationService: navigationService)let navigationViewController = NavigationViewController(for: indexedRouteResponse,navigationOptions: navigationOptions)navigationViewController.delegate = selfnavigationViewController.modalPresentationStyle = .fullScreen present(navigationViewController, animated: true) {self.navigationMapView = nilself.passiveLocationManager = nil}} // MARK: Create regions func createRegion() {guard let location = passiveLocationManager?.location?.coordinate else { return }if region == nil {// Generate a rectangle based on current user locationlet distance: CLLocationDistance = 1e4let directions: [CLLocationDegrees] = [45, 135, 225, 315, 45]let coordinates = directions.map { location.coordinate(at: distance, facing: $0) }region = Region(bbox: coordinates, identifier: "Current location")}addRegionBoxLine()} func addRegionBoxLine() {guard let style = navigationMapView?.mapView.mapboxMap.style,let coordinates = region?.bbox else { return }do {let identifier = "regionBox"var source = GeoJSONSource()source.data = .geometry(.lineString(.init(coordinates)))try style.addSource(source, id: identifier) var layer = LineLayer(id: identifier)layer.source = identifierlayer.lineWidth = .constant(3.0)layer.lineColor = .constant(.init(.red))try style.addPersistentLayer(layer)} catch {print("Error \(error.localizedDescription) occured while adding box for region boundary.")}} // MARK: Download offline Regions func downloadTileRegion() {// Create style packageguard let region = region,let stylePackLoadOptions = StylePackLoadOptions(glyphsRasterizationMode: .ideographsRasterizedLocally, metadata: nil) else { return }_ = offlineManager.loadStylePack(for: styleURI, loadOptions: stylePackLoadOptions, completion: { [weak self] result inguard let self = self else { return }switch result {case .success(let stylePack):print("Style pack \(stylePack.styleURI) downloaded!")self.download(region: region)case .failure(let error):print("Error while downloading style pack: \(error).")}})} func download(region: Region) {tileRegionLoadOptions(for: region) { [weak self] loadOptions inguard let self = self, let loadOptions = loadOptions else { return }// loadTileRegions returns a Cancelable that allows developers to cancel downloading a region_ = self.tileStore.loadTileRegion(forId: region.identifier, loadOptions: loadOptions, progress: { progress inprint(progress)}, completion: { result inswitch result {case .success(let region):print("\(region.id) downloaded!")self.showDownloadCompletionAlert()case .failure(let error):print("Error while downloading region: \(error)")}})}} // Helper method for creating TileRegionLoadOptions that are needed to download regionsfunc tileRegionLoadOptions(for region: Region, completion: @escaping (TileRegionLoadOptions?) -> Void) {let tilesetDescriptorOptions = TilesetDescriptorOptions(styleURI: styleURI,zoomRange: zoomMin...zoomMax)let mapsDescriptor = offlineManager.createTilesetDescriptor(for: tilesetDescriptorOptions)TilesetDescriptorFactory.getLatest { navigationDescriptor incompletion(TileRegionLoadOptions(geometry: Polygon([region.bbox]).geometry,descriptors: [ mapsDescriptor, navigationDescriptor ],metadata: nil,acceptExpired: true,networkRestriction: .none))}} func showDownloadCompletionAlert() {DispatchQueue.main.async {let alertController = UIAlertController(title: "Downloading completed",message: "Long press location inside the box to get directions",preferredStyle: .alert)let approveAction = UIAlertAction(title: "OK", style: .default, handler: nil)alertController.addAction(approveAction)self.present(alertController, animated: true, completion: nil)}}} extension OfflineRegionsViewController: NavigationMapViewDelegate {func navigationMapView(_ navigationMapView: NavigationMapView, didSelect route: Route) {guard let index = routeResponse?.routes?.firstIndex(where: { $0 === route }),index != routeIndex else { return }routeIndex = index}} extension OfflineRegionsViewController: NavigationViewControllerDelegate {func navigationViewControllerDidDismiss(_ navigationViewController: NavigationViewController, byCanceling canceled: Bool) {navigationViewController.dismiss(animated: false) {self.setupNavigationMapView()self.addStartButton()}}}