Skip to content

Commit 68e93da

Browse files
committed
Attributed string formatting
Added a new variant of string(for:legIndex:numberOfLegs:roadClasses:modifyValueByKey:) that deals in attributed strings.
1 parent b48a5a0 commit 68e93da

File tree

1 file changed

+107
-19
lines changed

1 file changed

+107
-19
lines changed

OSRMTextInstructions/OSRMTextInstructions.swift

Lines changed: 107 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ import MapboxDirections
44
// Will automatically read localized Instructions.plist
55
let OSRMTextInstructionsStrings = NSDictionary(contentsOfFile: Bundle(for: OSRMInstructionFormatter.self).path(forResource: "Instructions", ofType: "plist")!)!
66

7-
extension String {
8-
public var sentenceCased: String {
9-
return String(characters.prefix(1)).uppercased() + String(characters.dropFirst())
10-
}
7+
protocol Tokenized {
8+
associatedtype T
119

1210
/**
1311
Replaces `{tokens}` in the receiver using the given closure.
1412
*/
13+
func replacingTokens(using interpolator: ((TokenType) -> T)) -> T
14+
}
15+
16+
extension String: Tokenized {
17+
public var sentenceCased: String {
18+
return String(characters.prefix(1)).uppercased() + String(characters.dropFirst())
19+
}
20+
1521
public func replacingTokens(using interpolator: ((TokenType) -> String)) -> String {
1622
let scanner = Scanner(string: self)
1723
scanner.charactersToBeSkipped = nil
@@ -52,6 +58,48 @@ extension String {
5258
}
5359
}
5460

61+
extension NSAttributedString: Tokenized {
62+
func replacingTokens(using interpolator: ((TokenType) -> NSAttributedString)) -> NSAttributedString {
63+
let scanner = Scanner(string: string)
64+
scanner.charactersToBeSkipped = nil
65+
let result = NSMutableAttributedString()
66+
while !scanner.isAtEnd {
67+
var buffer: NSString?
68+
69+
if scanner.scanUpTo("{", into: &buffer) {
70+
result.append(NSAttributedString(string: buffer! as String))
71+
}
72+
guard scanner.scanString("{", into: nil) else {
73+
continue
74+
}
75+
76+
var token: NSString?
77+
guard scanner.scanUpTo("}", into: &token) else {
78+
continue
79+
}
80+
81+
if scanner.scanString("}", into: nil) {
82+
if let tokenType = TokenType(description: token! as String) {
83+
result.append(interpolator(tokenType))
84+
}
85+
} else {
86+
result.append(NSAttributedString(string: token! as String))
87+
}
88+
}
89+
90+
// remove excess spaces
91+
let wholeRange = NSRange(location: 0, length: result.mutableString.length)
92+
result.mutableString.replaceOccurrences(of: "\\s\\s", with: " ", options: .regularExpression, range: wholeRange)
93+
94+
// capitalize
95+
let meta = OSRMTextInstructionsStrings["meta"] as! [String: Any]
96+
if meta["capitalizeFirstLetter"] as? Bool ?? false {
97+
result.replaceCharacters(in: NSRange(location: 0, length: 1), with: String(result.string.characters.first!).uppercased())
98+
}
99+
return result as NSAttributedString
100+
}
101+
}
102+
55103
public class OSRMInstructionFormatter: Formatter {
56104
let version: String
57105
let instructions: [String: Any]
@@ -169,14 +217,34 @@ public class OSRMInstructionFormatter: Formatter {
169217
/**
170218
Creates an instruction given a step and options.
171219

172-
- parameter step:
220+
- parameter step: The step to format.
173221
- parameter legIndex: Current leg index the user is currently on.
174222
- parameter numberOfLegs: Total number of `RouteLeg` for the given `Route`.
175223
- parameter roadClasses: Option set representing the classes of road for the `RouteStep`.
176224
- parameter modifyValueByKey: Allows for mutating the instruction at given parts of the instruction.
177225
- returns: An instruction as a `String`.
178226
*/
179227
public func string(for obj: Any?, legIndex: Int?, numberOfLegs: Int?, roadClasses: RoadClasses? = RoadClasses([]), modifyValueByKey: ((TokenType, String) -> String)?) -> String? {
228+
var modifyAttributedValueByKey: ((TokenType, NSAttributedString) -> NSAttributedString)?
229+
if let modifyValueByKey = modifyValueByKey {
230+
modifyAttributedValueByKey = { (key: TokenType, value: NSAttributedString) -> NSAttributedString in
231+
return NSAttributedString(string: modifyValueByKey(key, value.string))
232+
}
233+
}
234+
return attributedString(for: obj, legIndex: legIndex, numberOfLegs: numberOfLegs, roadClasses: roadClasses, modifyValueByKey: modifyAttributedValueByKey)?.string
235+
}
236+
237+
/**
238+
Creates an instruction as an attributed string given a step and options.
239+
240+
- parameter step: The step to format.
241+
- parameter legIndex: Current leg index the user is currently on.
242+
- parameter numberOfLegs: Total number of `RouteLeg` for the given `Route`.
243+
- parameter roadClasses: Option set representing the classes of road for the `RouteStep`.
244+
- parameter modifyValueByKey: Allows for mutating the instruction at given parts of the instruction.
245+
- returns: An instruction as an `NSAttributedString`.
246+
*/
247+
public func attributedString(for obj: Any?, legIndex: Int?, numberOfLegs: Int?, roadClasses: RoadClasses? = RoadClasses([]), modifyValueByKey: ((TokenType, NSAttributedString) -> NSAttributedString)?) -> NSAttributedString? {
180248
guard let step = obj as? RouteStep else {
181249
return nil
182250
}
@@ -198,14 +266,14 @@ public class OSRMInstructionFormatter: Formatter {
198266

199267
var instructionObject: InstructionsByToken
200268
var rotaryName = ""
201-
var wayName: String
269+
var wayName: NSAttributedString
202270
switch type {
203271
case .takeRotary, .takeRoundabout:
204272
// Special instruction types have an intermediate level keyed to “default”.
205273
let instructionsByModifier = instructions[type.description] as! [String: InstructionsByModifier]
206274
let defaultInstructions = instructionsByModifier["default"]!
207275

208-
wayName = step.exitNames?.first ?? ""
276+
wayName = NSAttributedString(string: step.exitNames?.first ?? "")
209277
if let _rotaryName = step.names?.first, let _ = step.exitIndex, let obj = defaultInstructions["name_exit"] {
210278
instructionObject = obj
211279
rotaryName = _rotaryName
@@ -234,24 +302,43 @@ public class OSRMInstructionFormatter: Formatter {
234302
let isMotorway = roadClasses?.contains(.motorway) ?? false
235303

236304
if let name = name, let ref = ref, name != ref, !isMotorway {
305+
let attributedName = NSAttributedString(string: name)
306+
let attributedRef = NSAttributedString(string: ref)
237307
let phrases = instructions["phrase"] as! [String: String]
238-
let phrase = phrases["name and ref"]!
239-
wayName = phrase.replacingTokens(using: { (tokenType) -> String in
308+
let phrase = NSAttributedString(string: phrases["name and ref"]!)
309+
wayName = phrase.replacingTokens(using: { (tokenType) -> NSAttributedString in
240310
switch tokenType {
241311
case .wayName:
242-
return modifyValueByKey?(.wayName, name) ?? name
312+
return modifyValueByKey?(.wayName, attributedName) ?? attributedName
243313
case .code:
244-
return modifyValueByKey?(.code, ref) ?? ref
314+
return modifyValueByKey?(.code, attributedRef) ?? attributedRef
245315
default:
246316
fatalError("Unexpected token type \(tokenType) in name-and-ref phrase")
247317
}
248318
})
249319
} else if let ref = ref, isMotorway, let decimalRange = ref.rangeOfCharacter(from: .decimalDigits), !decimalRange.isEmpty {
250-
wayName = modifyValueByKey != nil ? "\(modifyValueByKey!(.code, ref))" : ref
320+
let attributedRef = NSAttributedString(string: ref)
321+
if let modifyValueByKey = modifyValueByKey {
322+
wayName = modifyValueByKey(.code, attributedRef)
323+
} else {
324+
wayName = attributedRef
325+
}
251326
} else if name == nil, let ref = ref {
252-
wayName = modifyValueByKey != nil ? "\(modifyValueByKey!(.code, ref))" : ref
327+
let attributedRef = NSAttributedString(string: ref)
328+
if let modifyValueByKey = modifyValueByKey {
329+
wayName = modifyValueByKey(.code, attributedRef)
330+
} else {
331+
wayName = attributedRef
332+
}
333+
} else if let name = name {
334+
let attributedName = NSAttributedString(string: name)
335+
if let modifyValueByKey = modifyValueByKey {
336+
wayName = modifyValueByKey(.wayName, attributedName)
337+
} else {
338+
wayName = attributedName
339+
}
253340
} else {
254-
wayName = name != nil ? modifyValueByKey != nil ? "\(modifyValueByKey!(.wayName, name!))" : name! : ""
341+
wayName = NSAttributedString()
255342
}
256343
}
257344

@@ -284,7 +371,7 @@ public class OSRMInstructionFormatter: Formatter {
284371
instruction = obj
285372
} else if let _ = step.exitCodes?.first, let obj = instructionObject["exit"] {
286373
instruction = obj
287-
} else if !wayName.isEmpty, let obj = instructionObject["name"] {
374+
} else if !wayName.string.isEmpty, let obj = instructionObject["name"] {
288375
instruction = obj
289376
} else {
290377
instruction = instructionObject["default"]!
@@ -307,11 +394,11 @@ public class OSRMInstructionFormatter: Formatter {
307394
if step.finalHeading != nil { bearing = Int(step.finalHeading! as Double) }
308395

309396
// Replace tokens
310-
let result = instruction.replacingTokens { (tokenType) -> String in
397+
let result = NSAttributedString(string: instruction).replacingTokens { (tokenType) -> NSAttributedString in
311398
var replacement: String
312399
switch tokenType {
313400
case .code: replacement = step.codes?.first ?? ""
314-
case .wayName: replacement = wayName
401+
case .wayName: replacement = "" // ignored
315402
case .destination: replacement = destination
316403
case .exitCode: replacement = exitCode
317404
case .exitIndex: replacement = exitOrdinal
@@ -324,9 +411,10 @@ public class OSRMInstructionFormatter: Formatter {
324411
fatalError("Unexpected token type \(tokenType) in individual instruction")
325412
}
326413
if tokenType == .wayName {
327-
return replacement // already modified above
414+
return wayName // already modified above
328415
} else {
329-
return modifyValueByKey?(tokenType, replacement) ?? replacement
416+
let attributedReplacement = NSAttributedString(string: replacement)
417+
return modifyValueByKey?(tokenType, attributedReplacement) ?? attributedReplacement
330418
}
331419
}
332420

0 commit comments

Comments
 (0)