-
Notifications
You must be signed in to change notification settings - Fork 301
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
Add abbreviations #1169
Add abbreviations #1169
Changes from all commits
360e826
e715d06
c0ed919
a7ca4a3
c228a61
88fdb3c
81e13ac
4c2b917
62cd6f2
a8d8faf
977727d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,26 +14,63 @@ class InstructionPresenter { | |
var onShieldDownload: ShieldDownloadCompletion? | ||
|
||
private let imageRepository = ImageRepository.shared | ||
|
||
func attributedText() -> NSAttributedString { | ||
guard let label = self.label else { | ||
return NSAttributedString() | ||
} | ||
|
||
let string = NSMutableAttributedString() | ||
|
||
for component in instruction { | ||
fittedAttributedComponents().forEach { string.append($0) } | ||
return string | ||
} | ||
|
||
func fittedAttributedComponents() -> [NSAttributedString] { | ||
guard let label = self.label else { return [] } | ||
var attributedPairs = self.attributedPairs() | ||
let availableBounds = label.availableBounds() | ||
let totalWidth = attributedPairs.attributedStrings.map { $0.size() }.reduce(.zero, +).width | ||
let stringFits = totalWidth <= availableBounds.width | ||
|
||
guard !stringFits else { return attributedPairs.attributedStrings } | ||
|
||
let indexedComponents = attributedPairs.components.enumerated().map { IndexedVisualInstructionComponent(component: $1, index: $0) } | ||
let filtered = indexedComponents.filter { $0.component.abbreviation != nil } | ||
let sorted = filtered.sorted { $0.component.abbreviationPriority < $1.component.abbreviationPriority } | ||
for component in sorted { | ||
let isFirst = component.index == 0 | ||
let joinChar = isFirst ? "" : " " | ||
guard component.component.type == .text else { continue } | ||
guard let abbreviation = component.component.abbreviation else { continue } | ||
|
||
attributedPairs.attributedStrings[component.index] = NSAttributedString(string: joinChar + abbreviation, attributes: attributesForLabel(label)) | ||
let newWidth = attributedPairs.attributedStrings.map { $0.size() }.reduce(.zero, +).width | ||
|
||
if newWidth <= availableBounds.width { | ||
break | ||
} | ||
} | ||
|
||
return attributedPairs.attributedStrings | ||
} | ||
|
||
typealias AttributedInstructionComponents = (components: [VisualInstructionComponent], attributedStrings: [NSAttributedString]) | ||
|
||
func attributedPairs() -> AttributedInstructionComponents { | ||
guard let label = self.label else { return (components: [], attributedStrings: []) } | ||
var strings = [NSAttributedString]() | ||
var processedComponents = [VisualInstructionComponent]() | ||
let components = instruction | ||
|
||
for component in components { | ||
let isFirst = component == instruction.first | ||
let joinChar = !isFirst ? " " : "" | ||
let joinChar = isFirst ? "" : " " | ||
|
||
if let shieldKey = component.shieldKey() { | ||
if let cachedImage = imageRepository.cachedImageForKey(shieldKey) { | ||
string.append(NSAttributedString(string: joinChar)) | ||
string.append(attributedString(withFont: label.font, shieldImage: cachedImage)) | ||
processedComponents.append(component) | ||
strings.append(attributedString(withFont: label.font, shieldImage: cachedImage)) | ||
} else { | ||
// Display road code while shield is downloaded | ||
if let text = component.text { | ||
string.append(NSAttributedString(string: joinChar + text, attributes: attributesForLabel(label))) | ||
processedComponents.append(component) | ||
strings.append(NSAttributedString(string: joinChar + text, attributes: attributesForLabel(label))) | ||
} | ||
shieldImageForComponent(component, height: label.shieldHeight, completion: { [weak self] (image) in | ||
guard image != nil else { | ||
|
@@ -45,14 +82,27 @@ class InstructionPresenter { | |
}) | ||
} | ||
} else if let text = component.text { | ||
if component.type == .delimiter && instructionHasDownloadedAllShields() { | ||
continue | ||
// Hide delimiter if one of the adjacent components is a shield | ||
if component.type == .delimiter { | ||
let componentBefore = components.component(before: component) | ||
let componentAfter = components.component(after: component) | ||
if let shieldKey = componentBefore?.shieldKey(), | ||
imageRepository.cachedImageForKey(shieldKey) != nil { | ||
continue | ||
} | ||
if let shieldKey = componentAfter?.shieldKey(), | ||
imageRepository.cachedImageForKey(shieldKey) != nil { | ||
continue | ||
} | ||
} | ||
string.append(NSAttributedString(string: (joinChar + text).abbreviated(toFit: label.availableBounds(), font: label.font), attributes: attributesForLabel(label))) | ||
processedComponents.append(component) | ||
strings.append(NSAttributedString(string: (joinChar + text), attributes: attributesForLabel(label))) | ||
} | ||
} | ||
|
||
assert(processedComponents.count == strings.count, "The number of processed components must match the number of attributed strings") | ||
|
||
return string | ||
return (components: processedComponents, attributedStrings: strings) | ||
} | ||
|
||
private func shieldImageForComponent(_ component: VisualInstructionComponent, height: CGFloat, completion: @escaping (UIImage?) -> Void) { | ||
|
@@ -103,3 +153,39 @@ class ShieldAttachment: NSTextAttachment { | |
return CGRect(x: 0, y: font.descender - image.size.height / 2 + mid + 2, width: image.size.width, height: image.size.height).integral | ||
} | ||
} | ||
|
||
extension CGSize { | ||
fileprivate static var greatestFiniteSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude) | ||
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. @bsudekum should we consider an 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. Not necessary for private or fileprivate where we don't expose anything to ObjC or even outside this file to prevent conflicts and polluted namespaces. |
||
|
||
fileprivate static func +(lhs: CGSize, rhs: CGSize) -> CGSize { | ||
return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) | ||
} | ||
} | ||
|
||
fileprivate struct IndexedVisualInstructionComponent { | ||
let component: Array<VisualInstructionComponent>.Element | ||
let index: Array<VisualInstructionComponent>.Index | ||
} | ||
|
||
extension Array where Element == VisualInstructionComponent { | ||
|
||
fileprivate func component(before component: VisualInstructionComponent) -> VisualInstructionComponent? { | ||
guard let index = self.index(of: component) else { | ||
return nil | ||
} | ||
if index > 0 { | ||
return self[index-1] | ||
} | ||
return nil | ||
} | ||
|
||
fileprivate func component(after component: VisualInstructionComponent) -> VisualInstructionComponent? { | ||
guard let index = self.index(of: component) else { | ||
return nil | ||
} | ||
if index+1 < self.endIndex { | ||
return self[index+1] | ||
} | ||
return nil | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,8 +79,7 @@ open class BaseInstructionsBannerView: UIControl { | |
override open func prepareForInterfaceBuilder() { | ||
super.prepareForInterfaceBuilder() | ||
maneuverView.isStart = true | ||
|
||
primaryLabel.instruction = [VisualInstructionComponent(type: .destination, text: "Primary text label", imageURL: nil, maneuverType: .none, maneuverDirection: .none)] | ||
primaryLabel.instruction = [VisualInstructionComponent(type: .text, text: "Primary text label", imageURL: nil, maneuverType: .none, maneuverDirection: .none, abbreviation: nil, abbreviationPriority: NSNotFound)] | ||
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. This is another observation as well. Should we consider wrapping our strings in 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. Abbreviations will be localized server-side starting from this PR. |
||
|
||
distance = 100 | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -53,7 +53,7 @@ open class NextBannerView: UIView { | |
override open func prepareForInterfaceBuilder() { | ||
super.prepareForInterfaceBuilder() | ||
maneuverView.isEnd = true | ||
instructionLabel.instruction = [VisualInstructionComponent(type: .destination, text: "Next step", imageURL: nil, maneuverType: .none, maneuverDirection: .none)] | ||
instructionLabel.instruction = [VisualInstructionComponent(type: .text, text: "Next step", imageURL: nil, maneuverType: .none, maneuverDirection: .none, abbreviation: nil, abbreviationPriority: NSNotFound)] | ||
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.
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. This is a placeholder. 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. This placeholder is never shown on the UI, I reckon. (@frederoni mentioned that localization will be done at server-side... so cool 👍) |
||
} | ||
|
||
func setupLayout() { | ||
|
This file was deleted.
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.
Would it make sense to check we're within bounds here? I'm just slightly weary of bracket lookups like this and how fragile they are.
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 agree about the fragility, but this could cover up an unwanted bug where we could abbreviate
"West Fremont Avenue" as "West W Ave" or crash due to out of bounds.
So instead, I added an assertion to make sure we would catch such a bug early in a8d8faf