Skip to content
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

Offline routing #1768

Merged
merged 22 commits into from
Oct 29, 2018
Merged
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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "thirdparty/osrm-text-instructions"]
path = thirdparty/osrm-text-instructions
url = https://github.com/Project-OSRM/osrm-text-instructions.git
2 changes: 1 addition & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" ~> 4.3
binary "https://www.mapbox.com/ios-sdk/MapboxNavigationNative.json" ~> 3.2
github "mapbox/MapboxDirections.swift" ~> 0.24
github "mapbox/MapboxDirections.swift" ~> 0.24.1
github "mapbox/turf-swift" ~> 0.2
github "mapbox/mapbox-events-ios" ~> 0.6
github "ceeK/Solar" ~> 2.1.0
Expand Down
2 changes: 1 addition & 1 deletion MapboxCoreNavigation.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ Pod::Spec.new do |s|
s.module_name = "MapboxCoreNavigation"

s.dependency "MapboxNavigationNative", "~> 3.2.0"
s.dependency "MapboxDirections.swift", "~> 0.24.0" # Always pin to a patch release if pre-1.0
s.dependency "MapboxDirections.swift", "~> 0.24.1" # Always pin to a patch release if pre-1.0
s.dependency "MapboxMobileEvents", "~> 0.6.0" # Always pin to a patch release if pre-1.0
s.dependency "Turf", "~> 0.2.0" # Always pin to a patch release if pre-1.0

Expand Down
1 change: 1 addition & 0 deletions MapboxCoreNavigation/MBRouteController.h
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,4 @@ extern const MBRouteControllerNotificationUserInfoKey MBRouteControllerIsProacti
@interface NSString (MD5)
- (NSString * _Nonnull)md5;
@end

2 changes: 2 additions & 0 deletions MapboxCoreNavigation/MBRouteController.m
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

NSString *const MBErrorDomain = @"ErrorDomain";


@implementation NSString (MD5)
- (NSString * _Nonnull)md5 {
const char *cStr = [self UTF8String];
Expand All @@ -30,3 +31,4 @@ - (NSString * _Nonnull)md5 {
return output;
}
@end

140 changes: 140 additions & 0 deletions MapboxCoreNavigation/OfflineDirections.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Foundation
import MapboxDirections
import MapboxNavigationNative

public typealias OfflineDirectionsCompletionHandler = (_ numberOfTiles: UInt64) -> Void

enum OfflineRoutingError: Error, LocalizedError {
case unexpectedRouteResult(String)
case corruptRouteData(String)
case responseError(String)

public var localizedDescription: String {
switch self {
case .corruptRouteData(let value):
return value
case .unexpectedRouteResult(let value):
return value
case .responseError(let value):
return value
}
}

var errorDescription: String? {
return localizedDescription
}
}

struct OfflineDirectionsConstants {
static let offlineSerialQueueLabel = Bundle.mapboxCoreNavigation.bundleIdentifier!.appending(".offline")
static let serialQueue = DispatchQueue(label: OfflineDirectionsConstants.offlineSerialQueueLabel)
}

/**
Defines additional functionality similar to `Directions` with support for offline routing.
*/
@objc(MBOfflineDirectionsProtocol)

This comment was marked as resolved.

public protocol OfflineRoutingProtocol {

/**
Initializes a newly created directions object with an optional access token and host.

- parameter tilesPath: The location where the tiles has been sideloaded to.
- parameter translationsPath: The location where the translations has been sideloaded to.
- parameter accessToken: A Mapbox [access token](https://www.mapbox.com/help/define-access-token/). If an access token is not specified when initializing the directions object, it should be specified in the `MGLMapboxAccessToken` key in the main application bundle’s Info.plist.
- parameter host: An optional hostname to the server API. The [Mapbox Directions API](https://www.mapbox.com/api-documentation/?language=Swift#directions) endpoint is used by default.
*/
init(tilesURL: URL, translationsURL: URL, accessToken: String?, host: String?, completionHandler: @escaping OfflineDirectionsCompletionHandler)

/**
Begins asynchronously calculating the route or routes using the given options and delivers the results to a closure.

This method retrieves the routes asynchronously via MapboxNavigationNative.

Routes may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). They may be cached but may not be stored permanently. To use the results in other contexts or store them permanently, [upgrade to a Mapbox enterprise plan](https://www.mapbox.com/directions/#pricing).

- parameter options: A `RouteOptions` object specifying the requirements for the resulting routes.
- parameter completionHandler: The closure (block) to call with the resulting routes. This closure is executed on the application’s main thread.
*/
func calculate(_ options: RouteOptions, offline: Bool, completionHandler: @escaping Directions.RouteCompletionHandler)
}

@objc(MBNavigationDirections)
public class NavigationDirections: Directions, OfflineRoutingProtocol {

public required init(tilesURL: URL, translationsURL: URL, accessToken: String?, host: String? = nil, completionHandler: @escaping OfflineDirectionsCompletionHandler) {

super.init(accessToken: accessToken, host: host)

OfflineDirectionsConstants.serialQueue.sync {
let tilesPath = tilesURL.absoluteString.replacingOccurrences(of: "file://", with: "")
let translationsPath = translationsURL.absoluteString.replacingOccurrences(of: "file://", with: "")
let tileCount = self.navigator.configureRouter(forTilesPath: tilesPath, translationsPath: translationsPath)

DispatchQueue.main.async {
completionHandler(tileCount)
}
}
}

public func calculate(_ options: RouteOptions, offline: Bool = false, completionHandler: @escaping Directions.RouteCompletionHandler) {

guard offline == true else {
return calculate(options, completionHandler: completionHandler)
}

let url = self.url(forCalculating: options)

OfflineDirectionsConstants.serialQueue.sync { [weak self] in

guard let result = self?.navigator.getRouteForDirectionsUri(url.absoluteString) else {
let error = OfflineRoutingError.unexpectedRouteResult("Unexpected routing result")
return completionHandler(nil, nil, error as NSError)
}

guard let data = result.json.data(using: .utf8) else {
let error = OfflineRoutingError.corruptRouteData("Corrupt route data")
return completionHandler(nil, nil, error as NSError)
}

do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
if let errorValue = json["error"] as? String {
DispatchQueue.main.async {
let error = OfflineRoutingError.responseError(errorValue)
return completionHandler(nil, nil, error as NSError)
}
} else {
let response = options.response(from: json)

DispatchQueue.main.async {
return completionHandler(response.0, response.1, nil)
}
}

} catch {
DispatchQueue.main.async {
return completionHandler(nil, nil, error as NSError)
}
}
}
}

var _navigator: MBNavigator!
var navigator: MBNavigator {

assert(currentQueueName() == OfflineDirectionsConstants.offlineSerialQueueLabel,
"The offline navigator must be accessed from the dedicated serial queue")

if _navigator == nil {
self._navigator = MBNavigator()
}

return _navigator
}
}

fileprivate func currentQueueName() -> String? {
let name = __dispatch_queue_get_label(nil)
return String(cString: name, encoding: .utf8)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PODS:
- Mapbox-iOS-SDK (4.5.0)
- MapboxCoreNavigation (0.23.0):
- MapboxDirections.swift (~> 0.24.0)
- MapboxDirections.swift (~> 0.24.1)
- MapboxMobileEvents (~> 0.6.0)
- MapboxNavigationNative (~> 3.2.0)
- Turf (~> 0.2.0)
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
75 changes: 75 additions & 0 deletions MapboxCoreNavigationTests/OfflineRoutingTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import XCTest
import MapboxDirections
@testable import MapboxCoreNavigation


class OfflineRoutingTests: XCTestCase {

func testOfflineDirections() {
let bundle = Bundle(for: OfflineRoutingTests.self)
let tilesURL = URL(fileURLWithPath: bundle.bundlePath.appending("/routing/liechtenstein"))
let translationsURL = URL(fileURLWithPath: bundle.bundlePath.appending("/translations"))

let setupExpectation = expectation(description: "Set up offline routing")

let directions = NavigationDirections(tilesURL: tilesURL, translationsURL: translationsURL, accessToken: "foo") { (numberOfTiles) in
XCTAssertEqual(numberOfTiles, 5)
setupExpectation.fulfill()
}

wait(for: [setupExpectation], timeout: 2)

// Coordinates within Liechtenstein
let coordinates = [CLLocationCoordinate2D(latitude: 47.1192, longitude: 9.5412),
CLLocationCoordinate2D(latitude: 47.1153, longitude: 9.5531)]

let options = NavigationRouteOptions(coordinates: coordinates, profileIdentifier: .automobile)
let calculateRouteExpectation = expectation(description: "Calculate route offline")
var route: Route?

directions.calculate(options, offline: true) { (waypoints, routes, error) in
XCTAssertNil(error)
XCTAssertNotNil(waypoints)
XCTAssertNotNil(routes)
route = routes!.first!
calculateRouteExpectation.fulfill()
}

wait(for: [calculateRouteExpectation], timeout: 2)

XCTAssertNotNil(route)
XCTAssertEqual(route!.coordinates!.count, 239)
}

func testOfflineDirectionsError() {
let bundle = Bundle(for: OfflineRoutingTests.self)
let tilesURL = URL(fileURLWithPath: bundle.bundlePath).appendingPathComponent("/routing/liechtenstein")
let translationsURL = URL(fileURLWithPath: bundle.bundlePath).appendingPathComponent("/translations")

let setupExpectation = expectation(description: "Set up offline routing")

let directions = NavigationDirections(tilesURL: tilesURL, translationsURL: translationsURL, accessToken: "foo") { (numberOfTiles) in
XCTAssertEqual(numberOfTiles, 5)
setupExpectation.fulfill()
}

wait(for: [setupExpectation], timeout: 2)

// Coordinates in SF
let coordinates = [CLLocationCoordinate2D(latitude: 37.7870, longitude: -122.4261),
CLLocationCoordinate2D(latitude: 37.7805, longitude: -122.4073)]

let options = NavigationRouteOptions(coordinates: coordinates, profileIdentifier: .automobile)
let calculateRouteExpectation = expectation(description: "Calculate route offline")

directions.calculate(options, offline: true) { (waypoints, routes, error) in
XCTAssertNotNil(error)
XCTAssertEqual(error!.localizedDescription, "No suitable edges near location")
XCTAssertNil(routes)
XCTAssertNil(waypoints)
calculateRouteExpectation.fulfill()
}

wait(for: [calculateRouteExpectation], timeout: 2)
}
}
Loading