在 Swift 中解码 HERE REST API 折线?

Decoding HERE REST API polyline in Swift?

我正在使用 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)
}

使用提到的库不起作用。

编码问题有答案:, 实施此文档: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。

截至目前,swift 中尚无现成的多段线编码库,仅支持以下语言。 https://github.com/heremaps/flexible-polyline

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

我将 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
  }
}