【问题标题】:Decoding HERE REST API polyline in Swift?在 Swift 中解码 HERE REST API 折线?
【发布时间】:2020-08-12 08:33:36
【问题描述】:

我正在使用 HERE REST API 来获取 Swift 中的公交路线。

通常当我在 Swift 中解码折线时,我会使用这个很棒的库 https://github.com/raphaelmor/Polyline/,它适用于 OpenTripPlanner、GoogleDirections、Graphhopper(无海拔)等

HERE 折线的编码方式似乎不同

func testDecodePolyline() throws {
    let herePolyline = "BHwp7v0W0_uykO89CkU-yIs8B28K89CgkHq8BkmE6a-gF-uBquFooBqvKm3Cg1FooByzEmoB6-Hs8Bm6EooB0kDkUquFq8B4_MqrDi5MykD2jU0nF47F21BzZqwF"
    let coordinates: [CLLocationCoordinate2D]? = decodePolyline(herePolyline)
    XCTAssertNotNil(coordinates)
}

使用提到的库不起作用。

编码问题有答案:HERE Polyline Encoding: JavaScript -> Swift, 实施本文档:https://developer.here.com/documentation/places/dev_guide/topics/location-contexts.html#location-contexts__here-polyline-encoding

还有一个来自 HERE 的库,支持多种语言,但不支持 Swift 或 Objective-C:https://github.com/heremaps/flexible-polyline

如何在 Swift 中从 REST API 解码 HERE 折线?

我更喜欢不使用 iOS HERE SDK 的解决方案。如果必须,我会安装 iOS HERE SDK。

【问题讨论】:

    标签: ios swift here-api polyline


    【解决方案1】:

    我将https://github.com/heremaps/flexible-polyline/blob/master/java/src/com/here/flexpolyline/PolylineEncoderDecoder.java 从 Java 翻译成 Swift:

    public class HEREPolylineEncoderDecoder {
        public static let FORMAT_VERSION: Int64 = 1;
    
        public enum PolylineEncoderDecoderError: Error {
            case IllegalArgumentException(_ cause: String)
        }
    
        //Base64 URL-safe characters
        public static let ENCODING_TABLE: [Character]  = Array("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")
    
        public static let DECODING_TABLE: [Int64] = [
                                                62, -1, -1, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1,
                                                0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
                                                22, 23, 24, 25, -1, -1, -1, -1, 63, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
                                                36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
            ]
        /**
         * Encode the list of coordinate triples.<BR><BR>
         * The third dimension value will be eligible for encoding only when ThirdDimension is other than ABSENT.
         * This is lossy compression based on precision accuracy.
         *
         * @param coordinates {@link List} of coordinate triples that to be encoded.
         * @param precision   Floating point precision of the coordinate to be encoded.
         * @param thirdDimension {@link ThirdDimension} which may be a level, altitude, elevation or some other custom value
         * @param thirdDimPrecision Floating point precision for thirdDimension value
         * @return URL-safe encoded {@link String} for the given coordinates.
         */
        public static func encode(coordinates: [LatLngZ], precision: Int64, thirdDimension: ThirdDimension, thirdDimPrecision: Int64) throws -> String {
            if (coordinates.count == 0) {
                throw PolylineEncoderDecoderError.IllegalArgumentException("Invalid coordinates!")
            }
            let enc: Encoder = try Encoder(precision, thirdDimension, thirdDimPrecision)
            for coordinate in coordinates {
                enc.add(coordinate)
            }
            return enc.getEncoded()
        }
    
        /**
         * Decode the encoded input {@link String} to {@link List} of coordinate triples.<BR><BR>
         * @param encoded URL-safe encoded {@link String}
         * @return {@link List} of coordinate triples that are decoded from input
         *
         * @see PolylineDecoder#getThirdDimension(String) getThirdDimension
         * @see LatLngZ
         */
        public static func decode(_ encoded: String) throws -> [LatLngZ] {
    
            if (encoded.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) {
                throw PolylineEncoderDecoderError.IllegalArgumentException("Invalid argument!")
            }
            var result = [LatLngZ]()
            let dec = try Decoder(encoded)
            var lat: Double = 0.0
            var lng: Double = 0.0
            var z  : Double = 0.0
    
            while (try dec.decodeOne(&lat, &lng, &z)) {
                result.append(LatLngZ(lat, lng, z))
                lat = 0.0
                lng = 0.0
                z   = 0.0
            }
            return result
        }
    
        /**
         * ThirdDimension type from the encoded input {@link String}
         * @param encoded URL-safe encoded coordinate triples {@link String}
         * @return type of {@link ThirdDimension}
         */
        public func getThirdDimension(encoded: String) throws -> ThirdDimension {
            var index: Int64 = 0
            var header: Int64 = 0
            try Decoder.decodeHeaderFromString(encoded, &index, &header)
            guard let td =  ThirdDimension(rawValue: (header >> 4) & 7) else {
                throw PolylineEncoderDecoderError.IllegalArgumentException("thirdDimPrecision out of range")
            }
            return td
        }
    
        public func getVersion() -> Int64 {
            return HEREPolylineEncoderDecoder.FORMAT_VERSION
        }
    
        /*
         * Single instance for configuration, validation and encoding for an input request.
         */
        private class Encoder {
    
            private var result: String
            private let latConveter: Converter
            private let lngConveter: Converter
            private let zConveter: Converter
            private let thirdDimension: ThirdDimension
    
            public init(_ precision: Int64, _ thirdDimension: ThirdDimension, _ thirdDimPrecision: Int64) throws {
                self.latConveter = Converter(precision)
                self.lngConveter = Converter(precision)
                self.zConveter = Converter(thirdDimPrecision)
                self.thirdDimension = thirdDimension
                self.result = ""
                try encodeHeader(precision, self.thirdDimension.rawValue, thirdDimPrecision);
            }
    
            private func encodeHeader(_ precision: Int64, _ thirdDimensionValue: Int64, _ thirdDimPrecision: Int64) throws {
                /*
                 * Encode the `precision`, `third_dim` and `third_dim_precision` into one encoded char
                 */
                if (precision < 0 || precision > 15) {
                    throw PolylineEncoderDecoderError.IllegalArgumentException("precision out of range")
                }
    
                if (thirdDimPrecision < 0 || thirdDimPrecision > 15) {
                    throw PolylineEncoderDecoderError.IllegalArgumentException("thirdDimPrecision out of range")
                }
    
                if (thirdDimensionValue < 0 || thirdDimensionValue > 7) {
                    throw PolylineEncoderDecoderError.IllegalArgumentException("thirdDimensionValue out of range")
                }
                let res: Int64 = (thirdDimPrecision << 7) | (thirdDimensionValue << 4) | precision
                Converter.encodeUnsignedVarint(HEREPolylineEncoderDecoder.FORMAT_VERSION, &result)
                Converter.encodeUnsignedVarint(res, &result)
            }
    
            private func add(_ lat: Double, _ lng: Double) {
                latConveter.encodeValue(lat, &result);
                lngConveter.encodeValue(lng, &result);
            }
    
            private func add(_ lat: Double, _ lng: Double, _ z: Double) {
                add(lat, lng);
                if (self.thirdDimension != ThirdDimension.ABSENT) {
                    zConveter.encodeValue(z, &result);
                }
            }
    
            fileprivate func add(_ tuple: LatLngZ) {
                add(tuple.lat, tuple.lng, tuple.z);
            }
    
            fileprivate func getEncoded() -> String {
                return self.result
            }
        }
    
        /*
         * Single instance for decoding an input request.
         */
        private class Decoder {
    
            private let encoded: String
            private var index: Int64
            private let latConveter: Converter
            private let lngConveter: Converter
            private let zConveter: Converter
    
            private let precision: Int64
            private let thirdDimPrecision: Int64
            private let thirdDimension: ThirdDimension
    
    
            public init(_ encoded: String) throws {
                self.encoded = encoded;
                self.index = 0
    
                // decodeHeader():
                var header: Int64 = 0
                try HEREPolylineEncoderDecoder.Decoder.decodeHeaderFromString(encoded, &index, &header);
                self.precision = (header & 15); // we pick the first 4 bits only
                header = (header >> 4);
                guard let td = ThirdDimension(rawValue: header & 7) else {
                    throw PolylineEncoderDecoderError.IllegalArgumentException("thirdDimensionValue out of range")
                }
                self.thirdDimension = td
                self.thirdDimPrecision = ((header >> 3) & 15);
                // end decodeHeader()
    
                self.latConveter = Converter(precision)
                self.lngConveter = Converter(precision)
                self.zConveter = Converter(thirdDimPrecision)
            }
    
            private func hasThirdDimension() -> Bool {
                return thirdDimension != ThirdDimension.ABSENT
            }
    
            fileprivate static func decodeHeaderFromString(_ encoded: String, _ index: inout Int64, _ header: inout Int64) throws {
                var value: Int64 = 0
    
                // Decode the header version
                if(!Converter.decodeUnsignedVarint(Array(encoded), &index, &value)) {
                    throw PolylineEncoderDecoderError.IllegalArgumentException("Invalid encoding")
                }
                if (value != FORMAT_VERSION) {
                    throw PolylineEncoderDecoderError.IllegalArgumentException("Invalid format version")
                }
                // Decode the polyline header
                if(!Converter.decodeUnsignedVarint(Array(encoded), &index, &value)) {
                    throw PolylineEncoderDecoderError.IllegalArgumentException("Invalid encoding")
                }
                header = value
            }
    
    
            fileprivate func decodeOne(_ lat: inout Double,
                                   _ lng: inout Double,
                                   _ z: inout Double) throws -> Bool {
                if (index == encoded.count) {
                    return false
                }
                if (!latConveter.decodeValue(encoded, &index, &lat)) {
                    throw PolylineEncoderDecoderError.IllegalArgumentException("Invalid encoding")
                }
                if (!lngConveter.decodeValue(encoded, &index, &lng)) {
                    throw PolylineEncoderDecoderError.IllegalArgumentException("Invalid encoding")
                }
                if (hasThirdDimension()) {
                    if (!zConveter.decodeValue(encoded, &index, &z)) {
                        throw PolylineEncoderDecoderError.IllegalArgumentException("Invalid encoding")
                    }
                }
                return true;
            }
        }
    
        //Decode a single char to the corresponding value
        private static func decodeChar(_ charValue: Character) -> Int64 {
            let pos: Int = Int(charValue.asciiValue ?? 0) - 45;
            if (pos < 0 || pos > 77) {
                return -1;
            }
            return DECODING_TABLE[pos];
        }
    
        /*
         * Stateful instance for encoding and decoding on a sequence of Coordinates part of an request.
         * Instance should be specific to type of coordinates (e.g. Lat, Lng)
         * so that specific type delta is computed for encoding.
         * Lat0 Lng0 3rd0 (Lat1-Lat0) (Lng1-Lng0) (3rdDim1-3rdDim0)
         */
        public class Converter {
    
            private var multiplier: Double = 0;
            private var lastValue: Int64 = 0;
    
            public init(_ precision: Int64) {
                // could be replaced by iterative inter muliplication, only calculated once
                self.multiplier = pow(10.0, Double(precision))
            }
    
    
            fileprivate static func encodeUnsignedVarint(_ val: Int64, _ result: inout String) {
                var value = val // make parameter mutable
                while (value > 0x1F) {
                    let pos: Int =  Int((value & 0x1F) | 0x20);
                    result.append(ENCODING_TABLE[pos]);
                    // value >>= 5;
                    value = value >> 5
                }
                result.append(ENCODING_TABLE[Int(value)]);
            }
    
            func encodeValue(_ value: Double, _ result: inout String) {
                /*
                 * Round-half-up
                 * round(-1.4) --> -1
                 * round(-1.5) --> -2
                 * round(-2.5) --> -3
                 */
                let scaledValue: Int64 = Int64(abs(value * multiplier).rounded() * value.signum().rounded())
                var delta: Int64 = scaledValue - lastValue
                let negative: Bool = delta < 0
    
                lastValue = scaledValue
    
                // make room on lowest bit
                delta = delta << 1
    
                // invert bits if the value is negative
                if (negative) {
                    delta = ~delta;
                }
                HEREPolylineEncoderDecoder.Converter.encodeUnsignedVarint(delta, &result);
            }
    
            fileprivate static func decodeUnsignedVarint(_ encoded: [Character],
                                                     _ index: inout Int64,
                                                     _ result: inout Int64) -> Bool {
                var shift: Int16 = 0
                var delta: Int64 = 0
                var value: Int64
    
                while (index < encoded.count) {
                    value = decodeChar(encoded[Int(index)])
                    if (value < 0) {
                        return false;
                    }
                    index = index + 1
                    delta |= (value & 0x1F) << shift;
                    if ((value & 0x20) == 0) {
                        result = delta
                        return true;
                    } else {
                        shift += 5;
                    }
                }
    
                if (shift > 0) {
                    return false;
                }
                return true;
            }
    
            //Decode single coordinate (say lat|lng|z) starting at index
            func decodeValue(_ encoded: String,
                             _ index: inout Int64,
                             _ coordinate: inout Double) -> Bool {
                var delta: Int64 = 0
                if (!HEREPolylineEncoderDecoder.Converter.decodeUnsignedVarint(Array(encoded), &index, &delta)) {
                    return false;
                }
                if ((delta & 1) != 0) {
                    delta = ~delta
                }
                delta = delta >> 1
                lastValue = lastValue + delta
                coordinate = (Double(lastValue) / multiplier)
                return true;
            }
        } // class Converter
    
    
        /**
         *     3rd dimension specification.
         *  Example a level, altitude, elevation or some other custom value.
         *  ABSENT is default when there is no third dimension en/decoding required.
         */
        public enum ThirdDimension: Int64 {
            case ABSENT // (0),
            case LEVEL // (1),
            case ALTITUDE // (2),
            case ELEVATION // (3),
            case RESERVED1 // (4),
            case RESERVED2 // (5),
            case CUSTOM1 // (6),
            case CUSTOM2 // (7);
        }
    
        /**
         * Coordinate triple
         */
        public class LatLngZ: CustomStringConvertible, Equatable {
            public let lat: Double
            public let lng: Double
            public let z: Double
    
            init(_ latitude: Double,_ longitude: Double, _ thirdDimension: Double = 0.0) {
                self.lat = latitude
                self.lng = longitude
                self.z   = thirdDimension
            }
    
            public func toString() -> String {
                return description
            }
    
            public var description: String {
                return "LatLngZ [lat=\(lat), lng=\(lng), z=\(z)]"
            }
            public static func == (lhs: HEREPolylineEncoderDecoder.LatLngZ, rhs: HEREPolylineEncoderDecoder.LatLngZ) -> Bool {
                return lhs.lat == rhs.lat
                    && lhs.lng == rhs.lng
                    && lhs.z   == rhs.z
            }
        } // inner class LatLngZ
    } // class HEREPolylineEncoderDecoder
    
    extension FloatingPoint {
      @inlinable
      func signum( ) -> Self {
        if self < 0 { return -1 }
        if self > 0 { return 1 }
        return 0
      }
    }
    

    【讨论】:

      【解决方案2】:

      截至目前,在 swift 中还没有现成的用于折线编码的库,并且仅支持以下语言。 https://github.com/heremaps/flexible-polyline

      我们无法对任何第三方库发表评论,但是我们可以与工程部门核实此开发是否正在进行中。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2022-08-04
        • 1970-01-01
        • 2014-12-15
        • 2012-02-11
        • 1970-01-01
        • 1970-01-01
        • 2013-11-15
        • 1970-01-01
        相关资源
        最近更新 更多