Skip to content

Commit

Permalink
RouteLeg.closures integration (#755)
Browse files Browse the repository at this point in the history
* vk-NAVIOS-367-leg-closures: added new RouteLeg.Closure entity and corresponding parameter to RouteLeg; added new annotation request to DirectionsOptions; Added single refresh method to cover refreshing all types of entities; unit tests updated; CHANGELOG updated; API breackage logged.
  • Loading branch information
Udumft committed Oct 25, 2022
1 parent 171f910 commit 76c46e9
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 16 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* Added the `Waypoint.layer` property, which can ensure that the route begins on the correct road if it is above or below another road. ([#745](https://github.com/mapbox/mapbox-directions-swift/pull/745))
* Expanded `AttributeOptions` to allow user options with custom values. See `AttributeOptions.customOptionsByRawValue` for reference. ([#748](https://github.com/mapbox/mapbox-directions-swift/pull/748))
* Fixed incorrect shape indicies in `RouteLeg.incidents` after route refresh. ([#752](https://github.com/mapbox/mapbox-directions-swift/pull/752))
* Added `RouteLeg.closures` for displaying live-traffic closure on the route. This information is only available for the `mapbox/driving-traffic` profile and when `RouteOptions.attributeOptions` property contains `AttributeOptions.closures`. ([#755](https://github.com/mapbox/mapbox-directions-swift/pull/755))
* Added `Route.refresh(from:refreshParameters:)` method as a single entry point for refreshing various parameters of the `Route` object. ([#755](https://github.com/mapbox/mapbox-directions-swift/pull/755))

## 2.7.0

Expand Down
14 changes: 14 additions & 0 deletions Sources/MapboxDirections/AttributeOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ public struct AttributeOptions: CustomValueOptionSet, CustomStringConvertible {
rawValue = 0
}

/**
Live-traffic closures along the road segment.
When this attribute is specified, the `RouteLeg.closures` property is filled with relevant data.
This attribute requires `ProfileIdentifier.automobileAvoidingTraffic`.
*/
public static let closures = AttributeOptions(rawValue: 1)

/**
Distance (in meters) along the segment.
Expand Down Expand Up @@ -71,6 +80,8 @@ public struct AttributeOptions: CustomValueOptionSet, CustomStringConvertible {
var attributeOptions: AttributeOptions = []
for description in descriptions {
switch description {
case "closure":
attributeOptions.update(with: .closures)
case "distance":
attributeOptions.update(with: .distance)
case "duration":
Expand All @@ -94,6 +105,9 @@ public struct AttributeOptions: CustomValueOptionSet, CustomStringConvertible {

public var description: String {
var descriptions: [String] = []
if contains(.closures) {
descriptions.append("closure")
}
if contains(.distance) {
descriptions.append("distance")
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/MapboxDirections/Directions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ open class Directions: NSObject {
- parameter credentials: An object containing the credentials used to make the request.
- parameter result: A `Result` enum that represents the `RouteRefreshResponse` if the request returned successfully, or the error if it did not.
- postcondition: To update the original route, pass `RouteRefreshResponse.route` into the `Route.refreshLegAttributes(from:)` and `Route.refreshLegIncidents(from:)` methods.
- postcondition: To update the original route, pass `RouteRefreshResponse.route` into the `Route.refreshLegAttributes(from:)`, `Route.refreshLegIncidents(from:)`, `Route.refreshLegClosures(from:legIndex:legShapeIndex:)` or `Route.refresh(from:refreshParameters:)` methods.
*/
public typealias RouteRefreshCompletionHandler = (_ credentials: Credentials, _ result: Result<RouteRefreshResponse, DirectionsError>) -> Void

Expand Down
4 changes: 4 additions & 0 deletions Sources/MapboxDirections/RefreshedRoute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,18 +41,21 @@ public struct RefreshedRouteLeg: ForeignMemberContainer {

public var attributes: RouteLeg.Attributes
public var incidents: [Incident]?
public var closures: [RouteLeg.Closure]?
}

extension RefreshedRouteLeg: Codable {
enum CodingKeys: String, CodingKey {
case attributes = "annotation"
case incidents = "incidents"
case closures = "closures"
}

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
attributes = try container.decode(RouteLeg.Attributes.self, forKey: .attributes)
incidents = try container.decodeIfPresent([Incident].self, forKey: .incidents)
closures = try container.decodeIfPresent([RouteLeg.Closure].self, forKey: .closures)

try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}
Expand All @@ -61,6 +64,7 @@ extension RefreshedRouteLeg: Codable {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(attributes, forKey: .attributes)
try container.encodeIfPresent(incidents, forKey: .incidents)
try container.encodeIfPresent(closures, forKey: .closures)

try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
Expand Down
73 changes: 70 additions & 3 deletions Sources/MapboxDirections/RouteLeg.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ open class RouteLeg: Codable, ForeignMemberContainerClass {
case administrativeRegions = "admins"
case incidents
case viaWaypoints = "via_waypoints"
case closures = "closures"
}

// MARK: Creating a Leg
Expand Down Expand Up @@ -100,6 +101,10 @@ open class RouteLeg: Codable, ForeignMemberContainerClass {
self.incidents = incidents
}

if let closures = try container.decodeIfPresent([Closure].self, forKey: .closures) {
self.closures = closures
}

if let viaWaypoints = try container.decodeIfPresent([SilentWaypoint].self, forKey: .viaWaypoints) {
self.viaWaypoints = viaWaypoints
}
Expand Down Expand Up @@ -131,6 +136,9 @@ open class RouteLeg: Codable, ForeignMemberContainerClass {
if let incidents = incidents {
try container.encode(incidents, forKey: .incidents)
}
if let closures = closures {
try container.encode(closures, forKey: .closures)
}

if let viaWaypoints = viaWaypoints {
try container.encode(viaWaypoints, forKey: .viaWaypoints)
Expand Down Expand Up @@ -255,6 +263,13 @@ open class RouteLeg: Codable, ForeignMemberContainerClass {
*/
open var segmentMaximumSpeedLimits: [Measurement<UnitSpeed>?]?

/**
An array of `Closure` objects describing live-traffic related closures that occur along the route.
This information is only available for the `mapbox/driving-traffic` profile and when `RouteOptions.attributeOptions` property contains `AttributeOptions.closures`.
*/
open var closures: [Closure]?

/**
The full collection of attributes along the leg.
*/
Expand Down Expand Up @@ -288,16 +303,30 @@ open class RouteLeg: Codable, ForeignMemberContainerClass {
segmentMaximumSpeedLimits?.replaceIfPossible(subrange: refreshRange, with: newAttributes.segmentMaximumSpeedLimits)
}

private func adjustShapeIndexRange(_ range: Range<Int>, startLegShapeIndex: Int) -> Range<Int> {
let startIndex = startLegShapeIndex + range.lowerBound
let endIndex = startLegShapeIndex + range.upperBound
return startIndex..<endIndex
}

func refreshIncidents(newIncidents: [Incident]?, startLegShapeIndex: Int = 0) {
incidents = newIncidents?.map { incident in
var adjustedIncident = incident
let startIndex = startLegShapeIndex + incident.shapeIndexRange.lowerBound
let endIndex = startLegShapeIndex + incident.shapeIndexRange.upperBound
adjustedIncident.shapeIndexRange = startIndex..<endIndex
adjustedIncident.shapeIndexRange = adjustShapeIndexRange(incident.shapeIndexRange,
startLegShapeIndex: startLegShapeIndex)
return adjustedIncident
}
}

func refreshClosures(newClosures: [Closure]?, startLegShapeIndex: Int = 0) {
closures = newClosures?.map { closure in
var adjustedClosure = closure
adjustedClosure.shapeIndexRange = adjustShapeIndexRange(closure.shapeIndexRange,
startLegShapeIndex: startLegShapeIndex)
return adjustedClosure
}
}

/**
Returns the ISO 3166-1 alpha-2 region code for the administrative region through which the given intersection passes. The intersection is identified by its step index and intersection index.
Expand Down Expand Up @@ -422,6 +451,44 @@ extension RouteLeg: CustomQuickLookConvertible {
}
}

extension RouteLeg {
/**
Live-traffic related closure that occured along the route.
*/
public struct Closure: Codable, Equatable, ForeignMemberContainer {
public var foreignMembers: JSONObject = [:]

private enum CodingKeys: String, CodingKey {
case geometryIndexStart = "geometry_index_start"
case geometryIndexEnd = "geometry_index_end"
}

/**
The range of segments within the current leg, where the closure spans.
*/
public var shapeIndexRange: Range<Int>

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let geometryIndexStart = try container.decode(Int.self, forKey: .geometryIndexStart)
let geometryIndexEnd = try container.decode(Int.self, forKey: .geometryIndexEnd)
shapeIndexRange = geometryIndexStart..<geometryIndexEnd

try decodeForeignMembers(notKeyedBy: CodingKeys.self, with: decoder)
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(shapeIndexRange.lowerBound, forKey: .geometryIndexStart)
try container.encode(shapeIndexRange.upperBound, forKey: .geometryIndexEnd)

try encodeForeignMembers(notKeyedBy: CodingKeys.self, to: encoder)
}
}
}

public extension Array where Element == RouteLeg {
/**
Populates source and destination information for each leg with waypoint information, typically gathered from `DirectionsOptions`.
Expand Down
113 changes: 112 additions & 1 deletion Sources/MapboxDirections/RouteRefreshResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public struct RouteRefreshResponse: ForeignMemberContainer {
/**
A skeleton route that contains only the time-sensitive information that has been updated.
Use the `Route.refreshLegAttributes(from:)`, `Route.refreshLegAttributes(from:legIndex:legShapeIndex:)` and `Route.refreshLegIncidents(from:)`, `Route.refreshLegIncidents(from:legIndex:legShapeIndex:)` methods to merge this object with the original route to continue using the original route with updated information.
Use the `Route.refreshLegAttributes(from:)`, `Route.refreshLegAttributes(from:legIndex:legShapeIndex:)`, `Route.refreshLegIncidents(from:)`, `Route.refreshLegIncidents(from:legIndex:legShapeIndex:)`, `Route.refreshLegClosures(from:legIndex:legShapeIndex:)` or `Route.refresh(from:refreshParameters:)` methods to merge this object with the original route to continue using the original route with updated information.
*/
public var route: RefreshedRoute

Expand Down Expand Up @@ -101,6 +101,96 @@ extension RouteRefreshResponse: Codable {
}

extension Route {
/**
Configuration for applying `RouteRefreshSource` updates to a route.
*/
public struct RefreshParameters {
/**
Configuration for type of information to be merged during refreshing.
*/
public struct PropertiesToMerge: OptionSet {
public var rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
/**
Will update route annotations.
*/
static public let annotations = PropertiesToMerge(rawValue: 1)
/**
Will update route `Incidents`.
*/
static public let incidents = PropertiesToMerge(rawValue: 1 << 1)
/**
Will update route `Closures`.
*/
static public let closures = PropertiesToMerge(rawValue: 1 << 2)

/**
Includes `annotations`, `incidents` and `closures`.
*/
static public let everything: PropertiesToMerge = [PropertiesToMerge.annotations, PropertiesToMerge.closures, PropertiesToMerge.incidents]
}
/**
Configures starting point to run the partial route refreshing.
*/
public struct StartingIndex {
/**
The index of a leg, from which to start applying the refreshed data.
*/
public let legIndex: Int
/**
Index of a geometry of the `legIndex` leg, where to start refreshing from.
*/
public let legShapeIndex: Int
/**
Creates new `StartingIndex`.
*/
public init(legIndex: Int, legShapeIndex: Int) {
self.legIndex = legIndex
self.legShapeIndex = legShapeIndex
}
}
/**
Configuration for type of information to be merged during refreshing.
*/
public var propertiesToMerge: PropertiesToMerge
/**
Configures starting point to run the partial route refreshing.
If set to `nil` - route will be refreshed from the beginning.
*/
public var startingIndex: StartingIndex?
/**
Creates new `RefreshParameters`.
*/
public init(propertiesToMerge: PropertiesToMerge = .everything, startingIndex: StartingIndex? = nil) {
self.propertiesToMerge = propertiesToMerge
self.startingIndex = startingIndex
}
}

/**
Merges various properties from `refreshedRoute` legs to the reciever.
- parameter refreshedRoute: The route containing leg data to merge into the receiver. If this route contains fewer legs than the receiver, this method skips legs from the beginning of the route to make up the difference, so that merging the data from a one-leg route affects only the last leg of the receiver.
- parameter refreshParameters: Configuration about what exactly should be updated and from which geometry position.
*/
public func refresh(from refreshedRoute: RouteRefreshSource, refreshParameters: RefreshParameters = RefreshParameters()) {
let legIndex = refreshParameters.startingIndex?.legIndex ?? 0
let legShapeIndex = refreshParameters.startingIndex?.legShapeIndex ?? 0

if refreshParameters.propertiesToMerge.contains(.annotations) {
refreshLegAttributes(from: refreshedRoute, legIndex: legIndex, legShapeIndex: legShapeIndex)
}
if refreshParameters.propertiesToMerge.contains(.incidents) {
refreshLegIncidents(from: refreshedRoute, legIndex: legIndex, legShapeIndex: legShapeIndex)
}
if refreshParameters.propertiesToMerge.contains(.closures) {
refreshLegClosures(from: refreshedRoute, legIndex: legIndex, legShapeIndex: legShapeIndex)
}
}

/**
Merges the attributes of the given route’s legs into the receiver’s legs.
Expand Down Expand Up @@ -157,4 +247,25 @@ extension Route {
}
}
}

/**
Merges the closures of the given route’s legs into the receiver’s legs.
- parameter refreshedRoute: The route containing leg closures to merge into the receiver. If this route contains fewer legs than the receiver, this method skips legs from the beginning of the route to make up the difference, so that merging the closures from a one-leg route affects only the last leg of the receiver.
- parameter legIndex: The index of a leg, from which to start applying the refreshed closures.
- parameter legShapeIndex: Index of a geometry of the `legIndex` leg, where to start refreshing from.
*/
public func refreshLegClosures(from refreshedRoute: RouteRefreshSource, legIndex: Int = 0, legShapeIndex: Int = 0) {
let endRefreshIndex = legIndex + refreshedRoute.refreshedLegs.count
for (index, leg) in legs.enumerated() {
if (legIndex..<endRefreshIndex).contains(index) {
let refreshedLeg = refreshedRoute.refreshedLegs[index-legIndex]
let startIndex = index == legIndex ? legShapeIndex : 0
leg.refreshClosures(newClosures: refreshedLeg.refreshedClosures,
startLegShapeIndex: startIndex)
} else {
leg.closures = nil
}
}
}
}
12 changes: 12 additions & 0 deletions Sources/MapboxDirections/RouteRefreshSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ public protocol RouteRefreshSource {
public protocol RouteLegRefreshSource {
var refreshedAttributes: RouteLeg.Attributes { get }
var refreshedIncidents: [Incident]? { get }
var refreshedClosures: [RouteLeg.Closure]? { get }
}

public extension RouteLegRefreshSource {
var refreshedIncidents: [Incident]? {
return nil
}
var refreshedClosures: [RouteLeg.Closure]? {
return nil
}
}

extension Route: RouteRefreshSource {
Expand All @@ -35,6 +39,10 @@ extension RouteLeg: RouteLegRefreshSource {
public var refreshedIncidents: [Incident]? {
incidents
}

public var refreshedClosures: [RouteLeg.Closure]? {
closures
}
}

extension RefreshedRoute: RouteRefreshSource {
Expand All @@ -51,4 +59,8 @@ extension RefreshedRouteLeg: RouteLegRefreshSource {
public var refreshedIncidents: [Incident]? {
incidents
}

public var refreshedClosures: [RouteLeg.Closure]? {
closures
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
"route": {
"legs": [
{
"closures": [
{
"geometry_index_start": 16,
"geometry_index_end": 20,
}
],
"incidents": [
{
"id": "12727074056824787215",
Expand Down Expand Up @@ -337,6 +343,12 @@
}
},
{
"closures": [
{
"geometry_index_start": 9,
"geometry_index_end": 14,
}
],
"incidents": [
{
"id": "12779545487967908590",
Expand Down
Loading

0 comments on commit 76c46e9

Please sign in to comment.