Skip to content

Commit

Permalink
Options & Credentials decoding (#655)
Browse files Browse the repository at this point in the history
* vk-1324-options-decoding: added init methods to create Route options by parsing URL request. Unit test added; added Credentials init method by decoding a request URL; CHANGELOG entry added
  • Loading branch information
Udumft committed Mar 15, 2022
1 parent 2b14b8c commit 2db7fda
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

* Added `VisualInstruction.Component.ShieldRepresentation` struct and the `VisualInstruction.Component.ImageRepresentation.shield` property containing metadata for displaying a highway shield consistent with map styles used by the Mapbox Maps SDK. ([#644](https://github.com/mapbox/mapbox-directions-swift/pull/644), [#647](https://github.com/mapbox/mapbox-directions-swift/pull/647))
* Added a `RouteLeg.viaWaypoints` property that lists the non-leg-separating waypoints (also known as “silent waypoints”) along a `RouteLeg`. Previously, you had to filter `DirectionsOptions.waypoints` to include only the items whose `Waypoints.separatesLegs` property was set to `true`, then zip them with `RouteResponse.routes`. This approach still works in some cases but is not guaranteed to be reliable for all Mapbox Directions API responses in the future. ([#656](https://github.com/mapbox/mapbox-directions-swift/pull/656))
* Added `DirectionsOptions(url:)`, `RouteOptions(url:)` and extended existing `DirectionsOptions(waypoints:profileIdentifier:queryItems:)`, `RouteOptions(waypoints:profileIdentifier:queryItems:)`, `MatchOptions(waypoints:profileIdentifier:queryItems:)` and related convenience init methods for deserializing corresponding options object using appropriate request URL or it's query items. ([#655](https://github.com/mapbox/mapbox-directions-swift/pull/655))

## v2.2.0

Expand Down
17 changes: 17 additions & 0 deletions Sources/MapboxDirections/Credentials.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ public struct Credentials: Equatable {
self.host = URL(string: "https://api.mapbox.com")!
}
}

/**
:nodoc:
Attempts to get `host` and `accessToken` from provided URL to create `Credentials` instance.
If it is impossible to extract parameter(s) - default values will be used.
*/
public init(requestURL url: URL) {
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
let accessToken = components?
.queryItems?
.first { $0.name == "access_token" }?
.value
components?.path = "/"
components?.queryItems = nil
self.init(accessToken: accessToken, host: components?.url)
}
}

@available(*, deprecated, renamed: "Credentials")
Expand Down
130 changes: 129 additions & 1 deletion Sources/MapboxDirections/DirectionsOptions.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
import Polyline
import Turf

/**
Maximum length of an HTTP request URL for the purposes of switching from GET to
Expand Down Expand Up @@ -122,10 +123,136 @@ open class DirectionsOptions: Codable {
- 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.
- parameter queryItems: URL query items to be parsed and applied as configuration to the resulting options.
*/
required public init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = nil) {
required public init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = nil, queryItems: [URLQueryItem]? = nil) {
self.waypoints = waypoints
self.profileIdentifier = profileIdentifier ?? .automobile

guard let queryItems = queryItems else {
return
}

let mappedQueryItems = Dictionary<String, String>(queryItems.compactMap {
guard let value = $0.value else { return nil }
return ($0.name, value)
},
uniquingKeysWith: { (_, latestValue) in
return latestValue
})

if let mappedValue = mappedQueryItems[CodingKeys.shapeFormat.stringValue],
let shapeFormat = RouteShapeFormat(rawValue: mappedValue) {
self.shapeFormat = shapeFormat
}
if let mappedValue = mappedQueryItems[CodingKeys.routeShapeResolution.stringValue],
let routeShapeResolution = RouteShapeResolution(rawValue: mappedValue) {
self.routeShapeResolution = routeShapeResolution
}
if mappedQueryItems[CodingKeys.includesSteps.stringValue] == "true" {
self.includesSteps = true
}
if let mappedValue = mappedQueryItems[CodingKeys.locale.stringValue] {
self.locale = Locale(identifier: mappedValue)
}
if mappedQueryItems[CodingKeys.includesSpokenInstructions.stringValue] == "true" {
self.includesSpokenInstructions = true
}
if let mappedValue = mappedQueryItems[CodingKeys.distanceMeasurementSystem.stringValue],
let measurementSystem = MeasurementSystem(rawValue: mappedValue) {
self.distanceMeasurementSystem = measurementSystem
}
if mappedQueryItems[CodingKeys.includesVisualInstructions.stringValue] == "true" {
self.includesVisualInstructions = true
}
if let mappedValue = mappedQueryItems[CodingKeys.attributeOptions.stringValue],
let attributeOptions = AttributeOptions(descriptions: mappedValue.components(separatedBy: ",")) {
self.attributeOptions = attributeOptions
}
if let mappedValue = mappedQueryItems["waypoints"] {
let indicies = mappedValue.components(separatedBy: ";").compactMap { Int($0) }
if !indicies.isEmpty {
waypoints.enumerated().forEach {
$0.element.separatesLegs = indicies.contains($0.offset)
}
}
}

let waypointsData = [mappedQueryItems["approaches"]?.components(separatedBy: ";"),
mappedQueryItems["bearings"]?.components(separatedBy: ";"),
mappedQueryItems["radiuses"]?.components(separatedBy: ";"),
mappedQueryItems["waypoint_names"]?.components(separatedBy: ";"),
mappedQueryItems["snapping_include_closures"]?.components(separatedBy: ";")
] as [[String]?]

let getElement: ((_ array: [String]?, _ index: Int) -> String?) = { array, index in
if array?.count ?? -1 > index {
return array?[index]
}
return nil
}

waypoints.enumerated().forEach {
if let approach = getElement(waypointsData[0], $0.offset) {
$0.element.allowsArrivingOnOppositeSide = approach == "unrestricted" ? true : false
}

if let descriptions = getElement(waypointsData[1], $0.offset)?.components(separatedBy: ",") {
$0.element.heading = LocationDirection(descriptions.first!)
$0.element.headingAccuracy = LocationDirection(descriptions.last!)
}

if let accuracy = getElement(waypointsData[2], $0.offset) {
$0.element.coordinateAccuracy = LocationAccuracy(accuracy)
}

if let snaps = getElement(waypointsData[4], $0.offset) {
$0.element.allowsSnappingToClosedRoad = snaps == "true"
}
}

waypoints.filter { $0.separatesLegs }.enumerated().forEach {
if let name = getElement(waypointsData[3], $0.offset) {
$0.element.name = name
}
}
}

/**
Creates new options object by deserializing given `url`
Initialization fails if it is unable to extract `waypoints` list and `profileIdentifier`. If other properties are failed to decode - it will just skip them.
- parameter url: An URL, used to make a route request.
*/
public convenience init?(url: URL) {
guard url.pathComponents.count >= 3 else {
return nil
}

let waypointsString = url.lastPathComponent.replacingOccurrences(of: ".json", with: "")
let waypoints: [Waypoint] = waypointsString.components(separatedBy: ";").compactMap {
let coordinates = $0.components(separatedBy: ",")
guard coordinates.count == 2,
let latitudeString = coordinates.last,
let longitudeString = coordinates.first,
let latitude = LocationDegrees(latitudeString),
let longitude = LocationDegrees(longitudeString) else {
return nil
}
return Waypoint(coordinate: .init(latitude: latitude,
longitude: longitude))
}

guard waypoints.count >= 2 else {
return nil
}

let profileIdentifier = ProfileIdentifier(rawValue: url.pathComponents.dropLast().suffix(2).joined(separator: "/"))

self.init(waypoints: waypoints,
profileIdentifier: profileIdentifier,
queryItems: URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems)
}


Expand Down Expand Up @@ -369,6 +496,7 @@ open class DirectionsOptions: Codable {
return queryItems
}


var bearings: String? {
guard waypoints.contains(where: { $0.heading ?? -1 >= 0 }) else {
return nil
Expand Down
39 changes: 33 additions & 6 deletions Sources/MapboxDirections/MapMatching/MatchOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ open class MatchOptions: DirectionsOptions {
- 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.
- parameter queryItems: URL query items to be parsed and applied as configuration to the resulting options.
*/
public convenience init(locations: [CLLocation], profileIdentifier: ProfileIdentifier? = nil) {
public convenience init(locations: [CLLocation], profileIdentifier: ProfileIdentifier? = nil, queryItems: [URLQueryItem]? = nil) {
let waypoints = locations.map {
Waypoint(location: $0)
}
self.init(waypoints: waypoints, profileIdentifier: profileIdentifier)
self.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems)
}
#endif

Expand All @@ -32,16 +33,42 @@ open class MatchOptions: DirectionsOptions {
- 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.
- parameter queryItems: URL query items to be parsed and applied as configuration to the resulting options.
*/
public convenience init(coordinates: [LocationCoordinate2D], profileIdentifier: ProfileIdentifier? = nil) {
public convenience init(coordinates: [LocationCoordinate2D], profileIdentifier: ProfileIdentifier? = nil, queryItems: [URLQueryItem]? = nil) {
let waypoints = coordinates.map {
Waypoint(coordinate: $0)
}
self.init(waypoints: waypoints, profileIdentifier: profileIdentifier)
self.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems)
}

public required init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = nil) {
super.init(waypoints: waypoints, profileIdentifier: profileIdentifier)
public required init(waypoints: [Waypoint], profileIdentifier: ProfileIdentifier? = nil, queryItems: [URLQueryItem]? = nil) {
super.init(waypoints: waypoints, profileIdentifier: profileIdentifier, queryItems: queryItems)

guard let queryItems = queryItems else {
return
}

let mappedQueryItems = Dictionary<String, String>(queryItems.compactMap {
guard let value = $0.value else { return nil }
return ($0.name, value)
},
uniquingKeysWith: { (_, latestValue) in
return latestValue
})

if mappedQueryItems[CodingKeys.resamplesTraces.stringValue] == "true" {
self.resamplesTraces = true
}

if let mappedValue = mappedQueryItems["waypoints"] {
let indicies = mappedValue.components(separatedBy: ";").compactMap { Int($0) }
if !indicies.isEmpty {
waypoints.enumerated().forEach {
$0.element.separatesLegs = indicies.contains($0.offset)
}
}
}
}

private enum CodingKeys: String, CodingKey {
Expand Down
Loading

0 comments on commit 2db7fda

Please sign in to comment.