diff --git a/Cartfile.resolved b/Cartfile.resolved index ccb1adcfbd9..7d8c5f7b252 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ -binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" "3.7.6" +binary "https://www.mapbox.com/ios-sdk/Mapbox-iOS-SDK.json" "3.7.8" github "ceeK/Solar" "2.1.0" github "mapbox/MapboxDirections.swift" "v0.19.1" github "mapbox/mapbox-events-ios" "v0.4.0" diff --git a/Examples/Swift/CustomViewController.swift b/Examples/Swift/CustomViewController.swift index c3121853fe3..b0ae26ab454 100644 --- a/Examples/Swift/CustomViewController.swift +++ b/Examples/Swift/CustomViewController.swift @@ -12,7 +12,7 @@ class CustomViewController: UIViewController, MGLMapViewDelegate, AVSpeechSynthe let directions = Directions.shared var routeController: RouteController! - let textDistanceFormatter = DistanceFormatter(approximate: true) + let textDistanceFormatter = FixVoiceDistanceFormatter(approximate: true) var userRoute: Route? var simulateLocation = false diff --git a/MapboxCoreNavigation/DistanceFormatter.swift b/MapboxCoreNavigation/DistanceFormatter.swift index 8aaa9b7f7c4..92a4e1ab9c9 100644 --- a/MapboxCoreNavigation/DistanceFormatter.swift +++ b/MapboxCoreNavigation/DistanceFormatter.swift @@ -154,8 +154,8 @@ public class DistanceFormatter: LengthFormatter { func threshold(for distance: CLLocationDistance) -> RoundingTable.Threshold { if NavigationSettings.shared.usesMetric { return roundingTableMetric.threshold(for: distance) - } else if numberFormatter.locale.identifier == "en-GB" { - return roundingTableUK.threshold(for: distance) + } else if numberFormatter.locale.identifier == "en-GB" { // Fix wrong voice metric for Region UK + return roundingTableMetric.threshold(for: distance) } else { return roundingTableImperial.threshold(for: distance) } diff --git a/MapboxNavigation.xcodeproj/project.pbxproj b/MapboxNavigation.xcodeproj/project.pbxproj index c524d2e780a..ec3aaa1452e 100644 --- a/MapboxNavigation.xcodeproj/project.pbxproj +++ b/MapboxNavigation.xcodeproj/project.pbxproj @@ -175,6 +175,7 @@ 8DB63A3A1FBBCA2200928389 /* RatingControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DB63A391FBBCA2200928389 /* RatingControl.swift */; }; 8DE879661FBB9980002F06C0 /* EndOfRouteViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DE879651FBB9980002F06C0 /* EndOfRouteViewController.swift */; }; 8DF399B21FB257B30034904C /* UIGestureRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF399B11FB257B30034904C /* UIGestureRecognizer.swift */; }; + 992A619220B6EA8F0061255A /* FixVoiceDistanceFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 992A619120B6EA8F0061255A /* FixVoiceDistanceFormatter.swift */; }; C51245F21F19471C00E33B52 /* MapboxMobileEvents.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C549F8311F17F2C5001A0A2D /* MapboxMobileEvents.framework */; }; C51245F31F19471C00E33B52 /* MapboxMobileEvents.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C549F8311F17F2C5001A0A2D /* MapboxMobileEvents.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; C51DF8661F38C31C006C6A15 /* Locale.swift in Sources */ = {isa = PBXBuildFile; fileRef = C51DF8651F38C31C006C6A15 /* Locale.swift */; }; @@ -548,6 +549,7 @@ 8DB63A391FBBCA2200928389 /* RatingControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RatingControl.swift; sourceTree = ""; }; 8DE879651FBB9980002F06C0 /* EndOfRouteViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndOfRouteViewController.swift; sourceTree = ""; }; 8DF399B11FB257B30034904C /* UIGestureRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIGestureRecognizer.swift; sourceTree = ""; }; + 992A619120B6EA8F0061255A /* FixVoiceDistanceFormatter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FixVoiceDistanceFormatter.swift; sourceTree = ""; }; C51DF8651F38C31C006C6A15 /* Locale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Locale.swift; sourceTree = ""; }; C520EE911EBB84F9008805BC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Navigation.storyboard; sourceTree = ""; }; C520EE941EBBBD55008805BC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Base.lproj/Navigation.strings; sourceTree = ""; }; @@ -857,6 +859,7 @@ 35F611C31F1E1C0500C43249 /* FeedbackViewController.swift */, 8D8EA9BB20575CD80077F478 /* FeedbackCollectionViewCell.swift */, 35025F3E1F051DD2002BA3EA /* DialogViewController.swift */, + 992A619120B6EA8F0061255A /* FixVoiceDistanceFormatter.swift */, C5A6B2DC1F4CE8E8004260EA /* StyleType.swift */, 35B1E2941F1FF8EC00A13D32 /* UserCourseView.swift */, 35CB1E121F97DD740011CC44 /* FeedbackItem.swift */, @@ -1772,6 +1775,7 @@ C51DF8671F38C337006C6A15 /* Date.swift in Sources */, 359D283C1F9DC14F00FDE9C9 /* UICollectionView.swift in Sources */, 8D24A2F820409A890098CBF8 /* CGSize.swift in Sources */, + 992A619220B6EA8F0061255A /* FixVoiceDistanceFormatter.swift in Sources */, 35CB1E131F97DD740011CC44 /* FeedbackItem.swift in Sources */, 35F611C41F1E1C0500C43249 /* FeedbackViewController.swift in Sources */, 353AA5601FCEF583009F0384 /* StyleManager.swift in Sources */, diff --git a/MapboxNavigation/BottomBannerView.swift b/MapboxNavigation/BottomBannerView.swift index 80a588c640b..c74c337da39 100644 --- a/MapboxNavigation/BottomBannerView.swift +++ b/MapboxNavigation/BottomBannerView.swift @@ -21,7 +21,7 @@ open class BottomBannerView: UIView { let dateFormatter = DateFormatter() let dateComponentsFormatter = DateComponentsFormatter() - let distanceFormatter = DistanceFormatter(approximate: true) + let distanceFormatter = FixVoiceDistanceFormatter(approximate: true) var verticalCompactConstraints = [NSLayoutConstraint]() var verticalRegularConstraints = [NSLayoutConstraint]() diff --git a/MapboxNavigation/FixVoiceDistanceFormatter.swift b/MapboxNavigation/FixVoiceDistanceFormatter.swift new file mode 100644 index 00000000000..ca88dc699ec --- /dev/null +++ b/MapboxNavigation/FixVoiceDistanceFormatter.swift @@ -0,0 +1,232 @@ +import CoreLocation + +extension CLLocationDistance { + + static let metersPerMile: CLLocationDistance = 1_609.344 + static let feetPerMeter: CLLocationDistance = 3.28084 + + // Returns the distance converted to miles + var miles: Double { + return self / .metersPerMile + } + + // Returns the distance converted to feet + var feet: Double { + return self * .feetPerMeter + } + + // Returns the distance converted to yards + var yards: Double { + return feet / 3 + } + + // Returns the distance converted to kilometers + var kilometers: Double { + return self / 1000 + } + + // Returns the distance in meters converted from miles + func inMiles() -> Double { + return self * .metersPerMile + } + + // Returns the distance in meters converted from yards + func inYards() -> Double { + return self * .feetPerMeter / 3 + } + + func converted(to unit: LengthFormatter.Unit) -> Double { + switch unit { + case .millimeter: + return self / 1_000 + case .centimeter: + return self / 100 + case .meter: + return self + case .kilometer: + return kilometers + case .inch: + return feet / 12 + case .foot: + return feet + case .yard: + return yards + case .mile: + return miles + } + } +} + +struct RoundingTable { + struct Threshold { + let maximumDistance: CLLocationDistance + let roundingIncrement: Double + let unit: LengthFormatter.Unit + let maximumFractionDigits: Int + + func localizedDistanceString(for distance: CLLocationDistance, using formatter: FixVoiceDistanceFormatter) -> String { + switch unit { + case .mile: + return formatter.string(fromValue: distance.miles, unit: unit) + case .foot: + return formatter.string(fromValue: distance.feet, unit: unit) + case .yard: + return formatter.string(fromValue: distance.yards, unit: unit) + case .kilometer: + return formatter.string(fromValue: distance.kilometers, unit: unit) + default: + return formatter.string(fromValue: distance, unit: unit) + } + } + } + + let thresholds: [Threshold] + + func threshold(for distance: CLLocationDistance) -> Threshold { + for threshold in thresholds { + if distance < threshold.maximumDistance { + return threshold + } + } + return thresholds.last! + } +} + +extension NSAttributedStringKey { + public static let quantity = NSAttributedStringKey(rawValue: "MBQuantity") +} + +/// Provides appropriately formatted, localized descriptions of linear distances. +@objc(MBFixVoiceDistanceFormatter) +public class FixVoiceDistanceFormatter: LengthFormatter { + /// True to favor brevity over precision. + var approx: Bool + + let nonFractionalLengthFormatter = LengthFormatter() + + /// Indicates the most recently used unit + public private(set) var unit: LengthFormatter.Unit = .millimeter + + /** + Specifies the preferred distance measurement unit. + - note: Anything but `kilometer` and `mile` will fall back to the default measurement for the current locale. + Meters and feets will be used when the presented distances are small enough. See `DistanceFormatter` for more information. + */ + @objc public dynamic var distanceUnit : LengthFormatter.Unit = Locale.current.usesMetric ? .kilometer : .mile + + var usesMetric: Bool { + get { + switch distanceUnit { + case .kilometer: + return true + case .mile: + return false + default: + return Locale.current.usesMetric + } + } + } + + // Rounding tables for metric, imperial, and UK measurement systems. The last threshold is used as a default. + lazy var roundingTableMetric: RoundingTable = { + return RoundingTable(thresholds: [.init(maximumDistance: 25, roundingIncrement: 5, unit: .meter, maximumFractionDigits: 0), + .init(maximumDistance: 100, roundingIncrement: 25, unit: .meter, maximumFractionDigits: 0), + .init(maximumDistance: 999, roundingIncrement: 50, unit: .meter, maximumFractionDigits: 0), + .init(maximumDistance: 3_000, roundingIncrement: 0, unit: .kilometer, maximumFractionDigits: 1), + .init(maximumDistance: 5_000, roundingIncrement: 0, unit: .kilometer, maximumFractionDigits: 0)]) + }() + + lazy var roundingTableUK: RoundingTable = { + return RoundingTable(thresholds: [.init(maximumDistance: 20.inYards(), roundingIncrement: 10, unit: .yard, maximumFractionDigits: 0), + .init(maximumDistance: 100.inYards(), roundingIncrement: 25, unit: .yard, maximumFractionDigits: 0), + .init(maximumDistance: 0.1.inMiles(), roundingIncrement: 50, unit: .yard, maximumFractionDigits: 1), + .init(maximumDistance: 3.inMiles(), roundingIncrement: 0.1, unit: .mile, maximumFractionDigits: 1), + .init(maximumDistance: 5.inMiles(), roundingIncrement: 0, unit: .mile, maximumFractionDigits: 0)]) + }() + + lazy var roundingTableImperial: RoundingTable = { + return RoundingTable(thresholds: [.init(maximumDistance: 0.1.inMiles(), roundingIncrement: 50, unit: .foot, maximumFractionDigits: 0), + .init(maximumDistance: 3.inMiles(), roundingIncrement: 0.1, unit: .mile, maximumFractionDigits: 1), + .init(maximumDistance: 5.inMiles(), roundingIncrement: 0, unit: .mile, maximumFractionDigits: 0)]) + }() + + /** + Intializes a new `FixVoiceDistanceFormatter`. + + - parameter approximate: approximates the distances. + */ + @objc public init(approximate: Bool = false) { + self.approx = approximate + super.init() + self.numberFormatter.locale = .nationalizedCurrent + } + + public required init?(coder decoder: NSCoder) { + approx = decoder.decodeBool(forKey: "approximate") + super.init(coder: decoder) + } + + public override func encode(with aCoder: NSCoder) { + super.encode(with: aCoder) + aCoder.encode(approx, forKey: "approximate") + } + + func threshold(for distance: CLLocationDistance) -> RoundingTable.Threshold { + if usesMetric { + return roundingTableMetric.threshold(for: distance) + } else if numberFormatter.locale.identifier == "en-GB" { // Fix wrong voice metric for Region UK + return roundingTableMetric.threshold(for: distance) + } else { + return roundingTableImperial.threshold(for: distance) + } + } + + /** + Returns a more human readable `String` from a given `CLLocationDistance`. + + The user’s `Locale` is used here to set the units. + */ + @objc public func string(from distance: CLLocationDistance) -> String { + numberFormatter.positivePrefix = "" + numberFormatter.positiveSuffix = "" + numberFormatter.decimalSeparator = nonFractionalLengthFormatter.numberFormatter.decimalSeparator + numberFormatter.alwaysShowsDecimalSeparator = nonFractionalLengthFormatter.numberFormatter.alwaysShowsDecimalSeparator + numberFormatter.usesSignificantDigits = false + return formattedDistance(distance) + } + + @objc public override func string(fromMeters numberInMeters: Double) -> String { + return self.string(from: numberInMeters) + } + + func formattedDistance(_ distance: CLLocationDistance) -> String { + let threshold = self.threshold(for: distance) + numberFormatter.maximumFractionDigits = threshold.maximumFractionDigits + numberFormatter.roundingIncrement = threshold.roundingIncrement as NSNumber + unit = threshold.unit + return threshold.localizedDistanceString(for: distance, using: self) + } + + /** + Returns an attributed string containing the formatted, converted distance. + + `NSAttributedStringKey.quantity` is applied to the numeric quantity. + */ + @objc public override func attributedString(for obj: Any, withDefaultAttributes attrs: [NSAttributedStringKey : Any]? = nil) -> NSAttributedString? { + guard let distance = obj as? CLLocationDistance else { + return nil + } + + let string = self.string(from: distance) + let attributedString = NSMutableAttributedString(string: string, attributes: attrs) + let convertedDistance = distance.converted(to: threshold(for: distance).unit) + if let quantityString = numberFormatter.string(from: convertedDistance as NSNumber) { + // NSMutableAttributedString methods accept NSRange, not Range. + let quantityRange = (string as NSString).range(of: quantityString) + if quantityRange.location != NSNotFound { + attributedString.addAttribute(.quantity, value: distance as NSNumber, range: quantityRange) + } + } + return attributedString + } +} diff --git a/MapboxNavigation/InstructionsBannerView.swift b/MapboxNavigation/InstructionsBannerView.swift index 4222bbece14..b4cd6a01f5a 100644 --- a/MapboxNavigation/InstructionsBannerView.swift +++ b/MapboxNavigation/InstructionsBannerView.swift @@ -33,7 +33,7 @@ open class BaseInstructionsBannerView: UIControl { var centerYConstraints = [NSLayoutConstraint]() var baselineConstraints = [NSLayoutConstraint]() - let distanceFormatter = DistanceFormatter(approximate: true) + let distanceFormatter = FixVoiceDistanceFormatter(approximate: true) var distance: CLLocationDistance? { didSet { diff --git a/MapboxNavigation/MapboxVoiceController.swift b/MapboxNavigation/MapboxVoiceController.swift index e918b537e2d..353cd747aac 100644 --- a/MapboxNavigation/MapboxVoiceController.swift +++ b/MapboxNavigation/MapboxVoiceController.swift @@ -87,10 +87,11 @@ open class MapboxVoiceController: RouteVoiceController { let modifiedInstruction = voiceControllerDelegate?.voiceController?(self, willSpeak: instruction, routeProgress: routeProgress!) ?? instruction lastSpokenInstruction = modifiedInstruction + /* // Remove Cache Voice Data if let data = cachedDataForKey(modifiedInstruction.ssmlText) { play(data) return - } + }*/ fetchAndSpeak(instruction: modifiedInstruction) } diff --git a/MapboxNavigation/RouteMapViewController.swift b/MapboxNavigation/RouteMapViewController.swift index 10b4a32e4b1..9d22b504145 100644 --- a/MapboxNavigation/RouteMapViewController.swift +++ b/MapboxNavigation/RouteMapViewController.swift @@ -78,7 +78,7 @@ class RouteMapViewController: UIViewController { populateName(for: destination, populated: { self.destination = $0 }) } } - let distanceFormatter = DistanceFormatter(approximate: true) + let distanceFormatter = FixVoiceDistanceFormatter(approximate: true) var arrowCurrentStep: RouteStep? var isInOverviewMode = false { didSet {