Skip to content

Commit c313a32

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

File tree

1 file changed

+112
-18
lines changed

1 file changed

+112
-18
lines changed

OSRMTextInstructions/OSRMTextInstructions.swift

Lines changed: 112 additions & 18 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+
public 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]
@@ -179,14 +227,39 @@ public class OSRMInstructionFormatter: Formatter {
179227
/**
180228
Creates an instruction given a step and options.
181229

182-
- parameter step:
230+
- parameter step: The step to format.
183231
- parameter legIndex: Current leg index the user is currently on.
184232
- parameter numberOfLegs: Total number of `RouteLeg` for the given `Route`.
185233
- parameter roadClasses: Option set representing the classes of road for the `RouteStep`.
186234
- parameter modifyValueByKey: Allows for mutating the instruction at given parts of the instruction.
187235
- returns: An instruction as a `String`.
188236
*/
189237
public func string(for obj: Any?, legIndex: Int?, numberOfLegs: Int?, roadClasses: RoadClasses? = RoadClasses([]), modifyValueByKey: ((TokenType, String) -> String)?) -> String? {
238+
guard let obj = obj else {
239+
return nil
240+
}
241+
242+
var modifyAttributedValueByKey: ((TokenType, NSAttributedString) -> NSAttributedString)?
243+
if let modifyValueByKey = modifyValueByKey {
244+
modifyAttributedValueByKey = { (key: TokenType, value: NSAttributedString) -> NSAttributedString in
245+
return NSAttributedString(string: modifyValueByKey(key, value.string))
246+
}
247+
}
248+
return attributedString(for: obj, legIndex: legIndex, numberOfLegs: numberOfLegs, roadClasses: roadClasses, modifyValueByKey: modifyAttributedValueByKey)?.string
249+
}
250+
251+
/**
252+
Creates an instruction as an attributed string given a step and options.
253+
254+
- parameter obj: The step to format.
255+
- parameter attrs: The default attributes to use for the returned attributed string.
256+
- parameter legIndex: Current leg index the user is currently on.
257+
- parameter numberOfLegs: Total number of `RouteLeg` for the given `Route`.
258+
- parameter roadClasses: Option set representing the classes of road for the `RouteStep`.
259+
- parameter modifyValueByKey: Allows for mutating the instruction at given parts of the instruction.
260+
- returns: An instruction as an `NSAttributedString`.
261+
*/
262+
public func attributedString(for obj: Any, withDefaultAttributes attrs: [String : Any]? = nil, legIndex: Int?, numberOfLegs: Int?, roadClasses: RoadClasses? = RoadClasses([]), modifyValueByKey: ((TokenType, NSAttributedString) -> NSAttributedString)?) -> NSAttributedString? {
190263
guard let step = obj as? RouteStep else {
191264
return nil
192265
}
@@ -208,14 +281,14 @@ public class OSRMInstructionFormatter: Formatter {
208281

209282
var instructionObject: InstructionsByToken
210283
var rotaryName = ""
211-
var wayName: String
284+
var wayName: NSAttributedString
212285
switch type {
213286
case .takeRotary, .takeRoundabout:
214287
// Special instruction types have an intermediate level keyed to “default”.
215288
let instructionsByModifier = instructions[type.description] as! [String: InstructionsByModifier]
216289
let defaultInstructions = instructionsByModifier["default"]!
217290

218-
wayName = step.exitNames?.first ?? ""
291+
wayName = NSAttributedString(string: step.exitNames?.first ?? "", attributes: attrs)
219292
if let _rotaryName = step.names?.first, let _ = step.exitIndex, let obj = defaultInstructions["name_exit"] {
220293
instructionObject = obj
221294
rotaryName = _rotaryName
@@ -244,22 +317,42 @@ public class OSRMInstructionFormatter: Formatter {
244317
let isMotorway = roadClasses?.contains(.motorway) ?? false
245318

246319
if let name = name, let ref = ref, name != ref, !isMotorway {
247-
wayName = phrase(named: .nameWithCode).replacingTokens(using: { (tokenType) -> String in
320+
let attributedName = NSAttributedString(string: name, attributes: attrs)
321+
let attributedRef = NSAttributedString(string: ref, attributes: attrs)
322+
let phrase = NSAttributedString(string: self.phrase(named: .nameWithCode), attributes: attrs)
323+
wayName = phrase.replacingTokens(using: { (tokenType) -> NSAttributedString in
248324
switch tokenType {
249325
case .wayName:
250-
return modifyValueByKey?(.wayName, name) ?? name
326+
return modifyValueByKey?(.wayName, attributedName) ?? attributedName
251327
case .code:
252-
return modifyValueByKey?(.code, ref) ?? ref
328+
return modifyValueByKey?(.code, attributedRef) ?? attributedRef
253329
default:
254330
fatalError("Unexpected token type \(tokenType) in name-and-ref phrase")
255331
}
256332
})
257333
} else if let ref = ref, isMotorway, let decimalRange = ref.rangeOfCharacter(from: .decimalDigits), !decimalRange.isEmpty {
258-
wayName = modifyValueByKey != nil ? "\(modifyValueByKey!(.code, ref))" : ref
334+
let attributedRef = NSAttributedString(string: ref, attributes: attrs)
335+
if let modifyValueByKey = modifyValueByKey {
336+
wayName = modifyValueByKey(.code, attributedRef)
337+
} else {
338+
wayName = attributedRef
339+
}
259340
} else if name == nil, let ref = ref {
260-
wayName = modifyValueByKey != nil ? "\(modifyValueByKey!(.code, ref))" : ref
341+
let attributedRef = NSAttributedString(string: ref, attributes: attrs)
342+
if let modifyValueByKey = modifyValueByKey {
343+
wayName = modifyValueByKey(.code, attributedRef)
344+
} else {
345+
wayName = attributedRef
346+
}
347+
} else if let name = name {
348+
let attributedName = NSAttributedString(string: name, attributes: attrs)
349+
if let modifyValueByKey = modifyValueByKey {
350+
wayName = modifyValueByKey(.wayName, attributedName)
351+
} else {
352+
wayName = attributedName
353+
}
261354
} else {
262-
wayName = name != nil ? modifyValueByKey != nil ? "\(modifyValueByKey!(.wayName, name!))" : name! : ""
355+
wayName = NSAttributedString()
263356
}
264357
}
265358

@@ -292,7 +385,7 @@ public class OSRMInstructionFormatter: Formatter {
292385
instruction = obj
293386
} else if let _ = step.exitCodes?.first, let obj = instructionObject["exit"] {
294387
instruction = obj
295-
} else if !wayName.isEmpty, let obj = instructionObject["name"] {
388+
} else if !wayName.string.isEmpty, let obj = instructionObject["name"] {
296389
instruction = obj
297390
} else {
298391
instruction = instructionObject["default"]!
@@ -315,11 +408,11 @@ public class OSRMInstructionFormatter: Formatter {
315408
if step.finalHeading != nil { bearing = Int(step.finalHeading! as Double) }
316409

317410
// Replace tokens
318-
let result = instruction.replacingTokens { (tokenType) -> String in
411+
let result = NSAttributedString(string: instruction, attributes: attrs).replacingTokens { (tokenType) -> NSAttributedString in
319412
var replacement: String
320413
switch tokenType {
321414
case .code: replacement = step.codes?.first ?? ""
322-
case .wayName: replacement = wayName
415+
case .wayName: replacement = "" // ignored
323416
case .destination: replacement = destination
324417
case .exitCode: replacement = exitCode
325418
case .exitIndex: replacement = exitOrdinal
@@ -332,9 +425,10 @@ public class OSRMInstructionFormatter: Formatter {
332425
fatalError("Unexpected token type \(tokenType) in individual instruction")
333426
}
334427
if tokenType == .wayName {
335-
return replacement // already modified above
428+
return wayName // already modified above
336429
} else {
337-
return modifyValueByKey?(tokenType, replacement) ?? replacement
430+
let attributedReplacement = NSAttributedString(string: replacement, attributes: attrs)
431+
return modifyValueByKey?(tokenType, attributedReplacement) ?? attributedReplacement
338432
}
339433
}
340434

0 commit comments

Comments
 (0)