diff --git a/Sources/GeoJSONKitTurf/GeoJSON+LineString+DecodePolyline.swift b/Sources/GeoJSONKitTurf/GeoJSON+LineString+DecodePolyline.swift deleted file mode 100644 index a48cd98..0000000 --- a/Sources/GeoJSONKitTurf/GeoJSON+LineString+DecodePolyline.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// GeoJSON+LineString+DecodePolylineString.swift -// -// Created by Adrian Schoenig on 18/2/17. -// -// -import Foundation - -import GeoJSONKit - -extension GeoJSON.LineString { - public init(encodedPolyline: String) { - let bytes = encodedPolyline.utf8CString - let length = bytes.count - 1 // ignore 0 at end - var idx = 0 - - var array: [GeoJSON.Position] = [] - - var latitude = 0.0 - var longitude = 0.0 - while idx < length { - var byte = 0 - var res = 0 - var shift = 0 - - repeat { - if idx > length { - break - } - byte = Int(bytes[idx]) - 63 - idx += 1 - res |= (byte & 0x1F) << shift - shift += 5 - } while byte >= 0x20 - - let deltaLat = ((res & 1) != 0 ? ~(res >> 1) : (res >> 1)); - latitude += Double(deltaLat) - - shift = 0 - res = 0 - - repeat { - if idx > length { - break - } - byte = Int(bytes[idx]) - 0x3F - idx += 1 - res |= (byte & 0x1F) << shift - shift += 5 - } while byte >= 0x20 - - let deltaLon = ((res & 1) != 0 ? ~(res >> 1) : (res >> 1)); - longitude += Double(deltaLon) - - let finalLat = latitude * 1E-5 - let finalLon = longitude * 1E-5 - let coordinate = GeoJSON.Position(latitude: finalLat, longitude: finalLon) - array.append(coordinate) - } - - self.init(positions: array) - } -} diff --git a/Sources/GeoJSONKitTurf/GeoJSON+LineString+EncodedPolyline.swift b/Sources/GeoJSONKitTurf/GeoJSON+LineString+EncodedPolyline.swift new file mode 100644 index 0000000..82ffb3d --- /dev/null +++ b/Sources/GeoJSONKitTurf/GeoJSON+LineString+EncodedPolyline.swift @@ -0,0 +1,137 @@ +// +// GeoJSON+LineString+EncodedPolyline.swift +// +// Created by Adrian Schoenig on 18/2/17. +// +// +import Foundation + +import GeoJSONKit + +extension GeoJSON.LineString { + + // MARK: - Decode + + public init(encodedPolyline: String) { + let bytes = encodedPolyline.utf8CString + let length = bytes.count - 1 // ignore 0 at end + var idx = 0 + + var array: [GeoJSON.Position] = [] + + var latitude = 0.0 + var longitude = 0.0 + while idx < length { + var byte = 0 + var res = 0 + var shift = 0 + + repeat { + if idx > length { + break + } + byte = Int(bytes[idx]) - 63 + idx += 1 + res |= (byte & 0x1F) << shift + shift += 5 + } while byte >= 0x20 + + let deltaLat = ((res & 1) != 0 ? ~(res >> 1) : (res >> 1)); + latitude += Double(deltaLat) + + shift = 0 + res = 0 + + repeat { + if idx > length { + break + } + byte = Int(bytes[idx]) - 0x3F + idx += 1 + res |= (byte & 0x1F) << shift + shift += 5 + } while byte >= 0x20 + + let deltaLon = ((res & 1) != 0 ? ~(res >> 1) : (res >> 1)); + longitude += Double(deltaLon) + + let finalLat = latitude * 1E-5 + let finalLon = longitude * 1E-5 + let coordinate = GeoJSON.Position(latitude: finalLat, longitude: finalLon) + array.append(coordinate) + } + + self.init(positions: array) + } + + // MARK: - Encode + + /// Encodes this `GeoJSON.LineString` to a `String` + /// + /// Adopted from https://github.com/raphaelmor/Polyline/blob/master/Sources/Polyline/Polyline.swift + /// + /// - parameter precision: The precision used to encode coordinates (default: `1e5`) + /// - returns: A `String` representing the encoded polyline + public func encodedPolyline(precision: Double = 1e5) -> String { + var previousCoordinate = IntegerCoordinates(0, 0) + var encodedPolyline = "" + + for position in positions { + let intLatitude = Int(round(position.latitude * precision)) + let intLongitude = Int(round(position.longitude * precision)) + + let coordinatesDifference = (intLatitude - previousCoordinate.latitude, intLongitude - previousCoordinate.longitude) + encodedPolyline += Self.encodeCoordinate(coordinatesDifference) + + previousCoordinate = (intLatitude, intLongitude) + } + + return encodedPolyline + } + + private typealias IntegerCoordinates = (latitude: Int, longitude: Int) + + private static func encodeCoordinate(_ coordinate: IntegerCoordinates) -> String { + let latitudeString = encodeSingleComponent(coordinate.latitude) + let longitudeString = encodeSingleComponent(coordinate.longitude) + return latitudeString + longitudeString + } + + private static func encodeSingleComponent(_ value: Int) -> String { + var intValue = value + if intValue < 0 { + intValue = intValue << 1 + intValue = ~intValue + } else { + intValue = intValue << 1 + } + return encodeFiveBitComponents(intValue) + } + + private static func encodeLevel(_ level: UInt32) -> String { + return encodeFiveBitComponents(Int(level)) + } + + private static func encodeFiveBitComponents(_ value: Int) -> String { + var remainingComponents = value + + var fiveBitComponent = 0 + var returnString = String() + + repeat { + fiveBitComponent = remainingComponents & 0x1F + + if remainingComponents >= 0x20 { + fiveBitComponent |= 0x20 + } + + fiveBitComponent += 63 + + let char = UnicodeScalar(fiveBitComponent)! + returnString.append(String(char)) + remainingComponents = remainingComponents >> 5 + } while (remainingComponents != 0) + + return returnString + } +} diff --git a/Tests/GeoJSONKitTurfTests/EncodedPolylineTests.swift b/Tests/GeoJSONKitTurfTests/EncodedPolylineTests.swift new file mode 100644 index 0000000..dc661cb --- /dev/null +++ b/Tests/GeoJSONKitTurfTests/EncodedPolylineTests.swift @@ -0,0 +1,28 @@ +// +// EncodedPolylineTests.swift +// GeoJSONKitTurf +// +// Created by Adrian Schönig on 14/2/2025. +// + +#if canImport(Testing) && swift(>=6) +import Testing + +import GeoJSONKit +import GeoJSONKitTurf + +struct EncodedPolylineTests { + + @Test func testPalermoToRome() async throws { + + let polyline = "iasgFkxqpAbbCseDtrBmpDv_BgeEvtB_mDdjB_}Dz|@iuEjc@ihF`YcdFnn@__FjiAowErgAmoEvCwgFqOkbFiPghFoI{hFwNgfFcS}eFa}@e}EpRgyE|UslFfEgcFtWihFvSqhFbZi_Fm_AaoEknA}xDcBwiF}MsgF}IahF{EegFo_@kdFcNshFqYeiFmW}`FoVefF_NslFkyAeeEkrAilEw~AoeEyiAwpEcyBwpD{hAgqEc[weFyc@qbFiy@s|Ea`BkbEaUk`FvcAouEfiAaqE]{hFqf@uaFky@gsEymBavDytBkyDqaCw_DkbBu~De`Au_Fsj@wyEk\\agFrUycFfx@ozEdfBc`EhBarEclBiaEws@mtEgXijFMkjFeEmdFyn@mbFemAomE}fAwyE{yB_cDanDyy@_{C}nBu_DqlByaDqmBegDmuAitDkSknDwo@akAmjEyrAewEcsAavEwqAwfEo|AwlE_}AgbEkoC{iCmuCoyBahDu_BcsDcNcoDys@aqDq\\gwD{EyqD{h@uhD{eAmrDuIwuD^grD~VodDbvAizC`zBm`DncBgbDryBa~C{\\qoDomAwoDmg@euDwJ{nDlp@_fDvtAmeDx{AgoDbhAenDz]{pD~a@}kDf`AqjDdiA_oD~t@ymDlLizDlLicDxgAw_DnoBgmDnn@_sDtEyrDfTulDrl@ymDt_AolD~}@irDb{@ycD`~@ouDnr@agDxr@unD~q@anDbq@skDby@icDh_BmnD|z@meDxm@qpDzr@yxDj_@s|Cdl@krBxzDaqBzwDu{B~yCg_DnxB_gC~jCgpC`eC{cCb{C}qBluDijBlrD_|BxkDuiChlCotB|{CquBt|DutB~wDmeB`oDcbBnlE{mAjlEciC|dAygDhaAeqCvdCk_CljDe{BzcDg~B~`D{zBdeD{}Bh_Ds}BxlDivB|kD_nBvmDueBnaEugBtvDu~Ax}DgcCxdDgsBzeDgkBxyDu}AdcEgt@`wEqr@pgF{^v{E_PngF_W|gF}s@x~E}^tbFef@r`Fu`AhvEay@l}E}o@tdFqlAzgE}z@~xEmjBfvDmnAhmE}bBrvDwcAh_Fyp@h|EewAxbEosA|nDu|BlxCkjBv{D}gB|aEyeB`bEw`B||Dq~Az}D}tBznDy}BnwCmjB~vDoiB|_EigBjxDwgBr~D{_BlcE{uA`gEozAvcEylBhvDa{BhdDioBlqDulBf~D}_BtuDm_CfhDo~CrqAscC`lC_eCfzCuzBffDe|AnbE{mBhuDogClnCquC~pBo_Cr|CeqBhnDeiB`{DudBnzDeqBtnDuoBhpD_qBdrDwcBn}D{aBn`EefBdxDshBtzDumBdxDotAbeEazAzhEm_Bj}DcdB|zDylBpiDadBr|DqdBheEs_Bj~D{~A`cEiuAdlEm`AjsEsv@x`Feu@`mFcf@t}EmCfaF}s@jzDs{CfiBsuB|zCusB|jDedB|zDaxBjiDgNpR" + + let decoded = GeoJSON.LineString(encodedPolyline: polyline) + #expect(decoded.positions.count == 256) + + #expect(decoded.encodedPolyline() == polyline) + } + +} + +#endif