Skip to content

Commit

Permalink
vk-296-isochrone-api: replaced ContourDefinition enum with Definition…
Browse files Browse the repository at this point in the history
… struct. Tests and examples updated
  • Loading branch information
Udumft committed Oct 29, 2021
1 parent e653964 commit c77ca30
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 54 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,16 +172,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 {
Expand Down Expand Up @@ -219,7 +221,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
Expand All @@ -241,7 +243,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()
Expand Down
86 changes: 53 additions & 33 deletions Sources/MapboxDirections/IsochroneOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Expand Down Expand Up @@ -138,39 +144,43 @@ extension IsochroneOptions {
/**
Describes Individual contour bound and color.
*/
public enum ContourDefinition<Unt: Dimension> {
public struct Definition<Unt: Dimension> {
/**
Contour bound definition value.
Bound measurement value.
*/
public typealias Value = Measurement<Unt>
public var value: Measurement<Unt>
/**
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<Unt>, 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
}
}

Expand All @@ -179,14 +189,14 @@ extension IsochroneOptions {

This value will be rounded to minutes.
*/
case byExpectedTravelTimes(ContourDefinition<UnitDuration>)
case byExpectedTravelTimes([Definition<UnitDuration>])

/**
The distances to use for each isochrone contour.

Will be rounded to meters.
*/
case byDistances(ContourDefinition<UnitLength>)
case byDistances([Definition<UnitLength>])
}
}

Expand Down Expand Up @@ -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
}
}
33 changes: 18 additions & 15 deletions Tests/MapboxDirectionsTests/IsochroneTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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() }

Expand All @@ -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() }

Expand Down Expand Up @@ -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() }

Expand Down

0 comments on commit c77ca30

Please sign in to comment.