Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

MGLDistanceFormatter #3331

Closed
1ec5 opened this issue Dec 17, 2015 · 6 comments
Closed

MGLDistanceFormatter #3331

1ec5 opened this issue Dec 17, 2015 · 6 comments
Assignees
Labels
feature good first issue Good for newcomers iOS Mapbox Maps SDK for iOS localization Human language support and internationalization macOS Mapbox Maps SDK for macOS MapKit parity For feature parity with MapKit on iOS or macOS
Milestone

Comments

@1ec5
Copy link
Contributor

1ec5 commented Dec 17, 2015

MapKit provides a handy subclass of NSFormatter, MKDistanceFormatter, for turning CLLocationDistances into display-appropriate strings with units. The default unit is selected based on the system locale. Developers shouldn’t have to import MapKit for this functionality.

This is a starter-task because it has no dependencies on MGLMapView or mbgl.

/ref #949
/cc @friedbunny

@1ec5 1ec5 added iOS Mapbox Maps SDK for iOS macOS Mapbox Maps SDK for macOS starter-task labels Dec 17, 2015
@friedbunny
Copy link
Contributor

Good idea. 💡

@cph2117
Copy link

cph2117 commented Mar 15, 2016

Hi guys,
I spent a little time tonight and came up with something along the lines of a Mapbox version of MKDistanceFormatter. Unfortunately I did it in Swift before realizing that your entire iOS SDK is in Obj-C.

I'm sure you guys have your own code standards and way of writing things, but if you think that what I wrote could serve as a jumping off point for the real MGLDistanceFormatter, then I'd be happy to take what I wrote and convert it into Obj-C. Here is the Swift code :)

Cheers,
Connor

import Foundation
import CoreLocation

enum MGLDistanceFormatterUnits : UInt {
    case Metric
    case Imperial
    case ImperialWithYards
}
enum MGLDistanceFormatterUnitStyle : UInt {
    case Abbreviated
    case Full
}

//Using ISO 3166-1 --> https://www.iso.org/obp/ui/
enum MGLImperialCountries: String {
    case US //USA
    case GB //United Kingdom
    case MM //Myanmar
    case BS //Bahamas
    case BZ //Belize
}

public class MGLDistanceFormatter {

    private let METERS_PER_KM = 1000.0
    private let METERS_PER_MILE = 1609.34
    private let FEET_PER_METER = 3.28084
    private let FEET_PER_MILE = 5280.0
    private let FEET_PER_YD = 3.0
    private let YARDS_PER_METER = 1.09361

    private let ABB_MILES = "mi"
    private let ABB_METERS = "m"
    private let ABB_YARDS = "yds"
    private let ABB_KILOMETERS = "km"
    private let ABB_FEET = "ft"
    private let MILES = "miles"
    private let METERS = "meters"
    private let KILOMETERS = "kilometers"
    private let FEET = "feet"
    private let FOOT = "foot"
    private let YARDS = "yards"

    private var locale: NSLocale!
    private var localeDeterminedUnits: MGLDistanceFormatterUnits!

    var units: MGLDistanceFormatterUnits
    var unitStyle: MGLDistanceFormatterUnitStyle

    public init() {
        locale = NSLocale.currentLocale()
        unitStyle = .Abbreviated

        if let countryCode = locale.objectForKey(NSLocaleCountryCode) as? String {
            switch (countryCode) {
            case MGLImperialCountries.US.rawValue:
                localeDeterminedUnits = .Imperial
                break
            case MGLImperialCountries.GB.rawValue:
                localeDeterminedUnits = .Imperial
                break
            case MGLImperialCountries.MM.rawValue:
                localeDeterminedUnits = .Imperial
                break
            case MGLImperialCountries.BS.rawValue:
                localeDeterminedUnits = .Imperial
                break
            case MGLImperialCountries.BZ.rawValue:
                localeDeterminedUnits = .Imperial
                break
            default:
                localeDeterminedUnits = .Metric
                break
            }
        } else {
            localeDeterminedUnits = .Metric
        }

        units = localeDeterminedUnits

    }

    public func stringFromDistance(distance: CLLocationDistance) -> String {
        //CLLocationDistance is a typealias for Double in meters

        guard distance >= 0.0 else {
            return ""
        }

        var distanceString: String!
        var convertedDistance: Double! //Distance after converting to user-specified units

        //These two booleans are in reference to whether we should-- use feet as opposed to miles / use meters as opposed to kilometers
        var useFeet = true
        var useMeters = true

        switch (units) {
        case .Metric:

            convertedDistance = distance //convertedDistance remains in meters
            if convertedDistance > METERS_PER_KM {
                convertedDistance = convertedDistance/METERS_PER_KM
                useMeters = false
            }

            if unitStyle == .Abbreviated {
                if useMeters {
                    distanceString = String(format: "%.1f \(ABB_METERS)", convertedDistance)
                } else {
                    distanceString = String(format: "%.1f \(ABB_KILOMETERS)", convertedDistance)
                }
            } else {
                if useMeters {
                    distanceString = String(format: "%.1f \(METERS)", convertedDistance)
                } else {
                    distanceString = String(format: "%.1f \(KILOMETERS)", convertedDistance)
                }
            }

            break
        case .Imperial:

            convertedDistance = distance * FEET_PER_METER //convertedDistance is now in feet
            if convertedDistance > FEET_PER_MILE/2.0 {
                convertedDistance = convertedDistance/FEET_PER_MILE
                useFeet = false
            }

            if unitStyle == .Abbreviated {
                if useFeet {
                    distanceString = String(format: "%.1f \(ABB_FEET)", convertedDistance)

                } else {
                    distanceString = String(format: "%.1f \(ABB_MILES)", convertedDistance)

                }
            } else {
                if useFeet {
                    if convertedDistance == 1.0 {
                        distanceString = String(format: "%.1f \(FOOT)", convertedDistance)
                    } else {
                        distanceString = String(format: "%.1f \(FEET)", convertedDistance)
                    }
                } else {
                    distanceString = String(format: "%.1f \(MILES)", convertedDistance)
                }
            }

            break
        case .ImperialWithYards:
            convertedDistance = distance * YARDS_PER_METER //convertedDistance is now in yards

            if unitStyle == .Abbreviated {
                distanceString = String(format: "%.1f \(ABB_YARDS)", convertedDistance)
            } else {
                distanceString = String(format: "%.1f \(YARDS)", convertedDistance)
            }

            break
        }

        return distanceString
    }

    public func distanceFromString(distance: String) -> CLLocationDistance {

        //Split our distance string into an array with the first element being the actual distance and the second being the unit

        let distanceComponents = distance.characters.split{$0 == " "}.map(String.init)
        guard distanceComponents.count == 2 else {
            return 0.0
        }

        let numericalDistance = Double(distanceComponents[0])
        guard numericalDistance >= 0 else {
            return 0.0
        }

        let unit = distanceComponents[1]
        var distanceInMeters: Double!

        switch (unit) {
        case METERS, ABB_METERS:
            distanceInMeters = numericalDistance
            return distanceInMeters
        case FEET, FOOT, ABB_FEET:
            distanceInMeters = numericalDistance!/FEET_PER_METER
            return distanceInMeters
        case YARDS, ABB_YARDS:
            distanceInMeters = numericalDistance!/YARDS_PER_METER
            return distanceInMeters
        case KILOMETERS, ABB_KILOMETERS:
            distanceInMeters = numericalDistance!*METERS_PER_KM
            return distanceInMeters
        case MILES, ABB_MILES:
            distanceInMeters = numericalDistance!*METERS_PER_MILE
            return distanceInMeters
        default:
            return 0.0
        }
    }

}

@tmcw
Copy link
Contributor

tmcw commented Mar 21, 2016

@1ec5 should this be implemented on the ios level in obj-c, or in c++?

@tmcw tmcw self-assigned this Mar 21, 2016
@1ec5
Copy link
Contributor Author

1ec5 commented Mar 21, 2016

This and #3391 should be implemented at the SDK level. There's little to be gained by making the transformation cross-platform, especially if localization is supported.

The SDK is currently entirely written in Objective-C/Objective-C++. We'd need some build system changes in order to support writing SDK classes in Swift, since one of the supported targets is a static framework that I don't think has the necessary build flags set for Swift modules.

The sample implementation above is solid, although it doesn't support localization, which would be important for any formatter. I think I'd prefer an implementation based on NSLengthFormatter, with perhaps a special case to avoid outputting yards in the en-US locale.

@cph2117
Copy link

cph2117 commented Mar 21, 2016

@1ec5 I didn't even think about the localization. Definitely something I should've done. Happy to help going forward

@tmcw tmcw removed their assignment Mar 21, 2016
@1ec5
Copy link
Contributor Author

1ec5 commented Apr 25, 2016

We added some formatters with localization support in #4802. They might serve as an example of how to go about making this formatter respect the current system locale. Another point worth mentioning is that NSLengthFormatter already chooses between metric and Imperial units. The main thing MKDistanceFormatter provides over a raw NSLengthFormatter is unit thresholds that make more sense for navigational distances.

Some time ago, we implemented something along these lines in an internal testbed application, also written in Swift. Here’s what it looks like. Most of the code is “an elaborate hack” for displaying mixed numbers with vulgar fractions instead of decimal miles, to match the convention established by U.S. and U.K. highway authorities.

@1ec5 1ec5 added the MapKit parity For feature parity with MapKit on iOS or macOS label Aug 22, 2016
@1ec5 1ec5 added this to the ios-future milestone Sep 20, 2016
@1ec5 1ec5 added the good first issue Good for newcomers label Oct 18, 2016
@1ec5 1ec5 added the localization Human language support and internationalization label Dec 7, 2016
@1ec5 1ec5 mentioned this issue Dec 21, 2016
3 tasks
@boundsj boundsj modified the milestones: ios-v3.5.0, ios-future Feb 2, 2017
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
feature good first issue Good for newcomers iOS Mapbox Maps SDK for iOS localization Human language support and internationalization macOS Mapbox Maps SDK for macOS MapKit parity For feature parity with MapKit on iOS or macOS
Projects
None yet
Development

No branches or pull requests

7 participants