-
Notifications
You must be signed in to change notification settings - Fork 88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Structured road names, codes, destinations #91
Changes from 3 commits
d46b1da
4192690
09b6300
7c4491a
1ff4054
ce32527
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -371,6 +371,60 @@ public enum ManeuverDirection: Int, CustomStringConvertible { | |
} | ||
} | ||
|
||
extension String { | ||
internal func tagValuesSeparatedByString(separator: String) -> [String] { | ||
return componentsSeparatedByString(separator).map { $0.stringByTrimmingCharactersInSet(.whitespaceCharacterSet()) }.filter { !$0.isEmpty } | ||
} | ||
} | ||
|
||
/** | ||
Encapsulates all the information about a road. | ||
*/ | ||
struct Road { | ||
let names: [String]? | ||
let codes: [String]? | ||
let destinations: [String]? | ||
let destinationCodes: [String]? | ||
|
||
init(name: String?, ref: String?, destination: String?) { | ||
var codes: [String]? | ||
if let names = name, let gloss = ref ?? destination { | ||
// Mapbox Directions API v5 encodes the ref separately from the name but redundantly includes the ref or destination in the name for backwards compatibility. Remove the ref or destination from the name. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To clarify: This is not true, the |
||
let parenthetical = "(\(gloss))" | ||
if names == gloss { | ||
self.names = nil | ||
} else { | ||
self.names = names.stringByReplacingOccurrencesOfString(parenthetical, withString: "").tagValuesSeparatedByString(";") | ||
} | ||
codes = gloss.tagValuesSeparatedByString(";") | ||
} else if let names = name, let codesRange = names.rangeOfString("\\(.+?\\)$", options: .RegularExpressionSearch, range: names.startIndex..<names.endIndex) { | ||
// Mapbox Directions API v4 encodes the ref or destination inside a parenthetical. Remove the ref or destination from the name. | ||
let parenthetical = names.substringWithRange(codesRange) | ||
if names == ref ?? destination { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above, remove |
||
self.names = nil | ||
} else { | ||
self.names = names.stringByReplacingOccurrencesOfString(parenthetical, withString: "").tagValuesSeparatedByString(";") | ||
} | ||
codes = parenthetical.stringByTrimmingCharactersInSet(NSCharacterSet(charactersInString: "()")).tagValuesSeparatedByString(";") | ||
} else { | ||
self.names = name?.tagValuesSeparatedByString(";") | ||
codes = nil | ||
} | ||
|
||
// Mapbox Directions API v5 combines the destination’s ref and name. | ||
if let destination = destination where destination.containsString(": ") { | ||
let destinationComponents = destination.componentsSeparatedByString(": ") | ||
self.destinationCodes = destinationComponents.first?.tagValuesSeparatedByString(",") | ||
self.destinations = destinationComponents.dropFirst().joinWithSeparator(": ").tagValuesSeparatedByString(",") | ||
} else { | ||
self.destinationCodes = nil | ||
self.destinations = destination?.tagValuesSeparatedByString(",") | ||
} | ||
|
||
self.codes = codes | ||
} | ||
} | ||
|
||
/** | ||
A `RouteStep` object represents a single distinct maneuver along a route and the approach to the next maneuver. The route step object corresponds to a single instruction the user must follow to complete a portion of the route. For example, a step might require the user to turn then follow a road. | ||
|
||
|
@@ -382,7 +436,11 @@ public class RouteStep: NSObject, NSSecureCoding { | |
|
||
internal init(finalHeading: CLLocationDirection?, maneuverType: ManeuverType?, maneuverDirection: ManeuverDirection?, maneuverLocation: CLLocationCoordinate2D, name: String?, coordinates: [CLLocationCoordinate2D]?, json: JSONDictionary) { | ||
transportType = TransportType(description: json["mode"] as! String) | ||
destinations = json["destinations"] as? String | ||
|
||
let road = Road(name: name, ref: json["ref"] as? String, destination: json["destinations"] as? String) | ||
names = road.names | ||
codes = road.codes ?? road.destinationCodes | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why are destinationCodes mixed into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The idea here was to make it easier for applications to display a route shield for an upcoming ramp based on I’ve gone back and forth quite a bit about whether destination codes should be conflated with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed. |
||
destinations = road.destinations | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume these are properties on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I’ll remove |
||
|
||
let maneuver = json["maneuver"] as! JSONDictionary | ||
instructions = maneuver["instruction"] as! String | ||
|
@@ -399,8 +457,6 @@ public class RouteStep: NSObject, NSSecureCoding { | |
self.maneuverDirection = maneuverDirection | ||
exitIndex = maneuver["exit"] as? Int | ||
|
||
self.name = name | ||
|
||
self.maneuverLocation = maneuverLocation | ||
self.coordinates = coordinates | ||
} | ||
|
@@ -471,14 +527,15 @@ public class RouteStep: NSObject, NSSecureCoding { | |
exitIndex = decoder.containsValueForKey("exitIndex") ? decoder.decodeIntegerForKey("exitIndex") : nil | ||
distance = decoder.decodeDoubleForKey("distance") | ||
expectedTravelTime = decoder.decodeDoubleForKey("expectedTravelTime") | ||
name = decoder.decodeObjectForKey("name") as? String | ||
names = decoder.decodeObjectOfClasses([NSArray.self, NSString.self], forKey: "names") as? [String] | ||
|
||
guard let transportTypeDescription = decoder.decodeObjectOfClass(NSString.self, forKey: "transportType") as? String else { | ||
return nil | ||
} | ||
transportType = TransportType(description: transportTypeDescription) | ||
|
||
destinations = decoder.decodeObjectOfClass(NSString.self, forKey: "destinations") as? String | ||
codes = decoder.decodeObjectOfClasses([NSArray.self, NSString.self], forKey: "codes") as? [String] | ||
destinations = decoder.decodeObjectOfClasses([NSArray.self, NSString.self], forKey: "destinations") as? [String] | ||
|
||
intersections = decoder.decodeObjectOfClasses([NSArray.self, Intersection.self], forKey: "intersections") as? [Intersection] | ||
} | ||
|
@@ -519,8 +576,9 @@ public class RouteStep: NSObject, NSSecureCoding { | |
|
||
coder.encodeDouble(distance, forKey: "distance") | ||
coder.encodeDouble(expectedTravelTime, forKey: "expectedTravelTime") | ||
coder.encodeObject(name, forKey: "name") | ||
coder.encodeObject(names, forKey: "names") | ||
coder.encodeObject(transportType?.description, forKey: "transportType") | ||
coder.encodeObject(codes, forKey: "codes") | ||
coder.encodeObject(destinations, forKey: "destinations") | ||
} | ||
|
||
|
@@ -637,11 +695,20 @@ public class RouteStep: NSObject, NSSecureCoding { | |
public let expectedTravelTime: NSTimeInterval | ||
|
||
/** | ||
The name of the road or path leading from this step’s maneuver to the next step’s maneuver. | ||
The names of the road or path leading from this step’s maneuver to the next step’s maneuver. | ||
|
||
If the maneuver is a turning maneuver, the step’s name is the name of the road or path onto which the user turns. If you display the name to the user, you may need to abbreviate common words like “East” or “Boulevard” to ensure that it fits in the allotted space. | ||
*/ | ||
public let names: [String]? | ||
|
||
/** | ||
Any route reference codes assigned to the road or path leading from this step’s maneuver to the next step’s maneuver. | ||
|
||
A route reference code commonly consists of an alphabetic network code, a space, and a route number. You should not assume that the network code is globally unique: for example, a network code of “NH” may indicate a “National Highway” or “New Hampshire”. Moreover, a route number may not even uniqely identify a route within a given network. | ||
|
||
If the maneuver is a turning maneuver, the step’s name is the name of the road or path onto which the user turns. The name includes any route designations assigned to the road. If you display the name to the user, you may need to abbreviate common words like “East” or “Boulevard” to ensure that it fits in the allotted space. | ||
If a highway off-ramp is part of a numbered route, its reference code takes precedence over any reference code associated with the ramp’s destinations. | ||
*/ | ||
public let name: String? | ||
public let codes: [String]? | ||
|
||
// MARK: Getting Additional Step Details | ||
|
||
|
@@ -657,7 +724,7 @@ public class RouteStep: NSObject, NSSecureCoding { | |
|
||
This property is typically available in steps leading to or from a freeway or expressway. | ||
*/ | ||
public let destinations: String? | ||
public let destinations: [String]? | ||
|
||
/** | ||
An array of intersections along the step. | ||
|
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,7 +40,7 @@ class V5Tests: XCTestCase { | |
XCTAssertNil(error, "Error: \(error)") | ||
|
||
XCTAssertNotNil(routes) | ||
XCTAssertEqual(routes!.count, 1) | ||
XCTAssertEqual(routes!.count, 2) | ||
route = routes!.first! | ||
|
||
expectation.fulfill() | ||
|
@@ -54,7 +54,7 @@ class V5Tests: XCTestCase { | |
|
||
XCTAssertNotNil(route) | ||
XCTAssertNotNil(route!.coordinates) | ||
XCTAssertEqual(route!.coordinates!.count, 842) | ||
XCTAssertEqual(route!.coordinates!.count, 28_442) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the semantics of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
// confirming actual decoded values is important because the Directions API | ||
// uses an atypical precision level for polyline encoding | ||
|
@@ -63,40 +63,47 @@ class V5Tests: XCTestCase { | |
XCTAssertEqual(route!.legs.count, 1) | ||
|
||
let leg = route!.legs.first! | ||
XCTAssertEqual(leg.name, "CA 24, Camino Tassajara") | ||
XCTAssertEqual(leg.steps.count, 22) | ||
XCTAssertEqual(leg.name, "I 80, I 80;US 30") | ||
XCTAssertEqual(leg.steps.count, 59) | ||
|
||
let step = leg.steps[16] | ||
XCTAssertEqual(round(step.distance), 166) | ||
XCTAssertEqual(round(step.expectedTravelTime), 13) | ||
XCTAssertEqual(step.instructions, "Take the ramp on the right") | ||
let step = leg.steps[43] | ||
XCTAssertEqual(round(step.distance), 688) | ||
XCTAssertEqual(round(step.expectedTravelTime), 30) | ||
XCTAssertEqual(step.instructions, "Take the ramp on the right towards Washington") | ||
|
||
XCTAssertEqual(step.name, "") | ||
XCTAssertEqual(step.destinations, "Sycamore Valley Road") | ||
XCTAssertNil(step.names) | ||
XCTAssertNotNil(step.destinations) | ||
XCTAssertEqual(step.destinations ?? [], ["Washington"]) | ||
XCTAssertEqual(step.maneuverType, ManeuverType.TakeOffRamp) | ||
XCTAssertEqual(step.maneuverDirection, ManeuverDirection.SlightRight) | ||
XCTAssertEqual(step.initialHeading, 182) | ||
XCTAssertEqual(step.finalHeading, 196) | ||
XCTAssertEqual(step.initialHeading, 90) | ||
XCTAssertEqual(step.finalHeading, 96) | ||
|
||
XCTAssertNotNil(step.coordinates) | ||
XCTAssertEqual(step.coordinates!.count, 5) | ||
XCTAssertEqual(step.coordinates!.count, 17) | ||
XCTAssertEqual(step.coordinates!.count, Int(step.coordinateCount)) | ||
let coordinate = step.coordinates!.first! | ||
XCTAssertEqual(round(coordinate.latitude), 38) | ||
XCTAssertEqual(round(coordinate.longitude), -122) | ||
XCTAssertEqual(round(coordinate.latitude), 39) | ||
XCTAssertEqual(round(coordinate.longitude), -77) | ||
|
||
XCTAssertEqual(leg.steps[18].name, "Sycamore Valley Road West") | ||
XCTAssertNil(leg.steps[28].names) | ||
XCTAssertEqual(leg.steps[28].codes ?? [], ["I 80"]) | ||
XCTAssertEqual(leg.steps[28].destinations ?? [], ["Toll Road"]) | ||
|
||
let intersection = step.intersections!.first! | ||
XCTAssertEqual(intersection.outletIndexes, NSIndexSet(indexesInRange: NSRange(location: 1, length: 2))) | ||
XCTAssertEqual(intersection.approachIndex, 0) | ||
XCTAssertEqual(intersection.outletIndex, 2) | ||
XCTAssertEqual(intersection.headings, [0, 180, 195]) | ||
XCTAssertNotNil(intersection.location.latitude) | ||
XCTAssertNotNil(intersection.location.longitude) | ||
XCTAssertEqual(intersection.usableApproachLanes, NSIndexSet(indexesInRange: NSRange(location: 0, length: 2))) | ||
let intersections = leg.steps[40].intersections | ||
XCTAssertNotNil(intersections) | ||
XCTAssertEqual(intersections?.count, 7) | ||
let intersection = intersections?[2] | ||
XCTAssertEqual(intersection?.outletIndexes.containsIndex(0), true) | ||
XCTAssertEqual(intersection?.outletIndexes.containsIndexesInRange(NSRange(location: 2, length: 2)), true) | ||
XCTAssertEqual(intersection?.approachIndex, 1) | ||
XCTAssertEqual(intersection?.outletIndex, 3) | ||
XCTAssertEqual(intersection?.headings ?? [], [15, 90, 195, 270]) | ||
XCTAssertNotNil(intersection?.location.latitude) | ||
XCTAssertNotNil(intersection?.location.longitude) | ||
XCTAssertEqual(intersection?.usableApproachLanes ?? [], NSIndexSet(indexesInRange: NSRange(location: 1, length: 3))) | ||
|
||
let lane = intersection.approachLanes?.first | ||
let lane = intersection?.approachLanes?.first | ||
let indications = lane?.indications | ||
XCTAssertNotNil(indications) | ||
XCTAssertTrue(indications!.contains(.Left)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it might make sense to add a test for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a test for this case. |
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand the semantic meaning of
gloss
and the dictionary did not help me understand. The case that in Mapbox Directions v5destination
is mixed intoname
can not exist. Likewise it makes no sense to me thatcodes
would ever have the contents ofdestination
in them.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Turns out Directions API v5 used to conflate
destination
withref
in the parenthetical, but it no longer does so, much to my relief. I’ll gladly remove the fallbacks.