Skip to content

Commit 6be0c16

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 6be0c16

File tree

1 file changed

+107
-18
lines changed

1 file changed

+107
-18
lines changed

OSRMTextInstructions/OSRMTextInstructions.swift

Lines changed: 107 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+
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,34 @@ 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+
var modifyAttributedValueByKey: ((TokenType, NSAttributedString) -> NSAttributedString)?
239+
if let modifyValueByKey = modifyValueByKey {
240+
modifyAttributedValueByKey = { (key: TokenType, value: NSAttributedString) -> NSAttributedString in
241+
return NSAttributedString(string: modifyValueByKey(key, value.string))
242+
}
243+
}
244+
return attributedString(for: obj, legIndex: legIndex, numberOfLegs: numberOfLegs, roadClasses: roadClasses, modifyValueByKey: modifyAttributedValueByKey)?.string
245+
}
246+
247+
/**
248+
Creates an instruction as an attributed string given a step and options.
249+
250+
- parameter step: The step to format.
251+
- parameter legIndex: Current leg index the user is currently on.
252+
- parameter numberOfLegs: Total number of `RouteLeg` for the given `Route`.
253+
- parameter roadClasses: Option set representing the classes of road for the `RouteStep`.
254+
- parameter modifyValueByKey: Allows for mutating the instruction at given parts of the instruction.
255+
- returns: An instruction as an `NSAttributedString`.
256+
*/
257+
public func attributedString(for obj: Any?, legIndex: Int?, numberOfLegs: Int?, roadClasses: RoadClasses? = RoadClasses([]), modifyValueByKey: ((TokenType, NSAttributedString) -> NSAttributedString)?) -> NSAttributedString? {
190258
guard let step = obj as? RouteStep else {
191259
return nil
192260
}
@@ -208,14 +276,14 @@ public class OSRMInstructionFormatter: Formatter {
208276

209277
var instructionObject: InstructionsByToken
210278
var rotaryName = ""
211-
var wayName: String
279+
var wayName: NSAttributedString
212280
switch type {
213281
case .takeRotary, .takeRoundabout:
214282
// Special instruction types have an intermediate level keyed to “default”.
215283
let instructionsByModifier = instructions[type.description] as! [String: InstructionsByModifier]
216284
let defaultInstructions = instructionsByModifier["default"]!
217285

218-
wayName = step.exitNames?.first ?? ""
286+
wayName = NSAttributedString(string: step.exitNames?.first ?? "")
219287
if let _rotaryName = step.names?.first, let _ = step.exitIndex, let obj = defaultInstructions["name_exit"] {
220288
instructionObject = obj
221289
rotaryName = _rotaryName
@@ -244,22 +312,42 @@ public class OSRMInstructionFormatter: Formatter {
244312
let isMotorway = roadClasses?.contains(.motorway) ?? false
245313

246314
if let name = name, let ref = ref, name != ref, !isMotorway {
247-
wayName = phrase(named: .nameWithCode).replacingTokens(using: { (tokenType) -> String in
315+
let attributedName = NSAttributedString(string: name)
316+
let attributedRef = NSAttributedString(string: ref)
317+
let phrase = NSAttributedString(string: self.phrase(named: .nameWithCode))
318+
wayName = phrase.replacingTokens(using: { (tokenType) -> NSAttributedString in
248319
switch tokenType {
249320
case .wayName:
250-
return modifyValueByKey?(.wayName, name) ?? name
321+
return modifyValueByKey?(.wayName, attributedName) ?? attributedName
251322
case .code:
252-
return modifyValueByKey?(.code, ref) ?? ref
323+
return modifyValueByKey?(.code, attributedRef) ?? attributedRef
253324
default:
254325
fatalError("Unexpected token type \(tokenType) in name-and-ref phrase")
255326
}
256327
})
257328
} else if let ref = ref, isMotorway, let decimalRange = ref.rangeOfCharacter(from: .decimalDigits), !decimalRange.isEmpty {
258-
wayName = modifyValueByKey != nil ? "\(modifyValueByKey!(.code, ref))" : ref
329+
let attributedRef = NSAttributedString(string: ref)
330+
if let modifyValueByKey = modifyValueByKey {
331+
wayName = modifyValueByKey(.code, attributedRef)
332+
} else {
333+
wayName = attributedRef
334+
}
259335
} else if name == nil, let ref = ref {
260-
wayName = modifyValueByKey != nil ? "\(modifyValueByKey!(.code, ref))" : ref
336+
let attributedRef = NSAttributedString(string: ref)
337+
if let modifyValueByKey = modifyValueByKey {
338+
wayName = modifyValueByKey(.code, attributedRef)
339+
} else {
340+
wayName = attributedRef
341+
}
342+
} else if let name = name {
343+
let attributedName = NSAttributedString(string: name)
344+
if let modifyValueByKey = modifyValueByKey {
345+
wayName = modifyValueByKey(.wayName, attributedName)
346+
} else {
347+
wayName = attributedName
348+
}
261349
} else {
262-
wayName = name != nil ? modifyValueByKey != nil ? "\(modifyValueByKey!(.wayName, name!))" : name! : ""
350+
wayName = NSAttributedString()
263351
}
264352
}
265353

@@ -292,7 +380,7 @@ public class OSRMInstructionFormatter: Formatter {
292380
instruction = obj
293381
} else if let _ = step.exitCodes?.first, let obj = instructionObject["exit"] {
294382
instruction = obj
295-
} else if !wayName.isEmpty, let obj = instructionObject["name"] {
383+
} else if !wayName.string.isEmpty, let obj = instructionObject["name"] {
296384
instruction = obj
297385
} else {
298386
instruction = instructionObject["default"]!
@@ -315,11 +403,11 @@ public class OSRMInstructionFormatter: Formatter {
315403
if step.finalHeading != nil { bearing = Int(step.finalHeading! as Double) }
316404

317405
// Replace tokens
318-
let result = instruction.replacingTokens { (tokenType) -> String in
406+
let result = NSAttributedString(string: instruction).replacingTokens { (tokenType) -> NSAttributedString in
319407
var replacement: String
320408
switch tokenType {
321409
case .code: replacement = step.codes?.first ?? ""
322-
case .wayName: replacement = wayName
410+
case .wayName: replacement = "" // ignored
323411
case .destination: replacement = destination
324412
case .exitCode: replacement = exitCode
325413
case .exitIndex: replacement = exitOrdinal
@@ -332,9 +420,10 @@ public class OSRMInstructionFormatter: Formatter {
332420
fatalError("Unexpected token type \(tokenType) in individual instruction")
333421
}
334422
if tokenType == .wayName {
335-
return replacement // already modified above
423+
return wayName // already modified above
336424
} else {
337-
return modifyValueByKey?(tokenType, replacement) ?? replacement
425+
let attributedReplacement = NSAttributedString(string: replacement)
426+
return modifyValueByKey?(tokenType, attributedReplacement) ?? attributedReplacement
338427
}
339428
}
340429

0 commit comments

Comments
 (0)