File : core/lib/PolylineEncoder.js

1
/*
2
Copyright - 2017 2023 - wwwouaiebe - Contact: https://www.ouaie.be/
3
4
This  program is free software;
5
you can redistribute it and/or modify it under the terms of the
6
GNU General Public License as published by the Free Software Foundation;
7
either version 3 of the License, or any later version.
8
9
This program is distributed in the hope that it will be useful,
10
but WITHOUT ANY WARRANTY; without even the implied warranty of
11
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
GNU General Public License for more details.
13
14
You should have received a copy of the GNU General Public License
15
along with this program; if not, write to the Free Software
16
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
17
*/
18
/*
19
Changes:
20
    - v4.0.0:
21
        - created from v3.6.0
22
Doc reviewed 202208
23
 */
24
25
import { ZERO, ONE } from '../../main/Constants.js';
26
27
/*
28
Encoded Polyline Algorithm Format
29
30
Polyline encoding is a lossy compression algorithm that allows you to store a series of coordinates as a single string.
31
Point coordinates are encoded using signed values. If you only have a few static points, you may also wish to use the
32
interactive polyline encoding utility.
33
34
The encoding process converts a binary value into a series of character codes for ASCII characters using the familiar
35
base64 encoding scheme: to ensure proper display of these characters, encoded values are summed with 63
36
(the ASCII character '?') before converting them into ASCII. The algorithm also checks for additional character codes
37
for a given point by checking the least significant bit of each byte group; if this bit is set to 1, the point is not
38
yet fully formed and additional data must follow.
39
40
Additionally, to conserve space, points only include the offset from the previous point (except of course for the first
41
point). All points are encoded in Base64 as signed integers, as latitudes and longitudes are signed values. The encoding
42
format within a polyline needs to represent two coordinates representing latitude and longitude to a reasonable precision.
43
Given a maximum longitude of +/- 180 degrees to a precision of 5 decimal places (180.00000 to -180.00000), this results
44
in the need for a 32 bit signed binary integer value.
45
46
Note that the backslash is interpreted as an escape character within string literals. Any output of this utility should
47
convert backslash characters to double-backslashes within string literals.
48
49
The steps for encoding such a signed value are specified below.
50
51
    Take the initial signed value:
52
    -179.9832104
53
    Take the decimal value and multiply it by 1e5, rounding the result:
54
    -17998321
55
    Convert the decimal value to binary. Note that a negative value must be calculated using its two's complement by
56
    inverting the binary value and adding one to the result:
57
    00000001 00010010 10100001 11110001
58
    11111110 11101101 01011110 00001110
59
    11111110 11101101 01011110 00001111
60
    Left-shift the binary value one bit:
61
    11111101 11011010 10111100 00011110
62
    If the original decimal value is negative, invert this encoding:
63
    00000010 00100101 01000011 11100001
64
    Break the binary value out into 5-bit chunks (starting from the right hand side):
65
    00001 00010 01010 10000 11111 00001
66
    Place the 5-bit chunks into reverse order:
67
    00001 11111 10000 01010 00010 00001
68
    OR each value with 0x20 if another bit chunk follows:
69
    100001 111111 110000 101010 100010 000001
70
    Convert each value to decimal:
71
    33 63 48 42 34 1
72
    Add 63 to each value:
73
    96 126 111 105 97 64
74
    Convert each value to its ASCII equivalent:
75
    `~oia@
76
77
The table below shows some examples of encoded points, showing the encodings as a series of offsets from
78
previous points.
79
80
Example
81
82
Points: (38.5, -120.2), (40.7, -120.95), (43.252, -126.453)
83
Latitude Longitude    Latitude    Longitude      Change In      Change In      Encoded     Encoded     Encoded
84
                    in E5         in E5        Latitude    Longitude    Latitude    Longitude    Point
85
38.5     -120.2         3850000        -12020000     +3850000     -12020000     _p~iF         ~ps|U         _p~iF~ps|U
86
40.7     -120.95     4070000        -12095000     +220000     -75000         _ulL         nnqC         _ulLnnqC
87
43.252     -126.453     4325200        -12645300     +255200     -550300     _mqN         vxq`@         _mqNvxq`@
88
89
Encoded polyline: _p~iF~ps|U_ulLnnqC_mqNvxq`@
90
*/
91
92
/* ------------------------------------------------------------------------------------------------------------------------- */
93
/**
94
Encoder/decoder to encode or decode a polyline into a string.
95
96
See thePolylineEncoder for the one and only one instance of this class
97
98
Based on Mark McClure polyline encoder (more info needed...)
99
100
- See https://github.com/Project-OSRM/osrm-frontend/blob/master/WebContent/routing/OSRM.RoutingGeometry.js
101
- See https://github.com/graphhopper/directions-api-js-client/blob/master/src/GHUtil.js GHUtil.prototype.decodePath
102
- See https://developers.google.com/maps/documentation/utilities/polylinealgorithm
103
- See https://github.com/mapbox/polyline
104
*/
105
/* ------------------------------------------------------------------------------------------------------------------------- */
106
107
class PolylineEncoder {
108
109
    /**
110
    Simple constant to use for computations
111
    @type {Number}
112
    */
113
114
    // eslint-disable-next-line no-magic-numbers
115
    static get #DOT5 ( ) { return 0.5; }
116
117
    /**
118
    Simple constant to use for computations
119
    @type {Number}
120
    */
121
122
    // eslint-disable-next-line no-magic-numbers
123
    static get #NUMBER5 ( ) { return 5; }
124
125
    /**
126
    Simple constant to use for computations
127
    @type {Number}
128
    */
129
130
    // eslint-disable-next-line no-magic-numbers
131
    static get #NUMBER10 ( ) { return 10; }
132
133
    /**
134
    Simple constant to use for computations
135
    @type {Number}
136
    */
137
138
    // eslint-disable-next-line no-magic-numbers
139
    static get #NUMBER31 ( ) { return 0x1f; }
140
141
    /**
142
    Simple constant to use for computations
143
    @type {Number}
144
    */
145
146
    // eslint-disable-next-line no-magic-numbers
147
    static get #NUMBER32 ( ) { return 0x20; }
148
149
    /**
150
    Simple constant to use for computations
151
    @type {Number}
152
    */
153
154
    // eslint-disable-next-line no-magic-numbers
155
    static get #NUMBER63 ( ) { return 0x3f; }
156
157
    /**
158
    This method round a number in the same way than Python 2
159
    @param {Number} value The value to round
160
    @return {Number} The rounded value
161
    */
162
163
    #python2Round ( value ) {
164
        return Math.floor ( Math.abs ( value ) + PolylineEncoder.#DOT5 ) * ( ZERO <= value ? ONE : -ONE );
165
    }
166
167
    /**
168
    Helper method for the encode...
169
    @param {<array.<number>} current The current coordinates to encode
170
    @param {<array.<number>} previous The previously encoded coordinates
171
    @param {Number} factorD The precision to use
172
    */
173
174
    #encodeDelta ( current, previous, factorD ) {
175
        const currentCoordRound = this.#python2Round ( current * factorD );
176
        const previousCoordRound = this.#python2Round ( previous * factorD );
177
        let coordinateDelta = currentCoordRound - previousCoordRound;
178
        /* eslint-disable no-bitwise */
179
        coordinateDelta <<= ONE;
180
        if ( ZERO > currentCoordRound - previousCoordRound ) {
181
            coordinateDelta = ~ coordinateDelta;
182
        }
183
        let outputDelta = '';
184
        while ( PolylineEncoder.#NUMBER32 <= coordinateDelta ) {
185
            outputDelta +=
186
                String.fromCharCode (
187
                    (
188
                        PolylineEncoder.#NUMBER32
189
                        |
190
                        ( coordinateDelta & PolylineEncoder.#NUMBER31 )
191
                    ) +
192
                    PolylineEncoder.#NUMBER63
193
                );
194
            coordinateDelta >>= PolylineEncoder.#NUMBER5;
195
        }
196
        /* eslint-enable no-bitwise */
197
        outputDelta += String.fromCharCode ( coordinateDelta + PolylineEncoder.#NUMBER63 );
198
        return outputDelta;
199
    }
200
201
    /**
202
    tmp variable for decode and decodeDelta methods communication (cannot use parameter the two functions are modifying the
203
    value )
204
    @type {Number}
205
    */
206
207
    #index;
208
209
    /**
210
    Helper method for the decode...
211
    @param {String} encodedString The string to decode
212
    */
213
214
    #decodeDelta ( encodedString ) {
215
        let byte = null;
216
        let shift = ZERO;
217
        let result = ZERO;
218
        do {
219
            byte = encodedString.charCodeAt ( this.#index ++ ) - PolylineEncoder.#NUMBER63;
220
            /* eslint-disable no-bitwise */
221
            result |= ( byte & PolylineEncoder.#NUMBER31 ) << shift;
222
            shift += PolylineEncoder.#NUMBER5;
223
        } while ( PolylineEncoder.#NUMBER32 <= byte );
224
        return ( ( result & ONE ) ? ~ ( result >> ONE ) : ( result >> ONE ) );
225
        /* eslint-enable no-bitwise */
226
    }
227
228
    /**
229
    The constructor
230
    */
231
232
    constructor ( ) {
233
        Object.freeze ( this );
234
    }
235
236
    /**
237
    encode an array of coordinates to a string ( coordinates can be 1d or 2d or 3d or more...)
238
    @param {Array.<Array.<number>>} coordinatesArray the coordinates to encode
239
    @param {Array.<Number>} precisions an array with the precision to use for each dimension
240
    @return {String} the encoded coordinates
241
    */
242
243
    encode ( coordinatesArray, precisions ) {
244
        if ( ! coordinatesArray.length ) {
245
            return '';
246
        }
247
248
        const dimensions = precisions.length;
249
        const factors = Array.from ( precisions, precision => Math.pow ( PolylineEncoder.#NUMBER10, precision ) );
250
251
        let output = '';
252
        for ( let counter = 0; counter < dimensions; counter ++ ) {
253
            output += this.#encodeDelta ( coordinatesArray [ ZERO ] [ counter ], ZERO, factors [ counter ] );
254
        }
255
        for ( let coordCounter = ONE; coordCounter < coordinatesArray.length; coordCounter ++ ) {
256
            const currentCoord = coordinatesArray [ coordCounter ];
257
            const previousCoord = coordinatesArray [ coordCounter - ONE ];
258
            for ( let counter = 0; counter < dimensions; counter ++ ) {
259
                output += this.#encodeDelta ( currentCoord [ counter ], previousCoord [ counter ], factors [ counter ] );
260
            }
261
        }
262
263
        return output;
264
    }
265
266
    /**
267
    decode a string into an array of coordinates (coordinates can be 1d, 2d, 3d or more...)
268
    @param {String} encodedString the string to decode
269
    @param {Array.<Number>} precisions an array with the precision to use for each dimension
270
    @return {Array.<Array.<number>>} the decoded coordinates
271
    */
272
273
    decode ( encodedString, precisions ) {
274
        const dimensions = precisions.length;
275
        if ( ! encodedString || ZERO === encodedString.length ) {
276
            return [ ];
277
        }
278
279
        this.#index = ZERO;
280
        const allDecodedValues = [];
281
        const factors = Array.from ( precisions, precision => Math.pow ( PolylineEncoder.#NUMBER10, precision ) );
282
        const tmpValues = new Array ( dimensions ).fill ( ZERO );
283
284
        while ( this.#index < encodedString.length ) {
285
            const decodedValues = new Array ( dimensions ).fill ( ZERO );
286
            for ( let coordCounter = 0; coordCounter < dimensions; coordCounter ++ ) {
287
                tmpValues [ coordCounter ] += this.#decodeDelta ( encodedString );
288
                decodedValues [ coordCounter ] = tmpValues [ coordCounter ] / factors [ coordCounter ];
289
            }
290
            allDecodedValues.push ( decodedValues );
291
        }
292
293
        return allDecodedValues;
294
    }
295
}
296
297
export default PolylineEncoder;
298
299
/* --- End of file --------------------------------------------------------------------------------------------------------- */
300