monopolyline 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.js +473 -0
- package/index.test.ts +140 -1
- package/index.ts +225 -5
- package/monopolyline-0.0.2.tgz +0 -0
- package/monopolyline-0.0.3.tgz +0 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,9 @@ export type FieldType = 'decimal' | 'integer';
|
|
|
58
58
|
export type Schema =
|
|
59
59
|
| { type: 'object'; fields: Record<string, FieldType> }
|
|
60
60
|
| { type: 'tuple'; fields: FieldType[] };
|
|
61
|
+
|
|
62
|
+
// The library also provides default schemas for basic lat/lng
|
|
63
|
+
import { DEFAULT_SCHEMA, DEFAULT_TUPLE_SCHEMA } from 'monopolyline';
|
|
61
64
|
```
|
|
62
65
|
|
|
63
66
|
### Object Schema
|
|
@@ -117,3 +120,29 @@ When a schema is used or `includeTime`/`includeAccuracy` is enabled, the library
|
|
|
117
120
|
|
|
118
121
|
For extreme data compression on massive payloads, this string output pairs perfectly with LZ-based algorithms like `lz-string` or standard `pako` (Deflate). Because Polyline outputs ASCII characters, it achieves very high Deflate ratios.
|
|
119
122
|
|
|
123
|
+
|
|
124
|
+
### Merging Heterogeneous Chunks
|
|
125
|
+
You can merge multiple chunks encoded with different schemas or sparse strategies into a single unified polyline string by using `mergeChunks`. It automatically uses the fast-path for chunks that match the target format and falls back to full decode/re-encode for chunks that differ.
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { mergeChunks } from 'monopolyline';
|
|
129
|
+
|
|
130
|
+
// Create chunks with different options
|
|
131
|
+
const chunk1 = {
|
|
132
|
+
data: encodedString1,
|
|
133
|
+
options: { schema: { type: 'object', fields: { lat: 'decimal', lng: 'decimal', speed: 'integer' } }, sparseStrategy: 'bitmap' }
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const chunk2 = {
|
|
137
|
+
data: encodedString2,
|
|
138
|
+
options: { schema: { type: 'object', fields: { lat: 'decimal', lng: 'decimal', temp: 'integer' } }, sparseStrategy: 'naive' }
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Target format needs to encompass both
|
|
142
|
+
const targetOptions = {
|
|
143
|
+
schema: { type: 'object', fields: { lat: 'decimal', lng: 'decimal', speed: 'integer', temp: 'integer' } },
|
|
144
|
+
sparseStrategy: 'bitmap'
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const mergedStr = mergeChunks([chunk1, chunk2], targetOptions);
|
|
148
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type FieldType = 'decimal' | 'integer';
|
|
2
|
+
export type Schema = {
|
|
3
|
+
type: 'object';
|
|
4
|
+
fields: Record<string, FieldType>;
|
|
5
|
+
} | {
|
|
6
|
+
type: 'tuple';
|
|
7
|
+
fields: FieldType[];
|
|
8
|
+
};
|
|
9
|
+
export declare const DEFAULT_SCHEMA: Schema;
|
|
10
|
+
export declare const DEFAULT_TUPLE_SCHEMA: Schema;
|
|
11
|
+
export interface Point {
|
|
12
|
+
lat: number;
|
|
13
|
+
lng: number;
|
|
14
|
+
time?: number;
|
|
15
|
+
acc?: number;
|
|
16
|
+
}
|
|
17
|
+
export type SparseStrategy = 'naive' | 'bitmap' | 'columnar';
|
|
18
|
+
export interface EncodeOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Number of decimal places for 'decimal' fields.
|
|
21
|
+
* Default: 5 (Standard Google Polyline)
|
|
22
|
+
*/
|
|
23
|
+
precision?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Schema definition to make encoding/decoding generic.
|
|
26
|
+
* If provided, `includeTime` and `includeAccuracy` are ignored.
|
|
27
|
+
*/
|
|
28
|
+
schema?: Schema;
|
|
29
|
+
/**
|
|
30
|
+
* Include the time dimension in the encoded polyline.
|
|
31
|
+
* Default: false
|
|
32
|
+
* @deprecated Use `schema` instead for generic data structures.
|
|
33
|
+
*/
|
|
34
|
+
includeTime?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Include the accuracy dimension in the encoded polyline.
|
|
37
|
+
* Default: false
|
|
38
|
+
* @deprecated Use `schema` instead for generic data structures.
|
|
39
|
+
*/
|
|
40
|
+
includeAccuracy?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Strategy for encoding sparse data (missing values).
|
|
43
|
+
* 'naive' (default): Encodes missing values as 0 delta (repeats previous value).
|
|
44
|
+
* 'bitmap': Adds a small presence bitmask to each point to skip missing values.
|
|
45
|
+
* 'columnar': Splits the array into columns (lats, lngs, etc) with run-length presence flags.
|
|
46
|
+
*/
|
|
47
|
+
sparseStrategy?: SparseStrategy;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Encodes an array of data into an extended polyline string.
|
|
51
|
+
*/
|
|
52
|
+
export declare function encode(data: any[], options?: EncodeOptions): string;
|
|
53
|
+
/**
|
|
54
|
+
* Decodes an extended polyline string back into an array of data.
|
|
55
|
+
*/
|
|
56
|
+
export declare function decode(str: string, options?: EncodeOptions): any[];
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_TUPLE_SCHEMA = exports.DEFAULT_SCHEMA = void 0;
|
|
4
|
+
exports.encode = encode;
|
|
5
|
+
exports.decode = decode;
|
|
6
|
+
exports.DEFAULT_SCHEMA = {
|
|
7
|
+
type: 'object',
|
|
8
|
+
fields: {
|
|
9
|
+
lat: 'decimal',
|
|
10
|
+
lng: 'decimal'
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
exports.DEFAULT_TUPLE_SCHEMA = {
|
|
14
|
+
type: 'tuple',
|
|
15
|
+
fields: ['decimal', 'decimal']
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Encodes a numeric value using ZigZag encoding and base64-like chunking.
|
|
19
|
+
*/
|
|
20
|
+
function encodeNumber(value) {
|
|
21
|
+
let zz = value < 0 ? -value * 2 - 1 : value * 2;
|
|
22
|
+
let str = "";
|
|
23
|
+
while (zz >= 0x20) {
|
|
24
|
+
str += String.fromCharCode((0x20 | (zz & 0x1f)) + 63);
|
|
25
|
+
zz = Math.floor(zz / 32);
|
|
26
|
+
}
|
|
27
|
+
str += String.fromCharCode(zz + 63);
|
|
28
|
+
return str;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Decodes a single numeric value from the encoded string starting at the given index.
|
|
32
|
+
*/
|
|
33
|
+
function decodeNumber(str, index) {
|
|
34
|
+
let b = 0;
|
|
35
|
+
let result = 0;
|
|
36
|
+
let factor = 1;
|
|
37
|
+
do {
|
|
38
|
+
if (index >= str.length) {
|
|
39
|
+
throw new Error("Invalid polyline string: unexpected end of string");
|
|
40
|
+
}
|
|
41
|
+
b = str.charCodeAt(index++) - 63;
|
|
42
|
+
result += (b & 0x1f) * factor;
|
|
43
|
+
factor *= 32;
|
|
44
|
+
} while (b >= 0x20);
|
|
45
|
+
const value = result % 2 === 1 ? -(Math.floor(result / 2) + 1) : Math.floor(result / 2);
|
|
46
|
+
return [value, index];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Encodes an array of data into an extended polyline string.
|
|
50
|
+
*/
|
|
51
|
+
function encode(data, options = {}) {
|
|
52
|
+
if (!options.schema) {
|
|
53
|
+
return encodeLegacy(data, options);
|
|
54
|
+
}
|
|
55
|
+
return encodeGeneric(data, options);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Decodes an extended polyline string back into an array of data.
|
|
59
|
+
*/
|
|
60
|
+
function decode(str, options = {}) {
|
|
61
|
+
if (!options.schema) {
|
|
62
|
+
return decodeLegacy(str, options);
|
|
63
|
+
}
|
|
64
|
+
return decodeGeneric(str, options);
|
|
65
|
+
}
|
|
66
|
+
function encodeGeneric(data, options) {
|
|
67
|
+
var _a, _b;
|
|
68
|
+
const precision = (_a = options.precision) !== null && _a !== void 0 ? _a : 5;
|
|
69
|
+
const factor = Math.pow(10, precision);
|
|
70
|
+
const sparseStrategy = (_b = options.sparseStrategy) !== null && _b !== void 0 ? _b : 'naive';
|
|
71
|
+
const schema = options.schema;
|
|
72
|
+
const schemaFields = [];
|
|
73
|
+
if (schema.type === 'object') {
|
|
74
|
+
for (const key of Object.keys(schema.fields)) {
|
|
75
|
+
schemaFields.push({ key, type: schema.fields[key] });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
for (let i = 0; i < schema.fields.length; i++) {
|
|
80
|
+
schemaFields.push({ key: i, type: schema.fields[i] });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
let result = "";
|
|
84
|
+
if (sparseStrategy === 'columnar') {
|
|
85
|
+
result += encodeNumber(data.length);
|
|
86
|
+
for (let i = 0; i < schemaFields.length; i++) {
|
|
87
|
+
const field = schemaFields[i];
|
|
88
|
+
let prev = 0;
|
|
89
|
+
let presence = 0;
|
|
90
|
+
let bitIndex = 0;
|
|
91
|
+
let presenceStrs = "";
|
|
92
|
+
let dataStrs = "";
|
|
93
|
+
for (const pt of data) {
|
|
94
|
+
const val = pt[field.key];
|
|
95
|
+
if (val != null) {
|
|
96
|
+
presence |= (1 << bitIndex);
|
|
97
|
+
const scaled = field.type === 'decimal' ? Math.round(val * factor) : Math.round(val);
|
|
98
|
+
dataStrs += encodeNumber(scaled - prev);
|
|
99
|
+
prev = scaled;
|
|
100
|
+
}
|
|
101
|
+
bitIndex++;
|
|
102
|
+
if (bitIndex === 30) {
|
|
103
|
+
presenceStrs += encodeNumber(presence);
|
|
104
|
+
presence = 0;
|
|
105
|
+
bitIndex = 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (bitIndex > 0)
|
|
109
|
+
presenceStrs += encodeNumber(presence);
|
|
110
|
+
result += presenceStrs + dataStrs;
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
// naive or bitmap
|
|
115
|
+
const prevs = new Array(schemaFields.length).fill(0);
|
|
116
|
+
for (const pt of data) {
|
|
117
|
+
if (sparseStrategy === 'bitmap') {
|
|
118
|
+
let bitmask = 0;
|
|
119
|
+
for (let i = 0; i < schemaFields.length; i++) {
|
|
120
|
+
if (pt[schemaFields[i].key] != null)
|
|
121
|
+
bitmask |= (1 << i);
|
|
122
|
+
}
|
|
123
|
+
result += encodeNumber(bitmask);
|
|
124
|
+
}
|
|
125
|
+
for (let i = 0; i < schemaFields.length; i++) {
|
|
126
|
+
const field = schemaFields[i];
|
|
127
|
+
const val = pt[field.key];
|
|
128
|
+
if (sparseStrategy === 'bitmap') {
|
|
129
|
+
if (val != null) {
|
|
130
|
+
const scaled = field.type === 'decimal' ? Math.round(val * factor) : Math.round(val);
|
|
131
|
+
result += encodeNumber(scaled - prevs[i]);
|
|
132
|
+
prevs[i] = scaled;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// naive
|
|
137
|
+
if (val != null) {
|
|
138
|
+
const scaled = field.type === 'decimal' ? Math.round(val * factor) : Math.round(val);
|
|
139
|
+
result += encodeNumber(scaled - prevs[i]);
|
|
140
|
+
prevs[i] = scaled;
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
result += encodeNumber(0);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
function decodeGeneric(str, options) {
|
|
151
|
+
var _a, _b;
|
|
152
|
+
const precision = (_a = options.precision) !== null && _a !== void 0 ? _a : 5;
|
|
153
|
+
const factor = Math.pow(10, precision);
|
|
154
|
+
const sparseStrategy = (_b = options.sparseStrategy) !== null && _b !== void 0 ? _b : 'naive';
|
|
155
|
+
const schema = options.schema;
|
|
156
|
+
const schemaFields = [];
|
|
157
|
+
if (schema.type === 'object') {
|
|
158
|
+
for (const key of Object.keys(schema.fields)) {
|
|
159
|
+
schemaFields.push({ key, type: schema.fields[key] });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
for (let i = 0; i < schema.fields.length; i++) {
|
|
164
|
+
schemaFields.push({ key: i, type: schema.fields[i] });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
let index = 0;
|
|
168
|
+
if (sparseStrategy === 'columnar') {
|
|
169
|
+
if (str.length === 0)
|
|
170
|
+
return [];
|
|
171
|
+
let ptCount = 0;
|
|
172
|
+
[ptCount, index] = decodeNumber(str, index);
|
|
173
|
+
const results = Array.from({ length: ptCount }, () => (schema.type === 'tuple' ? [] : {}));
|
|
174
|
+
for (let f = 0; f < schemaFields.length; f++) {
|
|
175
|
+
const field = schemaFields[f];
|
|
176
|
+
const presenceFlags = [];
|
|
177
|
+
const numPresenceInts = Math.ceil(ptCount / 30);
|
|
178
|
+
for (let i = 0; i < numPresenceInts; i++) {
|
|
179
|
+
let p = 0;
|
|
180
|
+
[p, index] = decodeNumber(str, index);
|
|
181
|
+
for (let b = 0; b < 30 && (i * 30 + b) < ptCount; b++) {
|
|
182
|
+
presenceFlags.push((p & (1 << b)) !== 0);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
let prev = 0;
|
|
186
|
+
for (let i = 0; i < ptCount; i++) {
|
|
187
|
+
if (presenceFlags[i]) {
|
|
188
|
+
let delta = 0;
|
|
189
|
+
[delta, index] = decodeNumber(str, index);
|
|
190
|
+
prev += delta;
|
|
191
|
+
const val = field.type === 'decimal' ? prev / factor : prev;
|
|
192
|
+
results[i][field.key] = val;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return results;
|
|
197
|
+
}
|
|
198
|
+
const prevs = new Array(schemaFields.length).fill(0);
|
|
199
|
+
const results = [];
|
|
200
|
+
while (index < str.length) {
|
|
201
|
+
let bitmask = (1 << schemaFields.length) - 1; // default all present
|
|
202
|
+
if (sparseStrategy === 'bitmap') {
|
|
203
|
+
[bitmask, index] = decodeNumber(str, index);
|
|
204
|
+
}
|
|
205
|
+
const pt = schema.type === 'tuple' ? [] : {};
|
|
206
|
+
for (let f = 0; f < schemaFields.length; f++) {
|
|
207
|
+
const field = schemaFields[f];
|
|
208
|
+
if (sparseStrategy === 'bitmap') {
|
|
209
|
+
if ((bitmask & (1 << f)) !== 0) {
|
|
210
|
+
let delta = 0;
|
|
211
|
+
[delta, index] = decodeNumber(str, index);
|
|
212
|
+
prevs[f] += delta;
|
|
213
|
+
pt[field.key] = field.type === 'decimal' ? prevs[f] / factor : prevs[f];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// naive
|
|
218
|
+
let delta = 0;
|
|
219
|
+
[delta, index] = decodeNumber(str, index);
|
|
220
|
+
prevs[f] += delta;
|
|
221
|
+
pt[field.key] = field.type === 'decimal' ? prevs[f] / factor : prevs[f];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
results.push(pt);
|
|
225
|
+
}
|
|
226
|
+
return results;
|
|
227
|
+
}
|
|
228
|
+
function encodeLegacy(points, options) {
|
|
229
|
+
var _a, _b, _c, _d;
|
|
230
|
+
const precision = (_a = options.precision) !== null && _a !== void 0 ? _a : 5;
|
|
231
|
+
const factor = Math.pow(10, precision);
|
|
232
|
+
const includeTime = (_b = options.includeTime) !== null && _b !== void 0 ? _b : false;
|
|
233
|
+
const includeAccuracy = (_c = options.includeAccuracy) !== null && _c !== void 0 ? _c : false;
|
|
234
|
+
const sparseStrategy = (_d = options.sparseStrategy) !== null && _d !== void 0 ? _d : 'naive';
|
|
235
|
+
let result = "";
|
|
236
|
+
if (sparseStrategy === 'columnar') {
|
|
237
|
+
result += encodeNumber(points.length);
|
|
238
|
+
let prevLat = 0, prevLng = 0;
|
|
239
|
+
for (const point of points) {
|
|
240
|
+
const lat = Math.round(point.lat * factor);
|
|
241
|
+
const lng = Math.round(point.lng * factor);
|
|
242
|
+
result += encodeNumber(lat - prevLat);
|
|
243
|
+
result += encodeNumber(lng - prevLng);
|
|
244
|
+
prevLat = lat;
|
|
245
|
+
prevLng = lng;
|
|
246
|
+
}
|
|
247
|
+
if (includeTime) {
|
|
248
|
+
let prevTime = 0;
|
|
249
|
+
let timePresence = 0;
|
|
250
|
+
let bitIndex = 0;
|
|
251
|
+
let presenceStrs = "";
|
|
252
|
+
let dataStrs = "";
|
|
253
|
+
for (const point of points) {
|
|
254
|
+
if (point.time != null) {
|
|
255
|
+
timePresence |= (1 << bitIndex);
|
|
256
|
+
const time = Math.round(point.time);
|
|
257
|
+
dataStrs += encodeNumber(time - prevTime);
|
|
258
|
+
prevTime = time;
|
|
259
|
+
}
|
|
260
|
+
bitIndex++;
|
|
261
|
+
if (bitIndex === 30) {
|
|
262
|
+
presenceStrs += encodeNumber(timePresence);
|
|
263
|
+
timePresence = 0;
|
|
264
|
+
bitIndex = 0;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (bitIndex > 0)
|
|
268
|
+
presenceStrs += encodeNumber(timePresence);
|
|
269
|
+
result += presenceStrs + dataStrs;
|
|
270
|
+
}
|
|
271
|
+
if (includeAccuracy) {
|
|
272
|
+
let prevAcc = 0;
|
|
273
|
+
let accPresence = 0;
|
|
274
|
+
let bitIndex = 0;
|
|
275
|
+
let presenceStrs = "";
|
|
276
|
+
let dataStrs = "";
|
|
277
|
+
for (const point of points) {
|
|
278
|
+
if (point.acc != null) {
|
|
279
|
+
accPresence |= (1 << bitIndex);
|
|
280
|
+
const acc = Math.round(point.acc);
|
|
281
|
+
dataStrs += encodeNumber(acc - prevAcc);
|
|
282
|
+
prevAcc = acc;
|
|
283
|
+
}
|
|
284
|
+
bitIndex++;
|
|
285
|
+
if (bitIndex === 30) {
|
|
286
|
+
presenceStrs += encodeNumber(accPresence);
|
|
287
|
+
accPresence = 0;
|
|
288
|
+
bitIndex = 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (bitIndex > 0)
|
|
292
|
+
presenceStrs += encodeNumber(accPresence);
|
|
293
|
+
result += presenceStrs + dataStrs;
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
let prevLat = 0;
|
|
298
|
+
let prevLng = 0;
|
|
299
|
+
let prevTime = 0;
|
|
300
|
+
let prevAcc = 0;
|
|
301
|
+
for (const point of points) {
|
|
302
|
+
const lat = Math.round(point.lat * factor);
|
|
303
|
+
const lng = Math.round(point.lng * factor);
|
|
304
|
+
if (sparseStrategy === 'bitmap' && (includeTime || includeAccuracy)) {
|
|
305
|
+
let bitmask = 0;
|
|
306
|
+
if (includeTime && point.time != null)
|
|
307
|
+
bitmask |= 1;
|
|
308
|
+
if (includeAccuracy && point.acc != null)
|
|
309
|
+
bitmask |= 2;
|
|
310
|
+
result += encodeNumber(bitmask);
|
|
311
|
+
}
|
|
312
|
+
result += encodeNumber(lat - prevLat);
|
|
313
|
+
result += encodeNumber(lng - prevLng);
|
|
314
|
+
prevLat = lat;
|
|
315
|
+
prevLng = lng;
|
|
316
|
+
if (sparseStrategy === 'bitmap') {
|
|
317
|
+
if (includeTime && point.time != null) {
|
|
318
|
+
const time = Math.round(point.time);
|
|
319
|
+
result += encodeNumber(time - prevTime);
|
|
320
|
+
prevTime = time;
|
|
321
|
+
}
|
|
322
|
+
if (includeAccuracy && point.acc != null) {
|
|
323
|
+
const acc = Math.round(point.acc);
|
|
324
|
+
result += encodeNumber(acc - prevAcc);
|
|
325
|
+
prevAcc = acc;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
if (includeTime) {
|
|
330
|
+
if (point.time != null) {
|
|
331
|
+
const time = Math.round(point.time);
|
|
332
|
+
result += encodeNumber(time - prevTime);
|
|
333
|
+
prevTime = time;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
result += encodeNumber(0);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (includeAccuracy) {
|
|
340
|
+
if (point.acc != null) {
|
|
341
|
+
const acc = Math.round(point.acc);
|
|
342
|
+
result += encodeNumber(acc - prevAcc);
|
|
343
|
+
prevAcc = acc;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
result += encodeNumber(0);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
function decodeLegacy(str, options) {
|
|
354
|
+
var _a, _b, _c, _d;
|
|
355
|
+
const precision = (_a = options.precision) !== null && _a !== void 0 ? _a : 5;
|
|
356
|
+
const factor = Math.pow(10, precision);
|
|
357
|
+
const includeTime = (_b = options.includeTime) !== null && _b !== void 0 ? _b : false;
|
|
358
|
+
const includeAccuracy = (_c = options.includeAccuracy) !== null && _c !== void 0 ? _c : false;
|
|
359
|
+
const sparseStrategy = (_d = options.sparseStrategy) !== null && _d !== void 0 ? _d : 'naive';
|
|
360
|
+
let index = 0;
|
|
361
|
+
const points = [];
|
|
362
|
+
if (sparseStrategy === 'columnar') {
|
|
363
|
+
if (str.length === 0)
|
|
364
|
+
return points;
|
|
365
|
+
let ptCount = 0;
|
|
366
|
+
[ptCount, index] = decodeNumber(str, index);
|
|
367
|
+
let prevLat = 0;
|
|
368
|
+
let prevLng = 0;
|
|
369
|
+
for (let i = 0; i < ptCount; i++) {
|
|
370
|
+
let dLat = 0, dLng = 0;
|
|
371
|
+
[dLat, index] = decodeNumber(str, index);
|
|
372
|
+
[dLng, index] = decodeNumber(str, index);
|
|
373
|
+
prevLat += dLat;
|
|
374
|
+
prevLng += dLng;
|
|
375
|
+
points.push({ lat: prevLat / factor, lng: prevLng / factor });
|
|
376
|
+
}
|
|
377
|
+
if (includeTime) {
|
|
378
|
+
const presenceFlags = [];
|
|
379
|
+
const numPresenceInts = Math.ceil(ptCount / 30);
|
|
380
|
+
for (let i = 0; i < numPresenceInts; i++) {
|
|
381
|
+
let p = 0;
|
|
382
|
+
[p, index] = decodeNumber(str, index);
|
|
383
|
+
for (let b = 0; b < 30 && (i * 30 + b) < ptCount; b++) {
|
|
384
|
+
presenceFlags.push((p & (1 << b)) !== 0);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
let prevTime = 0;
|
|
388
|
+
for (let i = 0; i < ptCount; i++) {
|
|
389
|
+
if (presenceFlags[i]) {
|
|
390
|
+
let dTime = 0;
|
|
391
|
+
[dTime, index] = decodeNumber(str, index);
|
|
392
|
+
prevTime += dTime;
|
|
393
|
+
points[i].time = prevTime;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (includeAccuracy) {
|
|
398
|
+
const presenceFlags = [];
|
|
399
|
+
const numPresenceInts = Math.ceil(ptCount / 30);
|
|
400
|
+
for (let i = 0; i < numPresenceInts; i++) {
|
|
401
|
+
let p = 0;
|
|
402
|
+
[p, index] = decodeNumber(str, index);
|
|
403
|
+
for (let b = 0; b < 30 && (i * 30 + b) < ptCount; b++) {
|
|
404
|
+
presenceFlags.push((p & (1 << b)) !== 0);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
let prevAcc = 0;
|
|
408
|
+
for (let i = 0; i < ptCount; i++) {
|
|
409
|
+
if (presenceFlags[i]) {
|
|
410
|
+
let dAcc = 0;
|
|
411
|
+
[dAcc, index] = decodeNumber(str, index);
|
|
412
|
+
prevAcc += dAcc;
|
|
413
|
+
points[i].acc = prevAcc;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return points;
|
|
418
|
+
}
|
|
419
|
+
let prevLat = 0;
|
|
420
|
+
let prevLng = 0;
|
|
421
|
+
let prevTime = 0;
|
|
422
|
+
let prevAcc = 0;
|
|
423
|
+
while (index < str.length) {
|
|
424
|
+
let bitmask = 3;
|
|
425
|
+
if (sparseStrategy === 'bitmap' && (includeTime || includeAccuracy)) {
|
|
426
|
+
[bitmask, index] = decodeNumber(str, index);
|
|
427
|
+
}
|
|
428
|
+
let dLat = 0;
|
|
429
|
+
let dLng = 0;
|
|
430
|
+
[dLat, index] = decodeNumber(str, index);
|
|
431
|
+
[dLng, index] = decodeNumber(str, index);
|
|
432
|
+
prevLat += dLat;
|
|
433
|
+
prevLng += dLng;
|
|
434
|
+
const point = {
|
|
435
|
+
lat: prevLat / factor,
|
|
436
|
+
lng: prevLng / factor,
|
|
437
|
+
};
|
|
438
|
+
if (sparseStrategy === 'bitmap') {
|
|
439
|
+
if (includeTime && (bitmask & 1)) {
|
|
440
|
+
let dTime = 0;
|
|
441
|
+
[dTime, index] = decodeNumber(str, index);
|
|
442
|
+
prevTime += dTime;
|
|
443
|
+
point.time = prevTime;
|
|
444
|
+
}
|
|
445
|
+
if (includeAccuracy && (bitmask & 2)) {
|
|
446
|
+
let dAcc = 0;
|
|
447
|
+
[dAcc, index] = decodeNumber(str, index);
|
|
448
|
+
prevAcc += dAcc;
|
|
449
|
+
point.acc = prevAcc;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
if (includeTime) {
|
|
454
|
+
let dTime = 0;
|
|
455
|
+
[dTime, index] = decodeNumber(str, index);
|
|
456
|
+
if (dTime !== 0) {
|
|
457
|
+
prevTime += dTime;
|
|
458
|
+
point.time = prevTime;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
if (includeAccuracy) {
|
|
462
|
+
let dAcc = 0;
|
|
463
|
+
[dAcc, index] = decodeNumber(str, index);
|
|
464
|
+
if (dAcc !== 0) {
|
|
465
|
+
prevAcc += dAcc;
|
|
466
|
+
point.acc = prevAcc;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
points.push(point);
|
|
471
|
+
}
|
|
472
|
+
return points;
|
|
473
|
+
}
|
package/index.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
import { encode, decode, EncodeOptions, Schema } from './index';
|
|
2
|
+
import { encode, decode, merge, mergeMany, mergeChunks, EncodeOptions, Schema } from './index';
|
|
3
3
|
|
|
4
4
|
describe('monopolyline backwards compatibility', () => {
|
|
5
5
|
it('encodes and decodes lat/lng successfully (naive)', () => {
|
|
@@ -140,3 +140,142 @@ describe('monopolyline generic schema', () => {
|
|
|
140
140
|
expect(decoded[2][3]).toBeUndefined();
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
describe('monopolyline merge', () => {
|
|
145
|
+
it('merges naive polylines correctly', () => {
|
|
146
|
+
const pointsA = [{ lat: 38.5, lng: -120.2 }, { lat: 40.7, lng: -120.95 }];
|
|
147
|
+
const pointsB = [{ lat: 43.252, lng: -126.453 }, { lat: 44.0, lng: -127.0 }];
|
|
148
|
+
const encodedA = encode(pointsA);
|
|
149
|
+
const encodedB = encode(pointsB);
|
|
150
|
+
const merged = merge(encodedA, encodedB);
|
|
151
|
+
const decoded = decode(merged);
|
|
152
|
+
|
|
153
|
+
expect(decoded.length).toBe(4);
|
|
154
|
+
expect(decoded[1].lat).toBeCloseTo(40.7, 5);
|
|
155
|
+
expect(decoded[2].lat).toBeCloseTo(43.252, 5);
|
|
156
|
+
expect(decoded[3].lat).toBeCloseTo(44.0, 5);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('merges bitmap sparse polylines correctly', () => {
|
|
160
|
+
const dataA = [
|
|
161
|
+
{ lat: 38.5, lng: -120.2, speed: 50 },
|
|
162
|
+
{ lat: 40.7, lng: -120.95 }
|
|
163
|
+
];
|
|
164
|
+
const dataB = [
|
|
165
|
+
{ lat: 43.2, lng: -126.4, speed: 60 },
|
|
166
|
+
{ lat: 44.0, lng: -127.0 }
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const opts: EncodeOptions = {
|
|
170
|
+
schema: {
|
|
171
|
+
type: 'object',
|
|
172
|
+
fields: {
|
|
173
|
+
lat: 'decimal',
|
|
174
|
+
lng: 'decimal',
|
|
175
|
+
speed: 'integer'
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
sparseStrategy: 'bitmap'
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const encodedA = encode(dataA, opts);
|
|
182
|
+
const encodedB = encode(dataB, opts);
|
|
183
|
+
const merged = merge(encodedA, encodedB, opts);
|
|
184
|
+
const decoded = decode(merged, opts);
|
|
185
|
+
|
|
186
|
+
expect(decoded.length).toBe(4);
|
|
187
|
+
expect(decoded[0].speed).toBe(50);
|
|
188
|
+
expect(decoded[1].speed).toBeUndefined();
|
|
189
|
+
expect(decoded[2].speed).toBe(60);
|
|
190
|
+
expect(decoded[3].speed).toBeUndefined();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('merges columnar sparse polylines correctly', () => {
|
|
194
|
+
const dataA = [
|
|
195
|
+
{ lat: 38.5, lng: -120.2, speed: 50 },
|
|
196
|
+
{ lat: 40.7, lng: -120.95 }
|
|
197
|
+
];
|
|
198
|
+
const dataB = [
|
|
199
|
+
{ lat: 43.2, lng: -126.4, speed: 60 },
|
|
200
|
+
{ lat: 44.0, lng: -127.0 }
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
const opts: EncodeOptions = {
|
|
204
|
+
schema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
fields: {
|
|
207
|
+
lat: 'decimal',
|
|
208
|
+
lng: 'decimal',
|
|
209
|
+
speed: 'integer'
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
sparseStrategy: 'columnar'
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const encodedA = encode(dataA, opts);
|
|
216
|
+
const encodedB = encode(dataB, opts);
|
|
217
|
+
|
|
218
|
+
// Suppress warning for testing
|
|
219
|
+
const merged = merge(encodedA, encodedB, { ...opts, suppressWarnings: true });
|
|
220
|
+
const decoded = decode(merged, opts);
|
|
221
|
+
|
|
222
|
+
expect(decoded.length).toBe(4);
|
|
223
|
+
expect(decoded[0].speed).toBe(50);
|
|
224
|
+
expect(decoded[1].speed).toBeUndefined();
|
|
225
|
+
expect(decoded[2].speed).toBe(60);
|
|
226
|
+
expect(decoded[3].speed).toBeUndefined();
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('monopolyline mergeChunks', () => {
|
|
231
|
+
it('merges heterogeneous chunks correctly', () => {
|
|
232
|
+
// Chunk 1: bitmap sparse, has speed
|
|
233
|
+
const dataA = [
|
|
234
|
+
{ lat: 38.5, lng: -120.2, speed: 50 },
|
|
235
|
+
{ lat: 40.7, lng: -120.95 }
|
|
236
|
+
];
|
|
237
|
+
const optsA: EncodeOptions = {
|
|
238
|
+
schema: { type: 'object', fields: { lat: 'decimal', lng: 'decimal', speed: 'integer' } },
|
|
239
|
+
sparseStrategy: 'bitmap'
|
|
240
|
+
};
|
|
241
|
+
const encodedA = encode(dataA, optsA);
|
|
242
|
+
|
|
243
|
+
// Chunk 2: naive, has heading instead of speed
|
|
244
|
+
const dataB = [
|
|
245
|
+
{ lat: 43.2, lng: -126.4, heading: 90 },
|
|
246
|
+
{ lat: 44.0, lng: -127.0, heading: 180 }
|
|
247
|
+
];
|
|
248
|
+
const optsB: EncodeOptions = {
|
|
249
|
+
schema: { type: 'object', fields: { lat: 'decimal', lng: 'decimal', heading: 'integer' } },
|
|
250
|
+
sparseStrategy: 'naive'
|
|
251
|
+
};
|
|
252
|
+
const encodedB = encode(dataB, optsB);
|
|
253
|
+
|
|
254
|
+
// Target format: needs speed and heading, bitmap
|
|
255
|
+
const targetOpts: EncodeOptions = {
|
|
256
|
+
schema: { type: 'object', fields: { lat: 'decimal', lng: 'decimal', speed: 'integer', heading: 'integer' } },
|
|
257
|
+
sparseStrategy: 'bitmap'
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const chunks = [
|
|
261
|
+
{ data: encodedA, options: optsA },
|
|
262
|
+
{ data: encodedB, options: optsB }
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
const merged = mergeChunks(chunks, targetOpts);
|
|
266
|
+
const decoded = decode(merged, targetOpts);
|
|
267
|
+
|
|
268
|
+
expect(decoded.length).toBe(4);
|
|
269
|
+
expect(decoded[0].speed).toBe(50);
|
|
270
|
+
expect(decoded[0].heading).toBeUndefined();
|
|
271
|
+
expect(decoded[1].speed).toBeUndefined();
|
|
272
|
+
|
|
273
|
+
expect(decoded[2].speed).toBeUndefined();
|
|
274
|
+
expect(decoded[2].heading).toBe(90);
|
|
275
|
+
expect(decoded[3].heading).toBe(180);
|
|
276
|
+
|
|
277
|
+
// Check points
|
|
278
|
+
expect(decoded[1].lat).toBeCloseTo(40.7, 5);
|
|
279
|
+
expect(decoded[2].lat).toBeCloseTo(43.2, 5);
|
|
280
|
+
});
|
|
281
|
+
});
|
package/index.ts
CHANGED
|
@@ -4,6 +4,19 @@ export type Schema =
|
|
|
4
4
|
| { type: 'object'; fields: Record<string, FieldType> }
|
|
5
5
|
| { type: 'tuple'; fields: FieldType[] };
|
|
6
6
|
|
|
7
|
+
export const DEFAULT_SCHEMA: Schema = {
|
|
8
|
+
type: 'object',
|
|
9
|
+
fields: {
|
|
10
|
+
lat: 'decimal',
|
|
11
|
+
lng: 'decimal'
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_TUPLE_SCHEMA: Schema = {
|
|
16
|
+
type: 'tuple',
|
|
17
|
+
fields: ['decimal', 'decimal']
|
|
18
|
+
};
|
|
19
|
+
|
|
7
20
|
export interface Point {
|
|
8
21
|
lat: number;
|
|
9
22
|
lng: number;
|
|
@@ -13,6 +26,10 @@ export interface Point {
|
|
|
13
26
|
|
|
14
27
|
export type SparseStrategy = 'naive' | 'bitmap' | 'columnar';
|
|
15
28
|
|
|
29
|
+
export interface EncodeState {
|
|
30
|
+
values: number[];
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
export interface EncodeOptions {
|
|
17
34
|
/**
|
|
18
35
|
* Number of decimal places for 'decimal' fields.
|
|
@@ -47,6 +64,31 @@ export interface EncodeOptions {
|
|
|
47
64
|
* 'columnar': Splits the array into columns (lats, lngs, etc) with run-length presence flags.
|
|
48
65
|
*/
|
|
49
66
|
sparseStrategy?: SparseStrategy;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Internal state used for merging polylines.
|
|
70
|
+
*/
|
|
71
|
+
initialState?: EncodeState;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface MergeOptions extends Omit<EncodeOptions, 'initialState'> {
|
|
75
|
+
/**
|
|
76
|
+
* Suppress warnings when merging 'columnar' sparseStrategy.
|
|
77
|
+
*/
|
|
78
|
+
suppressWarnings?: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface EncodedChunk {
|
|
82
|
+
/**
|
|
83
|
+
* The encoded polyline string for this chunk.
|
|
84
|
+
*/
|
|
85
|
+
data: string;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The encoding options used for this specific chunk.
|
|
89
|
+
* If omitted, it is assumed to match the targetOptions of the merge.
|
|
90
|
+
*/
|
|
91
|
+
options?: EncodeOptions;
|
|
50
92
|
}
|
|
51
93
|
|
|
52
94
|
/**
|
|
@@ -155,7 +197,7 @@ function encodeGeneric(data: any[], options: EncodeOptions): string {
|
|
|
155
197
|
}
|
|
156
198
|
|
|
157
199
|
// naive or bitmap
|
|
158
|
-
const prevs = new Array(schemaFields.length).fill(0);
|
|
200
|
+
const prevs = options.initialState?.values ? [...options.initialState.values] : new Array(schemaFields.length).fill(0);
|
|
159
201
|
|
|
160
202
|
for (const pt of data) {
|
|
161
203
|
if (sparseStrategy === 'bitmap') {
|
|
@@ -351,10 +393,10 @@ function encodeLegacy(points: Point[], options: EncodeOptions): string {
|
|
|
351
393
|
return result;
|
|
352
394
|
}
|
|
353
395
|
|
|
354
|
-
let prevLat = 0;
|
|
355
|
-
let prevLng = 0;
|
|
356
|
-
let prevTime = 0;
|
|
357
|
-
let prevAcc = 0;
|
|
396
|
+
let prevLat = options.initialState?.values[0] ?? 0;
|
|
397
|
+
let prevLng = options.initialState?.values[1] ?? 0;
|
|
398
|
+
let prevTime = options.initialState?.values[2] ?? 0;
|
|
399
|
+
let prevAcc = options.initialState?.values[3] ?? 0;
|
|
358
400
|
|
|
359
401
|
for (const point of points) {
|
|
360
402
|
const lat = Math.round(point.lat * factor);
|
|
@@ -540,3 +582,181 @@ function decodeLegacy(str: string, options: EncodeOptions): Point[] {
|
|
|
540
582
|
return points;
|
|
541
583
|
}
|
|
542
584
|
|
|
585
|
+
export function getFinalState(str: string, options: EncodeOptions = {}): EncodeState {
|
|
586
|
+
const precision = options.precision ?? 5;
|
|
587
|
+
const sparseStrategy = options.sparseStrategy ?? 'naive';
|
|
588
|
+
|
|
589
|
+
if (str.length === 0 || sparseStrategy === 'columnar') {
|
|
590
|
+
return { values: [] };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
let index = 0;
|
|
594
|
+
|
|
595
|
+
if (options.schema) {
|
|
596
|
+
const schema = options.schema;
|
|
597
|
+
const numFields = schema.type === 'object' ? Object.keys(schema.fields).length : schema.fields.length;
|
|
598
|
+
const prevs = new Array(numFields).fill(0);
|
|
599
|
+
|
|
600
|
+
while (index < str.length) {
|
|
601
|
+
let bitmask = (1 << numFields) - 1;
|
|
602
|
+
if (sparseStrategy === 'bitmap') {
|
|
603
|
+
[bitmask, index] = decodeNumber(str, index);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
for (let f = 0; f < numFields; f++) {
|
|
607
|
+
if (sparseStrategy === 'bitmap') {
|
|
608
|
+
if ((bitmask & (1 << f)) !== 0) {
|
|
609
|
+
let delta = 0;
|
|
610
|
+
[delta, index] = decodeNumber(str, index);
|
|
611
|
+
prevs[f] += delta;
|
|
612
|
+
}
|
|
613
|
+
} else {
|
|
614
|
+
let delta = 0;
|
|
615
|
+
[delta, index] = decodeNumber(str, index);
|
|
616
|
+
prevs[f] += delta;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return { values: prevs };
|
|
621
|
+
} else {
|
|
622
|
+
const includeTime = options.includeTime ?? false;
|
|
623
|
+
const includeAccuracy = options.includeAccuracy ?? false;
|
|
624
|
+
let prevLat = 0, prevLng = 0, prevTime = 0, prevAcc = 0;
|
|
625
|
+
|
|
626
|
+
while (index < str.length) {
|
|
627
|
+
let bitmask = 3;
|
|
628
|
+
if (sparseStrategy === 'bitmap' && (includeTime || includeAccuracy)) {
|
|
629
|
+
[bitmask, index] = decodeNumber(str, index);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
let dLat = 0, dLng = 0;
|
|
633
|
+
[dLat, index] = decodeNumber(str, index);
|
|
634
|
+
[dLng, index] = decodeNumber(str, index);
|
|
635
|
+
prevLat += dLat;
|
|
636
|
+
prevLng += dLng;
|
|
637
|
+
|
|
638
|
+
if (sparseStrategy === 'bitmap') {
|
|
639
|
+
if (includeTime && (bitmask & 1)) {
|
|
640
|
+
let dTime = 0;
|
|
641
|
+
[dTime, index] = decodeNumber(str, index);
|
|
642
|
+
prevTime += dTime;
|
|
643
|
+
}
|
|
644
|
+
if (includeAccuracy && (bitmask & 2)) {
|
|
645
|
+
let dAcc = 0;
|
|
646
|
+
[dAcc, index] = decodeNumber(str, index);
|
|
647
|
+
prevAcc += dAcc;
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
if (includeTime) {
|
|
651
|
+
let dTime = 0;
|
|
652
|
+
[dTime, index] = decodeNumber(str, index);
|
|
653
|
+
if (dTime !== 0) prevTime += dTime;
|
|
654
|
+
}
|
|
655
|
+
if (includeAccuracy) {
|
|
656
|
+
let dAcc = 0;
|
|
657
|
+
[dAcc, index] = decodeNumber(str, index);
|
|
658
|
+
if (dAcc !== 0) prevAcc += dAcc;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return { values: [prevLat, prevLng, prevTime, prevAcc] };
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function merge(encodedA: string, encodedB: string, options: MergeOptions = {}): string {
|
|
667
|
+
return mergeMany([encodedA, encodedB], options);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
export function mergeMany(encodedList: string[], options: MergeOptions = {}): string {
|
|
671
|
+
const list = encodedList.filter(s => s.length > 0);
|
|
672
|
+
if (list.length === 0) return "";
|
|
673
|
+
if (list.length === 1) return list[0];
|
|
674
|
+
|
|
675
|
+
const sparseStrategy = options.sparseStrategy ?? 'naive';
|
|
676
|
+
|
|
677
|
+
if (sparseStrategy === 'columnar') {
|
|
678
|
+
if (!options.suppressWarnings) {
|
|
679
|
+
console.warn("Merging 'columnar' encoded strings requires a full decode and re-encode. Pass { suppressWarnings: true } to suppress this warning.");
|
|
680
|
+
}
|
|
681
|
+
// Full decode and encode
|
|
682
|
+
const allPoints: any[] = [];
|
|
683
|
+
for (const str of list) {
|
|
684
|
+
const decoded = decode(str, options);
|
|
685
|
+
allPoints.push(...decoded);
|
|
686
|
+
}
|
|
687
|
+
return encode(allPoints, options);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Naive or Bitmap: fast path merging
|
|
691
|
+
let result = list[0];
|
|
692
|
+
let currentState = getFinalState(result, options);
|
|
693
|
+
|
|
694
|
+
for (let i = 1; i < list.length; i++) {
|
|
695
|
+
const strB = list[i];
|
|
696
|
+
const pointsB = decode(strB, options);
|
|
697
|
+
|
|
698
|
+
const encodeOpts: EncodeOptions = {
|
|
699
|
+
...options,
|
|
700
|
+
initialState: currentState
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
const encodedB = encode(pointsB, encodeOpts);
|
|
704
|
+
result += encodedB;
|
|
705
|
+
|
|
706
|
+
// Update state for the next merge
|
|
707
|
+
currentState = getFinalState(encodedB, encodeOpts);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return result;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function mergeChunks(chunks: EncodedChunk[], targetOptions: MergeOptions = {}): string {
|
|
714
|
+
const list = chunks.filter(c => c.data.length > 0);
|
|
715
|
+
if (list.length === 0) return "";
|
|
716
|
+
|
|
717
|
+
const targetStrategy = targetOptions.sparseStrategy ?? 'naive';
|
|
718
|
+
|
|
719
|
+
if (targetStrategy === 'columnar') {
|
|
720
|
+
if (!targetOptions.suppressWarnings) {
|
|
721
|
+
console.warn("Merging into 'columnar' requires full decode and re-encode. Pass { suppressWarnings: true } to suppress this warning.");
|
|
722
|
+
}
|
|
723
|
+
const allPoints: any[] = [];
|
|
724
|
+
for (const chunk of list) {
|
|
725
|
+
const decoded = decode(chunk.data, chunk.options ?? targetOptions);
|
|
726
|
+
allPoints.push(...decoded);
|
|
727
|
+
}
|
|
728
|
+
return encode(allPoints, targetOptions);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
let result = "";
|
|
732
|
+
let currentState: EncodeState | undefined = undefined;
|
|
733
|
+
|
|
734
|
+
for (let i = 0; i < list.length; i++) {
|
|
735
|
+
const chunk = list[i];
|
|
736
|
+
// If chunk options are provided, use them; otherwise assume it matches target options
|
|
737
|
+
const chunkOpts = chunk.options ?? targetOptions;
|
|
738
|
+
const isTargetFormat = !chunk.options; // If omitted, we assume it's already target format
|
|
739
|
+
|
|
740
|
+
if (i === 0 && isTargetFormat) {
|
|
741
|
+
// Fast-path for the very first chunk: if it's already in the target format, append as-is
|
|
742
|
+
result = chunk.data;
|
|
743
|
+
currentState = getFinalState(result, targetOptions);
|
|
744
|
+
} else {
|
|
745
|
+
// Decode the chunk using its specific options
|
|
746
|
+
const decodedPoints = decode(chunk.data, chunkOpts);
|
|
747
|
+
|
|
748
|
+
// Re-encode into the target format, applying the offset from the current state
|
|
749
|
+
const reencodeOpts: EncodeOptions = {
|
|
750
|
+
...targetOptions,
|
|
751
|
+
initialState: currentState
|
|
752
|
+
};
|
|
753
|
+
|
|
754
|
+
const encodedChunk = encode(decodedPoints, reencodeOpts);
|
|
755
|
+
result += encodedChunk;
|
|
756
|
+
|
|
757
|
+
currentState = getFinalState(encodedChunk, reencodeOpts);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return result;
|
|
762
|
+
}
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED