Skip to content

osrm-text-instructions v0.7.0 #38

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

Merged
merged 5 commits into from
Sep 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
language: objective-c
osx_image: xcode8.2
osx_image: xcode8.3
cache:
directories:
- Carthage
Expand Down
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
github "mapbox/MapboxDirections.swift" "v0.10.1"
github "mapbox/MapboxDirections.swift" "v0.10.4"
github "raphaelmor/Polyline" "v4.1.1"
122 changes: 72 additions & 50 deletions OSRMTextInstructions/OSRMTextInstructions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,48 @@ extension String {
public var sentenceCased: String {
return String(characters.prefix(1)).uppercased() + String(characters.dropFirst())
}

/**
Replaces `{tokens}` in the receiver using the given closure.
*/
public func replacingTokens(using interpolator: ((TokenType) -> String)) -> String {
let scanner = Scanner(string: self)
scanner.charactersToBeSkipped = nil
var result = ""
while !scanner.isAtEnd {
var buffer: NSString?

if scanner.scanUpTo("{", into: &buffer) {
result += buffer! as String
}
guard scanner.scanString("{", into: nil) else {
continue
}

var token: NSString?
guard scanner.scanUpTo("}", into: &token) else {
continue
}

if scanner.scanString("}", into: nil) {
if let tokenType = TokenType(description: token! as String) {
result += interpolator(tokenType)
}
} else {
result += token! as String
}
}

// remove excess spaces
result = result.replacingOccurrences(of: "\\s\\s", with: " ", options: .regularExpression)

// capitalize
let meta = OSRMTextInstructionsStrings["meta"] as! [String: Any]
if meta["capitalizeFirstLetter"] as? Bool ?? false {
result = result.sentenceCased
}
return result
}
}

public class OSRMInstructionFormatter: Formatter {
Expand Down Expand Up @@ -192,7 +234,18 @@ public class OSRMInstructionFormatter: Formatter {
let isMotorway = roadClasses?.contains(.motorway) ?? false

if let name = name, let ref = ref, name != ref, !isMotorway {
wayName = modifyValueByKey != nil ? "\(modifyValueByKey!(.wayName, name)) (\(modifyValueByKey!(.code, ref)))" : "\(name) (\(ref))"
let phrases = instructions["phrase"] as! [String: String]
let phrase = phrases["name and ref"]!
wayName = phrase.replacingTokens(using: { (tokenType) -> String in
switch tokenType {
case .wayName:
return modifyValueByKey?(.wayName, name) ?? name
case .code:
return modifyValueByKey?(.code, ref) ?? ref
default:
fatalError("Unexpected token type \(tokenType) in name-and-ref phrase")
}
})
} else if let ref = ref, isMotorway, let decimalRange = ref.rangeOfCharacter(from: .decimalDigits), !decimalRange.isEmpty {
wayName = modifyValueByKey != nil ? "\(modifyValueByKey!(.code, ref))" : ref
} else if name == nil, let ref = ref {
Expand Down Expand Up @@ -254,58 +307,27 @@ public class OSRMInstructionFormatter: Formatter {
if step.finalHeading != nil { bearing = Int(step.finalHeading! as Double) }

// Replace tokens
let scanner = Scanner(string: instruction)
scanner.charactersToBeSkipped = nil
var result = ""
while !scanner.isAtEnd {
var buffer: NSString?

if scanner.scanUpTo("{", into: &buffer) {
result += buffer! as String
}
guard scanner.scanString("{", into: nil) else {
continue
}

var token: NSString?
guard scanner.scanUpTo("}", into: &token) else {
continue
let result = instruction.replacingTokens { (tokenType) -> String in
var replacement: String
switch tokenType {
case .code: replacement = step.codes?.first ?? ""
case .wayName: replacement = wayName
case .destination: replacement = destination
case .exitCode: replacement = exitCode
case .exitIndex: replacement = exitOrdinal
case .rotaryName: replacement = rotaryName
case .laneInstruction: replacement = laneInstruction ?? ""
case .modifier: replacement = modifierConstant
case .direction: replacement = directionFromDegree(degree: bearing)
case .wayPoint: replacement = nthWaypoint ?? ""
case .firstInstruction, .secondInstruction, .distance:
fatalError("Unexpected token type \(tokenType) in individual instruction")
}

if scanner.scanString("}", into: nil) {
if let tokenType = TokenType(description: token! as String) {
var replacement: String
switch tokenType {
case .code: replacement = step.codes?.first ?? ""
case .wayName: replacement = wayName
case .destination: replacement = destination
case .exitCode: replacement = exitCode
case .exitIndex: replacement = exitOrdinal
case .rotaryName: replacement = rotaryName
case .laneInstruction: replacement = laneInstruction ?? ""
case .modifier: replacement = modifierConstant
case .direction: replacement = directionFromDegree(degree: bearing)
case .wayPoint: replacement = nthWaypoint ?? ""
}
if tokenType == .wayName {
result += replacement // already modified above
} else {
result += modifyValueByKey?(tokenType, replacement) ?? replacement
}
}
if tokenType == .wayName {
return replacement // already modified above
} else {
result += token! as String
return modifyValueByKey?(tokenType, replacement) ?? replacement
}

}

// remove excess spaces
result = result.replacingOccurrences(of: "\\s\\s", with: " ", options: .regularExpression)

// capitalize
let meta = OSRMTextInstructionsStrings["meta"] as! [String: Any]
if meta["capitalizeFirstLetter"] as? Bool ?? false {
result = result.sentenceCased
}

return result
Expand Down
21 changes: 19 additions & 2 deletions OSRMTextInstructions/TokenType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Foundation

@objc(OSRMTokenType)
public enum TokenType: Int, CustomStringConvertible {

// For individual instructions
case wayName
case destination
case rotaryName
Expand All @@ -14,10 +14,15 @@ public enum TokenType: Int, CustomStringConvertible {
case wayPoint
case code

// For phrases
case firstInstruction
case secondInstruction
case distance

public init?(description: String) {
let type: TokenType
switch description {
case "way_name":
case "way_name", "name":
type = .wayName
case "destination":
type = .destination
Expand All @@ -37,6 +42,12 @@ public enum TokenType: Int, CustomStringConvertible {
type = .wayPoint
case "ref":
type = .code
case "instruction_one":
type = .firstInstruction
case "instruction_two":
type = .secondInstruction
case "distance":
type = .distance
default:
return nil
}
Expand Down Expand Up @@ -65,6 +76,12 @@ public enum TokenType: Int, CustomStringConvertible {
return "nth"
case .code:
return "ref"
case .firstInstruction:
return "instruction_one"
case .secondInstruction:
return "instruction_two"
case .distance:
return "distance"
}
}
}
84 changes: 50 additions & 34 deletions OSRMTextInstructionsTests/OSRMTextInstructionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,31 +21,59 @@ class OSRMTextInstructionsTests: XCTestCase {
}

func testFixtures() {
do {
let bundle = Bundle(for: OSRMTextInstructionsTests.self)
let url = bundle.url(forResource: "v5", withExtension: nil, subdirectory: "osrm-text-instructions/test/fixtures/")!

let directoryContents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: [])
for type in directoryContents {
let typeDirectoryContents = try FileManager.default.contentsOfDirectory(at: type, includingPropertiesForKeys: nil, options: [])
for fixture in typeDirectoryContents {
let bundle = Bundle(for: OSRMTextInstructionsTests.self)
let url = bundle.url(forResource: "v5", withExtension: nil, subdirectory: "osrm-text-instructions/test/fixtures/")!

let phrases = instructions.instructions["phrase"] as! [String: String]

var directoryContents: [URL] = []
XCTAssertNoThrow(directoryContents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil, options: []))
for type in directoryContents {
var typeDirectoryContents: [URL] = []
XCTAssertNoThrow(typeDirectoryContents = try FileManager.default.contentsOfDirectory(at: type, includingPropertiesForKeys: nil, options: []))
for fixture in typeDirectoryContents {
if type.lastPathComponent == "phrase" {
var rawJSON = Data()
XCTAssertNoThrow(rawJSON = try Data(contentsOf: fixture, options: []))

var json: [String: Any] = [:]
XCTAssertNoThrow(json = try JSONSerialization.jsonObject(with: rawJSON, options: []) as! [String: Any])

let phrase = fixture.deletingPathExtension().lastPathComponent.replacingOccurrences(of: "_", with: " ")
let fixtureOptions = json["options"] as! [String: String]

let expectedValue = (json["phrases"] as! [String: String])["en"]
let actualValue = phrases[phrase]?.replacingTokens(using: { (tokenType) -> String in
var replacement: String?
switch tokenType {
case .firstInstruction:
replacement = fixtureOptions["instruction_one"]
case .secondInstruction:
replacement = fixtureOptions["instruction_two"]
case .distance:
replacement = fixtureOptions["distance"]
default:
XCTFail("Unexpected token type \(tokenType) in phrase \(phrase)")
}
XCTAssertNotNil(replacement, "Missing fixture option for \(tokenType)")
return replacement ?? ""
})
XCTAssertEqual(expectedValue, actualValue, fixture.path)
} else {
// parse fixture
guard let json = getFixture(url: fixture) else {
XCTAssert(false, "invalid json")
return
}
let json = getFixture(url: fixture)
let options = json["options"] as? [String: Any]

let step = RouteStep(json: json["step"] as! [String: Any])

var roadClasses: RoadClasses? = nil
if let classes = options?["classes"] as? [String] {
roadClasses = RoadClasses(descriptions: classes)
}

// compile instruction
let instruction = instructions.string(for: step, legIndex: options?["legIndex"] as? Int, numberOfLegs: options?["legCount"] as? Int, roadClasses: roadClasses, modifyValueByKey: nil)

// check generated instruction against fixture
XCTAssertEqual(
json["instruction"] as? String,
Expand All @@ -54,35 +82,23 @@ class OSRMTextInstructionsTests: XCTestCase {
)
}
}
} catch let error as NSError {
print(error.localizedDescription)
XCTAssert(false)
}
}

func getFixture(url: URL) -> NSMutableDictionary? {
func getFixture(url: URL) -> [String: Any] {
var rawJSON = Data()
do {
rawJSON = try Data(contentsOf: url, options: [])
} catch {
XCTAssert(false)
}
XCTAssertNoThrow(rawJSON = try Data(contentsOf: url, options: []))

let json: NSDictionary
do {
json = try JSONSerialization.jsonObject(with: rawJSON, options: []) as! NSDictionary
} catch {
XCTAssert(false)
return nil
}
var json: [String: Any] = [:]
XCTAssertNoThrow(json = try JSONSerialization.jsonObject(with: rawJSON, options: []) as! [String: Any])

// provide default values for properties that RouteStep
// needs, but that our not in the fixtures
let fixture: NSMutableDictionary = [:]
let maneuver: NSMutableDictionary = [
var fixture: [String: Any] = [:]
var maneuver: [String: Any] = [
"location": [ 1.0, 1.0 ]
]
let step: NSMutableDictionary = [
var step: [String: Any] = [
"mode": "driving"
]

Expand Down
2 changes: 1 addition & 1 deletion osrm-text-instructions