From efa922ee2fbf0efa3db5ec6111d5a8efe7c366d3 Mon Sep 17 00:00:00 2001 From: udumft Date: Mon, 25 Oct 2021 10:58:15 +0300 Subject: [PATCH 01/19] vk-296-isochrone-api: added Isochrone class with related entities. Unit tests added. --- Sources/MapboxDirections/Isochrone.swift | 120 +++++++++++++ .../IsochroneCredentials.swift | 20 +++ Sources/MapboxDirections/IsochroneError.swift | 77 ++++++++ .../IsochroneProfileIdentifier.swift | 7 + .../IsochroneTests.swift | 165 ++++++++++++++++++ 5 files changed, 389 insertions(+) create mode 100644 Sources/MapboxDirections/Isochrone.swift create mode 100644 Sources/MapboxDirections/IsochroneCredentials.swift create mode 100644 Sources/MapboxDirections/IsochroneError.swift create mode 100644 Sources/MapboxDirections/IsochroneProfileIdentifier.swift create mode 100644 Tests/MapboxDirectionsTests/IsochroneTests.swift diff --git a/Sources/MapboxDirections/Isochrone.swift b/Sources/MapboxDirections/Isochrone.swift new file mode 100644 index 000000000..bc4040b59 --- /dev/null +++ b/Sources/MapboxDirections/Isochrone.swift @@ -0,0 +1,120 @@ +import Foundation +import Turf + + +open class Isochrone { + + public typealias Session = (options: IsochroneOptions, credentials: IsochroneCredentials) + public typealias IsochroneCompletionHandler = (_ session: Session, _ result: Result) -> Void + + public let credentials: IsochroneCredentials + private let urlSession: URLSession + private let processingQueue: DispatchQueue + + public static let shared = Isochrone() + + public init(credentials: IsochroneCredentials = .init(), + urlSession: URLSession = .shared, + processingQueue: DispatchQueue = .global(qos: .userInitiated)) { + self.credentials = credentials + self.urlSession = urlSession + self.processingQueue = processingQueue + } + + @discardableResult open func calculate(_ options: IsochroneOptions, completionHandler: @escaping IsochroneCompletionHandler) -> URLSessionDataTask { + let session = (options: options, credentials: self.credentials) + let request = urlRequest(forCalculating: options) + let requestTask = urlSession.dataTask(with: request) { (possibleData, possibleResponse, possibleError) in + if let urlError = possibleError as? URLError { + DispatchQueue.main.async { + completionHandler(session, .failure(.network(urlError))) + } + return + } + + guard let response = possibleResponse, ["application/json", "text/html"].contains(response.mimeType) else { + DispatchQueue.main.async { + completionHandler(session, .failure(.invalidResponse(possibleResponse))) + } + return + } + + guard let data = possibleData else { + DispatchQueue.main.async { + completionHandler(session, .failure(.noData)) + } + return + } + + self.processingQueue.async { + do { + let decoder = JSONDecoder() + + guard let disposition = try? decoder.decode(ResponseDisposition.self, from: data) else { + let apiError = IsochroneError(code: nil, message: nil, response: possibleResponse, underlyingError: possibleError) + + DispatchQueue.main.async { + completionHandler(session, .failure(apiError)) + } + return + } + + guard (disposition.code == nil && disposition.message == nil) || disposition.code == "Ok" else { + let apiError = IsochroneError(code: disposition.code, message: disposition.message, response: response, underlyingError: possibleError) + DispatchQueue.main.async { + completionHandler(session, .failure(apiError)) + } + return + } + + let result = try decoder.decode(FeatureCollection.self, from: data) +// guard !result.features.isEmpty else { +// DispatchQueue.main.async { +// completionHandler(session, .failure(.unableToContour)) +// } +// return +// } + + DispatchQueue.main.async { + completionHandler(session, .success(result)) + } + } catch { + DispatchQueue.main.async { + let bailError = IsochroneError(code: nil, message: nil, response: response, underlyingError: error) + completionHandler(session, .failure(bailError)) + } + } + } + } + requestTask.priority = 1 + requestTask.resume() + + return requestTask + } + + open func url(forCalculating options: IsochroneOptions) -> URL { + + var params = options.urlQueryItems + params.append(URLQueryItem(name: "access_token", value: credentials.accessToken)) + + let unparameterizedURL = URL(string: options.path, relativeTo: credentials.host)! + var components = URLComponents(url: unparameterizedURL, resolvingAgainstBaseURL: true)! + components.queryItems = params + return components.url! + } + + open func urlRequest(forCalculating options: IsochroneOptions) -> URLRequest { + let getURL = self.url(forCalculating: options) + var request = URLRequest(url: getURL) +// if getURL.absoluteString.count > MaximumURLLength { +// request.url = url(forCalculating: options, httpMethod: "POST") +// +// let body = options.httpBody.data(using: .utf8) +// request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") +// request.httpMethod = "POST" +// request.httpBody = body +// } + request.setValue(userAgent, forHTTPHeaderField: "User-Agent") + return request + } +} diff --git a/Sources/MapboxDirections/IsochroneCredentials.swift b/Sources/MapboxDirections/IsochroneCredentials.swift new file mode 100644 index 000000000..ae0302e97 --- /dev/null +++ b/Sources/MapboxDirections/IsochroneCredentials.swift @@ -0,0 +1,20 @@ +import Foundation + +public struct IsochroneCredentials: Equatable { + public var accessToken: String? + public var host: URL + + public init(accessToken token: String? = nil, host: URL? = nil) { + let accessToken = token ?? defaultAccessToken + + precondition(accessToken != nil && !accessToken!.isEmpty, "A Mapbox access token is required. Go to . In Info.plist, set the MBXAccessToken key to your access token, or use the Directions(accessToken:host:) initializer.") + self.accessToken = accessToken! + if let host = host { + self.host = host + } else if let defaultHostString = defaultApiEndPointURLString, let defaultHost = URL(string: defaultHostString) { + self.host = defaultHost + } else { + self.host = URL(string: "https://api.mapbox.com")! + } + } +} diff --git a/Sources/MapboxDirections/IsochroneError.swift b/Sources/MapboxDirections/IsochroneError.swift new file mode 100644 index 000000000..7f4029dea --- /dev/null +++ b/Sources/MapboxDirections/IsochroneError.swift @@ -0,0 +1,77 @@ +import Foundation + +public enum IsochroneError: LocalizedError { + + public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { + if let response = response as? HTTPURLResponse { + switch (response.statusCode, code ?? "") { + case (200, "NoSegment"): + self = .unableToLocate + case (404, "ProfileNotFound"): + self = .profileNotFound + case (422, "InvalidInput"): + self = .invalidInput(message: message) + case (429, _): + self = .rateLimited(rateLimitInterval: response.rateLimitInterval, rateLimit: response.rateLimit, resetTime: response.rateLimitResetTime) + default: + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } else { + self = .unknown(response: response, underlying: error, code: code, message: message) + } + } + + /** + There is no network connection available to perform the network request. + */ + case network(_: URLError) + + /** + The server returned a response that isn’t correctly formatted. + */ + case invalidResponse(_: URLResponse?) + + /** + The server returned an empty response. + */ + case noData + + /** + There was no route contour found for the given coordinate. + + Check for impossible routes (for example, routes over oceans without ferry connections). + */ + case unableToContour + + /** + A specified location could not be associated with a roadway or pathway. + + Make sure the locations are close enough to a roadway or pathway. + */ + case unableToLocate + + /** + Unrecognized profile identifier. + + Make sure the `IsochroneOptions.profileIdentifier` option is set to one of the predefined values, such as `IsochroneProfileIdentifier.automobile`. + */ + case profileNotFound + + /** + The API recieved input that it didn't understand. + */ + case invalidInput(message: String?) + + /** + Too many requests have been made with the same access token within a certain period of time. + + Wait before retrying. + */ + case rateLimited(rateLimitInterval: TimeInterval?, rateLimit: UInt?, resetTime: Date?) + + /** + Unknown error case. Look at associated values for more details. + */ + + case unknown(response: URLResponse?, underlying: Error?, code: String?, message: String?) +} diff --git a/Sources/MapboxDirections/IsochroneProfileIdentifier.swift b/Sources/MapboxDirections/IsochroneProfileIdentifier.swift new file mode 100644 index 000000000..18e70a1a9 --- /dev/null +++ b/Sources/MapboxDirections/IsochroneProfileIdentifier.swift @@ -0,0 +1,7 @@ +import Foundation + +public enum IsochroneProfileIdentifier: String { + case automobile = "driving" + case cycling + case walking +} diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift new file mode 100644 index 000000000..83814a117 --- /dev/null +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -0,0 +1,165 @@ +import Foundation +@testable import MapboxDirections +import OHHTTPStubs +import Turf +import XCTest + +let IsochroneBogusCredentials = IsochroneCredentials(accessToken: BogusToken) + +let minimalValidResponse = """ +{ + "features": [], + "type": "FeatureCollection" +} +""" + +#if !os(Linux) +class IsochroneTests: XCTestCase { + + override func setUp() { + super.setUp() + } + + override func tearDown() { + HTTPStubs.removeAllStubs() + super.tearDown() + } + + func testConfiguration() { + let isochrone = Isochrone(credentials: IsochroneBogusCredentials) + XCTAssertEqual(isochrone.credentials, IsochroneBogusCredentials) + } + + func testRequest() { + let location = LocationCoordinate2D(latitude: 0, longitude: 1) + let options = IsochroneOptions(location: location, + contour: .meters([100, 200])) + options.colors = [IsochroneOptions.Color(red: 11, green: 12, blue: 13), + IsochroneOptions.Color(red: 21, green: 22, blue: 23)] + options.contoursPolygons = true + options.denoiseFactor = 0.5 + options.generalizeTolerance = 13 + + let isochrone = Isochrone(credentials: IsochroneBogusCredentials) + let url = isochrone.url(forCalculating: options) + let request = isochrone.urlRequest(forCalculating: options) + + guard let components = URLComponents(string: url.absoluteString), + let queryItems = components.queryItems else { + XCTFail("Invalid url"); return + } + XCTAssertEqual(queryItems.count, 6) + XCTAssertTrue(components.path.contains(location.requestDescription) ) + XCTAssertTrue(queryItems.contains(where: { $0.name == "access_token" && $0.value == BogusToken })) + XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_meters" && $0.value == "100;200"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_colors" && $0.value == "0B0C0D;151617"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "polygons" && $0.value == "true"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "denoise" && $0.value == "0.5"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "generalize" && $0.value == "13.0"})) + + XCTAssertEqual(request.httpMethod, "GET") + XCTAssertEqual(request.url, url) + } + + func testMinimalValidResponse() { + HTTPStubs.stubRequests(passingTest: { (request) -> Bool in + return request.url!.absoluteString.contains("https://api.mapbox.com/isochrone") + }) { (_) -> HTTPStubsResponse in + return HTTPStubsResponse(data: minimalValidResponse.data(using: .utf8)!, statusCode: 200, headers: ["Content-Type" : "text/html"]) + } + let expectation = self.expectation(description: "Async callback") + let isochrone = Isochrone(credentials: IsochroneBogusCredentials) + let options = IsochroneOptions(location: LocationCoordinate2D(latitude: 0, longitude: 1), + contour: .meters([100])) + isochrone.calculate(options, completionHandler: { (session, result) in + defer { expectation.fulfill() } + + guard case let .success(featureCollection) = result else { + XCTFail("Expecting success, error returned. \(result)") + return + } + + guard featureCollection.features.isEmpty else { + XCTFail("Wrong feature decoding.") + return + } + }) + wait(for: [expectation], timeout: 2.0) + } + + func testUnknownBadResponse() { + let message = "Lorem ipsum." + HTTPStubs.stubRequests(passingTest: { (request) -> Bool in + return request.url!.absoluteString.contains("https://api.mapbox.com/isochrone") + }) { (_) -> HTTPStubsResponse in + return HTTPStubsResponse(data: message.data(using: .utf8)!, statusCode: 420, headers: ["Content-Type" : "text/plain"]) + } + let expectation = self.expectation(description: "Async callback") + let isochrone = Isochrone(credentials: IsochroneBogusCredentials) + let options = IsochroneOptions(location: LocationCoordinate2D(latitude: 0, longitude: 1), + contour: .meters([100])) + isochrone.calculate(options, completionHandler: { (session, result) in + defer { expectation.fulfill() } + + guard case let .failure(error) = result else { + XCTFail("Expecting an error, none returned. \(result)") + return + } + + guard case .invalidResponse(_) = error else { + XCTFail("Wrong error type returned.") + return + } + }) + wait(for: [expectation], timeout: 2.0) + } + + func testRateLimitErrorParsing() { + let url = URL(string: "https://api.mapbox.com")! + let headerFields = ["X-Rate-Limit-Interval" : "60", "X-Rate-Limit-Limit" : "600", "X-Rate-Limit-Reset" : "1479460584"] + let response = HTTPURLResponse(url: url, statusCode: 429, httpVersion: nil, headerFields: headerFields) + + let resultError = IsochroneError(code: "429", message: "Hit rate limit", response: response, underlyingError: nil) + if case let .rateLimited(rateLimitInterval, rateLimit, resetTime) = resultError { + XCTAssertEqual(rateLimitInterval, 60.0) + XCTAssertEqual(rateLimit, 600) + XCTAssertEqual(resetTime, Date(timeIntervalSince1970: 1479460584)) + } else { + XCTFail("Code 429 should be interpreted as a rate limiting error.") + } + } + + func testDownNetwork() { + let notConnected = NSError(domain: NSURLErrorDomain, code: URLError.notConnectedToInternet.rawValue) as! URLError + + HTTPStubs.stubRequests(passingTest: { (request) -> Bool in + return request.url!.absoluteString.contains("https://api.mapbox.com/isochrone") + }) { (_) -> HTTPStubsResponse in + return HTTPStubsResponse(error: notConnected) + } + + let expectation = self.expectation(description: "Async callback") + let isochrone = Isochrone(credentials: IsochroneBogusCredentials) + let options = IsochroneOptions(location: LocationCoordinate2D(latitude: 0, longitude: 1), + contour: .meters([100])) + isochrone.calculate(options, completionHandler: { (session, result) in + defer { expectation.fulfill() } + + guard case let .failure(error) = result else { + XCTFail("Error expected, none returned. \(result)") + return + } + + guard case let .network(err) = error else { + XCTFail("Wrong error type returned. \(error)") + return + } + + // Comparing just the code and domain to avoid comparing unessential `UserInfo` that might be added. + XCTAssertEqual(type(of: err).errorDomain, type(of: notConnected).errorDomain) + XCTAssertEqual(err.code, notConnected.code) + }) + wait(for: [expectation], timeout: 2.0) + } +} +#endif From d3cae7a5f455f7e9e0b64ff6060e11253e2c7156 Mon Sep 17 00:00:00 2001 From: udumft Date: Mon, 25 Oct 2021 16:59:45 +0300 Subject: [PATCH 02/19] vk-296-isochrone-api: added code docs, changed IsochroneProfileIdentifier from enum to struct --- Sources/MapboxDirections/Isochrone.swift | 77 ++++++-- .../IsochroneCredentials.swift | 12 ++ Sources/MapboxDirections/IsochroneError.swift | 11 +- .../MapboxDirections/IsochroneOptions.swift | 182 ++++++++++++++++++ .../IsochroneProfileIdentifier.swift | 34 +++- .../IsochroneTests.swift | 2 +- 6 files changed, 290 insertions(+), 28 deletions(-) create mode 100644 Sources/MapboxDirections/IsochroneOptions.swift diff --git a/Sources/MapboxDirections/Isochrone.swift b/Sources/MapboxDirections/Isochrone.swift index bc4040b59..630b8681e 100644 --- a/Sources/MapboxDirections/Isochrone.swift +++ b/Sources/MapboxDirections/Isochrone.swift @@ -1,18 +1,54 @@ import Foundation import Turf - +/** + Computes areas that are reachable within a specified amount of time or distance from a location, and returns the reachable regions as contours of polygons or lines that you can display on a map. + */ open class Isochrone { + /** + A tuple type representing the isochrone session that was generated from the request. + + - parameter options: A `IsochroneOptions ` object representing the request parameter options. + + - parameter credentials: A object containing the credentials used to make the request. + */ public typealias Session = (options: IsochroneOptions, credentials: IsochroneCredentials) + + /** + A closure (block) to be called when a isochrone request is complete. + + - parameter session: A `Isochrone.Session` object containing session information + + - parameter result: A `Result` enum that represents the `FeatureCollection` if the request returned successfully, or the error if it did not. + */ public typealias IsochroneCompletionHandler = (_ session: Session, _ result: Result) -> Void + + // MARK: Creating an Isochrone Object + /** + The Authorization & Authentication credentials that are used for this service. + + If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. + */ public let credentials: IsochroneCredentials private let urlSession: URLSession private let processingQueue: DispatchQueue + /** + The shared directions object. + + To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be specified in the `MBXAccessToken` key in the main application bundle’s Info.plist. + */ public static let shared = Isochrone() + /** + Creates a new instance of Isochrone object. + - Parameters: + - credentials: Credentials that will be used to make API requests to Mapbox Isochrone API. + - urlSession: URLSession that will be used to submit API requests to Mapbox Isochrone API. + - processingQueue: A DispatchQueue that will be used for CPU intensive work. + */ public init(credentials: IsochroneCredentials = .init(), urlSession: URLSession = .shared, processingQueue: DispatchQueue = .global(qos: .userInitiated)) { @@ -21,6 +57,17 @@ open class Isochrone { self.processingQueue = processingQueue } + /** + Begins asynchronously calculating isochrone contours using the given options and delivers the results to a closure. + + This method retrieves the contours asynchronously from the [Mapbox Isochrone API](https://docs.mapbox.com/api/navigation/isochrone/) over a network connection. If a connection error or server error occurs, details about the error are passed into the given completion handler in lieu of the contours. + + Contours may be displayed atop a [Mapbox map](https://www.mapbox.com/maps/). + + - parameter options: A `IsochroneOptions` object specifying the requirements for the resulting contours. + - parameter completionHandler: The closure (block) to call with the resulting contours. This closure is executed on the application’s main thread. + - returns: The data task used to perform the HTTP request. If, while waiting for the completion handler to execute, you no longer want the resulting contours, cancel this task. + */ @discardableResult open func calculate(_ options: IsochroneOptions, completionHandler: @escaping IsochroneCompletionHandler) -> URLSessionDataTask { let session = (options: options, credentials: self.credentials) let request = urlRequest(forCalculating: options) @@ -68,12 +115,6 @@ open class Isochrone { } let result = try decoder.decode(FeatureCollection.self, from: data) -// guard !result.features.isEmpty else { -// DispatchQueue.main.async { -// completionHandler(session, .failure(.unableToContour)) -// } -// return -// } DispatchQueue.main.async { completionHandler(session, .success(result)) @@ -92,6 +133,14 @@ open class Isochrone { return requestTask } + // MARK: Request URL Preparation + + /** + The GET HTTP URL used to fetch the contours from the API. + + - parameter options: A `IsochroneOptions` object specifying the requirements for the resulting contours. + - returns: The URL to send the request to. + */ open func url(forCalculating options: IsochroneOptions) -> URL { var params = options.urlQueryItems @@ -103,17 +152,15 @@ open class Isochrone { return components.url! } + /** + The HTTP request used to fetch the contours from the API. + + - parameter options: A `IsochroneOptions` object specifying the requirements for the resulting routes. + - returns: A GET HTTP request to calculate the specified options. + */ open func urlRequest(forCalculating options: IsochroneOptions) -> URLRequest { let getURL = self.url(forCalculating: options) var request = URLRequest(url: getURL) -// if getURL.absoluteString.count > MaximumURLLength { -// request.url = url(forCalculating: options, httpMethod: "POST") -// -// let body = options.httpBody.data(using: .utf8) -// request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") -// request.httpMethod = "POST" -// request.httpBody = body -// } request.setValue(userAgent, forHTTPHeaderField: "User-Agent") return request } diff --git a/Sources/MapboxDirections/IsochroneCredentials.swift b/Sources/MapboxDirections/IsochroneCredentials.swift index ae0302e97..d7d1f8de0 100644 --- a/Sources/MapboxDirections/IsochroneCredentials.swift +++ b/Sources/MapboxDirections/IsochroneCredentials.swift @@ -1,9 +1,21 @@ import Foundation public struct IsochroneCredentials: Equatable { + /** + The mapbox access token. You can find this in your Mapbox account dashboard. + */ public var accessToken: String? + /** + The host to reach. Defaults to `api.mapbox.com`. + */ public var host: URL + /** + Intialize a new credential. + + - parameter accessToken: Optional. An access token to provide. If this value is nil, the SDK will attempt to find a token from your app's `info.plist`. + - parameter host: Optional. A parameter to pass a custom host. If `nil` is provided, the SDK will attempt to find a host from your app's `info.plist`, and barring that will default to `https://api.mapbox.com`. + */ public init(accessToken token: String? = nil, host: URL? = nil) { let accessToken = token ?? defaultAccessToken diff --git a/Sources/MapboxDirections/IsochroneError.swift b/Sources/MapboxDirections/IsochroneError.swift index 7f4029dea..6f2358efb 100644 --- a/Sources/MapboxDirections/IsochroneError.swift +++ b/Sources/MapboxDirections/IsochroneError.swift @@ -1,5 +1,8 @@ import Foundation +/** + An error that occurs when calculating isochrone contours. + */ public enum IsochroneError: LocalizedError { public init(code: String?, message: String?, response: URLResponse?, underlyingError error: Error?) { @@ -36,13 +39,6 @@ public enum IsochroneError: LocalizedError { */ case noData - /** - There was no route contour found for the given coordinate. - - Check for impossible routes (for example, routes over oceans without ferry connections). - */ - case unableToContour - /** A specified location could not be associated with a roadway or pathway. @@ -72,6 +68,5 @@ public enum IsochroneError: LocalizedError { /** Unknown error case. Look at associated values for more details. */ - case unknown(response: URLResponse?, underlying: Error?, code: String?, message: String?) } diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift new file mode 100644 index 000000000..5ae680b21 --- /dev/null +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -0,0 +1,182 @@ +import Foundation +import Turf + +#if canImport(UIKit) +import UIKit +#elseif canImport(AppKit) +import AppKit +#endif + + +/** + Options for calculating contours from the Mapbox Isochrone service. +*/ +public class IsochroneOptions { + + public init(location: LocationCoordinate2D, contour: Contour, profileIdentifier: IsochroneProfileIdentifier = .automobile) { + self.location = location + self.contour = contour + self.profileIdentifier = profileIdentifier + } + + // MARK: Configuring the Contour + + /** + A string specifying the primary mode of transportation for the contours. + + The default value of this property is `IsochroneProfileIdentifier.automobile`, which specifies driving directions. + */ + public var profileIdentifier: IsochroneProfileIdentifier + /** + A coordinate around which to center the isochrone lines. + */ + public var location: LocationCoordinate2D + /** + Contour distance or travel time definition. + */ + public var contour: Contour + + /** + The colors to use for each isochrone contour. + + Number of colors should match number of `contour`s. + If no colors are specified, the Isochrone API will assign a default rainbow color scheme to the output. + */ + public var colors: [Color]? + + /** + Specify whether to return the contours as GeoJSON polygons. + + Defaults to `false` which represents contours as linestrings. + */ + public var contoursPolygons: Bool? + + /** + Removes contours which are `denoiseFactor` times smaller than the biggest one. + + The default is 1.0. A value of 1.0 will only return the largest contour for a given value. A value of 0.5 drops any contours that are less than half the area of the largest contour in the set of contours for that same value. + */ + public var denoiseFactor: Float? + + /** + Value in meters used as the tolerance for Douglas-Peucker generalization. + + There is no upper bound. If no value is specified in the request, the Isochrone API will choose the most optimized generalization to use for the request. + + - note: Generalization of contours can lead to self-intersections, as well as intersections of adjacent contours. + */ + public var generalizeTolerance: LocationDistance? + + // MARK: Getting the Request URL + + /** + An array of URL query items to include in an HTTP request. + */ + var abridgedPath: String { + return "isochrone/v1/\(profileIdentifier.rawValue)" + } + + /** + The path of the request URL, not including the hostname or any parameters. + */ + var path: String { + return "\(abridgedPath)/\(location.requestDescription).json" + } + + /** + An array of URL query items (parameters) to include in an HTTP request. + */ + public var urlQueryItems: [URLQueryItem] { + var queryItems: [URLQueryItem] = [] + var contoursCount = 0 + + switch contour { + case .meters(let meters): + let value = meters.sorted().map { String(Int($0.rounded())) }.joined(separator: ";") + queryItems.append(URLQueryItem(name: "contours_meters", value: value)) + contoursCount = meters.count + case .minutes(let minutes): + let value = minutes.sorted().map { String($0) }.joined(separator: ";") + queryItems.append(URLQueryItem(name: "contours_minutes", value: value)) + contoursCount = minutes.count + } + + if let colors = colors, !colors.isEmpty { + assert(colors.count == contoursCount, "Contours `colors` count must match contours count!") + let value = colors.map { String(format:"%02X%02X%02X", $0.red, $0.green, $0.blue)}.joined(separator: ";") + queryItems.append(URLQueryItem(name: "contours_colors", value: value)) + } + + if let isPolygon = contoursPolygons { + queryItems.append(URLQueryItem(name: "polygons", value: String(isPolygon))) + } + + if let denoise = denoiseFactor { + queryItems.append(URLQueryItem(name: "denoise", value: String(denoise))) + } + + if let tolerance = generalizeTolerance { + queryItems.append(URLQueryItem(name: "generalize", value: String(tolerance))) + } + + return queryItems + } +} + +extension IsochroneOptions { + /** + Definition of contour limit. + */ + public enum Contour { + /** + The times in minutes to use for each isochrone contour. + */ + case minutes([UInt]) + /** + The distances to use for each isochrone contour. + + Will be rounded to the nearest integer. + */ + case meters([LocationDistance]) + } +} + +extension IsochroneOptions { + public struct Color { + public var red: Int + public var green: Int + public var blue: Int + + public init(red: Int, green: Int, blue: Int) { + self.red = red + self.green = green + self.blue = blue + } + + #if canImport(UIKit) + init(_ color: UIColor) { + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + + color.getRed(&red, + green: &green, + blue: &blue, + alpha: nil) + + self.red = Int(red * 255) + self.green = Int(green * 255) + self.blue = Int(blue * 255) + } + #elseif canImport(AppKit) + init?(_ color: NSColor) { + guard let convertedColor = color.usingColorSpace(.deviceRGB) else { + return nil + } + red = Int(convertedColor.redComponent * 255) + green = Int(convertedColor.greenComponent * 255) + blue = Int(convertedColor.blueComponent * 255) + } + #endif + } +} diff --git a/Sources/MapboxDirections/IsochroneProfileIdentifier.swift b/Sources/MapboxDirections/IsochroneProfileIdentifier.swift index 18e70a1a9..90ba60637 100644 --- a/Sources/MapboxDirections/IsochroneProfileIdentifier.swift +++ b/Sources/MapboxDirections/IsochroneProfileIdentifier.swift @@ -1,7 +1,33 @@ import Foundation -public enum IsochroneProfileIdentifier: String { - case automobile = "driving" - case cycling - case walking +/** + Options determining the primary mode of transportation for the contours. + */ +public struct IsochroneProfileIdentifier: Codable, RawRepresentable { + public init(rawValue: String) { + self.rawValue = rawValue + } + + public var rawValue: String + + /** + The returned contours are calculated for driving or riding a car, truck, or motorcycle. + + This profile prioritizes fast routes by preferring high-speed roads like highways. A driving route may use a ferry where necessary. + */ + public static let automobile: IsochroneProfileIdentifier = .init(rawValue: "mapbox/driving") + + /** + The returned contours are calculated for riding a bicycle. + + This profile prioritizes short, safe routes by avoiding highways and preferring cycling infrastructure, such as bike lanes on surface streets. A cycling route may, where necessary, use other modes of transportation, such as ferries or trains, or require dismounting the bicycle for a distance. + */ + public static let cycling: IsochroneProfileIdentifier = .init(rawValue: "mapbox/cycling") + + /** + The returned contours are calculated for walking or hiking. + + This profile prioritizes short routes, making use of sidewalks and trails where available. A walking route may use other modes of transportation, such as ferries or trains, where necessary. + */ + public static let walking: IsochroneProfileIdentifier = .init(rawValue: "mapbox/walking") } diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift index 83814a117..cbccde8dd 100644 --- a/Tests/MapboxDirectionsTests/IsochroneTests.swift +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -33,7 +33,7 @@ class IsochroneTests: XCTestCase { func testRequest() { let location = LocationCoordinate2D(latitude: 0, longitude: 1) let options = IsochroneOptions(location: location, - contour: .meters([100, 200])) + contour: .meters([99.5, 200.44])) options.colors = [IsochroneOptions.Color(red: 11, green: 12, blue: 13), IsochroneOptions.Color(red: 21, green: 22, blue: 23)] options.contoursPolygons = true From 05ba71d67ccdc74bb64f46a2e283485566e53da4 Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 26 Oct 2021 12:36:55 +0300 Subject: [PATCH 03/19] vk-296-isochrone-api: CHANGELOG updated --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6c8d53c0..22f27f93c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Added the `RouteOptions.initialManeuverAvoidanceRadius` property to avoid a sudden maneuver when calculating a route while the user is in motion. ([#609](https://github.com/mapbox/mapbox-directions-swift/pull/609)) * Added the `RoadClasses.unpaved` option for avoiding unpaved roads. ([#620](https://github.com/mapbox/mapbox-directions-swift/pull/620)) * Added the `RoadClasses.cashOnlyToll` property for avoiding toll roads that only accept cash payment. ([#620](https://github.com/mapbox/mapbox-directions-swift/pull/620)) +* Added `Ischrone` API wrapper. The [Mapbox Isochrone API](https://docs.mapbox.com/api/navigation/isochrone/) computes areas that are reachable within a specified amount of time from a location, and returns the reachable regions as contours of polygons or lines that you can display on a map. ([#621](https://github.com/mapbox/mapbox-directions-swift/pull/621)) * Added the `RouteOptions.maximumHeight` and `RouteOptions.maximumWidth` properties for ensuring that the resulting routes can accommodate a vehicle of a certain size. ([#623](https://github.com/mapbox/mapbox-directions-swift/pull/623)) * The `DirectionsPriority` struct now conforms to the `Codable` protocol. ([#623](https://github.com/mapbox/mapbox-directions-swift/pull/623)) * Fixed an issue where the `RouteOptions.alleyPriority`, `RouteOptions.walkwayPriority`, and `RouteOptions.speed` properties were excluded from the encoded representation of a `RouteOptions` object. ([#623](https://github.com/mapbox/mapbox-directions-swift/pull/623)) From a36b69be04a4450b80a28d2deed627fc5853fe25 Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 26 Oct 2021 11:06:13 +0300 Subject: [PATCH 04/19] vk-296-isochrone-api: Linux build fix --- Sources/MapboxDirections/Isochrone.swift | 3 +++ Sources/MapboxDirections/IsochroneError.swift | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Sources/MapboxDirections/Isochrone.swift b/Sources/MapboxDirections/Isochrone.swift index 630b8681e..1dd60214a 100644 --- a/Sources/MapboxDirections/Isochrone.swift +++ b/Sources/MapboxDirections/Isochrone.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif import Turf /** diff --git a/Sources/MapboxDirections/IsochroneError.swift b/Sources/MapboxDirections/IsochroneError.swift index 6f2358efb..e1f23294e 100644 --- a/Sources/MapboxDirections/IsochroneError.swift +++ b/Sources/MapboxDirections/IsochroneError.swift @@ -1,4 +1,7 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif /** An error that occurs when calculating isochrone contours. From b14e55d44a20d5143cafe65aa5e60a405b84e793 Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 26 Oct 2021 12:34:22 +0300 Subject: [PATCH 05/19] vk-296-isochrone-api: replace IsochroneCredentials definition to be a typealias; refactored IsochroneOptios.Contour and Color entities; extended tests to verify contours_minutes coding --- .../IsochroneCredentials.swift | 31 +------ .../MapboxDirections/IsochroneOptions.swift | 87 +++++++++++-------- .../IsochroneTests.swift | 27 ++++-- 3 files changed, 72 insertions(+), 73 deletions(-) diff --git a/Sources/MapboxDirections/IsochroneCredentials.swift b/Sources/MapboxDirections/IsochroneCredentials.swift index d7d1f8de0..fffebfe55 100644 --- a/Sources/MapboxDirections/IsochroneCredentials.swift +++ b/Sources/MapboxDirections/IsochroneCredentials.swift @@ -1,32 +1,3 @@ import Foundation -public struct IsochroneCredentials: Equatable { - /** - The mapbox access token. You can find this in your Mapbox account dashboard. - */ - public var accessToken: String? - /** - The host to reach. Defaults to `api.mapbox.com`. - */ - public var host: URL - - /** - Intialize a new credential. - - - parameter accessToken: Optional. An access token to provide. If this value is nil, the SDK will attempt to find a token from your app's `info.plist`. - - parameter host: Optional. A parameter to pass a custom host. If `nil` is provided, the SDK will attempt to find a host from your app's `info.plist`, and barring that will default to `https://api.mapbox.com`. - */ - public init(accessToken token: String? = nil, host: URL? = nil) { - let accessToken = token ?? defaultAccessToken - - precondition(accessToken != nil && !accessToken!.isEmpty, "A Mapbox access token is required. Go to . In Info.plist, set the MBXAccessToken key to your access token, or use the Directions(accessToken:host:) initializer.") - self.accessToken = accessToken! - if let host = host { - self.host = host - } else if let defaultHostString = defaultApiEndPointURLString, let defaultHost = URL(string: defaultHostString) { - self.host = defaultHost - } else { - self.host = URL(string: "https://api.mapbox.com")! - } - } -} +public typealias IsochroneCredentials = DirectionsCredentials diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift index 5ae680b21..b1384a35f 100644 --- a/Sources/MapboxDirections/IsochroneOptions.swift +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -13,9 +13,9 @@ import AppKit */ public class IsochroneOptions { - public init(location: LocationCoordinate2D, contour: Contour, profileIdentifier: IsochroneProfileIdentifier = .automobile) { + public init(location: LocationCoordinate2D, contours: Contours, profileIdentifier: IsochroneProfileIdentifier = .automobile) { self.location = location - self.contour = contour + self.contours = contours self.profileIdentifier = profileIdentifier } @@ -32,9 +32,9 @@ public class IsochroneOptions { */ public var location: LocationCoordinate2D /** - Contour distance or travel time definition. + Contours distance or travel time definition. */ - public var contour: Contour + public var contours: Contours /** The colors to use for each isochrone contour. @@ -90,20 +90,20 @@ public class IsochroneOptions { var queryItems: [URLQueryItem] = [] var contoursCount = 0 - switch contour { - case .meters(let meters): + switch contours { + case .distance(let meters): let value = meters.sorted().map { String(Int($0.rounded())) }.joined(separator: ";") queryItems.append(URLQueryItem(name: "contours_meters", value: value)) contoursCount = meters.count - case .minutes(let minutes): - let value = minutes.sorted().map { String($0) }.joined(separator: ";") + case .expectedTravelTime(let intervals): + let value = intervals.sorted().map { String(Int(($0 / 60.0).rounded())) }.joined(separator: ";") queryItems.append(URLQueryItem(name: "contours_minutes", value: value)) - contoursCount = minutes.count + contoursCount = intervals.count } if let colors = colors, !colors.isEmpty { assert(colors.count == contoursCount, "Contours `colors` count must match contours count!") - let value = colors.map { String(format:"%02X%02X%02X", $0.red, $0.green, $0.blue)}.joined(separator: ";") + let value = colors.map { queryColorDescription(color: $0)}.joined(separator: ";") queryItems.append(URLQueryItem(name: "contours_colors", value: value)) } @@ -127,21 +127,28 @@ extension IsochroneOptions { /** Definition of contour limit. */ - public enum Contour { + public enum Contours { /** - The times in minutes to use for each isochrone contour. + The desired travel times to use for each isochrone contour. + + This value will be rounded to the nearest minute. */ - case minutes([UInt]) + case expectedTravelTime([TimeInterval]) /** The distances to use for each isochrone contour. Will be rounded to the nearest integer. */ - case meters([LocationDistance]) + case distance([LocationDistance]) } } extension IsochroneOptions { + #if canImport(UIKit) + public typealias Color = UIColor + #elseif canImport(AppKit) + public typealias Color = NSColor + #else public struct Color { public var red: Int public var green: Int @@ -152,31 +159,41 @@ extension IsochroneOptions { self.green = green self.blue = blue } + } + #endif + + func queryColorDescription(color: Color) -> String { + let hexFormat = "%02X%02X%02X" #if canImport(UIKit) - init(_ color: UIColor) { - var red: CGFloat = 0 - var green: CGFloat = 0 - var blue: CGFloat = 0 - - color.getRed(&red, - green: &green, - blue: &blue, - alpha: nil) - - self.red = Int(red * 255) - self.green = Int(green * 255) - self.blue = Int(blue * 255) - } + var red: CGFloat = 0 + var green: CGFloat = 0 + var blue: CGFloat = 0 + + color.getRed(&red, + green: &green, + blue: &blue, + alpha: nil) + + return String(format: hexFormat, + Int(red * 255), + Int(green * 255), + Int(blue * 255)) #elseif canImport(AppKit) - init?(_ color: NSColor) { - guard let convertedColor = color.usingColorSpace(.deviceRGB) else { - return nil - } - red = Int(convertedColor.redComponent * 255) - green = Int(convertedColor.greenComponent * 255) - blue = Int(convertedColor.blueComponent * 255) + guard let convertedColor = color.usingColorSpace(.deviceRGB) else { + assertionFailure("Failed to convert Isochrone contour color to RGB space.") + return "000000" } + + return String(format: hexFormat, + Int(convertedColor.redComponent * 255), + Int(convertedColor.greenComponent * 255), + Int(convertedColor.blueComponent * 255)) + #else + return String(format: hexFormat, + color.red, + color.green, + color.blue) #endif } } diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift index cbccde8dd..05011f5d8 100644 --- a/Tests/MapboxDirectionsTests/IsochroneTests.swift +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -33,15 +33,15 @@ class IsochroneTests: XCTestCase { func testRequest() { let location = LocationCoordinate2D(latitude: 0, longitude: 1) let options = IsochroneOptions(location: location, - contour: .meters([99.5, 200.44])) - options.colors = [IsochroneOptions.Color(red: 11, green: 12, blue: 13), - IsochroneOptions.Color(red: 21, green: 22, blue: 23)] + contours: .distance([99.5, 200.44])) + options.colors = [IsochroneOptions.Color(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0), + IsochroneOptions.Color(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)] options.contoursPolygons = true options.denoiseFactor = 0.5 options.generalizeTolerance = 13 let isochrone = Isochrone(credentials: IsochroneBogusCredentials) - let url = isochrone.url(forCalculating: options) + var url = isochrone.url(forCalculating: options) let request = isochrone.urlRequest(forCalculating: options) guard let components = URLComponents(string: url.absoluteString), @@ -52,13 +52,24 @@ class IsochroneTests: XCTestCase { XCTAssertTrue(components.path.contains(location.requestDescription) ) XCTAssertTrue(queryItems.contains(where: { $0.name == "access_token" && $0.value == BogusToken })) XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_meters" && $0.value == "100;200"})) - XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_colors" && $0.value == "0B0C0D;151617"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_colors" && $0.value == "19334C;667F99"})) XCTAssertTrue(queryItems.contains(where: { $0.name == "polygons" && $0.value == "true"})) XCTAssertTrue(queryItems.contains(where: { $0.name == "denoise" && $0.value == "0.5"})) XCTAssertTrue(queryItems.contains(where: { $0.name == "generalize" && $0.value == "13.0"})) XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url, url) + + options.contours = .expectedTravelTime([31, 149]) + + url = isochrone.url(forCalculating: options) + + guard let components = URLComponents(string: url.absoluteString), + let queryItems = components.queryItems else { + XCTFail("Invalid url"); return + } + + XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_minutes" && $0.value == "1;2"})) } func testMinimalValidResponse() { @@ -70,7 +81,7 @@ class IsochroneTests: XCTestCase { let expectation = self.expectation(description: "Async callback") let isochrone = Isochrone(credentials: IsochroneBogusCredentials) let options = IsochroneOptions(location: LocationCoordinate2D(latitude: 0, longitude: 1), - contour: .meters([100])) + contours: .distance([100])) isochrone.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } @@ -97,7 +108,7 @@ class IsochroneTests: XCTestCase { let expectation = self.expectation(description: "Async callback") let isochrone = Isochrone(credentials: IsochroneBogusCredentials) let options = IsochroneOptions(location: LocationCoordinate2D(latitude: 0, longitude: 1), - contour: .meters([100])) + contours: .distance([100])) isochrone.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } @@ -141,7 +152,7 @@ class IsochroneTests: XCTestCase { let expectation = self.expectation(description: "Async callback") let isochrone = Isochrone(credentials: IsochroneBogusCredentials) let options = IsochroneOptions(location: LocationCoordinate2D(latitude: 0, longitude: 1), - contour: .meters([100])) + contours: .distance([100])) isochrone.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } From b36d42b5b218c3c91b6c0b6e5f60fe95c7cb62ec Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 26 Oct 2021 12:46:37 +0300 Subject: [PATCH 06/19] vk-296-isochrone-api: test data typo fix (+2 squashed commits) Squashed commits: [91ac378] vk-296-isochrone-api: linux tests rearranged [fd9d7dd] vk-296-isochrone-api: resolved OHTTPStubs Linux usage --- .../IsochroneTests.swift | 50 +++++++++++-------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift index 05011f5d8..1aecab54e 100644 --- a/Tests/MapboxDirectionsTests/IsochroneTests.swift +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -1,6 +1,11 @@ import Foundation @testable import MapboxDirections +#if !os(Linux) import OHHTTPStubs +#if SWIFT_PACKAGE +import OHHTTPStubsSwift +#endif +#endif import Turf import XCTest @@ -13,15 +18,12 @@ let minimalValidResponse = """ } """ -#if !os(Linux) class IsochroneTests: XCTestCase { - override func setUp() { - super.setUp() - } - override func tearDown() { + #if !os(Linux) HTTPStubs.removeAllStubs() + #endif super.tearDown() } @@ -34,8 +36,13 @@ class IsochroneTests: XCTestCase { let location = LocationCoordinate2D(latitude: 0, longitude: 1) let options = IsochroneOptions(location: location, contours: .distance([99.5, 200.44])) + #if !os(Linux) options.colors = [IsochroneOptions.Color(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0), IsochroneOptions.Color(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)] + #else + options.colors = [IsochroneOptions.Color(red: 25, green: 51, blue: 76, alpha: 255), + IsochroneOptions.Color(red: 102, green: 127, blue: 153, alpha: 255)] + #endif options.contoursPolygons = true options.denoiseFactor = 0.5 options.generalizeTolerance = 13 @@ -72,6 +79,7 @@ class IsochroneTests: XCTestCase { XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_minutes" && $0.value == "1;2"})) } + #if !os(Linux) func testMinimalValidResponse() { HTTPStubs.stubRequests(passingTest: { (request) -> Bool in return request.url!.absoluteString.contains("https://api.mapbox.com/isochrone") @@ -125,21 +133,6 @@ class IsochroneTests: XCTestCase { wait(for: [expectation], timeout: 2.0) } - func testRateLimitErrorParsing() { - let url = URL(string: "https://api.mapbox.com")! - let headerFields = ["X-Rate-Limit-Interval" : "60", "X-Rate-Limit-Limit" : "600", "X-Rate-Limit-Reset" : "1479460584"] - let response = HTTPURLResponse(url: url, statusCode: 429, httpVersion: nil, headerFields: headerFields) - - let resultError = IsochroneError(code: "429", message: "Hit rate limit", response: response, underlyingError: nil) - if case let .rateLimited(rateLimitInterval, rateLimit, resetTime) = resultError { - XCTAssertEqual(rateLimitInterval, 60.0) - XCTAssertEqual(rateLimit, 600) - XCTAssertEqual(resetTime, Date(timeIntervalSince1970: 1479460584)) - } else { - XCTFail("Code 429 should be interpreted as a rate limiting error.") - } - } - func testDownNetwork() { let notConnected = NSError(domain: NSURLErrorDomain, code: URLError.notConnectedToInternet.rawValue) as! URLError @@ -172,5 +165,20 @@ class IsochroneTests: XCTestCase { }) wait(for: [expectation], timeout: 2.0) } + + func testRateLimitErrorParsing() { + let url = URL(string: "https://api.mapbox.com")! + let headerFields = ["X-Rate-Limit-Interval" : "60", "X-Rate-Limit-Limit" : "600", "X-Rate-Limit-Reset" : "1479460584"] + let response = HTTPURLResponse(url: url, statusCode: 429, httpVersion: nil, headerFields: headerFields) + + let resultError = IsochroneError(code: "429", message: "Hit rate limit", response: response, underlyingError: nil) + if case let .rateLimited(rateLimitInterval, rateLimit, resetTime) = resultError { + XCTAssertEqual(rateLimitInterval, 60.0) + XCTAssertEqual(rateLimit, 600) + XCTAssertEqual(resetTime, Date(timeIntervalSince1970: 1479460584)) + } else { + XCTFail("Code 429 should be interpreted as a rate limiting error.") + } + } + #endif } -#endif From c2a31ff16cf680e65e67dfd06b8f9d29a1dd43fc Mon Sep 17 00:00:00 2001 From: udumft Date: Tue, 26 Oct 2021 13:06:16 +0300 Subject: [PATCH 07/19] vk-296-isochrone-api: linux build color fix --- Tests/MapboxDirectionsTests/IsochroneTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift index 1aecab54e..257eb473a 100644 --- a/Tests/MapboxDirectionsTests/IsochroneTests.swift +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -40,8 +40,8 @@ class IsochroneTests: XCTestCase { options.colors = [IsochroneOptions.Color(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0), IsochroneOptions.Color(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)] #else - options.colors = [IsochroneOptions.Color(red: 25, green: 51, blue: 76, alpha: 255), - IsochroneOptions.Color(red: 102, green: 127, blue: 153, alpha: 255)] + options.colors = [IsochroneOptions.Color(red: 25, green: 51, blue: 76), + IsochroneOptions.Color(red: 102, green: 127, blue: 153)] #endif options.contoursPolygons = true options.denoiseFactor = 0.5 From 5060f5532bfc69c3e7a68cd63a54d5b3110106cb Mon Sep 17 00:00:00 2001 From: udumft Date: Wed, 27 Oct 2021 16:23:13 +0300 Subject: [PATCH 08/19] vk-296-isochrone-api: refactoed IsochroneOptions: renamed members, reworked contours definition. --- .../MapboxDirections/IsochroneOptions.swift | 189 +++++++++++++----- .../{Isochrone.swift => Isochrones.swift} | 10 +- .../IsochroneTests.swift | 61 +++--- 3 files changed, 173 insertions(+), 87 deletions(-) rename Sources/MapboxDirections/{Isochrone.swift => Isochrones.swift} (96%) diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift index b1384a35f..d3c8f80a1 100644 --- a/Sources/MapboxDirections/IsochroneOptions.swift +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -13,14 +13,28 @@ import AppKit */ public class IsochroneOptions { - public init(location: LocationCoordinate2D, contours: Contours, profileIdentifier: IsochroneProfileIdentifier = .automobile) { - self.location = location + public init(centerCoordinate: LocationCoordinate2D, contours: Contours, profileIdentifier: IsochroneProfileIdentifier = .automobile) { + self.centerCoordinate = centerCoordinate self.contours = contours self.profileIdentifier = profileIdentifier } // MARK: Configuring the Contour + /** + Contours GeoJSON format. + */ + public enum ContourFormat { + /** + Requested contour will be presented as GeoJSON LineString. + */ + case lineString + /** + Requested contour will be presented as GeoJSON Polygon. + */ + case polygon + } + /** A string specifying the primary mode of transportation for the contours. @@ -30,42 +44,34 @@ public class IsochroneOptions { /** A coordinate around which to center the isochrone lines. */ - public var location: LocationCoordinate2D + public var centerCoordinate: LocationCoordinate2D /** - Contours distance or travel time definition. + Contours bounds and color sheme definition. */ public var contours: Contours /** - The colors to use for each isochrone contour. - - Number of colors should match number of `contour`s. - If no colors are specified, the Isochrone API will assign a default rainbow color scheme to the output. - */ - public var colors: [Color]? - - /** - Specify whether to return the contours as GeoJSON polygons. + Specifies the format of output contours. - Defaults to `false` which represents contours as linestrings. + Defaults to `.lineString` which represents contours as linestrings. */ - public var contoursPolygons: Bool? + public var contoursFormat: ContourFormat = .lineString /** - Removes contours which are `denoiseFactor` times smaller than the biggest one. + Removes contours which are `denoisingFactor` times smaller than the biggest one. The default is 1.0. A value of 1.0 will only return the largest contour for a given value. A value of 0.5 drops any contours that are less than half the area of the largest contour in the set of contours for that same value. */ - public var denoiseFactor: Float? + public var denoisingFactor: Float? /** - Value in meters used as the tolerance for Douglas-Peucker generalization. + Douglas-Peucker simplification tolerance. - There is no upper bound. If no value is specified in the request, the Isochrone API will choose the most optimized generalization to use for the request. + Higher means simpler geometries and faster performance. There is no upper bound. If no value is specified in the request, the Isochrone API will choose the most optimized value to use for the request. - - note: Generalization of contours can lead to self-intersections, as well as intersections of adjacent contours. + - note: Simplification of contours can lead to self-intersections, as well as intersections of adjacent contours. */ - public var generalizeTolerance: LocationDistance? + public var simplificationTolerance: LocationDistance? // MARK: Getting the Request URL @@ -80,7 +86,7 @@ public class IsochroneOptions { The path of the request URL, not including the hostname or any parameters. */ var path: String { - return "\(abridgedPath)/\(location.requestDescription).json" + return "\(abridgedPath)/\(centerCoordinate.requestDescription).json" } /** @@ -88,34 +94,33 @@ public class IsochroneOptions { */ public var urlQueryItems: [URLQueryItem] { var queryItems: [URLQueryItem] = [] - var contoursCount = 0 switch contours { - case .distance(let meters): - let value = meters.sorted().map { String(Int($0.rounded())) }.joined(separator: ";") - queryItems.append(URLQueryItem(name: "contours_meters", value: value)) - contoursCount = meters.count - case .expectedTravelTime(let intervals): - let value = intervals.sorted().map { String(Int(($0 / 60.0).rounded())) }.joined(separator: ";") - queryItems.append(URLQueryItem(name: "contours_minutes", value: value)) - contoursCount = intervals.count + case .byDistances(let definition): + let (values, colors) = definition.serialise() + + queryItems.append(URLQueryItem(name: "contours_meters", value: values)) + if let colors = colors { + queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) + } + case .byExpectedTravelTimes(let definition): + let (values, colors) = definition.serialise(roundedTo: 60) + + queryItems.append(URLQueryItem(name: "contours_minutes", value: values)) + if let colors = colors { + queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) + } } - if let colors = colors, !colors.isEmpty { - assert(colors.count == contoursCount, "Contours `colors` count must match contours count!") - let value = colors.map { queryColorDescription(color: $0)}.joined(separator: ";") - queryItems.append(URLQueryItem(name: "contours_colors", value: value)) + if contoursFormat == .polygon { + queryItems.append(URLQueryItem(name: "polygons", value: "true")) } - if let isPolygon = contoursPolygons { - queryItems.append(URLQueryItem(name: "polygons", value: String(isPolygon))) - } - - if let denoise = denoiseFactor { + if let denoise = denoisingFactor { queryItems.append(URLQueryItem(name: "denoise", value: String(denoise))) } - if let tolerance = generalizeTolerance { + if let tolerance = simplificationTolerance { queryItems.append(URLQueryItem(name: "generalize", value: String(tolerance))) } @@ -124,36 +129,110 @@ public class IsochroneOptions { } extension IsochroneOptions { + /** - Definition of contour limit. + Definition of contours limits. */ public enum Contours { + + /** + Describes Individual contour bound and color. + */ + public enum ContourDefinition { + /** + Contour bound definition value and contour color. + */ + public typealias ValueAndColor = (value: Value, color: Color) + + /** + Allows configuring just the bound, leaving coloring to a default rainbow scheme. + */ + case `default`([Value]) + /** + Allows configuring both the bound and contour color. + */ + case colored([ValueAndColor]) + } + /** The desired travel times to use for each isochrone contour. This value will be rounded to the nearest minute. */ - case expectedTravelTime([TimeInterval]) + case byExpectedTravelTimes(ContourDefinition) + /** The distances to use for each isochrone contour. Will be rounded to the nearest integer. */ - case distance([LocationDistance]) + case byDistances(ContourDefinition) + } +} + +fileprivate extension Array where Element == Double { + func composeURLValue(roundedTo base: Int) -> String { + map { String(Int(($0 / Double(base)).rounded())) }.joined(separator: ";") + } +} + +extension IsochroneOptions.Contours.ContourDefinition where Value == Double { + func serialise(roundedTo base: Int = 1) -> (String, String?) { + switch (self) { + case .default(let intervals): + return (intervals.composeURLValue(roundedTo: base), nil) + case .colored(let intervals): + let sorted = intervals.sorted { lhs, rhs in + lhs.value < rhs.value + } + + let values = sorted.map(\.value).composeURLValue(roundedTo: base) + let colors = sorted.map(\.color.queryDescription).joined(separator: ";") + return (values, colors) + } } } extension IsochroneOptions { #if canImport(UIKit) + /** + RGB-based color representation for Isochrone contour. + */ public typealias Color = UIColor #elseif canImport(AppKit) + /** + RGB-based color representation for Isochrone contour. + */ public typealias Color = NSColor #else + /** + RGB-based color representation for Isochrone contour. + + This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms that lack `UIKit` or `AppKit`. On Apple platforms, you can use `UIColor` or `NSColor` respectively anywhere you see this type. + */ public struct Color { + /** + Red color component. + + Value ranged from `0` up to `255`. + */ public var red: Int + /** + Green color component. + + Value ranged from `0` up to `255`. + */ public var green: Int + /** + Blue color component. + + Value ranged from `0` up to `255`. + */ public var blue: Int + /** + Creates new `Color` instance. + */ public init(red: Int, green: Int, blue: Int) { self.red = red self.green = green @@ -161,8 +240,10 @@ extension IsochroneOptions { } } #endif - - func queryColorDescription(color: Color) -> String { +} + +extension IsochroneOptions.Color { + var queryDescription: String { let hexFormat = "%02X%02X%02X" #if canImport(UIKit) @@ -170,17 +251,17 @@ extension IsochroneOptions { var green: CGFloat = 0 var blue: CGFloat = 0 - color.getRed(&red, - green: &green, - blue: &blue, - alpha: nil) + getRed(&red, + green: &green, + blue: &blue, + alpha: nil) return String(format: hexFormat, Int(red * 255), Int(green * 255), Int(blue * 255)) #elseif canImport(AppKit) - guard let convertedColor = color.usingColorSpace(.deviceRGB) else { + guard let convertedColor = usingColorSpace(.deviceRGB) else { assertionFailure("Failed to convert Isochrone contour color to RGB space.") return "000000" } @@ -191,9 +272,9 @@ extension IsochroneOptions { Int(convertedColor.blueComponent * 255)) #else return String(format: hexFormat, - color.red, - color.green, - color.blue) + red, + green, + blue) #endif } } diff --git a/Sources/MapboxDirections/Isochrone.swift b/Sources/MapboxDirections/Isochrones.swift similarity index 96% rename from Sources/MapboxDirections/Isochrone.swift rename to Sources/MapboxDirections/Isochrones.swift index 1dd60214a..9f03e1990 100644 --- a/Sources/MapboxDirections/Isochrone.swift +++ b/Sources/MapboxDirections/Isochrones.swift @@ -7,7 +7,7 @@ import Turf /** Computes areas that are reachable within a specified amount of time or distance from a location, and returns the reachable regions as contours of polygons or lines that you can display on a map. */ -open class Isochrone { +open class Isochrones { /** A tuple type representing the isochrone session that was generated from the request. @@ -21,13 +21,13 @@ open class Isochrone { /** A closure (block) to be called when a isochrone request is complete. - - parameter session: A `Isochrone.Session` object containing session information + - parameter session: A `Isochrones.Session` object containing session information - parameter result: A `Result` enum that represents the `FeatureCollection` if the request returned successfully, or the error if it did not. */ public typealias IsochroneCompletionHandler = (_ session: Session, _ result: Result) -> Void - // MARK: Creating an Isochrone Object + // MARK: Creating an Isochrones Object /** The Authorization & Authentication credentials that are used for this service. @@ -43,10 +43,10 @@ open class Isochrone { To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be specified in the `MBXAccessToken` key in the main application bundle’s Info.plist. */ - public static let shared = Isochrone() + public static let shared = Isochrones() /** - Creates a new instance of Isochrone object. + Creates a new instance of Isochrones object. - Parameters: - credentials: Credentials that will be used to make API requests to Mapbox Isochrone API. - urlSession: URLSession that will be used to submit API requests to Mapbox Isochrone API. diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift index 257eb473a..6c622b205 100644 --- a/Tests/MapboxDirectionsTests/IsochroneTests.swift +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -28,28 +28,33 @@ class IsochroneTests: XCTestCase { } func testConfiguration() { - let isochrone = Isochrone(credentials: IsochroneBogusCredentials) - XCTAssertEqual(isochrone.credentials, IsochroneBogusCredentials) + let isochrones = Isochrones(credentials: IsochroneBogusCredentials) + XCTAssertEqual(isochrones.credentials, IsochroneBogusCredentials) } func testRequest() { let location = LocationCoordinate2D(latitude: 0, longitude: 1) - let options = IsochroneOptions(location: location, - contours: .distance([99.5, 200.44])) + #if !os(Linux) - options.colors = [IsochroneOptions.Color(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0), - IsochroneOptions.Color(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)] + let options = IsochroneOptions(centerCoordinate: location, + contours: .byDistances(.colored([ + (99.5, .init(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0)), + (200.44, .init(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)) + ]))) #else - options.colors = [IsochroneOptions.Color(red: 25, green: 51, blue: 76), - IsochroneOptions.Color(red: 102, green: 127, blue: 153)] + let options = IsochroneOptions(centerCoordinate: location, + contours: .byDistances(.colored([ + (99.5, .init(red: 25, green: 51, blue: 76)), + (200.44, .init(red: 102, green: 127, blue: 153)) + ]))) #endif - options.contoursPolygons = true - options.denoiseFactor = 0.5 - options.generalizeTolerance = 13 + options.contoursFormat = .polygon + options.denoisingFactor = 0.5 + options.simplificationTolerance = 13 - let isochrone = Isochrone(credentials: IsochroneBogusCredentials) - var url = isochrone.url(forCalculating: options) - let request = isochrone.urlRequest(forCalculating: options) + let isochrones = Isochrones(credentials: IsochroneBogusCredentials) + var url = isochrones.url(forCalculating: options) + let request = isochrones.urlRequest(forCalculating: options) guard let components = URLComponents(string: url.absoluteString), let queryItems = components.queryItems else { @@ -67,9 +72,9 @@ class IsochroneTests: XCTestCase { XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url, url) - options.contours = .expectedTravelTime([31, 149]) + options.contours = .byExpectedTravelTimes(.default([31, 149])) - url = isochrone.url(forCalculating: options) + url = isochrones.url(forCalculating: options) guard let components = URLComponents(string: url.absoluteString), let queryItems = components.queryItems else { @@ -87,10 +92,10 @@ class IsochroneTests: XCTestCase { return HTTPStubsResponse(data: minimalValidResponse.data(using: .utf8)!, statusCode: 200, headers: ["Content-Type" : "text/html"]) } let expectation = self.expectation(description: "Async callback") - let isochrone = Isochrone(credentials: IsochroneBogusCredentials) - let options = IsochroneOptions(location: LocationCoordinate2D(latitude: 0, longitude: 1), - contours: .distance([100])) - isochrone.calculate(options, completionHandler: { (session, result) in + let isochrones = Isochrones(credentials: IsochroneBogusCredentials) + let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), + contours: .byDistances(.default([100]))) + isochrones.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } guard case let .success(featureCollection) = result else { @@ -114,10 +119,10 @@ class IsochroneTests: XCTestCase { return HTTPStubsResponse(data: message.data(using: .utf8)!, statusCode: 420, headers: ["Content-Type" : "text/plain"]) } let expectation = self.expectation(description: "Async callback") - let isochrone = Isochrone(credentials: IsochroneBogusCredentials) - let options = IsochroneOptions(location: LocationCoordinate2D(latitude: 0, longitude: 1), - contours: .distance([100])) - isochrone.calculate(options, completionHandler: { (session, result) in + let isochrones = Isochrones(credentials: IsochroneBogusCredentials) + let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), + contours: .byDistances(.default([100]))) + isochrones.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } guard case let .failure(error) = result else { @@ -143,10 +148,10 @@ class IsochroneTests: XCTestCase { } let expectation = self.expectation(description: "Async callback") - let isochrone = Isochrone(credentials: IsochroneBogusCredentials) - let options = IsochroneOptions(location: LocationCoordinate2D(latitude: 0, longitude: 1), - contours: .distance([100])) - isochrone.calculate(options, completionHandler: { (session, result) in + let isochrones = Isochrones(credentials: IsochroneBogusCredentials) + let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), + contours: .byDistances(.default([100]))) + isochrones.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } guard case let .failure(error) = result else { From 66fe022d2605f187926b28a98f379fd2f5ef6043 Mon Sep 17 00:00:00 2001 From: udumft Date: Wed, 27 Oct 2021 16:56:31 +0300 Subject: [PATCH 09/19] vk-296-isochrone-api: incorporated Measurement for contours bounds definition --- .../MapboxDirections/IsochroneOptions.swift | 57 ++++++++++--------- .../IsochroneTests.swift | 19 ++++--- 2 files changed, 41 insertions(+), 35 deletions(-) diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift index d3c8f80a1..e9bb38c1f 100644 --- a/Sources/MapboxDirections/IsochroneOptions.swift +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -97,14 +97,14 @@ public class IsochroneOptions { switch contours { case .byDistances(let definition): - let (values, colors) = definition.serialise() + let (values, colors) = definition.serialise(roundingTo: .meters) queryItems.append(URLQueryItem(name: "contours_meters", value: values)) if let colors = colors { queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) } case .byExpectedTravelTimes(let definition): - let (values, colors) = definition.serialise(roundedTo: 60) + let (values, colors) = definition.serialise(roundingTo: .minutes) queryItems.append(URLQueryItem(name: "contours_minutes", value: values)) if let colors = colors { @@ -138,9 +138,13 @@ extension IsochroneOptions { /** Describes Individual contour bound and color. */ - public enum ContourDefinition { + public enum ContourDefinition { /** - Contour bound definition value and contour color. + Contour bound definition value. + */ + public typealias Value = Measurement + /** + Contour bound definition value and contour color. */ public typealias ValueAndColor = (value: Value, color: Color) @@ -152,44 +156,43 @@ extension IsochroneOptions { Allows configuring both the bound and contour color. */ case colored([ValueAndColor]) + + func serialise(roundingTo unit: Unt) -> (String, String?) { + switch (self) { + case .default(let intervals): + + return (intervals.map { $0.converted(to: unit).value }.composeURLValue(), nil) + case .colored(let intervals): + let sorted = intervals.sorted { lhs, rhs in + lhs.value < rhs.value + } + + let values = sorted.map { $0.value.converted(to: unit).value }.composeURLValue() + let colors = sorted.map(\.color.queryDescription).joined(separator: ";") + return (values, colors) + } + } } /** The desired travel times to use for each isochrone contour. - This value will be rounded to the nearest minute. + This value will be rounded to minutes. */ - case byExpectedTravelTimes(ContourDefinition) + case byExpectedTravelTimes(ContourDefinition) /** The distances to use for each isochrone contour. - Will be rounded to the nearest integer. + Will be rounded to meters. */ - case byDistances(ContourDefinition) + case byDistances(ContourDefinition) } } fileprivate extension Array where Element == Double { - func composeURLValue(roundedTo base: Int) -> String { - map { String(Int(($0 / Double(base)).rounded())) }.joined(separator: ";") - } -} - -extension IsochroneOptions.Contours.ContourDefinition where Value == Double { - func serialise(roundedTo base: Int = 1) -> (String, String?) { - switch (self) { - case .default(let intervals): - return (intervals.composeURLValue(roundedTo: base), nil) - case .colored(let intervals): - let sorted = intervals.sorted { lhs, rhs in - lhs.value < rhs.value - } - - let values = sorted.map(\.value).composeURLValue(roundedTo: base) - let colors = sorted.map(\.color.queryDescription).joined(separator: ";") - return (values, colors) - } + func composeURLValue() -> String { + map { String(Int($0.rounded())) }.joined(separator: ";") } } diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift index 6c622b205..30e26ac27 100644 --- a/Tests/MapboxDirectionsTests/IsochroneTests.swift +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -36,16 +36,18 @@ class IsochroneTests: XCTestCase { let location = LocationCoordinate2D(latitude: 0, longitude: 1) #if !os(Linux) + let radius1 = Measurement(value: 99.5, unit: UnitLength.meters) + let radius2 = Measurement(value: 0.2, unit: UnitLength.kilometers) let options = IsochroneOptions(centerCoordinate: location, contours: .byDistances(.colored([ - (99.5, .init(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0)), - (200.44, .init(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)) + (radius1, .init(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0)), + (radius2, .init(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)) ]))) #else let options = IsochroneOptions(centerCoordinate: location, contours: .byDistances(.colored([ - (99.5, .init(red: 25, green: 51, blue: 76)), - (200.44, .init(red: 102, green: 127, blue: 153)) + (radius1, .init(red: 25, green: 51, blue: 76)), + (radius2, .init(red: 102, green: 127, blue: 153)) ]))) #endif options.contoursFormat = .polygon @@ -72,7 +74,8 @@ class IsochroneTests: XCTestCase { XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url, url) - options.contours = .byExpectedTravelTimes(.default([31, 149])) + options.contours = .byExpectedTravelTimes(.default([.init(value: 31, unit: .seconds), + .init(value: 2.1, unit: .minutes)])) url = isochrones.url(forCalculating: options) @@ -94,7 +97,7 @@ class IsochroneTests: XCTestCase { let expectation = self.expectation(description: "Async callback") let isochrones = Isochrones(credentials: IsochroneBogusCredentials) let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), - contours: .byDistances(.default([100]))) + contours: .byDistances(.default([.init(value: 100, unit: .meters)]))) isochrones.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } @@ -121,7 +124,7 @@ class IsochroneTests: XCTestCase { let expectation = self.expectation(description: "Async callback") let isochrones = Isochrones(credentials: IsochroneBogusCredentials) let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), - contours: .byDistances(.default([100]))) + contours: .byDistances(.default([.init(value: 100, unit: .meters)]))) isochrones.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } @@ -150,7 +153,7 @@ class IsochroneTests: XCTestCase { let expectation = self.expectation(description: "Async callback") let isochrones = Isochrones(credentials: IsochroneBogusCredentials) let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), - contours: .byDistances(.default([100]))) + contours: .byDistances(.default([.init(value: 100, unit: .meters)]))) isochrones.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } From 104a9b5ba57dab712a36fc609efc2b7eb00c170c Mon Sep 17 00:00:00 2001 From: udumft Date: Wed, 27 Oct 2021 17:29:33 +0300 Subject: [PATCH 10/19] vk-296-isochrone-api: linux compilation fixed. --- .../IsochroneTests.swift | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift index 30e26ac27..29cdbdf26 100644 --- a/Tests/MapboxDirectionsTests/IsochroneTests.swift +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -34,23 +34,23 @@ class IsochroneTests: XCTestCase { func testRequest() { let location = LocationCoordinate2D(latitude: 0, longitude: 1) - - #if !os(Linux) let radius1 = Measurement(value: 99.5, unit: UnitLength.meters) let radius2 = Measurement(value: 0.2, unit: UnitLength.kilometers) + + #if !os(Linux) let options = IsochroneOptions(centerCoordinate: location, contours: .byDistances(.colored([ - (radius1, .init(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0)), + (radius1, .init(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0)), (radius2, .init(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)) ]))) #else let options = IsochroneOptions(centerCoordinate: location, - contours: .byDistances(.colored([ - (radius1, .init(red: 25, green: 51, blue: 76)), - (radius2, .init(red: 102, green: 127, blue: 153)) + contours: IsochroneOptions.Contours.byDistances(.colored([ + (radius1, IsochroneOptions.Color(red: 25, green: 51, blue: 76)), + (radius2, IsochroneOptions.Color(red: 102, green: 127, blue: 153)) ]))) #endif - options.contoursFormat = .polygon + options.contoursFormat = IsochroneOptions.ContourFormat.polygon options.denoisingFactor = 0.5 options.simplificationTolerance = 13 @@ -74,8 +74,10 @@ class IsochroneTests: XCTestCase { XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url, url) - options.contours = .byExpectedTravelTimes(.default([.init(value: 31, unit: .seconds), - .init(value: 2.1, unit: .minutes)])) + options.contours = IsochroneOptions.Contours.byExpectedTravelTimes(.default([ + Measurement(value: 31, unit: UnitDuration.seconds), + Measurement(value: 2.1, unit: UnitDuration.minutes) + ])) url = isochrones.url(forCalculating: options) From e658370b4db6a730a347007bbb3fbbb336482ffb Mon Sep 17 00:00:00 2001 From: udumft Date: Thu, 28 Oct 2021 12:27:36 +0300 Subject: [PATCH 11/19] vk-296-isochrone-api: corrected naming, added color space converting for NSColor, refined contour query serialization code. --- .../MapboxDirections/IsochroneOptions.swift | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift index e9bb38c1f..6fd787872 100644 --- a/Sources/MapboxDirections/IsochroneOptions.swift +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -97,14 +97,14 @@ public class IsochroneOptions { switch contours { case .byDistances(let definition): - let (values, colors) = definition.serialise(roundingTo: .meters) + let (values, colors) = definition.serialize(roundingTo: .meters) queryItems.append(URLQueryItem(name: "contours_meters", value: values)) if let colors = colors { queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) } case .byExpectedTravelTimes(let definition): - let (values, colors) = definition.serialise(roundingTo: .minutes) + let (values, colors) = definition.serialize(roundingTo: .minutes) queryItems.append(URLQueryItem(name: "contours_minutes", value: values)) if let colors = colors { @@ -157,17 +157,17 @@ extension IsochroneOptions { */ case colored([ValueAndColor]) - func serialise(roundingTo unit: Unt) -> (String, String?) { + func serialize(roundingTo unit: Unt) -> (String, String?) { switch (self) { case .default(let intervals): - return (intervals.map { $0.converted(to: unit).value }.composeURLValue(), nil) + return (intervals.map { String(Int($0.converted(to: unit).value.rounded())) }.joined(separator: ";"), nil) case .colored(let intervals): let sorted = intervals.sorted { lhs, rhs in lhs.value < rhs.value } - let values = sorted.map { $0.value.converted(to: unit).value }.composeURLValue() + let values = sorted.map { String(Int($0.value.converted(to: unit).value.rounded())) }.joined(separator: ";") let colors = sorted.map(\.color.queryDescription).joined(separator: ";") return (values, colors) } @@ -190,12 +190,6 @@ extension IsochroneOptions { } } -fileprivate extension Array where Element == Double { - func composeURLValue() -> String { - map { String(Int($0.rounded())) }.joined(separator: ";") - } -} - extension IsochroneOptions { #if canImport(UIKit) /** @@ -209,7 +203,7 @@ extension IsochroneOptions { public typealias Color = NSColor #else /** - RGB-based color representation for Isochrone contour. + sRGB color space representation for Isochrone contour. This is a compatibility shim to keep the library’s public interface consistent between Apple and non-Apple platforms that lack `UIKit` or `AppKit`. On Apple platforms, you can use `UIColor` or `NSColor` respectively anywhere you see this type. */ @@ -264,9 +258,14 @@ extension IsochroneOptions.Color { Int(green * 255), Int(blue * 255)) #elseif canImport(AppKit) - guard let convertedColor = usingColorSpace(.deviceRGB) else { - assertionFailure("Failed to convert Isochrone contour color to RGB space.") - return "000000" + var convertedColor = self + if colorSpace != .sRGB { + guard let converted = usingColorSpace(.sRGB) else { + assertionFailure("Failed to convert Isochrone contour color to RGB space.") + return "000000" + } + + convertedColor = converted } return String(format: hexFormat, From 9420de44d7fe08bcaf0deb0bcd8a9ba18c229b5b Mon Sep 17 00:00:00 2001 From: udumft Date: Thu, 28 Oct 2021 15:14:52 +0300 Subject: [PATCH 12/19] vk-296-isochrone-api: added README example; fixed query encoding bug --- README.md | 63 +++++++++++++++++++ .../MapboxDirections/IsochroneOptions.swift | 6 +- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b2443cc37..7e74ae387 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,25 @@ let task = directions.calculate(options) { (session, result) in You can also use the `Directions.calculateRoutes(matching:completionHandler:)` method to get Route objects suitable for use anywhere a standard Directions API response would be used. +### Isochrone API + +`Isochrones` API uses the same access token initialization as `Directions`. Once that is configured, you need to fill `IsochronesOptions` parameters to calculate the desired geoJSON: + +```swift +let isochrones = Isochrones(credentials:IsochroneCredentials(accessToken: "<#your access token#>")) + +let isochroneOptions = IsochroneOptions(centerCoordinate: CLLocationCoordinate2D(latitude: 45.52, longitude: -122.681944), + contours: .byDistances(.colored([(.init(value: 500, unit: .meters), .orange), + (.init(value: 1, unit: .kilometers), .red)]))) + +isochrones.calculate(isochroneOptions) { session, result in + if case .success(let response) = result { + print(response) + } +} +``` +... + ## Usage with other Mapbox libraries ### Drawing the route on a map @@ -204,6 +223,50 @@ if var routeCoordinates = route.shape?.coordinates, routeCoordinates.count > 0 { The [Mapbox Navigation SDK for iOS](https://github.com/mapbox/mapbox-navigation-ios/) provides a full-fledged user interface for turn-by-turn navigation along routes supplied by MapboxDirections. +### Drawing Isochrones contours on a map snapshot + +[MapboxStatic.swift](https://github.com/mapbox/MapboxStatic.swift) provides the easies way to draw an Isochrone contour on a map. + +```swift +// main.swift +import MapboxStatic +import MapboxDirections + +let centerCoordinate = CLLocationCoordinate2D(latitude: 45.52, longitude: -122.681944) +let accessToken = "<#your access token#>" + +// Setup snapshot parameters +let camera = SnapshotCamera( + lookingAtCenter: centerCoordinate, + zoomLevel: 12) +let options = SnapshotOptions( + styleURL: URL(string: "<#your mapbox: style URL#>")!, + camera: camera, + size: CGSize(width: 200, height: 200)) + +// Request Isochrone contour to draw on a map +let isochrones = Isochrones(credentials:IsochroneCredentials(accessToken: accessToken)) +isochrones.calculate(IsochroneOptions(centerCoordinate: centerCoordinate, + contours: .byDistances(.default([.init(value: 500, unit: .meters)])))) { session, result in + if case .success(let response) = result { + // Serialize the geoJSON + let encoder = JSONEncoder() + let data = try! encoder.encode(response) + let geoJSONString = String(data: data, encoding: .utf8)! + let geoJSONOverlay = GeoJSON(objectString: geoJSONString) + + // Feed resulting geoJSON to snapshot options + options.overlays.append(geoJSONOverlay) + + let snapshot = Snapshot( + options: options, + accessToken: accessToken) + + // display the result! + drawImage(snapshot.image) + } +} +``` ## Directions CLI diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift index 6fd787872..a9e0e75c6 100644 --- a/Sources/MapboxDirections/IsochroneOptions.swift +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -161,14 +161,14 @@ extension IsochroneOptions { switch (self) { case .default(let intervals): - return (intervals.map { String(Int($0.converted(to: unit).value.rounded())) }.joined(separator: ";"), nil) + return (intervals.map { String(Int($0.converted(to: unit).value.rounded())) }.joined(separator: ","), nil) case .colored(let intervals): let sorted = intervals.sorted { lhs, rhs in lhs.value < rhs.value } - let values = sorted.map { String(Int($0.value.converted(to: unit).value.rounded())) }.joined(separator: ";") - let colors = sorted.map(\.color.queryDescription).joined(separator: ";") + let values = sorted.map { String(Int($0.value.converted(to: unit).value.rounded())) }.joined(separator: ",") + let colors = sorted.map(\.color.queryDescription).joined(separator: ",") return (values, colors) } } From d0584be303cf36eda6e08542c7081715bcfe0a50 Mon Sep 17 00:00:00 2001 From: udumft Date: Thu, 28 Oct 2021 16:46:29 +0300 Subject: [PATCH 13/19] vk-296-isochrone-api: deprecated DirectionsCredentials and DirectionsProfileIdentifier to reuse with Isohrones. Tests updated --- README.md | 6 +- .../MapboxDirections/AttributeOptions.swift | 4 +- Sources/MapboxDirections/Credentials.swift | 66 ++++++++++++++++++ Sources/MapboxDirections/Directions.swift | 16 ++--- .../DirectionsCredentials.swift | 67 +------------------ .../MapboxDirections/DirectionsError.swift | 4 +- .../MapboxDirections/DirectionsOptions.swift | 12 ++-- .../DirectionsProfileIdentifier.swift | 43 ++---------- .../MapboxDirections/DirectionsResult.swift | 4 +- .../IsochroneCredentials.swift | 3 - .../MapboxDirections/IsochroneOptions.swift | 6 +- .../IsochroneProfileIdentifier.swift | 33 --------- Sources/MapboxDirections/Isochrones.swift | 6 +- .../MapMatching/MapMatchingResponse.swift | 6 +- .../MapMatching/MatchOptions.swift | 14 ++-- .../MapboxDirections/ProfileIdentifier.swift | 44 ++++++++++++ Sources/MapboxDirections/QuickLook.swift | 2 +- Sources/MapboxDirections/RouteLeg.swift | 10 +-- Sources/MapboxDirections/RouteOptions.swift | 28 ++++---- .../RouteRefreshResponse.swift | 4 +- Sources/MapboxDirections/RouteResponse.swift | 8 +-- Sources/MapboxDirections/RouteStep.swift | 16 ++--- .../MapboxDirectionsCLI/CodingOperation.swift | 2 +- .../CredentialsTests.swift | 49 +++++++++++++- .../DirectionsCredentialsTests.swift | 51 -------------- .../DirectionsTests.swift | 2 +- .../IsochroneTests.swift | 8 +-- .../OfflineDirectionsTests.swift | 2 +- .../RouteOptionsTests.swift | 2 +- .../RouteResponseTests.swift | 2 +- .../RouteStepTests.swift | 2 +- Tests/MapboxDirectionsTests/V5Tests.swift | 2 +- .../WalkingOptionsTests.swift | 2 +- 33 files changed, 246 insertions(+), 280 deletions(-) create mode 100644 Sources/MapboxDirections/Credentials.swift delete mode 100644 Sources/MapboxDirections/IsochroneCredentials.swift delete mode 100644 Sources/MapboxDirections/IsochroneProfileIdentifier.swift create mode 100644 Sources/MapboxDirections/ProfileIdentifier.swift delete mode 100644 Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift diff --git a/README.md b/README.md index 7e74ae387..d7254330f 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ The main directions class is `Directions`. Create a directions object using your // main.swift import MapboxDirections -let directions = Directions(credentials: DirectionsCredentials(accessToken: "<#your access token#>")) +let directions = Directions(credentials: Credentials(accessToken: "<#your access token#>")) ``` Alternatively, you can place your access token in the `MBXAccessToken` key of your application’s Info.plist file, then use the shared directions object: @@ -183,7 +183,7 @@ You can also use the `Directions.calculateRoutes(matching:completionHandler:)` m `Isochrones` API uses the same access token initialization as `Directions`. Once that is configured, you need to fill `IsochronesOptions` parameters to calculate the desired geoJSON: ```swift -let isochrones = Isochrones(credentials:IsochroneCredentials(accessToken: "<#your access token#>")) +let isochrones = Isochrones(credentials: Credentials(accessToken: "<#your access token#>")) let isochroneOptions = IsochroneOptions(centerCoordinate: CLLocationCoordinate2D(latitude: 45.52, longitude: -122.681944), contours: .byDistances(.colored([(.init(value: 500, unit: .meters), .orange), @@ -245,7 +245,7 @@ let options = SnapshotOptions( size: CGSize(width: 200, height: 200)) // Request Isochrone contour to draw on a map -let isochrones = Isochrones(credentials:IsochroneCredentials(accessToken: accessToken)) +let isochrones = Isochrones(credentials: Credentials(accessToken: accessToken)) isochrones.calculate(IsochroneOptions(centerCoordinate: centerCoordinate, contours: .byDistances(.default([.init(value: 500, unit: .meters)])))) { session, result in if case .success(let response) = result { diff --git a/Sources/MapboxDirections/AttributeOptions.swift b/Sources/MapboxDirections/AttributeOptions.swift index 40097885e..f9096a29f 100644 --- a/Sources/MapboxDirections/AttributeOptions.swift +++ b/Sources/MapboxDirections/AttributeOptions.swift @@ -38,7 +38,7 @@ public struct AttributeOptions: OptionSet, CustomStringConvertible { When this attribute is specified, the `RouteLeg.congestionLevels` property contains one value for each segment in the leg’s full geometry. - This attribute requires `DirectionsProfileIdentifier.automobileAvoidingTraffic`. Any other profile identifier produces `CongestionLevel.unknown` for each segment along the route. + This attribute requires `ProfileIdentifier.automobileAvoidingTraffic`. Any other profile identifier produces `CongestionLevel.unknown` for each segment along the route. */ public static let congestionLevel = AttributeOptions(rawValue: 1 << 4) @@ -54,7 +54,7 @@ public struct AttributeOptions: OptionSet, CustomStringConvertible { When this attribute is specified, the `RouteLeg.numericCongestionLevels` property contains one value for each segment in the leg’s full geometry. - This attribute requires `DirectionsProfileIdentifier.automobileAvoidingTraffic`. Any other profile identifier produces `nil` for each segment along the route. + This attribute requires `ProfileIdentifier.automobileAvoidingTraffic`. Any other profile identifier produces `nil` for each segment along the route. */ public static let numericCongestionLevel = AttributeOptions(rawValue: 1 << 6) diff --git a/Sources/MapboxDirections/Credentials.swift b/Sources/MapboxDirections/Credentials.swift new file mode 100644 index 000000000..895f83779 --- /dev/null +++ b/Sources/MapboxDirections/Credentials.swift @@ -0,0 +1,66 @@ +import Foundation + +/// The Mapbox access token specified in the main application bundle’s Info.plist. +let defaultAccessToken: String? = + Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String ?? + Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAccessToken") as? String ?? + UserDefaults.standard.string(forKey: "MBXAccessToken") +let defaultApiEndPointURLString = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAPIBaseURL") as? String + +public struct Credentials: Equatable { + + /** + The mapbox access token. You can find this in your Mapbox account dashboard. + */ + public let accessToken: String? + + /** + The host to reach. defaults to `api.mapbox.com`. + */ + public let host: URL + + /** + The SKU Token associated with the request. Used for billing. + */ + public var skuToken: String? { + #if !os(Linux) + guard let mbx: AnyClass = NSClassFromString("MBXAccounts"), + mbx.responds(to: Selector(("serviceSkuToken"))), + let serviceSkuToken = mbx.value(forKeyPath: "serviceSkuToken") as? String + else { return nil } + + if mbx.responds(to: Selector(("serviceAccessToken"))) { + guard let serviceAccessToken = mbx.value(forKeyPath: "serviceAccessToken") as? String, + serviceAccessToken == accessToken + else { return nil } + + return serviceSkuToken + } + else { + return serviceSkuToken + } + #else + return nil + #endif + } + + /** + Intialize a new credential. + + - parameter accessToken: Optional. An access token to provide. If this value is nil, the SDK will attempt to find a token from your app's `info.plist`. + - parameter host: Optional. A parameter to pass a custom host. If `nil` is provided, the SDK will attempt to find a host from your app's `info.plist`, and barring that will default to `https://api.mapbox.com`. + */ + public init(accessToken token: String? = nil, host: URL? = nil) { + let accessToken = token ?? defaultAccessToken + + precondition(accessToken != nil && !accessToken!.isEmpty, "A Mapbox access token is required. Go to . In Info.plist, set the MBXAccessToken key to your access token, or use the Directions(accessToken:host:) initializer.") + self.accessToken = accessToken + if let host = host { + self.host = host + } else if let defaultHostString = defaultApiEndPointURLString, let defaultHost = URL(string: defaultHostString) { + self.host = defaultHost + } else { + self.host = URL(string: "https://api.mapbox.com")! + } + } +} diff --git a/Sources/MapboxDirections/Directions.swift b/Sources/MapboxDirections/Directions.swift index b7cd47bcf..ac3de20cf 100644 --- a/Sources/MapboxDirections/Directions.swift +++ b/Sources/MapboxDirections/Directions.swift @@ -75,7 +75,7 @@ open class Directions: NSObject { - parameter credentials: A object containing the credentials used to make the request. */ - public typealias Session = (options: DirectionsOptions, credentials: DirectionsCredentials) + public typealias Session = (options: DirectionsOptions, credentials: Credentials) /** A closure (block) to be called when a directions request is complete. @@ -103,7 +103,7 @@ open class Directions: NSObject { - postcondition: To update the original route, pass `RouteRefreshResponse.route` into the `Route.refreshLegAttributes(from:)` method. */ - public typealias RouteRefreshCompletionHandler = (_ credentials: DirectionsCredentials, _ result: Result) -> Void + public typealias RouteRefreshCompletionHandler = (_ credentials: Credentials, _ result: Result) -> Void // MARK: Creating a Directions Object @@ -119,14 +119,8 @@ open class Directions: NSObject { If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. */ - public let credentials: DirectionsCredentials + public let credentials: Credentials - /** - Initializes a newly created directions object with an optional access token and host. - - - parameter credentials: A `DirectionsCredentials` object that, optionally, contains customized Token and Endpoint information. If no credentials object is supplied, then defaults are used. - */ - private var authenticationParams: [URLQueryItem] { var params: [URLQueryItem] = [ URLQueryItem(name: "access_token", value: credentials.accessToken) @@ -148,7 +142,7 @@ open class Directions: NSObject { - urlSession: URLSession that will be used to submit API requests to Mapbox Directions API. - processingQueue: A DispatchQueue that will be used for CPU intensive work. */ - public init(credentials: DirectionsCredentials = .init(), + public init(credentials: Credentials = .init(), urlSession: URLSession = .shared, processingQueue: DispatchQueue = .global(qos: .userInitiated)) { self.credentials = credentials @@ -506,7 +500,7 @@ open class Directions: NSObject { open func urlRequest(forRefreshing responseIdentifier: String, routeIndex: Int, fromLegAtIndex startLegIndex: Int) -> URLRequest { let params: [URLQueryItem] = authenticationParams - var unparameterizedURL = URL(string: "directions-refresh/v1/\(DirectionsProfileIdentifier.automobileAvoidingTraffic.rawValue)", relativeTo: credentials.host)! + var unparameterizedURL = URL(string: "directions-refresh/v1/\(ProfileIdentifier.automobileAvoidingTraffic.rawValue)", relativeTo: credentials.host)! unparameterizedURL.appendPathComponent(responseIdentifier) unparameterizedURL.appendPathComponent(String(routeIndex)) unparameterizedURL.appendPathComponent(String(startLegIndex)) diff --git a/Sources/MapboxDirections/DirectionsCredentials.swift b/Sources/MapboxDirections/DirectionsCredentials.swift index 9f54496d9..75d866bea 100644 --- a/Sources/MapboxDirections/DirectionsCredentials.swift +++ b/Sources/MapboxDirections/DirectionsCredentials.swift @@ -1,67 +1,4 @@ import Foundation -/// The Mapbox access token specified in the main application bundle’s Info.plist. -let defaultAccessToken: String? = - Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String ?? - Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAccessToken") as? String ?? - UserDefaults.standard.string(forKey: "MBXAccessToken") -let defaultApiEndPointURLString = Bundle.main.object(forInfoDictionaryKey: "MGLMapboxAPIBaseURL") as? String - -public struct DirectionsCredentials: Equatable { - - /** - The mapbox access token. You can find this in your Mapbox account dashboard. - */ - public let accessToken: String? - - /** - The host to reach. defaults to `api.mapbox.com`. - */ - public let host: URL - - /** - The SKU Token associated with the request. Used for billing. - */ - public var skuToken: String? { - #if !os(Linux) - guard let mbx: AnyClass = NSClassFromString("MBXAccounts"), - mbx.responds(to: Selector(("serviceSkuToken"))), - let serviceSkuToken = mbx.value(forKeyPath: "serviceSkuToken") as? String - else { return nil } - - if mbx.responds(to: Selector(("serviceAccessToken"))) { - guard let serviceAccessToken = mbx.value(forKeyPath: "serviceAccessToken") as? String, - serviceAccessToken == accessToken - else { return nil } - - return serviceSkuToken - } - else { - return serviceSkuToken - } - #else - return nil - #endif - } - - /** - Intialize a new credential. - - - parameter accessToken: Optional. An access token to provide. If this value is nil, the SDK will attempt to find a token from your app's `info.plist`. - - parameter host: Optional. A parameter to pass a custom host. If `nil` is provided, the SDK will attempt to find a host from your app's `info.plist`, and barring that will default to `https://api.mapbox.com`. - */ - public init(accessToken token: String? = nil, host: URL? = nil) { - let accessToken = token ?? defaultAccessToken - - precondition(accessToken != nil && !accessToken!.isEmpty, "A Mapbox access token is required. Go to . In Info.plist, set the MBXAccessToken key to your access token, or use the Directions(accessToken:host:) initializer.") - self.accessToken = accessToken - if let host = host { - self.host = host - } else if let defaultHostString = defaultApiEndPointURLString, let defaultHost = URL(string: defaultHostString) { - self.host = defaultHost - } else { - self.host = URL(string: "https://api.mapbox.com")! - } - } -} - +@available(*, deprecated, renamed: "Credentials") +public typealias DirectionsCredentials = Credentials diff --git a/Sources/MapboxDirections/DirectionsError.swift b/Sources/MapboxDirections/DirectionsError.swift index 3a0b6931c..a4de44e34 100644 --- a/Sources/MapboxDirections/DirectionsError.swift +++ b/Sources/MapboxDirections/DirectionsError.swift @@ -87,7 +87,7 @@ public enum DirectionsError: LocalizedError { /** Unrecognized profile identifier. - Make sure the `DirectionsOptions.profileIdentifier` option is set to one of the predefined values, such as `DirectionsProfileIdentifier.automobile`. + Make sure the `DirectionsOptions.profileIdentifier` option is set to one of the predefined values, such as `ProfileIdentifier.automobile`. */ case profileNotFound @@ -166,7 +166,7 @@ public enum DirectionsError: LocalizedError { case .unableToLocate: return "Make sure the locations are close enough to a roadway or pathway. Try setting the coordinateAccuracy property of all the waypoints to nil." case .profileNotFound: - return "Make sure the profileIdentifier option is set to one of the provided constants, such as DirectionsProfileIdentifier.automobile." + return "Make sure the profileIdentifier option is set to one of the provided constants, such as ProfileIdentifier.automobile." case .requestTooLarge: return "Try specifying fewer waypoints or giving the waypoints shorter names." case let .rateLimited(rateLimitInterval: _, rateLimit: _, resetTime: rolloverTime): diff --git a/Sources/MapboxDirections/DirectionsOptions.swift b/Sources/MapboxDirections/DirectionsOptions.swift index 0c36f1264..c2f2d9584 100644 --- a/Sources/MapboxDirections/DirectionsOptions.swift +++ b/Sources/MapboxDirections/DirectionsOptions.swift @@ -120,10 +120,10 @@ open class DirectionsOptions: Codable { Do not call `DirectionsOptions(waypoints:profileIdentifier:)` directly; instead call the corresponding initializer of `RouteOptions` or `MatchOptions`. - - parameter waypoints: An array of `Waypoint` objects representing locations that the route should visit in chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. (Some profiles, such as `DirectionsProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter waypoints: An array of `Waypoint` objects representing locations that the route should visit in chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. (Some profiles, such as `ProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - required public init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = nil) { + required public init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = nil) { self.waypoints = waypoints self.profileIdentifier = profileIdentifier ?? .automobile } @@ -159,7 +159,7 @@ open class DirectionsOptions: Codable { public required init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) waypoints = try container.decode([Waypoint].self, forKey: .waypoints) - profileIdentifier = try container.decode(DirectionsProfileIdentifier.self, forKey: .profileIdentifier) + profileIdentifier = try container.decode(ProfileIdentifier.self, forKey: .profileIdentifier) includesSteps = try container.decode(Bool.self, forKey: .includesSteps) shapeFormat = try container.decode(RouteShapeFormat.self, forKey: .shapeFormat) routeShapeResolution = try container.decode(RouteShapeResolution.self, forKey: .routeShapeResolution) @@ -197,9 +197,9 @@ open class DirectionsOptions: Codable { /** A string specifying the primary mode of transportation for the routes. - The default value of this property is `DirectionsProfileIdentifier.automobile`, which specifies driving directions. + The default value of this property is `ProfileIdentifier.automobile`, which specifies driving directions. */ - open var profileIdentifier: DirectionsProfileIdentifier + open var profileIdentifier: ProfileIdentifier // MARK: Specifying the Response Format diff --git a/Sources/MapboxDirections/DirectionsProfileIdentifier.swift b/Sources/MapboxDirections/DirectionsProfileIdentifier.swift index 026baa5ed..612007df7 100644 --- a/Sources/MapboxDirections/DirectionsProfileIdentifier.swift +++ b/Sources/MapboxDirections/DirectionsProfileIdentifier.swift @@ -1,45 +1,10 @@ import Foundation -@available(*, deprecated, renamed: "DirectionsProfileIdentifier") -public typealias MBDirectionsProfileIdentifier = DirectionsProfileIdentifier +@available(*, deprecated, renamed: "ProfileIdentifier") +public typealias MBDirectionsProfileIdentifier = ProfileIdentifier /** Options determining the primary mode of transportation for the routes. */ -public struct DirectionsProfileIdentifier: Codable, Hashable, RawRepresentable { - public init(rawValue: String) { - self.rawValue = rawValue - } - - public var rawValue: String - - /** - The returned directions are appropriate for driving or riding a car, truck, or motorcycle. - - This profile prioritizes fast routes by preferring high-speed roads like highways. A driving route may use a ferry where necessary. - */ - public static let automobile: DirectionsProfileIdentifier = .init(rawValue: "mapbox/driving") - - /** - The returned directions are appropriate for driving or riding a car, truck, or motorcycle. - - This profile avoids traffic congestion based on current traffic data. A driving route may use a ferry where necessary. - - Traffic data is available in [a number of countries and territories worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/#traffic-data). Where traffic data is unavailable, this profile prefers high-speed roads like highways, similar to `DirectionsProfileIdentifier.Automobile`. - */ - public static let automobileAvoidingTraffic: DirectionsProfileIdentifier = .init(rawValue: "mapbox/driving-traffic") - - /** - The returned directions are appropriate for riding a bicycle. - - This profile prioritizes short, safe routes by avoiding highways and preferring cycling infrastructure, such as bike lanes on surface streets. A cycling route may, where necessary, use other modes of transportation, such as ferries or trains, or require dismounting the bicycle for a distance. - */ - public static let cycling: DirectionsProfileIdentifier = .init(rawValue: "mapbox/cycling") - - /** - The returned directions are appropriate for walking or hiking. - - This profile prioritizes short routes, making use of sidewalks and trails where available. A walking route may use other modes of transportation, such as ferries or trains, where necessary. - */ - public static let walking: DirectionsProfileIdentifier = .init(rawValue: "mapbox/walking") -} +@available(*, deprecated, renamed: "ProfileIdentifier") +public typealias DirectionsProfileIdentifier = ProfileIdentifier diff --git a/Sources/MapboxDirections/DirectionsResult.swift b/Sources/MapboxDirections/DirectionsResult.swift index e28e18194..ec276e056 100644 --- a/Sources/MapboxDirections/DirectionsResult.swift +++ b/Sources/MapboxDirections/DirectionsResult.swift @@ -132,7 +132,7 @@ open class DirectionsResult: Codable { /** The route’s expected travel time, measured in seconds. - The value of this property reflects the time it takes to traverse the entire route. It is the sum of the `expectedTravelTime` properties of the route’s legs. If the route was calculated using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin the route. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the route makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. + The value of this property reflects the time it takes to traverse the entire route. It is the sum of the `expectedTravelTime` properties of the route’s legs. If the route was calculated using the `ProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin the route. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the route makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. Do not assume that the user would travel along the route at a fixed speed. For more granular travel times, use the `RouteLeg.expectedTravelTime` or `RouteStep.expectedTravelTime`. For even more granularity, specify the `AttributeOptions.expectedTravelTime` option and use the `RouteLeg.expectedSegmentTravelTimes` property. */ @@ -141,7 +141,7 @@ open class DirectionsResult: Codable { /** The route’s typical travel time, measured in seconds. - The value of this property reflects the typical time it takes to traverse the entire route. It is the sum of the `typicalTravelTime` properties of the route’s legs. This property is available when using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin the route. If the route makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. + The value of this property reflects the typical time it takes to traverse the entire route. It is the sum of the `typicalTravelTime` properties of the route’s legs. This property is available when using the `ProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin the route. If the route makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. Do not assume that the user would travel along the route at a fixed speed. For more granular typical travel times, use the `RouteLeg.typicalTravelTime` or `RouteStep.typicalTravelTime`. */ diff --git a/Sources/MapboxDirections/IsochroneCredentials.swift b/Sources/MapboxDirections/IsochroneCredentials.swift deleted file mode 100644 index fffebfe55..000000000 --- a/Sources/MapboxDirections/IsochroneCredentials.swift +++ /dev/null @@ -1,3 +0,0 @@ -import Foundation - -public typealias IsochroneCredentials = DirectionsCredentials diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift index a9e0e75c6..1f947e13f 100644 --- a/Sources/MapboxDirections/IsochroneOptions.swift +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -13,7 +13,7 @@ import AppKit */ public class IsochroneOptions { - public init(centerCoordinate: LocationCoordinate2D, contours: Contours, profileIdentifier: IsochroneProfileIdentifier = .automobile) { + public init(centerCoordinate: LocationCoordinate2D, contours: Contours, profileIdentifier: ProfileIdentifier = .automobile) { self.centerCoordinate = centerCoordinate self.contours = contours self.profileIdentifier = profileIdentifier @@ -38,9 +38,9 @@ public class IsochroneOptions { /** A string specifying the primary mode of transportation for the contours. - The default value of this property is `IsochroneProfileIdentifier.automobile`, which specifies driving directions. + The default value of this property is `ProfileIdentifier.automobile`, which specifies driving directions. */ - public var profileIdentifier: IsochroneProfileIdentifier + public var profileIdentifier: ProfileIdentifier /** A coordinate around which to center the isochrone lines. */ diff --git a/Sources/MapboxDirections/IsochroneProfileIdentifier.swift b/Sources/MapboxDirections/IsochroneProfileIdentifier.swift deleted file mode 100644 index 90ba60637..000000000 --- a/Sources/MapboxDirections/IsochroneProfileIdentifier.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation - -/** - Options determining the primary mode of transportation for the contours. - */ -public struct IsochroneProfileIdentifier: Codable, RawRepresentable { - public init(rawValue: String) { - self.rawValue = rawValue - } - - public var rawValue: String - - /** - The returned contours are calculated for driving or riding a car, truck, or motorcycle. - - This profile prioritizes fast routes by preferring high-speed roads like highways. A driving route may use a ferry where necessary. - */ - public static let automobile: IsochroneProfileIdentifier = .init(rawValue: "mapbox/driving") - - /** - The returned contours are calculated for riding a bicycle. - - This profile prioritizes short, safe routes by avoiding highways and preferring cycling infrastructure, such as bike lanes on surface streets. A cycling route may, where necessary, use other modes of transportation, such as ferries or trains, or require dismounting the bicycle for a distance. - */ - public static let cycling: IsochroneProfileIdentifier = .init(rawValue: "mapbox/cycling") - - /** - The returned contours are calculated for walking or hiking. - - This profile prioritizes short routes, making use of sidewalks and trails where available. A walking route may use other modes of transportation, such as ferries or trains, where necessary. - */ - public static let walking: IsochroneProfileIdentifier = .init(rawValue: "mapbox/walking") -} diff --git a/Sources/MapboxDirections/Isochrones.swift b/Sources/MapboxDirections/Isochrones.swift index 9f03e1990..f8ed7d3aa 100644 --- a/Sources/MapboxDirections/Isochrones.swift +++ b/Sources/MapboxDirections/Isochrones.swift @@ -16,7 +16,7 @@ open class Isochrones { - parameter credentials: A object containing the credentials used to make the request. */ - public typealias Session = (options: IsochroneOptions, credentials: IsochroneCredentials) + public typealias Session = (options: IsochroneOptions, credentials: Credentials) /** A closure (block) to be called when a isochrone request is complete. @@ -34,7 +34,7 @@ open class Isochrones { If nothing is provided, the default behavior is to read credential values from the developer's Info.plist. */ - public let credentials: IsochroneCredentials + public let credentials: Credentials private let urlSession: URLSession private let processingQueue: DispatchQueue @@ -52,7 +52,7 @@ open class Isochrones { - urlSession: URLSession that will be used to submit API requests to Mapbox Isochrone API. - processingQueue: A DispatchQueue that will be used for CPU intensive work. */ - public init(credentials: IsochroneCredentials = .init(), + public init(credentials: Credentials = .init(), urlSession: URLSession = .shared, processingQueue: DispatchQueue = .global(qos: .userInitiated)) { self.credentials = credentials diff --git a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift index a9f5444b0..84e6b3d9e 100644 --- a/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift +++ b/Sources/MapboxDirections/MapMatching/MapMatchingResponse.swift @@ -10,7 +10,7 @@ public struct MapMatchingResponse { public var tracepoints: [Tracepoint?]? public let options: MatchOptions - public let credentials: DirectionsCredentials + public let credentials: Credentials /** The time when this `MapMatchingResponse` object was created, which is immediately upon recieving the raw URL response. @@ -28,7 +28,7 @@ extension MapMatchingResponse: Codable { case tracepoints } - public init(httpResponse: HTTPURLResponse?, matches: [Match]? = nil, tracepoints: [Tracepoint]? = nil, options: MatchOptions, credentials: DirectionsCredentials) { + public init(httpResponse: HTTPURLResponse?, matches: [Match]? = nil, tracepoints: [Tracepoint]? = nil, options: MatchOptions, credentials: Credentials) { self.httpResponse = httpResponse self.matches = matches self.tracepoints = tracepoints @@ -46,7 +46,7 @@ extension MapMatchingResponse: Codable { } self.options = options - guard let credentials = decoder.userInfo[.credentials] as? DirectionsCredentials else { + guard let credentials = decoder.userInfo[.credentials] as? Credentials else { throw DirectionsCodingError.missingCredentials } self.credentials = credentials diff --git a/Sources/MapboxDirections/MapMatching/MatchOptions.swift b/Sources/MapboxDirections/MapMatching/MatchOptions.swift index ac2a6f61c..d7853dac8 100644 --- a/Sources/MapboxDirections/MapMatching/MatchOptions.swift +++ b/Sources/MapboxDirections/MapMatching/MatchOptions.swift @@ -16,10 +16,10 @@ open class MatchOptions: DirectionsOptions { /** Initializes a match options object for matching locations against the road network. - - parameter locations: An array of `CLLocation` objects representing locations to attempt to match against the road network. The array should contain at least two locations (the source and destination) and at most 100 locations. (Some profiles, such as `DirectionsProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter locations: An array of `CLLocation` objects representing locations to attempt to match against the road network. The array should contain at least two locations (the source and destination) and at most 100 locations. (Some profiles, such as `ProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - public convenience init(locations: [CLLocation], profileIdentifier: DirectionsProfileIdentifier? = nil) { + public convenience init(locations: [CLLocation], profileIdentifier: ProfileIdentifier? = nil) { let waypoints = locations.map { Waypoint(location: $0) } @@ -30,17 +30,17 @@ open class MatchOptions: DirectionsOptions { /** Initializes a match options object for matching geographic coordinates against the road network. - - parameter coordinates: An array of geographic coordinates representing locations to attempt to match against the road network. The array should contain at least two locations (the source and destination) and at most 100 locations. (Some profiles, such as `DirectionsProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) Each coordinate is converted into a `Waypoint` object. - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter coordinates: An array of geographic coordinates representing locations to attempt to match against the road network. The array should contain at least two locations (the source and destination) and at most 100 locations. (Some profiles, such as `ProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://docs.mapbox.com/api/navigation/#directions).) Each coordinate is converted into a `Waypoint` object. + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - public convenience init(coordinates: [LocationCoordinate2D], profileIdentifier: DirectionsProfileIdentifier? = nil) { + public convenience init(coordinates: [LocationCoordinate2D], profileIdentifier: ProfileIdentifier? = nil) { let waypoints = coordinates.map { Waypoint(coordinate: $0) } self.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } - public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = nil) { + public required init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = nil) { super.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } diff --git a/Sources/MapboxDirections/ProfileIdentifier.swift b/Sources/MapboxDirections/ProfileIdentifier.swift new file mode 100644 index 000000000..c31457af1 --- /dev/null +++ b/Sources/MapboxDirections/ProfileIdentifier.swift @@ -0,0 +1,44 @@ +import Foundation + +/** + Options determining the primary mode of transportation. + */ +public struct ProfileIdentifier: Codable, Hashable, RawRepresentable { + public init(rawValue: String) { + self.rawValue = rawValue + } + + public var rawValue: String + + /** + The returned directions are appropriate for driving or riding a car, truck, or motorcycle. + + This profile prioritizes fast routes by preferring high-speed roads like highways. A driving route may use a ferry where necessary. + */ + public static let automobile: ProfileIdentifier = .init(rawValue: "mapbox/driving") + + /** + The returned directions are appropriate for driving or riding a car, truck, or motorcycle. + + This profile avoids traffic congestion based on current traffic data. A driving route may use a ferry where necessary. + + Traffic data is available in [a number of countries and territories worldwide](https://docs.mapbox.com/help/how-mapbox-works/directions/#traffic-data). Where traffic data is unavailable, this profile prefers high-speed roads like highways, similar to `ProfileIdentifier.Automobile`. + + - note: This profile is not supported by `Isochrones` API. + */ + public static let automobileAvoidingTraffic: ProfileIdentifier = .init(rawValue: "mapbox/driving-traffic") + + /** + The returned directions are appropriate for riding a bicycle. + + This profile prioritizes short, safe routes by avoiding highways and preferring cycling infrastructure, such as bike lanes on surface streets. A cycling route may, where necessary, use other modes of transportation, such as ferries or trains, or require dismounting the bicycle for a distance. + */ + public static let cycling: ProfileIdentifier = .init(rawValue: "mapbox/cycling") + + /** + The returned directions are appropriate for walking or hiking. + + This profile prioritizes short routes, making use of sidewalks and trails where available. A walking route may use other modes of transportation, such as ferries or trains, where necessary. + */ + public static let walking: ProfileIdentifier = .init(rawValue: "mapbox/walking") +} diff --git a/Sources/MapboxDirections/QuickLook.swift b/Sources/MapboxDirections/QuickLook.swift index 01971e8b0..51b1e4f36 100644 --- a/Sources/MapboxDirections/QuickLook.swift +++ b/Sources/MapboxDirections/QuickLook.swift @@ -15,7 +15,7 @@ protocol CustomQuickLookConvertible { /** Returns a URL to an image representation of the given coordinates via the [Mapbox Static Images API](https://docs.mapbox.com/api/maps/#static-images). */ -func debugQuickLookURL(illustrating shape: LineString, profileIdentifier: DirectionsProfileIdentifier = .automobile, accessToken: String? = defaultAccessToken) -> URL? { +func debugQuickLookURL(illustrating shape: LineString, profileIdentifier: ProfileIdentifier = .automobile, accessToken: String? = defaultAccessToken) -> URL? { guard let accessToken = accessToken else { return nil } diff --git a/Sources/MapboxDirections/RouteLeg.swift b/Sources/MapboxDirections/RouteLeg.swift index ca6d46312..a927bae3a 100644 --- a/Sources/MapboxDirections/RouteLeg.swift +++ b/Sources/MapboxDirections/RouteLeg.swift @@ -33,7 +33,7 @@ open class RouteLeg: Codable { - parameter typicalTravelTime: The route leg’s typical travel time, measured in seconds. - parameter profileIdentifier: The primary mode of transportation for the route leg. */ - public init(steps: [RouteStep], name: String, distance: Turf.LocationDistance, expectedTravelTime: TimeInterval, typicalTravelTime: TimeInterval? = nil, profileIdentifier: DirectionsProfileIdentifier) { + public init(steps: [RouteStep], name: String, distance: Turf.LocationDistance, expectedTravelTime: TimeInterval, typicalTravelTime: TimeInterval? = nil, profileIdentifier: ProfileIdentifier) { self.steps = steps self.name = name self.distance = distance @@ -63,7 +63,7 @@ open class RouteLeg: Codable { expectedTravelTime = try container.decode(TimeInterval.self, forKey: .expectedTravelTime) typicalTravelTime = try container.decodeIfPresent(TimeInterval.self, forKey: .typicalTravelTime) - if let profileIdentifier = try container.decodeIfPresent(DirectionsProfileIdentifier.self, forKey: .profileIdentifier) { + if let profileIdentifier = try container.decodeIfPresent(ProfileIdentifier.self, forKey: .profileIdentifier) { self.profileIdentifier = profileIdentifier } else if let options = decoder.userInfo[.options] as? DirectionsOptions { profileIdentifier = options.profileIdentifier @@ -287,7 +287,7 @@ open class RouteLeg: Codable { /** The route leg’s expected travel time, measured in seconds. - The value of this property reflects the time it takes to traverse the route leg. If the route was calculated using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin this leg. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the leg makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. + The value of this property reflects the time it takes to traverse the route leg. If the route was calculated using the `ProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin this leg. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the leg makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. Do not assume that the user would travel along the leg at a fixed speed. For the expected travel time on each individual segment along the leg, use the `RouteStep.expectedTravelTimes` property. For more granularity, specify the `AttributeOptions.expectedTravelTime` option and use the `expectedSegmentTravelTimes` property. */ @@ -313,7 +313,7 @@ open class RouteLeg: Codable { /** The route leg’s typical travel time, measured in seconds. - The value of this property reflects the typical time it takes to traverse the route leg. This property is available when using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin this leg. If the leg makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. + The value of this property reflects the typical time it takes to traverse the route leg. This property is available when using the `ProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin this leg. If the leg makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. Do not assume that the user would travel along the route at a fixed speed. For more granular typical travel times, use the `RouteStep.typicalTravelTime` property. */ @@ -326,7 +326,7 @@ open class RouteLeg: Codable { The value of this property depends on the `RouteOptions.profileIdentifier` property of the original `RouteOptions` object. This property reflects the primary mode of transportation used for the route leg. Individual steps along the route leg might use different modes of transportation as necessary. */ - public let profileIdentifier: DirectionsProfileIdentifier + public let profileIdentifier: ProfileIdentifier } extension RouteLeg: Equatable { diff --git a/Sources/MapboxDirections/RouteOptions.swift b/Sources/MapboxDirections/RouteOptions.swift index b1167cdc9..0c830ecf7 100644 --- a/Sources/MapboxDirections/RouteOptions.swift +++ b/Sources/MapboxDirections/RouteOptions.swift @@ -15,11 +15,11 @@ open class RouteOptions: DirectionsOptions { /** Initializes a route options object for routes between the given waypoints and an optional profile identifier. - - parameter waypoints: An array of `Waypoint` objects representing locations that the route should visit in chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. (Some profiles, such as `DirectionsProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://www.mapbox.com/api-documentation/#directions).) - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter waypoints: An array of `Waypoint` objects representing locations that the route should visit in chronological order. The array should contain at least two waypoints (the source and destination) and at most 25 waypoints. (Some profiles, such as `ProfileIdentifier.automobileAvoidingTraffic`, [may have lower limits](https://www.mapbox.com/api-documentation/#directions).) + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - public required init(waypoints: [Waypoint], profileIdentifier: DirectionsProfileIdentifier? = nil) { - let profilesDisallowingUTurns: [DirectionsProfileIdentifier] = [.automobile, .automobileAvoidingTraffic] + public required init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = nil) { + let profilesDisallowingUTurns: [ProfileIdentifier] = [.automobile, .automobileAvoidingTraffic] allowsUTurnAtWaypoint = !profilesDisallowingUTurns.contains(profileIdentifier ?? .automobile) super.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } @@ -31,9 +31,9 @@ open class RouteOptions: DirectionsOptions { - note: This initializer is intended for `CLLocation` objects created using the `CLLocation.init(latitude:longitude:)` initializer. If you intend to use a `CLLocation` object obtained from a `CLLocationManager` object, consider increasing the `horizontalAccuracy` or set it to a negative value to avoid overfitting, since the `Waypoint` class’s `coordinateAccuracy` property represents the maximum allowed deviation from the waypoint. - parameter locations: An array of `CLLocation` objects representing locations that the route should visit in chronological order. The array should contain at least two locations (the source and destination) and at most 25 locations. Each location object is converted into a `Waypoint` object. This class respects the `CLLocation` class’s `coordinate` and `horizontalAccuracy` properties, converting them into the `Waypoint` class’s `coordinate` and `coordinateAccuracy` properties, respectively. - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - public convenience init(locations: [CLLocation], profileIdentifier: DirectionsProfileIdentifier? = nil) { + public convenience init(locations: [CLLocation], profileIdentifier: ProfileIdentifier? = nil) { let waypoints = locations.map { Waypoint(location: $0) } self.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } @@ -43,9 +43,9 @@ open class RouteOptions: DirectionsOptions { Initializes a route options object for routes between the given geographic coordinates and an optional profile identifier. - parameter coordinates: An array of geographic coordinates representing locations that the route should visit in chronological order. The array should contain at least two locations (the source and destination) and at most 25 locations. Each coordinate is converted into a `Waypoint` object. - - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `DirectionsProfileIdentifier.automobile` is used by default. + - parameter profileIdentifier: A string specifying the primary mode of transportation for the routes. `ProfileIdentifier.automobile` is used by default. */ - public convenience init(coordinates: [LocationCoordinate2D], profileIdentifier: DirectionsProfileIdentifier? = nil) { + public convenience init(coordinates: [LocationCoordinate2D], profileIdentifier: ProfileIdentifier? = nil) { let waypoints = coordinates.map { Waypoint(coordinate: $0) } self.init(waypoints: waypoints, profileIdentifier: profileIdentifier) } @@ -145,7 +145,7 @@ open class RouteOptions: DirectionsOptions { Set this property to `true` if you expect the user to traverse each leg of the trip separately. For example, it would be quite easy for the user to effectively “U-turn” at a waypoint if the user first parks the car and patronizes a restaurant there before embarking on the next leg of the trip. Set this property to `false` if you expect the user to proceed to the next waypoint immediately upon arrival. For example, if the user only needs to drop off a passenger or package at the waypoint before continuing, it would be inconvenient to perform a U-turn at that location. - The default value of this property is `false` when the profile identifier is `DirectionsProfileIdentifier.automobile` or `DirectionsProfileIdentifier.automobileAvoidingTraffic` and `true` otherwise. + The default value of this property is `false` when the profile identifier is `ProfileIdentifier.automobile` or `ProfileIdentifier.automobileAvoidingTraffic` and `true` otherwise. */ open var allowsUTurnAtWaypoint: Bool @@ -159,7 +159,7 @@ open class RouteOptions: DirectionsOptions { /** The route classes that the calculated routes will allow. - This property has no effect unless the profile identifier is set to `DirectionsProfileIdentifier.automobile` or `DirectionsProfileIdentifier.automobileAvoidingTraffic`. + This property has no effect unless the profile identifier is set to `ProfileIdentifier.automobile` or `ProfileIdentifier.automobileAvoidingTraffic`. */ open var roadClassesToAllow: RoadClasses = [] @@ -167,7 +167,7 @@ open class RouteOptions: DirectionsOptions { The number that influences whether the route should prefer or avoid alleys or narrow service roads between buildings. If this property isn't explicitly set, the Directions API will choose the most reasonable value. - This property has no effect unless the profile identifier is set to `DirectionsProfileIdentifier.automobile` or `DirectionsProfileIdentifier.walking`. + This property has no effect unless the profile identifier is set to `ProfileIdentifier.automobile` or `ProfileIdentifier.walking`. The value of this property must be at least `DirectionsPriority.low` and at most `DirectionsPriority.high`. `DirectionsPriority.medium` neither prefers nor avoids alleys, while a negative value between `DirectionsPriority.low` and `DirectionsPriority.medium` avoids alleys, and a positive value between `DirectionsPriority.medium` and `DirectionsPriority.high` prefers alleys. A value of 0.9 is suitable for pedestrians who are comfortable with walking down alleys. */ @@ -177,7 +177,7 @@ open class RouteOptions: DirectionsOptions { The number that influences whether the route should prefer or avoid roads or paths that are set aside for pedestrian-only use (walkways or footpaths). If this property isn't explicitly set, the Directions API will choose the most reasonable value. - This property has no effect unless the profile identifier is set to `DirectionsProfileIdentifier.walking`. You can adjust this property to avoid [sidewalks and crosswalks that are mapped as separate footpaths](https://wiki.openstreetmap.org/wiki/Sidewalks#Sidewalk_as_separate_way), which may be more granular than needed for some forms of pedestrian navigation. + This property has no effect unless the profile identifier is set to `ProfileIdentifier.walking`. You can adjust this property to avoid [sidewalks and crosswalks that are mapped as separate footpaths](https://wiki.openstreetmap.org/wiki/Sidewalks#Sidewalk_as_separate_way), which may be more granular than needed for some forms of pedestrian navigation. The value of this property must be at least `DirectionsPriority.low` and at most `DirectionsPriority.high`. `DirectionsPriority.medium` neither prefers nor avoids walkways, while a negative value between `DirectionsPriority.low` and `DirectionsPriority.medium` avoids walkways, and a positive value between `DirectionsPriority.medium` and `DirectionsPriority.high` prefers walkways. A value of −0.1 results in less verbose routes in cities where sidewalks and crosswalks are generally mapped as separate footpaths. */ @@ -187,7 +187,7 @@ open class RouteOptions: DirectionsOptions { The expected uniform travel speed measured in meters per second. If this property isn't explicitly set, the Directions API will choose the most reasonable value. - This property has no effect unless the profile identifier is set to `DirectionsProfileIdentifier.walking`. You can adjust this property to account for running or for faster or slower gaits. When the profile identifier is set to another profile identifier, such as `DirectionsProfileIdentifier.driving`, this property is ignored in favor of the expected travel speed on each road along the route. This property may be supported by other routing profiles in the future. + This property has no effect unless the profile identifier is set to `ProfileIdentifier.walking`. You can adjust this property to account for running or for faster or slower gaits. When the profile identifier is set to another profile identifier, such as `ProfileIdentifier.driving`, this property is ignored in favor of the expected travel speed on each road along the route. This property may be supported by other routing profiles in the future. The value of this property must be at least `CLLocationSpeed.minimumWalking` and at most `CLLocationSpeed.maximumWalking`. `CLLocationSpeed.normalWalking` corresponds to a typical preferred walking speed. */ @@ -216,7 +216,7 @@ open class RouteOptions: DirectionsOptions { /** A Boolean value indicating whether `Directions` can refresh time-dependent properties of the `RouteLeg`s of the resulting `Route`s. - To refresh the `RouteLeg.expectedSegmentTravelTimes`, `RouteLeg.segmentSpeeds`, and `RouteLeg.segmentCongestionLevels` properties, use the `Directions.refreshRoute(responseIdentifier:routeIndex:fromLegAtIndex:completionHandler:)` method. This property is ignored unless `profileIdentifier` is `DirectionsProfileIdentifier.automobileAvoidingTraffic`. This option is set to `false` by default. + To refresh the `RouteLeg.expectedSegmentTravelTimes`, `RouteLeg.segmentSpeeds`, and `RouteLeg.segmentCongestionLevels` properties, use the `Directions.refreshRoute(responseIdentifier:routeIndex:fromLegAtIndex:completionHandler:)` method. This property is ignored unless `profileIdentifier` is `ProfileIdentifier.automobileAvoidingTraffic`. This option is set to `false` by default. */ open var refreshingEnabled = false diff --git a/Sources/MapboxDirections/RouteRefreshResponse.swift b/Sources/MapboxDirections/RouteRefreshResponse.swift index 0e57abaf4..9856d1a06 100644 --- a/Sources/MapboxDirections/RouteRefreshResponse.swift +++ b/Sources/MapboxDirections/RouteRefreshResponse.swift @@ -35,7 +35,7 @@ public struct RouteRefreshResponse { /** The credentials used to make the request. */ - public let credentials: DirectionsCredentials + public let credentials: Credentials /** The time when this `RouteRefreshResponse` object was created, which is immediately upon recieving the raw URL response. @@ -58,7 +58,7 @@ extension RouteRefreshResponse: Codable { self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse - guard let credentials = decoder.userInfo[.credentials] as? DirectionsCredentials else { + guard let credentials = decoder.userInfo[.credentials] as? Credentials else { throw DirectionsCodingError.missingCredentials } diff --git a/Sources/MapboxDirections/RouteResponse.swift b/Sources/MapboxDirections/RouteResponse.swift index 26ae801b4..b042ecd5d 100644 --- a/Sources/MapboxDirections/RouteResponse.swift +++ b/Sources/MapboxDirections/RouteResponse.swift @@ -16,7 +16,7 @@ public struct RouteResponse { public let waypoints: [Waypoint]? public let options: ResponseOptions - public let credentials: DirectionsCredentials + public let credentials: Credentials /** The time when this `RouteResponse` object was created, which is immediately upon recieving the raw URL response. @@ -38,7 +38,7 @@ extension RouteResponse: Codable { case waypoints } - public init(httpResponse: HTTPURLResponse?, identifier: String? = nil, routes: [Route]? = nil, waypoints: [Waypoint]? = nil, options: ResponseOptions, credentials: DirectionsCredentials) { + public init(httpResponse: HTTPURLResponse?, identifier: String? = nil, routes: [Route]? = nil, waypoints: [Waypoint]? = nil, options: ResponseOptions, credentials: Credentials) { self.httpResponse = httpResponse self.identifier = identifier self.routes = routes @@ -47,7 +47,7 @@ extension RouteResponse: Codable { self.credentials = credentials } - public init(matching response: MapMatchingResponse, options: MatchOptions, credentials: DirectionsCredentials) throws { + public init(matching response: MapMatchingResponse, options: MatchOptions, credentials: Credentials) throws { let decoder = JSONDecoder() let encoder = JSONEncoder() @@ -79,7 +79,7 @@ extension RouteResponse: Codable { self.httpResponse = decoder.userInfo[.httpResponse] as? HTTPURLResponse - guard let credentials = decoder.userInfo[.credentials] as? DirectionsCredentials else { + guard let credentials = decoder.userInfo[.credentials] as? Credentials else { throw DirectionsCodingError.missingCredentials } diff --git a/Sources/MapboxDirections/RouteStep.swift b/Sources/MapboxDirections/RouteStep.swift index 487d2c63e..82e1e0636 100644 --- a/Sources/MapboxDirections/RouteStep.swift +++ b/Sources/MapboxDirections/RouteStep.swift @@ -6,12 +6,12 @@ import Turf A `TransportType` specifies the mode of transportation used for part of a route. */ public enum TransportType: String, Codable { - // Possible transport types when the `profileIdentifier` is `DirectionsProfileIdentifier.automobile` or `DirectionsProfileIdentifier.automobileAvoidingTraffic` + // Possible transport types when the `profileIdentifier` is `ProfileIdentifier.automobile` or `ProfileIdentifier.automobileAvoidingTraffic` /** The route requires the user to drive or ride a car, truck, or motorcycle. - This is the usual transport type when the `profileIdentifier` is `DirectionsProfileIdentifier.automobile` or `DirectionsProfileIdentifier.automobileAvoidingTraffic`. + This is the usual transport type when the `profileIdentifier` is `ProfileIdentifier.automobile` or `ProfileIdentifier.automobileAvoidingTraffic`. */ case automobile = "driving" // automobile @@ -36,21 +36,21 @@ public enum TransportType: String, Codable { */ case inaccessible = "unaccessible" // automobile, walking, cycling - // Possible transport types when the `profileIdentifier` is `DirectionsProfileIdentifier.walking` + // Possible transport types when the `profileIdentifier` is `ProfileIdentifier.walking` /** The route requires the user to walk. - This is the usual transport type when the `profileIdentifier` is `DirectionsProfileIdentifier.walking`. For cycling directions, this value indicates that the user is expected to dismount. + This is the usual transport type when the `profileIdentifier` is `ProfileIdentifier.walking`. For cycling directions, this value indicates that the user is expected to dismount. */ case walking // walking, cycling - // Possible transport types when the `profileIdentifier` is `DirectionsProfileIdentifier.cycling` + // Possible transport types when the `profileIdentifier` is `ProfileIdentifier.cycling` /** The route requires the user to ride a bicycle. - This is the usual transport type when the `profileIdentifier` is `DirectionsProfileIdentifier.cycling`. + This is the usual transport type when the `profileIdentifier` is `ProfileIdentifier.cycling`. */ case cycling // cycling @@ -776,7 +776,7 @@ open class RouteStep: Codable { /** The step’s expected travel time, measured in seconds. - The value of this property reflects the time it takes to go from this step’s maneuver location to the next step’s maneuver location. If the route was calculated using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin this step. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the step makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. + The value of this property reflects the time it takes to go from this step’s maneuver location to the next step’s maneuver location. If the route was calculated using the `ProfileIdentifier.automobileAvoidingTraffic` profile, this property reflects current traffic conditions at the time of the request, not necessarily the traffic conditions at the time the user would begin this step. For other profiles, this property reflects travel time under ideal conditions and does not account for traffic congestion. If the step makes use of a ferry or train, the actual travel time may additionally be subject to the schedules of those services. Do not assume that the user would travel along the step at a fixed speed. For the expected travel time on each individual segment along the leg, specify the `AttributeOptions.expectedTravelTime` option and use the `RouteLeg.expectedSegmentTravelTimes` property. */ @@ -785,7 +785,7 @@ open class RouteStep: Codable { /** The step’s typical travel time, measured in seconds. - The value of this property reflects the typical time it takes to go from this step’s maneuver location to the next step’s maneuver location. This property is available when using the `DirectionsProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin this step. If the step makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. + The value of this property reflects the typical time it takes to go from this step’s maneuver location to the next step’s maneuver location. This property is available when using the `ProfileIdentifier.automobileAvoidingTraffic` profile. This property reflects typical traffic conditions at the time of the request, not necessarily the typical traffic conditions at the time the user would begin this step. If the step makes use of a ferry, the typical travel time may additionally be subject to the schedule of this service. Do not assume that the user would travel along the step at a fixed speed. */ diff --git a/Sources/MapboxDirectionsCLI/CodingOperation.swift b/Sources/MapboxDirectionsCLI/CodingOperation.swift index dc0e9e429..a0c2ef272 100644 --- a/Sources/MapboxDirectionsCLI/CodingOperation.swift +++ b/Sources/MapboxDirectionsCLI/CodingOperation.swift @@ -2,7 +2,7 @@ import Foundation import MapboxDirections -private let BogusCredentials = DirectionsCredentials(accessToken: "pk.feedCafeDadeDeadBeef-BadeBede.FadeCafeDadeDeed-BadeBede") +private let BogusCredentials = Credentials(accessToken: "pk.feedCafeDadeDeadBeef-BadeBede.FadeCafeDadeDeed-BadeBede") class CodingOperation { diff --git a/Tests/MapboxDirectionsTests/CredentialsTests.swift b/Tests/MapboxDirectionsTests/CredentialsTests.swift index d38ffd5e3..d363b41c9 100644 --- a/Tests/MapboxDirectionsTests/CredentialsTests.swift +++ b/Tests/MapboxDirectionsTests/CredentialsTests.swift @@ -5,9 +5,56 @@ class CredentialsTests: XCTestCase { func testCredentialsCreation() { let testURL = URL(string: "https://example.com")! - let subject = DirectionsCredentials(accessToken: "test", host: testURL) + let subject = Credentials(accessToken: "test", host: testURL) XCTAssertEqual(subject.accessToken, "test") XCTAssertEqual(subject.host, testURL) } + + func testDefaultConfiguration() { + let credentials = Credentials(accessToken: BogusToken) + XCTAssertEqual(credentials.accessToken, BogusToken) + XCTAssertEqual(credentials.host.absoluteString, "https://api.mapbox.com") + } + + func testCustomConfiguration() { + let token = "deadbeefcafebebe" + let host = URL(string: "https://example.com")! + let credentials = Credentials(accessToken: token, host: host) + XCTAssertEqual(credentials.accessToken, token) + XCTAssertEqual(credentials.host, host) + } + + func testAccessTokenInjection() { + let expected = "injected" + UserDefaults.standard.set(expected, forKey: "MBXAccessToken") + XCTAssertEqual(Directions.shared.credentials.accessToken, expected) + } + +#if !os(Linux) + func testSkuToken() { + let expectedToken = "a token" + MBXAccounts.serviceSkuToken = expectedToken + MBXAccounts.serviceAccessToken = Directions.shared.credentials.accessToken + XCTAssertEqual(Directions.shared.credentials.skuToken, expectedToken) + MBXAccounts.serviceSkuToken = nil + MBXAccounts.serviceAccessToken = nil + } + + func testSkuTokenWithMismatchedAccessToken() { + MBXAccounts.serviceSkuToken = "a token" + MBXAccounts.serviceAccessToken = UUID().uuidString + XCTAssertEqual(Directions.shared.credentials.skuToken, nil) + MBXAccounts.serviceSkuToken = nil + MBXAccounts.serviceAccessToken = nil + } +#endif +} + +#if !os(Linux) +@objc(MBXAccounts) +final class MBXAccounts: NSObject { + @objc static var serviceSkuToken: String? + @objc static var serviceAccessToken: String? } +#endif diff --git a/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift b/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift deleted file mode 100644 index f82550592..000000000 --- a/Tests/MapboxDirectionsTests/DirectionsCredentialsTests.swift +++ /dev/null @@ -1,51 +0,0 @@ -import XCTest -@testable import MapboxDirections - -class DirectionsCredentialsTests: XCTestCase { - func testDefaultConfiguration() { - let credentials = DirectionsCredentials(accessToken: BogusToken) - XCTAssertEqual(credentials.accessToken, BogusToken) - XCTAssertEqual(credentials.host.absoluteString, "https://api.mapbox.com") - } - - func testCustomConfiguration() { - let token = "deadbeefcafebebe" - let host = URL(string: "https://example.com")! - let credentials = DirectionsCredentials(accessToken: token, host: host) - XCTAssertEqual(credentials.accessToken, token) - XCTAssertEqual(credentials.host, host) - } - - func testAccessTokenInjection() { - let expected = "injected" - UserDefaults.standard.set(expected, forKey: "MBXAccessToken") - XCTAssertEqual(Directions.shared.credentials.accessToken, expected) - } - -#if !os(Linux) - func testSkuToken() { - let expectedToken = "a token" - MBXAccounts.serviceSkuToken = expectedToken - MBXAccounts.serviceAccessToken = Directions.shared.credentials.accessToken - XCTAssertEqual(Directions.shared.credentials.skuToken, expectedToken) - MBXAccounts.serviceSkuToken = nil - MBXAccounts.serviceAccessToken = nil - } - - func testSkuTokenWithMismatchedAccessToken() { - MBXAccounts.serviceSkuToken = "a token" - MBXAccounts.serviceAccessToken = UUID().uuidString - XCTAssertEqual(Directions.shared.credentials.skuToken, nil) - MBXAccounts.serviceSkuToken = nil - MBXAccounts.serviceAccessToken = nil - } -#endif -} - -#if !os(Linux) -@objc(MBXAccounts) -final class MBXAccounts: NSObject { - @objc static var serviceSkuToken: String? - @objc static var serviceAccessToken: String? -} -#endif diff --git a/Tests/MapboxDirectionsTests/DirectionsTests.swift b/Tests/MapboxDirectionsTests/DirectionsTests.swift index 81e350af4..fc3865983 100644 --- a/Tests/MapboxDirectionsTests/DirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/DirectionsTests.swift @@ -12,7 +12,7 @@ import Turf @testable import MapboxDirections let BogusToken = "pk.feedCafeDadeDeadBeef-BadeBede.FadeCafeDadeDeed-BadeBede" -let BogusCredentials = DirectionsCredentials(accessToken: BogusToken) +let BogusCredentials = Credentials(accessToken: BogusToken) let BadResponse = """ diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift index 29cdbdf26..5c46f1ab6 100644 --- a/Tests/MapboxDirectionsTests/IsochroneTests.swift +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -9,7 +9,7 @@ import OHHTTPStubsSwift import Turf import XCTest -let IsochroneBogusCredentials = IsochroneCredentials(accessToken: BogusToken) +let IsochroneBogusCredentials = Credentials(accessToken: BogusToken) let minimalValidResponse = """ { @@ -65,8 +65,8 @@ class IsochroneTests: XCTestCase { XCTAssertEqual(queryItems.count, 6) XCTAssertTrue(components.path.contains(location.requestDescription) ) XCTAssertTrue(queryItems.contains(where: { $0.name == "access_token" && $0.value == BogusToken })) - XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_meters" && $0.value == "100;200"})) - XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_colors" && $0.value == "19334C;667F99"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_meters" && $0.value == "100,200"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_colors" && $0.value == "19334C,667F99"})) XCTAssertTrue(queryItems.contains(where: { $0.name == "polygons" && $0.value == "true"})) XCTAssertTrue(queryItems.contains(where: { $0.name == "denoise" && $0.value == "0.5"})) XCTAssertTrue(queryItems.contains(where: { $0.name == "generalize" && $0.value == "13.0"})) @@ -86,7 +86,7 @@ class IsochroneTests: XCTestCase { XCTFail("Invalid url"); return } - XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_minutes" && $0.value == "1;2"})) + XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_minutes" && $0.value == "1,2"})) } #if !os(Linux) diff --git a/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift b/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift index a17efdd1c..0d88fc527 100644 --- a/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift +++ b/Tests/MapboxDirectionsTests/OfflineDirectionsTests.swift @@ -13,7 +13,7 @@ class OfflineDirectionsTests: XCTestCase { let hostURL = URL(string: "https://api.mapbox.com")! func testAvailableVersions() { - let credentials = DirectionsCredentials(accessToken: token, host: hostURL) + let credentials = Credentials(accessToken: token, host: hostURL) let directions = Directions(credentials: credentials) diff --git a/Tests/MapboxDirectionsTests/RouteOptionsTests.swift b/Tests/MapboxDirectionsTests/RouteOptionsTests.swift index a9f2b2f18..25f11a51d 100644 --- a/Tests/MapboxDirectionsTests/RouteOptionsTests.swift +++ b/Tests/MapboxDirectionsTests/RouteOptionsTests.swift @@ -140,7 +140,7 @@ class RouteOptionsTests: XCTestCase { let subject = RouteOptions(waypoints: waypoints) let decoder = JSONDecoder() decoder.userInfo[.options] = subject - decoder.userInfo[.credentials] = DirectionsCredentials(accessToken: "foo", host: URL(string: "https://test.website")!) + decoder.userInfo[.credentials] = Credentials(accessToken: "foo", host: URL(string: "https://test.website")!) var response: RouteResponse? XCTAssertNoThrow(response = try decoder.decode(RouteResponse.self, from: fixtureData)) XCTAssertNotNil(response) diff --git a/Tests/MapboxDirectionsTests/RouteResponseTests.swift b/Tests/MapboxDirectionsTests/RouteResponseTests.swift index cb31b40b9..e5afa1ca4 100644 --- a/Tests/MapboxDirectionsTests/RouteResponseTests.swift +++ b/Tests/MapboxDirectionsTests/RouteResponseTests.swift @@ -19,7 +19,7 @@ class RouteResponseTests: XCTestCase { let responseOptions = ResponseOptions.route(routeOptions) let accessToken = "deadbeefcafebebe" let host = URL(string: "https://example.com")! - let directionsCredentials = DirectionsCredentials(accessToken: accessToken, host: host) + let directionsCredentials = Credentials(accessToken: accessToken, host: host) let routeResponse = RouteResponse(httpResponse: nil, waypoints: waypoints, diff --git a/Tests/MapboxDirectionsTests/RouteStepTests.swift b/Tests/MapboxDirectionsTests/RouteStepTests.swift index 830679385..c996fdc7d 100644 --- a/Tests/MapboxDirectionsTests/RouteStepTests.swift +++ b/Tests/MapboxDirectionsTests/RouteStepTests.swift @@ -315,7 +315,7 @@ class RouteStepTests: XCTestCase { let decoder = JSONDecoder() decoder.userInfo[.options] = options - decoder.userInfo[.credentials] = DirectionsCredentials(accessToken: "foo", host: URL(string: "http://sample.website")) + decoder.userInfo[.credentials] = Credentials(accessToken: "foo", host: URL(string: "http://sample.website")) let result = try! decoder.decode(RouteResponse.self, from: data) let routes = result.routes diff --git a/Tests/MapboxDirectionsTests/V5Tests.swift b/Tests/MapboxDirectionsTests/V5Tests.swift index d3258dd25..e38072d30 100644 --- a/Tests/MapboxDirectionsTests/V5Tests.swift +++ b/Tests/MapboxDirectionsTests/V5Tests.swift @@ -292,7 +292,7 @@ class V5Tests: XCTestCase { let decoder = JSONDecoder() decoder.userInfo[.options] = options - decoder.userInfo[.credentials] = DirectionsCredentials(accessToken: "foo", host: URL(string: "http://sample.website")) + decoder.userInfo[.credentials] = Credentials(accessToken: "foo", host: URL(string: "http://sample.website")) let result = try! decoder.decode(RouteResponse.self, from: data) let routes = result.routes diff --git a/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift b/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift index 15fbc58f9..1705b35a9 100644 --- a/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift +++ b/Tests/MapboxDirectionsTests/WalkingOptionsTests.swift @@ -10,7 +10,7 @@ class WalkingOptionsTests: XCTestCase { Waypoint(coordinate: LocationCoordinate2D(latitude: 2, longitude: 3)) ] - let options = RouteOptions(waypoints: waypoints, profileIdentifier: DirectionsProfileIdentifier.walking) + let options = RouteOptions(waypoints: waypoints, profileIdentifier: ProfileIdentifier.walking) var queryItems = options.urlQueryItems XCTAssertNil(queryItems.first { $0.name == "alley_bias" }?.value) XCTAssertNil(queryItems.first { $0.name == "walkway_bias" }?.value) From 630bf2e3af19f17391479b6c823bcc2b31d7e236 Mon Sep 17 00:00:00 2001 From: udumft Date: Thu, 28 Oct 2021 16:53:42 +0300 Subject: [PATCH 14/19] vk-296-isochrone-api: respored xcodeproj file references --- MapboxDirections.xcodeproj/project.pbxproj | 66 +++++++++++++++++++--- README.md | 2 +- 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 2c158ea75..32b018c1c 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -33,6 +33,29 @@ 2B540809245B23BE006C820B /* incorrectRouteRefreshResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B540808245B23BE006C820B /* incorrectRouteRefreshResponse.json */; }; 2B54080A245B23BE006C820B /* incorrectRouteRefreshResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B540808245B23BE006C820B /* incorrectRouteRefreshResponse.json */; }; 2B54080B245B23BE006C820B /* incorrectRouteRefreshResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 2B540808245B23BE006C820B /* incorrectRouteRefreshResponse.json */; }; + 2B9F3881272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; + 2B9F3882272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; + 2B9F3883272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; + 2B9F3884272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */; }; + 2B9F3885272AE23A001DBA12 /* IsochroneOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */; }; + 2B9F3886272AE23A001DBA12 /* IsochroneOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */; }; + 2B9F3887272AE23A001DBA12 /* IsochroneOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */; }; + 2B9F3888272AE23A001DBA12 /* IsochroneOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */; }; + 2B9F3889272AE23A001DBA12 /* IsochroneError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */; }; + 2B9F388A272AE23A001DBA12 /* IsochroneError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */; }; + 2B9F388B272AE23A001DBA12 /* IsochroneError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */; }; + 2B9F388C272AE23A001DBA12 /* IsochroneError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */; }; + 2B9F388D272AE23A001DBA12 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387F272AE23A001DBA12 /* Credentials.swift */; }; + 2B9F388E272AE23A001DBA12 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387F272AE23A001DBA12 /* Credentials.swift */; }; + 2B9F388F272AE23A001DBA12 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387F272AE23A001DBA12 /* Credentials.swift */; }; + 2B9F3890272AE23A001DBA12 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F387F272AE23A001DBA12 /* Credentials.swift */; }; + 2B9F3891272AE23A001DBA12 /* Isochrones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3880272AE23A001DBA12 /* Isochrones.swift */; }; + 2B9F3892272AE23A001DBA12 /* Isochrones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3880272AE23A001DBA12 /* Isochrones.swift */; }; + 2B9F3893272AE23A001DBA12 /* Isochrones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3880272AE23A001DBA12 /* Isochrones.swift */; }; + 2B9F3894272AE23A001DBA12 /* Isochrones.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3880272AE23A001DBA12 /* Isochrones.swift */; }; + 2B9F389A272AE28B001DBA12 /* IsochroneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3895272AE277001DBA12 /* IsochroneTests.swift */; }; + 2B9F389B272AE28D001DBA12 /* IsochroneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3895272AE277001DBA12 /* IsochroneTests.swift */; }; + 2B9F389C272AE28E001DBA12 /* IsochroneTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B9F3895272AE277001DBA12 /* IsochroneTests.swift */; }; 2BA2E746257A667500D7AFC6 /* incidents.json in Resources */ = {isa = PBXBuildFile; fileRef = 2BA2E745257A667500D7AFC6 /* incidents.json */; }; 2BA2E747257A667500D7AFC6 /* incidents.json in Resources */ = {isa = PBXBuildFile; fileRef = 2BA2E745257A667500D7AFC6 /* incidents.json */; }; 2BA2E748257A667500D7AFC6 /* incidents.json in Resources */ = {isa = PBXBuildFile; fileRef = 2BA2E745257A667500D7AFC6 /* incidents.json */; }; @@ -92,9 +115,6 @@ 43538E3923ED463100E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; 43538E3A23ED463200E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; 43538E3B23ED463400E010D4 /* ResponseDisposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */; }; - 43538E3D23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; - 43538E3E23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; - 43538E3F23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */; }; 4376A52723FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; 4376A52823FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; 4376A52923FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; @@ -456,6 +476,12 @@ 2B5407FF245B097D006C820B /* routeRefreshResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = routeRefreshResponse.json; sourceTree = ""; }; 2B540804245B09E1006C820B /* routeRefreshRoute.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = routeRefreshRoute.json; sourceTree = ""; }; 2B540808245B23BE006C820B /* incorrectRouteRefreshResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = incorrectRouteRefreshResponse.json; sourceTree = ""; }; + 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileIdentifier.swift; sourceTree = ""; }; + 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IsochroneOptions.swift; sourceTree = ""; }; + 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IsochroneError.swift; sourceTree = ""; }; + 2B9F387F272AE23A001DBA12 /* Credentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; + 2B9F3880272AE23A001DBA12 /* Isochrones.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Isochrones.swift; sourceTree = ""; }; + 2B9F3895272AE277001DBA12 /* IsochroneTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IsochroneTests.swift; sourceTree = ""; }; 2BA2E745257A667500D7AFC6 /* incidents.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = incidents.json; sourceTree = ""; }; 2BA98970253F007600B643F6 /* mapbox-directions-swift */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = "mapbox-directions-swift"; sourceTree = BUILT_PRODUCTS_DIR; }; 2BBBD05D257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapboxStreetsRoadClass.swift; sourceTree = ""; }; @@ -474,7 +500,6 @@ 43208BAA2343F81900D8BD89 /* GeoJSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeoJSON.swift; sourceTree = ""; }; 43208BAC2343FF5500D8BD89 /* RouteResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteResponse.swift; sourceTree = ""; }; 43538E3623ED3B1600E010D4 /* ResponseDisposition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResponseDisposition.swift; sourceTree = ""; }; - 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsCredentialsTests.swift; sourceTree = ""; }; 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchOptionsTests.swift; sourceTree = ""; }; 438BFEBC233D7FA900457294 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 438BFEC0233D805500457294 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; @@ -771,6 +796,7 @@ C5DAAC9E20191683001F9261 /* MapMatching */, C51538CB1E807FF00093FF3E /* AttributeOptions.swift */, C58EA7A91E9D7EAD008F98CE /* Congestion.swift */, + 2B9F387F272AE23A001DBA12 /* Credentials.swift */, DD6254731AE70CB700017857 /* Directions.swift */, 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */, 4392557523440EC2006EEE88 /* DirectionsError.swift */, @@ -780,10 +806,14 @@ 431E93BE234664A200A71B44 /* DrivingSide.swift */, DA6C9D8C1CAE442B00094FBC /* Info.plist */, C57D55001DB5669600B94B74 /* Intersection.swift */, + 2B9F387E272AE23A001DBA12 /* IsochroneError.swift */, + 2B9F387D272AE23A001DBA12 /* IsochroneOptions.swift */, + 2B9F3880272AE23A001DBA12 /* Isochrones.swift */, C57D55071DB58C0200B94B74 /* Lane.swift */, DAA76D671DD127CB0015EC78 /* LaneIndication.swift */, DA6C9D8A1CAE442B00094FBC /* MapboxDirections.h */, 35828C9D217A003F00ED546E /* OfflineDirections.swift */, + 2B9F387C272AE23A001DBA12 /* ProfileIdentifier.swift */, DAD06E3823A008EB001A917D /* QuickLook.swift */, C59426061F1EA6C400C8E59C /* RoadClasses.swift */, 2BBBD05D257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift */, @@ -819,11 +849,11 @@ 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */, DAD06E34239F0B19001A917D /* DirectionsErrorTests.swift */, DA1A110A1D01045E009F82FA /* DirectionsTests.swift */, - 43538E3C23ED6A2000E010D4 /* DirectionsCredentialsTests.swift */, DA6C9DB11CAECA0E00094FBC /* Fixture.swift */, DAABF7912395AE9800CEEB61 /* GeoJSONTests.swift */, DA6C9D9A1CAE442B00094FBC /* Info.plist */, DAE33A1A1F215DF600C06039 /* IntersectionTests.swift */, + 2B9F3895272AE277001DBA12 /* IsochroneTests.swift */, DABE6C7D236A37E200D370F4 /* JSONSerialization.swift */, 3556CE9922649CF2009397B5 /* MapboxDirectionsTests-Bridging-Header.h */, C5DAACAE201AA92B001F9261 /* MatchTests.swift */, @@ -1356,6 +1386,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2B9F3892272AE23A001DBA12 /* Isochrones.swift in Sources */, 43538E3923ED463100E010D4 /* ResponseDisposition.swift in Sources */, 431E93C0234664A200A71B44 /* DrivingSide.swift in Sources */, C5DAAC9B2019167C001F9261 /* Match.swift in Sources */, @@ -1368,6 +1399,7 @@ 35828C9F217A003F00ED546E /* OfflineDirections.swift in Sources */, DAE7EA95230B5FD10003B211 /* Measurement.swift in Sources */, C5DAAC9F20195AAE001F9261 /* Tracepoint.swift in Sources */, + 2B9F3882272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */, 431E93CC23466C2500A71B44 /* RouteResponse.swift in Sources */, 431E93C423466B0F00A71B44 /* GeoJSON.swift in Sources */, C5990B4A2045E72800D7DFD4 /* DirectionsResult.swift in Sources */, @@ -1385,6 +1417,7 @@ C5990B4D2045E74800D7DFD4 /* DirectionsOptions.swift in Sources */, 43EBD3AE23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D691DD127CB0015EC78 /* LaneIndication.swift in Sources */, + 2B9F388E272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F942350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A10CB1D00F969009F82FA /* RouteStep.swift in Sources */, C57D55031DB566A700B94B74 /* Intersection.swift in Sources */, @@ -1400,8 +1433,10 @@ F4F508392524D6F10044F2D0 /* RestStop.swift in Sources */, C54549FC2073F1EF002E273F /* Array.swift in Sources */, C547EC691DB59F8F009817F3 /* Lane.swift in Sources */, + 2B9F388A272AE23A001DBA12 /* IsochroneError.swift in Sources */, 431E93C823466B4000A71B44 /* CoreLocation.swift in Sources */, AEDC212120B6125C0052DED8 /* VisualInstructionComponent.swift in Sources */, + 2B9F3886272AE23A001DBA12 /* IsochroneOptions.swift in Sources */, 439255792344113D006EEE88 /* DirectionsError.swift in Sources */, DA1A10C71D00F969009F82FA /* Directions.swift in Sources */, ); @@ -1419,6 +1454,7 @@ C53A02291E92C27A009837BD /* AnnotationTests.swift in Sources */, 4376A52823FB13D400C6038D /* MatchOptionsTests.swift in Sources */, DA688B3F21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, + 2B9F389B272AE28D001DBA12 /* IsochroneTests.swift in Sources */, DAD06E36239F0B19001A917D /* DirectionsErrorTests.swift in Sources */, C596663A2048AECD00C45CE5 /* RoutableMatchTests.swift in Sources */, DA8F3A7323B56D3B00B56786 /* RouteLegTests.swift in Sources */, @@ -1435,7 +1471,6 @@ 35CC310C2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, DABE6C7F236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7932395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, - 43538E3E23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */, F4D785F01DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1444,6 +1479,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2B9F3893272AE23A001DBA12 /* Isochrones.swift in Sources */, 43538E3A23ED463200E010D4 /* ResponseDisposition.swift in Sources */, 431E93C1234664A200A71B44 /* DrivingSide.swift in Sources */, C5DAAC9C2019167D001F9261 /* Match.swift in Sources */, @@ -1456,6 +1492,7 @@ 35828CA0217A003F00ED546E /* OfflineDirections.swift in Sources */, DAE7EA96230B5FD10003B211 /* Measurement.swift in Sources */, C5DAACA020195AAF001F9261 /* Tracepoint.swift in Sources */, + 2B9F3883272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */, 431E93CD23466C2700A71B44 /* RouteResponse.swift in Sources */, 431E93C523466B1000A71B44 /* GeoJSON.swift in Sources */, C5990B4B2045E72900D7DFD4 /* DirectionsResult.swift in Sources */, @@ -1473,6 +1510,7 @@ C5990B4E2045E74900D7DFD4 /* DirectionsOptions.swift in Sources */, 43EBD3AF23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D6A1DD127CB0015EC78 /* LaneIndication.swift in Sources */, + 2B9F388F272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F952350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A10F11D010247009F82FA /* RouteStep.swift in Sources */, C57D55041DB566A800B94B74 /* Intersection.swift in Sources */, @@ -1488,8 +1526,10 @@ F4F5083A2524D6F10044F2D0 /* RestStop.swift in Sources */, C54549FD2073F1F0002E273F /* Array.swift in Sources */, C547EC6A1DB59F90009817F3 /* Lane.swift in Sources */, + 2B9F388B272AE23A001DBA12 /* IsochroneError.swift in Sources */, 431E93C923466B4100A71B44 /* CoreLocation.swift in Sources */, AEDC212220B6125D0052DED8 /* VisualInstructionComponent.swift in Sources */, + 2B9F3887272AE23A001DBA12 /* IsochroneOptions.swift in Sources */, 4392557A2344113E006EEE88 /* DirectionsError.swift in Sources */, DA1A10ED1D010247009F82FA /* Directions.swift in Sources */, ); @@ -1507,6 +1547,7 @@ C53A022A1E92C27B009837BD /* AnnotationTests.swift in Sources */, 4376A52923FB13D400C6038D /* MatchOptionsTests.swift in Sources */, DA688B4021B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, + 2B9F389C272AE28E001DBA12 /* IsochroneTests.swift in Sources */, DAD06E37239F0B19001A917D /* DirectionsErrorTests.swift in Sources */, C596663B2048AECE00C45CE5 /* RoutableMatchTests.swift in Sources */, DA8F3A7423B56D3B00B56786 /* RouteLegTests.swift in Sources */, @@ -1523,7 +1564,6 @@ 35CC310D2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, DABE6C80236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7942395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, - 43538E3F23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */, F4D785F11DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1532,6 +1572,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2B9F3894272AE23A001DBA12 /* Isochrones.swift in Sources */, 43538E3B23ED463400E010D4 /* ResponseDisposition.swift in Sources */, 431E93C2234664A200A71B44 /* DrivingSide.swift in Sources */, C5DAAC9D2019167E001F9261 /* Match.swift in Sources */, @@ -1544,6 +1585,7 @@ 35828CA1217A003F00ED546E /* OfflineDirections.swift in Sources */, DAE7EA97230B5FD10003B211 /* Measurement.swift in Sources */, C5DAACA120195AAF001F9261 /* Tracepoint.swift in Sources */, + 2B9F3884272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */, 431E93CE23466C2800A71B44 /* RouteResponse.swift in Sources */, 431E93C623466B1100A71B44 /* GeoJSON.swift in Sources */, C5990B4C2045E72A00D7DFD4 /* DirectionsResult.swift in Sources */, @@ -1561,6 +1603,7 @@ C5990B4F2045E74A00D7DFD4 /* DirectionsOptions.swift in Sources */, 43EBD3B023DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D6B1DD127CB0015EC78 /* LaneIndication.swift in Sources */, + 2B9F3890272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F962350F952007B591E /* MapMatchingResponse.swift in Sources */, DA1A11081D0103A3009F82FA /* RouteStep.swift in Sources */, C57D55051DB566A900B94B74 /* Intersection.swift in Sources */, @@ -1576,8 +1619,10 @@ F4F5083B2524D6F10044F2D0 /* RestStop.swift in Sources */, C54549FE2073F1F1002E273F /* Array.swift in Sources */, C547EC6B1DB59F91009817F3 /* Lane.swift in Sources */, + 2B9F388C272AE23A001DBA12 /* IsochroneError.swift in Sources */, 431E93CA23466B4200A71B44 /* CoreLocation.swift in Sources */, AEDC212320B6125E0052DED8 /* VisualInstructionComponent.swift in Sources */, + 2B9F3888272AE23A001DBA12 /* IsochroneOptions.swift in Sources */, 4392557B2344113F006EEE88 /* DirectionsError.swift in Sources */, DA1A11041D0103A3009F82FA /* Directions.swift in Sources */, ); @@ -1587,6 +1632,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 2B9F3891272AE23A001DBA12 /* Isochrones.swift in Sources */, 43538E3823ED463100E010D4 /* ResponseDisposition.swift in Sources */, 431E93BF234664A200A71B44 /* DrivingSide.swift in Sources */, C51538CC1E807FF00093FF3E /* AttributeOptions.swift in Sources */, @@ -1599,6 +1645,7 @@ 35828C9E217A003F00ED546E /* OfflineDirections.swift in Sources */, DAE7EA94230B5FD10003B211 /* Measurement.swift in Sources */, C59426071F1EA6C400C8E59C /* RoadClasses.swift in Sources */, + 2B9F3881272AE23A001DBA12 /* ProfileIdentifier.swift in Sources */, 431E93CB23466C2400A71B44 /* RouteResponse.swift in Sources */, 431E93C323466B0E00A71B44 /* GeoJSON.swift in Sources */, 35EFD00B207DFACA00BF3873 /* VisualInstruction.swift in Sources */, @@ -1616,6 +1663,7 @@ C59094C1203DE6BC00EB2417 /* DirectionsResult.swift in Sources */, 43EBD3AD23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAC05F1A1CFC077C00FA0071 /* RouteLeg.swift in Sources */, + 2B9F388D272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F932350F952007B591E /* MapMatchingResponse.swift in Sources */, C5434B8A200693D00069E887 /* Tracepoint.swift in Sources */, DA6C9DA61CAE462800094FBC /* Directions.swift in Sources */, @@ -1631,8 +1679,10 @@ F4F508382524D6F10044F2D0 /* RestStop.swift in Sources */, 438BFEC2233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */, C57D55011DB5669600B94B74 /* Intersection.swift in Sources */, + 2B9F3889272AE23A001DBA12 /* IsochroneError.swift in Sources */, 431E93C723466B3F00A71B44 /* CoreLocation.swift in Sources */, AEDC211D20B6104B0052DED8 /* VisualInstructionComponent.swift in Sources */, + 2B9F3885272AE23A001DBA12 /* IsochroneOptions.swift in Sources */, 439255772344113B006EEE88 /* DirectionsError.swift in Sources */, DA2E03E91CB0E0B000D1269A /* RouteStep.swift in Sources */, ); @@ -1650,6 +1700,7 @@ C5247D711E818A24004B6154 /* AnnotationTests.swift in Sources */, 4376A52723FB13D400C6038D /* MatchOptionsTests.swift in Sources */, DA688B3E21B89ECD00C9BB25 /* VisualInstructionComponentTests.swift in Sources */, + 2B9F389A272AE28B001DBA12 /* IsochroneTests.swift in Sources */, DAD06E35239F0B19001A917D /* DirectionsErrorTests.swift in Sources */, C59666392048A20E00C45CE5 /* RoutableMatchTests.swift in Sources */, DA8F3A7223B56D3B00B56786 /* RouteLegTests.swift in Sources */, @@ -1667,7 +1718,6 @@ 35CC310B2285739700EA1966 /* WalkingOptionsTests.swift in Sources */, DABE6C7E236A37E200D370F4 /* JSONSerialization.swift in Sources */, DAABF7922395AE9800CEEB61 /* GeoJSONTests.swift in Sources */, - 43538E3D23ED6A2000E010D4 /* DirectionsCredentialsTests.swift in Sources */, F4D785EF1DDD82C100FF4665 /* RouteStepTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/README.md b/README.md index d7254330f..6f40e157a 100644 --- a/README.md +++ b/README.md @@ -262,7 +262,7 @@ isochrones.calculate(IsochroneOptions(centerCoordinate: centerCoordinate, options: options, accessToken: accessToken) - // display the result! + // Display the result! drawImage(snapshot.image) } } From 4548f2c25700de6bdd5da6c83de82b8de6fbda30 Mon Sep 17 00:00:00 2001 From: udumft Date: Thu, 28 Oct 2021 17:01:52 +0300 Subject: [PATCH 15/19] vk-296-isochrone-api: test compilation issue fixed --- Tests/MapboxDirectionsTests/IsochroneTests.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift index 5c46f1ab6..6018f23ad 100644 --- a/Tests/MapboxDirectionsTests/IsochroneTests.swift +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -81,12 +81,12 @@ class IsochroneTests: XCTestCase { url = isochrones.url(forCalculating: options) - guard let components = URLComponents(string: url.absoluteString), - let queryItems = components.queryItems else { + guard let componentsByTravelTime = URLComponents(string: url.absoluteString), + let queryItemsByTravelTime = componentsByTravelTime.queryItems else { XCTFail("Invalid url"); return } - XCTAssertTrue(queryItems.contains(where: { $0.name == "contours_minutes" && $0.value == "1,2"})) + XCTAssertTrue(queryItemsByTravelTime.contains(where: { $0.name == "contours_minutes" && $0.value == "1,2"})) } #if !os(Linux) From f979052bcad3b1ac852d8aa53cc8338d7311c0dd Mon Sep 17 00:00:00 2001 From: udumft Date: Fri, 29 Oct 2021 14:58:54 +0300 Subject: [PATCH 16/19] vk-296-isochrone-api: replaced ContourDefinition enum with Definition struct. Tests and examples updated --- README.md | 14 +-- .../MapboxDirections/IsochroneOptions.swift | 86 ++++++++++++------- .../IsochroneTests.swift | 33 +++---- 3 files changed, 79 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 6f40e157a..ec64801f4 100644 --- a/README.md +++ b/README.md @@ -178,16 +178,18 @@ let task = directions.calculate(options) { (session, result) in You can also use the `Directions.calculateRoutes(matching:completionHandler:)` method to get Route objects suitable for use anywhere a standard Directions API response would be used. -### Isochrone API +### Build an isochrone map -`Isochrones` API uses the same access token initialization as `Directions`. Once that is configured, you need to fill `IsochronesOptions` parameters to calculate the desired geoJSON: +Tell the user how far they can travel within certain distances or times of a given location using the Isochrone API. `Isochrones` uses the same access token initialization as `Directions`. Once that is configured, you need to fill `IsochronesOptions` parameters to calculate the desired GeoJSON: ```swift let isochrones = Isochrones(credentials: Credentials(accessToken: "<#your access token#>")) let isochroneOptions = IsochroneOptions(centerCoordinate: CLLocationCoordinate2D(latitude: 45.52, longitude: -122.681944), - contours: .byDistances(.colored([(.init(value: 500, unit: .meters), .orange), - (.init(value: 1, unit: .kilometers), .red)]))) + contours: .byDistances([ + .init(value: 500, unit: .meters, color: .orange), + .init(value: 1, unit: .kilometers, color: .red) + ])) isochrones.calculate(isochroneOptions) { session, result in if case .success(let response) = result { @@ -225,7 +227,7 @@ The [Mapbox Navigation SDK for iOS](https://github.com/mapbox/mapbox-navigation- ### Drawing Isochrones contours on a map snapshot -[MapboxStatic.swift](https://github.com/mapbox/MapboxStatic.swift) provides the easies way to draw an Isochrone contour on a map. +[MapboxStatic.swift](https://github.com/mapbox/MapboxStatic.swift) provides an easy way to draw a isochrone contours on a map. ```swift // main.swift @@ -247,7 +249,7 @@ let options = SnapshotOptions( // Request Isochrone contour to draw on a map let isochrones = Isochrones(credentials: Credentials(accessToken: accessToken)) isochrones.calculate(IsochroneOptions(centerCoordinate: centerCoordinate, - contours: .byDistances(.default([.init(value: 500, unit: .meters)])))) { session, result in + contours: .byDistances([.init(value: 500, unit: .meters)]))) { session, result in if case .success(let response) = result { // Serialize the geoJSON let encoder = JSONEncoder() diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift index 1f947e13f..8a5fb4ecd 100644 --- a/Sources/MapboxDirections/IsochroneOptions.swift +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -96,18 +96,24 @@ public class IsochroneOptions { var queryItems: [URLQueryItem] = [] switch contours { - case .byDistances(let definition): - let (values, colors) = definition.serialize(roundingTo: .meters) + case .byDistances(let definitions): + let fallbackColor = definitions.allSatisfy { $0.color != nil } ? nil : Color.fallbackColor - queryItems.append(URLQueryItem(name: "contours_meters", value: values)) - if let colors = colors { + queryItems.append(URLQueryItem(name: "contours_meters", + value: definitions.map { $0.queryValueDescription(roundingTo: .meters) }.joined(separator: ","))) + + let colors = definitions.compactMap { $0.queryColorDescription(fallbackColor: fallbackColor) }.joined(separator: ",") + if !colors.isEmpty { queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) } - case .byExpectedTravelTimes(let definition): - let (values, colors) = definition.serialize(roundingTo: .minutes) + case .byExpectedTravelTimes(let definitions): + let fallbackColor = definitions.allSatisfy { $0.color != nil } ? nil : Color.fallbackColor + + queryItems.append(URLQueryItem(name: "contours_minutes", + value: definitions.map { $0.queryValueDescription(roundingTo: .minutes) }.joined(separator: ","))) - queryItems.append(URLQueryItem(name: "contours_minutes", value: values)) - if let colors = colors { + let colors = definitions.compactMap { $0.queryColorDescription(fallbackColor: fallbackColor) }.joined(separator: ",") + if !colors.isEmpty { queryItems.append(URLQueryItem(name: "contours_colors", value: colors)) } } @@ -138,39 +144,43 @@ extension IsochroneOptions { /** Describes Individual contour bound and color. */ - public enum ContourDefinition { + public struct Definition { /** - Contour bound definition value. + Bound measurement value. */ - public typealias Value = Measurement + public var value: Measurement /** - Contour bound definition value and contour color. + Contour fill color. + + If `nil` - default rainbow color sheme will be used for each contour. + - important: `color` value should be specified for everyone or none requested contours. Otherwise all missing colors will be grayed. */ - public typealias ValueAndColor = (value: Value, color: Color) + public var color: Color? /** - Allows configuring just the bound, leaving coloring to a default rainbow scheme. + Initializes new contour Definition. */ - case `default`([Value]) + public init(value: Measurement, color: Color? = nil) { + self.value = value + self.color = color + } + /** - Allows configuring both the bound and contour color. + Initializes new contour Definition. + + Convenience initializer for encapsulating `Measurement` initialization. */ - case colored([ValueAndColor]) + public init(value: Double, unit: Unt, color: Color? = nil) { + self.init(value: Measurement(value: value, unit: unit), + color: color) + } + + func queryValueDescription(roundingTo unit: Unt) -> String { + return String(Int(value.converted(to: unit).value.rounded())) + } - func serialize(roundingTo unit: Unt) -> (String, String?) { - switch (self) { - case .default(let intervals): - - return (intervals.map { String(Int($0.converted(to: unit).value.rounded())) }.joined(separator: ","), nil) - case .colored(let intervals): - let sorted = intervals.sorted { lhs, rhs in - lhs.value < rhs.value - } - - let values = sorted.map { String(Int($0.value.converted(to: unit).value.rounded())) }.joined(separator: ",") - let colors = sorted.map(\.color.queryDescription).joined(separator: ",") - return (values, colors) - } + func queryColorDescription(fallbackColor: Color?) -> String? { + return (color ?? fallbackColor)?.queryDescription } } @@ -179,14 +189,14 @@ extension IsochroneOptions { This value will be rounded to minutes. */ - case byExpectedTravelTimes(ContourDefinition) + case byExpectedTravelTimes([Definition]) /** The distances to use for each isochrone contour. Will be rounded to meters. */ - case byDistances(ContourDefinition) + case byDistances([Definition]) } } @@ -279,4 +289,14 @@ extension IsochroneOptions.Color { blue) #endif } + + static var fallbackColor: IsochroneOptions.Color { + #if canImport(UIKit) + return gray + #elseif canImport(AppKit) + return gray + #else + return Color(red: 128, green: 128, blue: 128) + #endif + } } diff --git a/Tests/MapboxDirectionsTests/IsochroneTests.swift b/Tests/MapboxDirectionsTests/IsochroneTests.swift index 6018f23ad..7279a642e 100644 --- a/Tests/MapboxDirectionsTests/IsochroneTests.swift +++ b/Tests/MapboxDirectionsTests/IsochroneTests.swift @@ -39,16 +39,18 @@ class IsochroneTests: XCTestCase { #if !os(Linux) let options = IsochroneOptions(centerCoordinate: location, - contours: .byDistances(.colored([ - (radius1, .init(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0)), - (radius2, .init(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)) - ]))) + contours: .byDistances([ + .init(value: radius1, color: .init(red: 0.1, green: 0.2, blue: 0.3, alpha: 1.0)), + .init(value: radius2, color: .init(red: 0.4, green: 0.5, blue: 0.6, alpha: 1.0)) + ])) #else + let contour1 = IsochroneOptions.Contours.Definition(value: radius1, color: IsochroneOptions.Color(red: 25, green: 51, blue: 76)) + let contour2 = IsochroneOptions.Contours.Definition(value: radius2, color: IsochroneOptions.Color(red: 102, green: 127, blue: 153)) let options = IsochroneOptions(centerCoordinate: location, - contours: IsochroneOptions.Contours.byDistances(.colored([ - (radius1, IsochroneOptions.Color(red: 25, green: 51, blue: 76)), - (radius2, IsochroneOptions.Color(red: 102, green: 127, blue: 153)) - ]))) + contours: IsochroneOptions.Contours.byDistances([ + contour1, + contour2 + ])) #endif options.contoursFormat = IsochroneOptions.ContourFormat.polygon options.denoisingFactor = 0.5 @@ -74,10 +76,10 @@ class IsochroneTests: XCTestCase { XCTAssertEqual(request.httpMethod, "GET") XCTAssertEqual(request.url, url) - options.contours = IsochroneOptions.Contours.byExpectedTravelTimes(.default([ - Measurement(value: 31, unit: UnitDuration.seconds), - Measurement(value: 2.1, unit: UnitDuration.minutes) - ])) + options.contours = IsochroneOptions.Contours.byExpectedTravelTimes([ + IsochroneOptions.Contours.Definition(value: 31, unit: UnitDuration.seconds), + IsochroneOptions.Contours.Definition(value: 2.1, unit: UnitDuration.minutes) + ]) url = isochrones.url(forCalculating: options) @@ -99,7 +101,8 @@ class IsochroneTests: XCTestCase { let expectation = self.expectation(description: "Async callback") let isochrones = Isochrones(credentials: IsochroneBogusCredentials) let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), - contours: .byDistances(.default([.init(value: 100, unit: .meters)]))) + contours: .byDistances([.init(value: 100, + unit: .meters)])) isochrones.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } @@ -126,7 +129,7 @@ class IsochroneTests: XCTestCase { let expectation = self.expectation(description: "Async callback") let isochrones = Isochrones(credentials: IsochroneBogusCredentials) let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), - contours: .byDistances(.default([.init(value: 100, unit: .meters)]))) + contours: .byDistances([.init(value: 100, unit: .meters)])) isochrones.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } @@ -155,7 +158,7 @@ class IsochroneTests: XCTestCase { let expectation = self.expectation(description: "Async callback") let isochrones = Isochrones(credentials: IsochroneBogusCredentials) let options = IsochroneOptions(centerCoordinate: LocationCoordinate2D(latitude: 0, longitude: 1), - contours: .byDistances(.default([.init(value: 100, unit: .meters)]))) + contours: .byDistances([.init(value: 100, unit: .meters)])) isochrones.calculate(options, completionHandler: { (session, result) in defer { expectation.fulfill() } From 96b852ddf529526209d8608bfb50e18a5d629648 Mon Sep 17 00:00:00 2001 From: udumft Date: Fri, 29 Oct 2021 15:11:17 +0300 Subject: [PATCH 17/19] vk-296-isochrone-api: linux build fix --- Sources/MapboxDirections/IsochroneOptions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift index 8a5fb4ecd..64e3a5e10 100644 --- a/Sources/MapboxDirections/IsochroneOptions.swift +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -296,7 +296,7 @@ extension IsochroneOptions.Color { #elseif canImport(AppKit) return gray #else - return Color(red: 128, green: 128, blue: 128) + return IsochroneOptions.Color(red: 128, green: 128, blue: 128) #endif } } From c1962932a33b1949eaa57de47c88f6f3373238b8 Mon Sep 17 00:00:00 2001 From: udumft Date: Mon, 8 Nov 2021 11:40:39 +0300 Subject: [PATCH 18/19] vk-296-isochrones-api: code docs corrected. --- Sources/MapboxDirections/DirectionsOptions.swift | 2 +- Sources/MapboxDirections/IsochroneOptions.swift | 2 +- Sources/MapboxDirections/Isochrones.swift | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/MapboxDirections/DirectionsOptions.swift b/Sources/MapboxDirections/DirectionsOptions.swift index c2f2d9584..ed51fbe8c 100644 --- a/Sources/MapboxDirections/DirectionsOptions.swift +++ b/Sources/MapboxDirections/DirectionsOptions.swift @@ -291,7 +291,7 @@ open class DirectionsOptions: Codable { // MARK: Getting the Request URL /** - An array of URL query items to include in an HTTP request. + The path of the request URL, specifying service name, version and profile. The query items are included in the URL of a GET request or the body of a POST request. */ diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift index 64e3a5e10..37a1cb399 100644 --- a/Sources/MapboxDirections/IsochroneOptions.swift +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -76,7 +76,7 @@ public class IsochroneOptions { // MARK: Getting the Request URL /** - An array of URL query items to include in an HTTP request. + The path of the request URL, specifying service name, version and profile. */ var abridgedPath: String { return "isochrone/v1/\(profileIdentifier.rawValue)" diff --git a/Sources/MapboxDirections/Isochrones.swift b/Sources/MapboxDirections/Isochrones.swift index f8ed7d3aa..69873916d 100644 --- a/Sources/MapboxDirections/Isochrones.swift +++ b/Sources/MapboxDirections/Isochrones.swift @@ -39,7 +39,7 @@ open class Isochrones { private let processingQueue: DispatchQueue /** - The shared directions object. + The shared isochrones object. To use this object, a Mapbox [access token](https://docs.mapbox.com/help/glossary/access-token/) should be specified in the `MBXAccessToken` key in the main application bundle’s Info.plist. */ From 4b4ddf5454c734be59acb1ab0b08c29a5c328395 Mon Sep 17 00:00:00 2001 From: udumft Date: Thu, 11 Nov 2021 11:34:41 +0300 Subject: [PATCH 19/19] vk-296-isochrone-api: CHANGELOG corrected; moved DirectionsCredentials and DirectionsProfileIdentifier alieases to original implementations; code docs corrected. --- CHANGELOG.md | 3 ++- MapboxDirections.xcodeproj/project.pbxproj | 20 ------------------- Sources/MapboxDirections/Credentials.swift | 3 +++ .../DirectionsCredentials.swift | 4 ---- .../DirectionsProfileIdentifier.swift | 10 ---------- .../MapboxDirections/IsochroneOptions.swift | 3 +-- .../MapboxDirections/ProfileIdentifier.swift | 10 ++++++++++ 7 files changed, 16 insertions(+), 37 deletions(-) delete mode 100644 Sources/MapboxDirections/DirectionsCredentials.swift delete mode 100644 Sources/MapboxDirections/DirectionsProfileIdentifier.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 22f27f93c..5d44cdc8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ * Added the `RouteOptions.initialManeuverAvoidanceRadius` property to avoid a sudden maneuver when calculating a route while the user is in motion. ([#609](https://github.com/mapbox/mapbox-directions-swift/pull/609)) * Added the `RoadClasses.unpaved` option for avoiding unpaved roads. ([#620](https://github.com/mapbox/mapbox-directions-swift/pull/620)) * Added the `RoadClasses.cashOnlyToll` property for avoiding toll roads that only accept cash payment. ([#620](https://github.com/mapbox/mapbox-directions-swift/pull/620)) -* Added `Ischrone` API wrapper. The [Mapbox Isochrone API](https://docs.mapbox.com/api/navigation/isochrone/) computes areas that are reachable within a specified amount of time from a location, and returns the reachable regions as contours of polygons or lines that you can display on a map. ([#621](https://github.com/mapbox/mapbox-directions-swift/pull/621)) +* Added `Isochrones`, which connects to the [Mapbox Isochrone API](https://docs.mapbox.com/api/navigation/isochrone/) to compute areas that are reachable within a specified amount of time from a location and return the reachable regions as contours of polygons or lines that you can display on a map. ([#621](https://github.com/mapbox/mapbox-directions-swift/pull/621)) +* Renamed `DirectionsCredentials` and `DirectionsProfileIdentifier` to `Credentials` and `ProfileIdentifier`, respectively. ([#621](https://github.com/mapbox/mapbox-directions-swift/pull/621)) * Added the `RouteOptions.maximumHeight` and `RouteOptions.maximumWidth` properties for ensuring that the resulting routes can accommodate a vehicle of a certain size. ([#623](https://github.com/mapbox/mapbox-directions-swift/pull/623)) * The `DirectionsPriority` struct now conforms to the `Codable` protocol. ([#623](https://github.com/mapbox/mapbox-directions-swift/pull/623)) * Fixed an issue where the `RouteOptions.alleyPriority`, `RouteOptions.walkwayPriority`, and `RouteOptions.speed` properties were excluded from the encoded representation of a `RouteOptions` object. ([#623](https://github.com/mapbox/mapbox-directions-swift/pull/623)) diff --git a/MapboxDirections.xcodeproj/project.pbxproj b/MapboxDirections.xcodeproj/project.pbxproj index 32b018c1c..60d15c27f 100644 --- a/MapboxDirections.xcodeproj/project.pbxproj +++ b/MapboxDirections.xcodeproj/project.pbxproj @@ -118,10 +118,6 @@ 4376A52723FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; 4376A52823FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; 4376A52923FB13D400C6038D /* MatchOptionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */; }; - 438BFEC2233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; - 438BFEC3233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; - 438BFEC4233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; - 438BFEC5233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */; }; 439255772344113B006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; 439255792344113D006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; 4392557A2344113E006EEE88 /* DirectionsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4392557523440EC2006EEE88 /* DirectionsError.swift */; }; @@ -133,10 +129,6 @@ 43D992FD2437B93E008A2D74 /* CredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */; }; 43D992FE2437B93F008A2D74 /* CredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */; }; 43D992FF2437B940008A2D74 /* CredentialsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */; }; - 43EBD3AD23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; - 43EBD3AE23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; - 43EBD3AF23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; - 43EBD3B023DBC06800B09D05 /* DirectionsCredentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */; }; 43F89F932350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; 43F89F942350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; 43F89F952350F952007B591E /* MapMatchingResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43F89F922350F952007B591E /* MapMatchingResponse.swift */; }; @@ -503,10 +495,8 @@ 4376A52623FB13D400C6038D /* MatchOptionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MatchOptionsTests.swift; sourceTree = ""; }; 438BFEBC233D7FA900457294 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 438BFEC0233D805500457294 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; - 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsProfileIdentifier.swift; sourceTree = ""; }; 4392557523440EC2006EEE88 /* DirectionsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectionsError.swift; sourceTree = ""; }; 43D992FB2437B8D2008A2D74 /* CredentialsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialsTests.swift; sourceTree = ""; }; - 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectionsCredentials.swift; sourceTree = ""; }; 43F89F922350F952007B591E /* MapMatchingResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMatchingResponse.swift; sourceTree = ""; }; 8A3B4C9A24EB55F60085DA64 /* RouteResponseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteResponseTests.swift; sourceTree = ""; }; 8A41B0FC24F5C2390021FFDC /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; @@ -798,10 +788,8 @@ C58EA7A91E9D7EAD008F98CE /* Congestion.swift */, 2B9F387F272AE23A001DBA12 /* Credentials.swift */, DD6254731AE70CB700017857 /* Directions.swift */, - 43EBD3AC23DBC06800B09D05 /* DirectionsCredentials.swift */, 4392557523440EC2006EEE88 /* DirectionsError.swift */, C59094BE203B800300EB2417 /* DirectionsOptions.swift */, - 438BFEC1233D854D00457294 /* DirectionsProfileIdentifier.swift */, C59094C0203DE6BC00EB2417 /* DirectionsResult.swift */, 431E93BE234664A200A71B44 /* DrivingSide.swift */, DA6C9D8C1CAE442B00094FBC /* Info.plist */, @@ -1415,7 +1403,6 @@ 2B5407F32452FA8C006C820B /* RefreshedRoute.swift in Sources */, 431E93D023466D7500A71B44 /* Codable.swift in Sources */, C5990B4D2045E74800D7DFD4 /* DirectionsOptions.swift in Sources */, - 43EBD3AE23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D691DD127CB0015EC78 /* LaneIndication.swift in Sources */, 2B9F388E272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F942350F952007B591E /* MapMatchingResponse.swift in Sources */, @@ -1428,7 +1415,6 @@ F4F5084C2524DC280044F2D0 /* AdministrativeRegion.swift in Sources */, 2BBBD05F257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift in Sources */, 2BBBD08E257FA1CD004EB3D6 /* BlockedLanes.swift in Sources */, - 438BFEC3233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */, F4CF2C582523B66300A6D0B6 /* TollCollection.swift in Sources */, F4F508392524D6F10044F2D0 /* RestStop.swift in Sources */, C54549FC2073F1EF002E273F /* Array.swift in Sources */, @@ -1508,7 +1494,6 @@ 2B5407F42452FA8C006C820B /* RefreshedRoute.swift in Sources */, 431E93D123466D7600A71B44 /* Codable.swift in Sources */, C5990B4E2045E74900D7DFD4 /* DirectionsOptions.swift in Sources */, - 43EBD3AF23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D6A1DD127CB0015EC78 /* LaneIndication.swift in Sources */, 2B9F388F272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F952350F952007B591E /* MapMatchingResponse.swift in Sources */, @@ -1521,7 +1506,6 @@ F4F5084D2524DC280044F2D0 /* AdministrativeRegion.swift in Sources */, 2BBBD060257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift in Sources */, 2BBBD08F257FA1CD004EB3D6 /* BlockedLanes.swift in Sources */, - 438BFEC4233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */, F4CF2C592523B66300A6D0B6 /* TollCollection.swift in Sources */, F4F5083A2524D6F10044F2D0 /* RestStop.swift in Sources */, C54549FD2073F1F0002E273F /* Array.swift in Sources */, @@ -1601,7 +1585,6 @@ 2B5407F52452FA8C006C820B /* RefreshedRoute.swift in Sources */, 431E93D223466D7700A71B44 /* Codable.swift in Sources */, C5990B4F2045E74A00D7DFD4 /* DirectionsOptions.swift in Sources */, - 43EBD3B023DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAA76D6B1DD127CB0015EC78 /* LaneIndication.swift in Sources */, 2B9F3890272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F962350F952007B591E /* MapMatchingResponse.swift in Sources */, @@ -1614,7 +1597,6 @@ F4F5084E2524DC280044F2D0 /* AdministrativeRegion.swift in Sources */, 2BBBD061257E61ED004EB3D6 /* MapboxStreetsRoadClass.swift in Sources */, 2BBBD090257FA1CD004EB3D6 /* BlockedLanes.swift in Sources */, - 438BFEC5233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */, F4CF2C5A2523B66300A6D0B6 /* TollCollection.swift in Sources */, F4F5083B2524D6F10044F2D0 /* RestStop.swift in Sources */, C54549FE2073F1F1002E273F /* Array.swift in Sources */, @@ -1661,7 +1643,6 @@ 2B5407F22452FA8C006C820B /* RefreshedRoute.swift in Sources */, 431E93CF23466D7400A71B44 /* Codable.swift in Sources */, C59094C1203DE6BC00EB2417 /* DirectionsResult.swift in Sources */, - 43EBD3AD23DBC06800B09D05 /* DirectionsCredentials.swift in Sources */, DAC05F1A1CFC077C00FA0071 /* RouteLeg.swift in Sources */, 2B9F388D272AE23A001DBA12 /* Credentials.swift in Sources */, 43F89F932350F952007B591E /* MapMatchingResponse.swift in Sources */, @@ -1677,7 +1658,6 @@ 8D381B6A1FDB101F008D5A58 /* String.swift in Sources */, F4CF2C572523B66300A6D0B6 /* TollCollection.swift in Sources */, F4F508382524D6F10044F2D0 /* RestStop.swift in Sources */, - 438BFEC2233D854D00457294 /* DirectionsProfileIdentifier.swift in Sources */, C57D55011DB5669600B94B74 /* Intersection.swift in Sources */, 2B9F3889272AE23A001DBA12 /* IsochroneError.swift in Sources */, 431E93C723466B3F00A71B44 /* CoreLocation.swift in Sources */, diff --git a/Sources/MapboxDirections/Credentials.swift b/Sources/MapboxDirections/Credentials.swift index 895f83779..cf38ce273 100644 --- a/Sources/MapboxDirections/Credentials.swift +++ b/Sources/MapboxDirections/Credentials.swift @@ -64,3 +64,6 @@ public struct Credentials: Equatable { } } } + +@available(*, deprecated, renamed: "Credentials") +public typealias DirectionsCredentials = Credentials diff --git a/Sources/MapboxDirections/DirectionsCredentials.swift b/Sources/MapboxDirections/DirectionsCredentials.swift deleted file mode 100644 index 75d866bea..000000000 --- a/Sources/MapboxDirections/DirectionsCredentials.swift +++ /dev/null @@ -1,4 +0,0 @@ -import Foundation - -@available(*, deprecated, renamed: "Credentials") -public typealias DirectionsCredentials = Credentials diff --git a/Sources/MapboxDirections/DirectionsProfileIdentifier.swift b/Sources/MapboxDirections/DirectionsProfileIdentifier.swift deleted file mode 100644 index 612007df7..000000000 --- a/Sources/MapboxDirections/DirectionsProfileIdentifier.swift +++ /dev/null @@ -1,10 +0,0 @@ -import Foundation - -@available(*, deprecated, renamed: "ProfileIdentifier") -public typealias MBDirectionsProfileIdentifier = ProfileIdentifier - -/** - Options determining the primary mode of transportation for the routes. - */ -@available(*, deprecated, renamed: "ProfileIdentifier") -public typealias DirectionsProfileIdentifier = ProfileIdentifier diff --git a/Sources/MapboxDirections/IsochroneOptions.swift b/Sources/MapboxDirections/IsochroneOptions.swift index 37a1cb399..fce2fcb48 100644 --- a/Sources/MapboxDirections/IsochroneOptions.swift +++ b/Sources/MapboxDirections/IsochroneOptions.swift @@ -152,8 +152,7 @@ extension IsochroneOptions { /** Contour fill color. - If `nil` - default rainbow color sheme will be used for each contour. - - important: `color` value should be specified for everyone or none requested contours. Otherwise all missing colors will be grayed. + If this property is unspecified, the contour is colored gray. If this property is not specified for any contour, the contours are rainbow-colored. */ public var color: Color? diff --git a/Sources/MapboxDirections/ProfileIdentifier.swift b/Sources/MapboxDirections/ProfileIdentifier.swift index c31457af1..edb8686ff 100644 --- a/Sources/MapboxDirections/ProfileIdentifier.swift +++ b/Sources/MapboxDirections/ProfileIdentifier.swift @@ -42,3 +42,13 @@ public struct ProfileIdentifier: Codable, Hashable, RawRepresentable { */ public static let walking: ProfileIdentifier = .init(rawValue: "mapbox/walking") } + + +@available(*, deprecated, renamed: "ProfileIdentifier") +public typealias MBDirectionsProfileIdentifier = ProfileIdentifier + +/** + Options determining the primary mode of transportation for the routes. + */ +@available(*, deprecated, renamed: "ProfileIdentifier") +public typealias DirectionsProfileIdentifier = ProfileIdentifier