UNPKG

geo-polyline-tools

Version:

A javaScript library that provides utility functions to encode and decode polyline coordinates

196 lines (169 loc) 7.62 kB
/* eslint-disable no-param-reassign */ /* eslint-disable func-names */ /* eslint-disable no-plusplus */ /* Based on Google's [Encoded Polyline Algorithm](https://developers.google.com/maps/documentation/utilities/polylinealgorithm) */ /* The `polyline` object contains functions for encoding and decoding polyline coordinates based on Google's Encoded Polyline Algorithm. It provides methods for encoding and decoding latitude and longitude coordinates arrays, as well as converting GeoJSON LineString features/geometry to polyline strings and vice versa. The `polyline` object also includes helper functions for rounding values and flipping coordinates. */ type Geometry = { type: string, coordinates: number[][] } type GeoJSON = { type: string, geometry?: Geometry } type Polyline = { toGeoJSON: (str: string, precision: number) => Geometry; fromGeoJSON: (geojson: GeoJSON, precision: number) => string; decode: (str: string, precision: number) => number[][]; encode: (coords: number[][], precision: number) => string; } const polyline: Partial<Polyline> = {} /** * The function `py2_round` implements a rounding strategy similar to Python 2 for both positive and * negative values. * @param value - The `value` parameter in the `py2_round` function represents the number that needs to * be rounded using the rounding strategy similar to Python 2. The function calculates the rounded * value based on the Python 2 rounding strategy, which differs from the default rounding behavior in * JavaScript for negative values. * @returns The function `py2_round` is returning a value that is rounded using the same strategy as * Python 2 for both positive and negative numbers. It uses `Math.floor(Math.abs(value) + 0.5) * (value * >= 0 ? 1 : -1)` to achieve this. */ function py2_round(value: number): number { // Google's polyline algorithm uses the same rounding strategy as Python 2, which is different from JS for negative values return Math.floor(Math.abs(value) + 0.5) * (value >= 0 ? 1 : -1) } /** * The function `encode` takes three parameters, performs some calculations, and returns an encoded * output string. * @param current - The `current` parameter represents the current value that you want to encode. * @param previous - The `previous` parameter in the `encode` function represents the previous value * that was encoded. It is used to calculate the difference between the current value and the previous * value in order to encode the data. * @param factor - The `factor` parameter in the `encode` function is used to scale the `current` and * `previous` values before calculating the coordinate. It is multiplied with both `current` and * `previous` values to adjust the scale of the coordinates. * @returns The function `encode` returns a string that is the result of encoding the difference * between the current and previous values multiplied by 2 using a specific encoding algorithm. */ function encode(current: number, previous: number, factor: number): string { current = py2_round(current * factor) previous = py2_round(previous * factor) let coordinate = (current - previous) * 2 if (coordinate < 0) { coordinate = -coordinate - 1 } let output = '' while (coordinate >= 0x20) { output += String.fromCharCode((0x20 | (coordinate & 0x1f)) + 63) coordinate /= 32 } output += String.fromCharCode((coordinate | 0) + 63) return output } /** * The `flipped` function takes an array of coordinates and returns a new array with each coordinate's * elements flipped. * @param coords - It looks like the `flipped` function takes an array of coordinates as input and * returns a new array where each coordinate is flipped, swapping its x and y values. * @returns The `flipped` function takes an array of coordinates and returns a new array where each * coordinate is flipped, swapping the x and y values. */ function flipped(coords: number[][]): number[][] { const _flipped = [] for (let i = 0; i < coords.length; i++) { const coord = coords[i].slice() _flipped.push([coord[1], coord[0]]) } return _flipped } // Encodes the given [latitude, longitude] coordinates array /* The `polyline.encode` function is responsible for encoding a given array of [latitude, longitude] coordinates into a polyline string. Here's a breakdown of what the function does: */ polyline.encode = function (coordinates: number[][], precision: number = 5) { if (!coordinates.length) { return '' } const factor = 10 ** (Number.isInteger(precision) ? precision : 5) let output = encode(coordinates[0][0], 0, factor) + encode(coordinates[0][1], 0, factor) for (let i = 1; i < coordinates.length; i++) { const a = coordinates[i]; const b = coordinates[i - 1] output += encode(a[0], b[0], factor) output += encode(a[1], b[1], factor) } return output } /** * Decodes to a [latitude, longitude] coordinates array. * This is adapted from the implementation in Project-OSRM. * @see https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js */ // Default 'percision' value is 5 /* The `polyline.decode` function is responsible for decoding a polyline string into a latitude and longitude coordinates array. Here's a breakdown of what the function does: */ polyline.decode = function (str: string, precision: number = 5): number[][] { let index = 0 let lat = 0 let lng = 0 const coordinates: number[][] = [] let shift = 0 let result = 0 let byte = null let latitude_change let longitude_change const factor = 10 ** (Number.isInteger(precision) ? precision : 5) // Coordinates have variable length when encoded, so just keep // track of whether we've hit the end of the string. In each // loop iteration, a single coordinate is decoded. while (index < str.length) { // Reset shift, result, and byte byte = null shift = 1 result = 0 do { byte = str.charCodeAt(index++) - 63 result += (byte & 0x1f) * shift shift *= 32 } while (byte >= 0x20) latitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2) shift = 1 result = 0 do { byte = str.charCodeAt(index++) - 63 result += (byte & 0x1f) * shift shift *= 32 } while (byte >= 0x20) longitude_change = (result & 1) ? ((-result - 1) / 2) : (result / 2) lat += latitude_change lng += longitude_change coordinates.push([lat / factor, lng / factor]) } return coordinates } // Encodes a GeoJSON LineString feature/geometry /* The `polyline.fromGeoJSON` function is responsible for encoding a GeoJSON LineString feature or geometry into a polyline string. Here's a breakdown of what the function does: */ polyline.fromGeoJSON = function (geojson: any, precision: number = 5): string { if (geojson && geojson.type === 'Feature') { geojson = geojson.geometry } if (!geojson || geojson.type !== 'LineString') { throw new Error('Input must be a GeoJSON LineString') } return polyline.encode ? polyline.encode(flipped(geojson.coordinates), precision) : '' } // Decodes to a GeoJSON LineString geometry /* The `polyline.toGeoJSON` function is responsible for decoding a polyline string into a GeoJSON LineString geometry. Here's a breakdown of what the function does: */ polyline.toGeoJSON = function (str: string, precision: number = 5) { // const coords = polyline.decode(str, precision) const coords = polyline.decode ? polyline.decode(str, precision) : [] return { type: 'LineString', coordinates: flipped(coords) } } export default polyline