monopolyline 0.0.2
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 +119 -0
- package/extended-polyline-0.0.1.tgz +0 -0
- package/index.test.ts +142 -0
- package/index.ts +542 -0
- package/monopolyline-0.0.1.tgz +0 -0
- package/package.json +23 -0
- package/tsconfig.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# monopolyline
|
|
2
|
+
|
|
3
|
+
A flexible compression library for trajectories and tabular data. Extends standard Google Polyline encoding to support custom schemas, tuples, timestamps, and missing telemetry data while preserving massive bandwidth savings.
|
|
4
|
+
|
|
5
|
+
- **Custom Decimal Precision**: Compress down to integer coordinates (`precision: 0`) or retain extreme sub-meter detail (`precision: 10`).
|
|
6
|
+
- **Generic Schema Support**: Define custom object or tuple structures (`{ lat: 'decimal', speed: 'integer' }`) for generic data encoding.
|
|
7
|
+
- **Sparse Data Strategies**: Choose from `naive`, `bitmap`, and `columnar` strategies to massively optimize missing telemetry data depending on your dataset shape.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install monopolyline
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage (Legacy Mode)
|
|
16
|
+
|
|
17
|
+
If no schema is provided, the library defaults to legacy mode (`lat`, `lng`, `time`, `acc`).
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
import { encode, decode } from 'monopolyline';
|
|
21
|
+
|
|
22
|
+
// Example GPS points
|
|
23
|
+
const points = [
|
|
24
|
+
{ lat: 37.7749, lng: -122.4194, time: 1672531200000, acc: 10 },
|
|
25
|
+
{ lat: 37.7750, lng: -122.4195, time: 1672531205000, acc: 12 },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// Encode
|
|
29
|
+
const encoded = encode(points, {
|
|
30
|
+
precision: 5,
|
|
31
|
+
includeTime: true,
|
|
32
|
+
includeAccuracy: true,
|
|
33
|
+
sparseStrategy: 'bitmap'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Decode
|
|
37
|
+
const decoded = decode(encoded, {
|
|
38
|
+
precision: 5,
|
|
39
|
+
includeTime: true,
|
|
40
|
+
includeAccuracy: true,
|
|
41
|
+
sparseStrategy: 'bitmap'
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage (Generic Schema)
|
|
46
|
+
|
|
47
|
+
You can define custom fields. Schema types support `object` and `tuple` formats. You can also import the `Schema` type for TypeScript support.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { encode, decode, Schema } from 'monopolyline';
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Schema Type Definition
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
export type FieldType = 'decimal' | 'integer';
|
|
57
|
+
|
|
58
|
+
export type Schema =
|
|
59
|
+
| { type: 'object'; fields: Record<string, FieldType> }
|
|
60
|
+
| { type: 'tuple'; fields: FieldType[] };
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Object Schema
|
|
64
|
+
```typescript
|
|
65
|
+
const data = [
|
|
66
|
+
{ lat: 38.5, lng: -120.2, speed: 50 },
|
|
67
|
+
{ lat: 40.7, lng: -120.95, speed: null }, // Missing data
|
|
68
|
+
{ lat: 43.2, lng: -126.4, speed: 60 }
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const opts = {
|
|
72
|
+
schema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
fields: {
|
|
75
|
+
lat: 'decimal',
|
|
76
|
+
lng: 'decimal',
|
|
77
|
+
speed: 'integer'
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
sparseStrategy: 'bitmap'
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const encoded = encode(data, opts);
|
|
84
|
+
const decoded = decode(encoded, opts);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Tuple Schema
|
|
88
|
+
Great for minimizing JSON payload overhead before compression.
|
|
89
|
+
```typescript
|
|
90
|
+
const data = [
|
|
91
|
+
[38.5, -120.2, 50],
|
|
92
|
+
[40.7, -120.95, null],
|
|
93
|
+
[43.2, -126.4, 60]
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const opts = {
|
|
97
|
+
schema: {
|
|
98
|
+
type: 'tuple',
|
|
99
|
+
fields: ['decimal', 'decimal', 'integer']
|
|
100
|
+
},
|
|
101
|
+
sparseStrategy: 'columnar'
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const encoded = encode(data, opts);
|
|
105
|
+
const decoded = decode(encoded, opts);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Sparse Strategies API
|
|
109
|
+
|
|
110
|
+
When a schema is used or `includeTime`/`includeAccuracy` is enabled, the library needs to know how to handle missing data.
|
|
111
|
+
|
|
112
|
+
- `naive` (default): Assumes data is dense. If a field is missing, it falls back to a zero-delta (repeating the previous value). Very fast, but wasteful on sparse data.
|
|
113
|
+
- `bitmap`: Prepends a bitmask for each row to indicate which dimensions are present. The most balanced approach for moderate or uneven sparsity.
|
|
114
|
+
- `columnar`: Radically restructures the encoding block into columns (e.g. Latitudes -> Longitudes -> Speeds) and prepends an efficient bit-packed presence map array. Best for highly sparse data, though slightly heavier computationally.
|
|
115
|
+
|
|
116
|
+
## Advanced Usage
|
|
117
|
+
|
|
118
|
+
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
|
+
|
|
Binary file
|
package/index.test.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { encode, decode, EncodeOptions, Schema } from './index';
|
|
3
|
+
|
|
4
|
+
describe('monopolyline backwards compatibility', () => {
|
|
5
|
+
it('encodes and decodes lat/lng successfully (naive)', () => {
|
|
6
|
+
const points = [
|
|
7
|
+
{ lat: 38.5, lng: -120.2 },
|
|
8
|
+
{ lat: 40.7, lng: -120.95 },
|
|
9
|
+
{ lat: 43.252, lng: -126.453 }
|
|
10
|
+
];
|
|
11
|
+
const encoded = encode(points);
|
|
12
|
+
const decoded = decode(encoded);
|
|
13
|
+
expect(decoded.length).toBe(3);
|
|
14
|
+
expect(decoded[0].lat).toBeCloseTo(38.5, 5);
|
|
15
|
+
expect(decoded[2].lat).toBeCloseTo(43.252, 5);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('handles optional time and acc correctly in bitmap mode', () => {
|
|
19
|
+
const points = [
|
|
20
|
+
{ lat: 38.5, lng: -120.2, time: 1000, acc: 10 },
|
|
21
|
+
{ lat: 40.7, lng: -120.95, time: 1005 },
|
|
22
|
+
{ lat: 43.2, lng: -126.4, acc: 20 },
|
|
23
|
+
{ lat: 43.3, lng: -126.5 }
|
|
24
|
+
];
|
|
25
|
+
const opts: EncodeOptions = {
|
|
26
|
+
includeTime: true,
|
|
27
|
+
includeAccuracy: true,
|
|
28
|
+
sparseStrategy: 'bitmap'
|
|
29
|
+
};
|
|
30
|
+
const encoded = encode(points, opts);
|
|
31
|
+
const decoded = decode(encoded, opts);
|
|
32
|
+
expect(decoded[0].time).toBe(1000);
|
|
33
|
+
expect(decoded[0].acc).toBe(10);
|
|
34
|
+
expect(decoded[1].time).toBe(1005);
|
|
35
|
+
expect(decoded[1].acc).toBeUndefined();
|
|
36
|
+
expect(decoded[2].time).toBeUndefined();
|
|
37
|
+
expect(decoded[2].acc).toBe(20);
|
|
38
|
+
expect(decoded[3].time).toBeUndefined();
|
|
39
|
+
expect(decoded[3].acc).toBeUndefined();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('handles optional time and acc correctly in columnar mode', () => {
|
|
43
|
+
const points = [
|
|
44
|
+
{ lat: 38.5, lng: -120.2, time: 1000, acc: 10 },
|
|
45
|
+
{ lat: 40.7, lng: -120.95, time: 1005 },
|
|
46
|
+
{ lat: 43.2, lng: -126.4, acc: 20 },
|
|
47
|
+
{ lat: 43.3, lng: -126.5 }
|
|
48
|
+
];
|
|
49
|
+
const opts: EncodeOptions = {
|
|
50
|
+
includeTime: true,
|
|
51
|
+
includeAccuracy: true,
|
|
52
|
+
sparseStrategy: 'columnar'
|
|
53
|
+
};
|
|
54
|
+
const encoded = encode(points, opts);
|
|
55
|
+
const decoded = decode(encoded, opts);
|
|
56
|
+
expect(decoded[0].time).toBe(1000);
|
|
57
|
+
expect(decoded[0].acc).toBe(10);
|
|
58
|
+
expect(decoded[1].time).toBe(1005);
|
|
59
|
+
expect(decoded[1].acc).toBeUndefined();
|
|
60
|
+
expect(decoded[2].time).toBeUndefined();
|
|
61
|
+
expect(decoded[2].acc).toBe(20);
|
|
62
|
+
expect(decoded[3].time).toBeUndefined();
|
|
63
|
+
expect(decoded[3].acc).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('monopolyline generic schema', () => {
|
|
68
|
+
it('supports object schema with naive strategy', () => {
|
|
69
|
+
const data = [
|
|
70
|
+
{ lat: 38.5, lng: -120.2, speed: 50 },
|
|
71
|
+
{ lat: 40.7, lng: -120.95, speed: 55 }, // if a field is missing, naive uses previous value
|
|
72
|
+
{ lat: 43.2, lng: -126.4 }
|
|
73
|
+
];
|
|
74
|
+
const opts: EncodeOptions = {
|
|
75
|
+
schema: {
|
|
76
|
+
type: 'object',
|
|
77
|
+
fields: {
|
|
78
|
+
lat: 'decimal',
|
|
79
|
+
lng: 'decimal',
|
|
80
|
+
speed: 'integer'
|
|
81
|
+
}
|
|
82
|
+
} as Schema,
|
|
83
|
+
sparseStrategy: 'naive'
|
|
84
|
+
};
|
|
85
|
+
const encoded = encode(data, opts);
|
|
86
|
+
const decoded = decode(encoded, opts);
|
|
87
|
+
expect(decoded.length).toBe(3);
|
|
88
|
+
expect(decoded[0].speed).toBe(50);
|
|
89
|
+
expect(decoded[1].speed).toBe(55);
|
|
90
|
+
expect(decoded[2].speed).toBe(55); // Naive carries forward the last known delta which was 0
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('supports object schema with bitmap strategy', () => {
|
|
94
|
+
const data = [
|
|
95
|
+
{ lat: 38.5, lng: -120.2, speed: 50 },
|
|
96
|
+
{ lat: 40.7, lng: -120.95 },
|
|
97
|
+
{ lat: 43.2, lng: -126.4, speed: 60 }
|
|
98
|
+
];
|
|
99
|
+
const opts: EncodeOptions = {
|
|
100
|
+
schema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
fields: {
|
|
103
|
+
lat: 'decimal',
|
|
104
|
+
lng: 'decimal',
|
|
105
|
+
speed: 'integer'
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
sparseStrategy: 'bitmap'
|
|
109
|
+
};
|
|
110
|
+
const encoded = encode(data, opts);
|
|
111
|
+
const decoded = decode(encoded, opts);
|
|
112
|
+
expect(decoded.length).toBe(3);
|
|
113
|
+
expect(decoded[0].speed).toBe(50);
|
|
114
|
+
expect(decoded[1].speed).toBeUndefined();
|
|
115
|
+
expect(decoded[2].speed).toBe(60);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('supports tuple schema with columnar strategy', () => {
|
|
119
|
+
const data = [
|
|
120
|
+
[38.5, -120.2, 50, 100],
|
|
121
|
+
[40.7, -120.95, null, 105],
|
|
122
|
+
[43.2, -126.4, 60, null]
|
|
123
|
+
];
|
|
124
|
+
const opts: EncodeOptions = {
|
|
125
|
+
schema: {
|
|
126
|
+
type: 'tuple',
|
|
127
|
+
fields: ['decimal', 'decimal', 'integer', 'integer']
|
|
128
|
+
},
|
|
129
|
+
sparseStrategy: 'columnar'
|
|
130
|
+
};
|
|
131
|
+
const encoded = encode(data, opts);
|
|
132
|
+
const decoded = decode(encoded, opts);
|
|
133
|
+
|
|
134
|
+
expect(decoded.length).toBe(3);
|
|
135
|
+
expect(decoded[0][0]).toBeCloseTo(38.5, 5);
|
|
136
|
+
expect(decoded[0][2]).toBe(50);
|
|
137
|
+
expect(decoded[1][2]).toBeUndefined();
|
|
138
|
+
expect(decoded[1][3]).toBe(105);
|
|
139
|
+
expect(decoded[2][2]).toBe(60);
|
|
140
|
+
expect(decoded[2][3]).toBeUndefined();
|
|
141
|
+
});
|
|
142
|
+
});
|
package/index.ts
ADDED
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
export type FieldType = 'decimal' | 'integer';
|
|
2
|
+
|
|
3
|
+
export type Schema =
|
|
4
|
+
| { type: 'object'; fields: Record<string, FieldType> }
|
|
5
|
+
| { type: 'tuple'; fields: FieldType[] };
|
|
6
|
+
|
|
7
|
+
export interface Point {
|
|
8
|
+
lat: number;
|
|
9
|
+
lng: number;
|
|
10
|
+
time?: number; // Epoch timestamp in milliseconds
|
|
11
|
+
acc?: number; // Accuracy in meters
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type SparseStrategy = 'naive' | 'bitmap' | 'columnar';
|
|
15
|
+
|
|
16
|
+
export interface EncodeOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Number of decimal places for 'decimal' fields.
|
|
19
|
+
* Default: 5 (Standard Google Polyline)
|
|
20
|
+
*/
|
|
21
|
+
precision?: number;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Schema definition to make encoding/decoding generic.
|
|
25
|
+
* If provided, `includeTime` and `includeAccuracy` are ignored.
|
|
26
|
+
*/
|
|
27
|
+
schema?: Schema;
|
|
28
|
+
|
|
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
|
+
/**
|
|
37
|
+
* Include the accuracy dimension in the encoded polyline.
|
|
38
|
+
* Default: false
|
|
39
|
+
* @deprecated Use `schema` instead for generic data structures.
|
|
40
|
+
*/
|
|
41
|
+
includeAccuracy?: boolean;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Strategy for encoding sparse data (missing values).
|
|
45
|
+
* 'naive' (default): Encodes missing values as 0 delta (repeats previous value).
|
|
46
|
+
* 'bitmap': Adds a small presence bitmask to each point to skip missing values.
|
|
47
|
+
* 'columnar': Splits the array into columns (lats, lngs, etc) with run-length presence flags.
|
|
48
|
+
*/
|
|
49
|
+
sparseStrategy?: SparseStrategy;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Encodes a numeric value using ZigZag encoding and base64-like chunking.
|
|
54
|
+
*/
|
|
55
|
+
function encodeNumber(value: number): string {
|
|
56
|
+
let zz = value < 0 ? -value * 2 - 1 : value * 2;
|
|
57
|
+
let str = "";
|
|
58
|
+
|
|
59
|
+
while (zz >= 0x20) {
|
|
60
|
+
str += String.fromCharCode((0x20 | (zz & 0x1f)) + 63);
|
|
61
|
+
zz = Math.floor(zz / 32);
|
|
62
|
+
}
|
|
63
|
+
str += String.fromCharCode(zz + 63);
|
|
64
|
+
return str;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decodes a single numeric value from the encoded string starting at the given index.
|
|
69
|
+
*/
|
|
70
|
+
function decodeNumber(str: string, index: number): [number, number] {
|
|
71
|
+
let b = 0;
|
|
72
|
+
let result = 0;
|
|
73
|
+
let factor = 1;
|
|
74
|
+
|
|
75
|
+
do {
|
|
76
|
+
if (index >= str.length) {
|
|
77
|
+
throw new Error("Invalid polyline string: unexpected end of string");
|
|
78
|
+
}
|
|
79
|
+
b = str.charCodeAt(index++) - 63;
|
|
80
|
+
result += (b & 0x1f) * factor;
|
|
81
|
+
factor *= 32;
|
|
82
|
+
} while (b >= 0x20);
|
|
83
|
+
|
|
84
|
+
const value = result % 2 === 1 ? -(Math.floor(result / 2) + 1) : Math.floor(result / 2);
|
|
85
|
+
return [value, index];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Encodes an array of data into an extended polyline string.
|
|
90
|
+
*/
|
|
91
|
+
export function encode(data: any[], options: EncodeOptions = {}): string {
|
|
92
|
+
if (!options.schema) {
|
|
93
|
+
return encodeLegacy(data as Point[], options);
|
|
94
|
+
}
|
|
95
|
+
return encodeGeneric(data, options);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Decodes an extended polyline string back into an array of data.
|
|
100
|
+
*/
|
|
101
|
+
export function decode(str: string, options: EncodeOptions = {}): any[] {
|
|
102
|
+
if (!options.schema) {
|
|
103
|
+
return decodeLegacy(str, options);
|
|
104
|
+
}
|
|
105
|
+
return decodeGeneric(str, options);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function encodeGeneric(data: any[], options: EncodeOptions): string {
|
|
109
|
+
const precision = options.precision ?? 5;
|
|
110
|
+
const factor = Math.pow(10, precision);
|
|
111
|
+
const sparseStrategy = options.sparseStrategy ?? 'naive';
|
|
112
|
+
const schema = options.schema!;
|
|
113
|
+
|
|
114
|
+
const schemaFields: { key: string | number, type: FieldType }[] = [];
|
|
115
|
+
if (schema.type === 'object') {
|
|
116
|
+
for (const key of Object.keys(schema.fields)) {
|
|
117
|
+
schemaFields.push({ key, type: schema.fields[key] });
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
for (let i = 0; i < schema.fields.length; i++) {
|
|
121
|
+
schemaFields.push({ key: i, type: schema.fields[i] });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let result = "";
|
|
126
|
+
|
|
127
|
+
if (sparseStrategy === 'columnar') {
|
|
128
|
+
result += encodeNumber(data.length);
|
|
129
|
+
for (let i = 0; i < schemaFields.length; i++) {
|
|
130
|
+
const field = schemaFields[i];
|
|
131
|
+
let prev = 0;
|
|
132
|
+
let presence = 0;
|
|
133
|
+
let bitIndex = 0;
|
|
134
|
+
let presenceStrs = "";
|
|
135
|
+
let dataStrs = "";
|
|
136
|
+
for (const pt of data) {
|
|
137
|
+
const val = pt[field.key];
|
|
138
|
+
if (val != null) {
|
|
139
|
+
presence |= (1 << bitIndex);
|
|
140
|
+
const scaled = field.type === 'decimal' ? Math.round((val as number) * factor) : Math.round(val as number);
|
|
141
|
+
dataStrs += encodeNumber(scaled - prev);
|
|
142
|
+
prev = scaled;
|
|
143
|
+
}
|
|
144
|
+
bitIndex++;
|
|
145
|
+
if (bitIndex === 30) {
|
|
146
|
+
presenceStrs += encodeNumber(presence);
|
|
147
|
+
presence = 0;
|
|
148
|
+
bitIndex = 0;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (bitIndex > 0) presenceStrs += encodeNumber(presence);
|
|
152
|
+
result += presenceStrs + dataStrs;
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// naive or bitmap
|
|
158
|
+
const prevs = new Array(schemaFields.length).fill(0);
|
|
159
|
+
|
|
160
|
+
for (const pt of data) {
|
|
161
|
+
if (sparseStrategy === 'bitmap') {
|
|
162
|
+
let bitmask = 0;
|
|
163
|
+
for (let i = 0; i < schemaFields.length; i++) {
|
|
164
|
+
if (pt[schemaFields[i].key] != null) bitmask |= (1 << i);
|
|
165
|
+
}
|
|
166
|
+
result += encodeNumber(bitmask);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (let i = 0; i < schemaFields.length; i++) {
|
|
170
|
+
const field = schemaFields[i];
|
|
171
|
+
const val = pt[field.key];
|
|
172
|
+
if (sparseStrategy === 'bitmap') {
|
|
173
|
+
if (val != null) {
|
|
174
|
+
const scaled = field.type === 'decimal' ? Math.round((val as number) * factor) : Math.round(val as number);
|
|
175
|
+
result += encodeNumber(scaled - prevs[i]);
|
|
176
|
+
prevs[i] = scaled;
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
// naive
|
|
180
|
+
if (val != null) {
|
|
181
|
+
const scaled = field.type === 'decimal' ? Math.round((val as number) * factor) : Math.round(val as number);
|
|
182
|
+
result += encodeNumber(scaled - prevs[i]);
|
|
183
|
+
prevs[i] = scaled;
|
|
184
|
+
} else {
|
|
185
|
+
result += encodeNumber(0);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function decodeGeneric(str: string, options: EncodeOptions): any[] {
|
|
195
|
+
const precision = options.precision ?? 5;
|
|
196
|
+
const factor = Math.pow(10, precision);
|
|
197
|
+
const sparseStrategy = options.sparseStrategy ?? 'naive';
|
|
198
|
+
const schema = options.schema!;
|
|
199
|
+
|
|
200
|
+
const schemaFields: { key: string | number, type: FieldType }[] = [];
|
|
201
|
+
if (schema.type === 'object') {
|
|
202
|
+
for (const key of Object.keys(schema.fields)) {
|
|
203
|
+
schemaFields.push({ key, type: schema.fields[key] });
|
|
204
|
+
}
|
|
205
|
+
} else {
|
|
206
|
+
for (let i = 0; i < schema.fields.length; i++) {
|
|
207
|
+
schemaFields.push({ key: i, type: schema.fields[i] });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let index = 0;
|
|
212
|
+
|
|
213
|
+
if (sparseStrategy === 'columnar') {
|
|
214
|
+
if (str.length === 0) return [];
|
|
215
|
+
let ptCount = 0;
|
|
216
|
+
[ptCount, index] = decodeNumber(str, index);
|
|
217
|
+
|
|
218
|
+
const results: any[] = Array.from({ length: ptCount }, () => (schema.type === 'tuple' ? [] : {}));
|
|
219
|
+
|
|
220
|
+
for (let f = 0; f < schemaFields.length; f++) {
|
|
221
|
+
const field = schemaFields[f];
|
|
222
|
+
|
|
223
|
+
const presenceFlags: boolean[] = [];
|
|
224
|
+
const numPresenceInts = Math.ceil(ptCount / 30);
|
|
225
|
+
for (let i = 0; i < numPresenceInts; i++) {
|
|
226
|
+
let p = 0;
|
|
227
|
+
[p, index] = decodeNumber(str, index);
|
|
228
|
+
for (let b = 0; b < 30 && (i * 30 + b) < ptCount; b++) {
|
|
229
|
+
presenceFlags.push((p & (1 << b)) !== 0);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let prev = 0;
|
|
234
|
+
for (let i = 0; i < ptCount; i++) {
|
|
235
|
+
if (presenceFlags[i]) {
|
|
236
|
+
let delta = 0;
|
|
237
|
+
[delta, index] = decodeNumber(str, index);
|
|
238
|
+
prev += delta;
|
|
239
|
+
const val = field.type === 'decimal' ? prev / factor : prev;
|
|
240
|
+
results[i][field.key] = val;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return results;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const prevs = new Array(schemaFields.length).fill(0);
|
|
248
|
+
const results: any[] = [];
|
|
249
|
+
|
|
250
|
+
while (index < str.length) {
|
|
251
|
+
let bitmask = (1 << schemaFields.length) - 1; // default all present
|
|
252
|
+
if (sparseStrategy === 'bitmap') {
|
|
253
|
+
[bitmask, index] = decodeNumber(str, index);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const pt: any = schema.type === 'tuple' ? [] : {};
|
|
257
|
+
|
|
258
|
+
for (let f = 0; f < schemaFields.length; f++) {
|
|
259
|
+
const field = schemaFields[f];
|
|
260
|
+
|
|
261
|
+
if (sparseStrategy === 'bitmap') {
|
|
262
|
+
if ((bitmask & (1 << f)) !== 0) {
|
|
263
|
+
let delta = 0;
|
|
264
|
+
[delta, index] = decodeNumber(str, index);
|
|
265
|
+
prevs[f] += delta;
|
|
266
|
+
pt[field.key] = field.type === 'decimal' ? prevs[f] / factor : prevs[f];
|
|
267
|
+
}
|
|
268
|
+
} else {
|
|
269
|
+
// naive
|
|
270
|
+
let delta = 0;
|
|
271
|
+
[delta, index] = decodeNumber(str, index);
|
|
272
|
+
prevs[f] += delta;
|
|
273
|
+
pt[field.key] = field.type === 'decimal' ? prevs[f] / factor : prevs[f];
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
results.push(pt);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return results;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function encodeLegacy(points: Point[], options: EncodeOptions): string {
|
|
284
|
+
const precision = options.precision ?? 5;
|
|
285
|
+
const factor = Math.pow(10, precision);
|
|
286
|
+
const includeTime = options.includeTime ?? false;
|
|
287
|
+
const includeAccuracy = options.includeAccuracy ?? false;
|
|
288
|
+
const sparseStrategy = options.sparseStrategy ?? 'naive';
|
|
289
|
+
|
|
290
|
+
let result = "";
|
|
291
|
+
|
|
292
|
+
if (sparseStrategy === 'columnar') {
|
|
293
|
+
result += encodeNumber(points.length);
|
|
294
|
+
|
|
295
|
+
let prevLat = 0, prevLng = 0;
|
|
296
|
+
for (const point of points) {
|
|
297
|
+
const lat = Math.round(point.lat * factor);
|
|
298
|
+
const lng = Math.round(point.lng * factor);
|
|
299
|
+
result += encodeNumber(lat - prevLat);
|
|
300
|
+
result += encodeNumber(lng - prevLng);
|
|
301
|
+
prevLat = lat; prevLng = lng;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
if (includeTime) {
|
|
305
|
+
let prevTime = 0;
|
|
306
|
+
let timePresence = 0;
|
|
307
|
+
let bitIndex = 0;
|
|
308
|
+
let presenceStrs = "";
|
|
309
|
+
let dataStrs = "";
|
|
310
|
+
for (const point of points) {
|
|
311
|
+
if (point.time != null) {
|
|
312
|
+
timePresence |= (1 << bitIndex);
|
|
313
|
+
const time = Math.round(point.time);
|
|
314
|
+
dataStrs += encodeNumber(time - prevTime);
|
|
315
|
+
prevTime = time;
|
|
316
|
+
}
|
|
317
|
+
bitIndex++;
|
|
318
|
+
if (bitIndex === 30) {
|
|
319
|
+
presenceStrs += encodeNumber(timePresence);
|
|
320
|
+
timePresence = 0;
|
|
321
|
+
bitIndex = 0;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (bitIndex > 0) presenceStrs += encodeNumber(timePresence);
|
|
325
|
+
result += presenceStrs + dataStrs;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (includeAccuracy) {
|
|
329
|
+
let prevAcc = 0;
|
|
330
|
+
let accPresence = 0;
|
|
331
|
+
let bitIndex = 0;
|
|
332
|
+
let presenceStrs = "";
|
|
333
|
+
let dataStrs = "";
|
|
334
|
+
for (const point of points) {
|
|
335
|
+
if (point.acc != null) {
|
|
336
|
+
accPresence |= (1 << bitIndex);
|
|
337
|
+
const acc = Math.round(point.acc);
|
|
338
|
+
dataStrs += encodeNumber(acc - prevAcc);
|
|
339
|
+
prevAcc = acc;
|
|
340
|
+
}
|
|
341
|
+
bitIndex++;
|
|
342
|
+
if (bitIndex === 30) {
|
|
343
|
+
presenceStrs += encodeNumber(accPresence);
|
|
344
|
+
accPresence = 0;
|
|
345
|
+
bitIndex = 0;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (bitIndex > 0) presenceStrs += encodeNumber(accPresence);
|
|
349
|
+
result += presenceStrs + dataStrs;
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
let prevLat = 0;
|
|
355
|
+
let prevLng = 0;
|
|
356
|
+
let prevTime = 0;
|
|
357
|
+
let prevAcc = 0;
|
|
358
|
+
|
|
359
|
+
for (const point of points) {
|
|
360
|
+
const lat = Math.round(point.lat * factor);
|
|
361
|
+
const lng = Math.round(point.lng * factor);
|
|
362
|
+
|
|
363
|
+
if (sparseStrategy === 'bitmap' && (includeTime || includeAccuracy)) {
|
|
364
|
+
let bitmask = 0;
|
|
365
|
+
if (includeTime && point.time != null) bitmask |= 1;
|
|
366
|
+
if (includeAccuracy && point.acc != null) bitmask |= 2;
|
|
367
|
+
result += encodeNumber(bitmask);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
result += encodeNumber(lat - prevLat);
|
|
371
|
+
result += encodeNumber(lng - prevLng);
|
|
372
|
+
prevLat = lat;
|
|
373
|
+
prevLng = lng;
|
|
374
|
+
|
|
375
|
+
if (sparseStrategy === 'bitmap') {
|
|
376
|
+
if (includeTime && point.time != null) {
|
|
377
|
+
const time = Math.round(point.time);
|
|
378
|
+
result += encodeNumber(time - prevTime);
|
|
379
|
+
prevTime = time;
|
|
380
|
+
}
|
|
381
|
+
if (includeAccuracy && point.acc != null) {
|
|
382
|
+
const acc = Math.round(point.acc);
|
|
383
|
+
result += encodeNumber(acc - prevAcc);
|
|
384
|
+
prevAcc = acc;
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
if (includeTime) {
|
|
388
|
+
if (point.time != null) {
|
|
389
|
+
const time = Math.round(point.time);
|
|
390
|
+
result += encodeNumber(time - prevTime);
|
|
391
|
+
prevTime = time;
|
|
392
|
+
} else {
|
|
393
|
+
result += encodeNumber(0);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
if (includeAccuracy) {
|
|
397
|
+
if (point.acc != null) {
|
|
398
|
+
const acc = Math.round(point.acc);
|
|
399
|
+
result += encodeNumber(acc - prevAcc);
|
|
400
|
+
prevAcc = acc;
|
|
401
|
+
} else {
|
|
402
|
+
result += encodeNumber(0);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function decodeLegacy(str: string, options: EncodeOptions): Point[] {
|
|
412
|
+
const precision = options.precision ?? 5;
|
|
413
|
+
const factor = Math.pow(10, precision);
|
|
414
|
+
const includeTime = options.includeTime ?? false;
|
|
415
|
+
const includeAccuracy = options.includeAccuracy ?? false;
|
|
416
|
+
const sparseStrategy = options.sparseStrategy ?? 'naive';
|
|
417
|
+
|
|
418
|
+
let index = 0;
|
|
419
|
+
const points: Point[] = [];
|
|
420
|
+
|
|
421
|
+
if (sparseStrategy === 'columnar') {
|
|
422
|
+
if (str.length === 0) return points;
|
|
423
|
+
let ptCount = 0;
|
|
424
|
+
[ptCount, index] = decodeNumber(str, index);
|
|
425
|
+
|
|
426
|
+
let prevLat = 0;
|
|
427
|
+
let prevLng = 0;
|
|
428
|
+
for (let i = 0; i < ptCount; i++) {
|
|
429
|
+
let dLat = 0, dLng = 0;
|
|
430
|
+
[dLat, index] = decodeNumber(str, index);
|
|
431
|
+
[dLng, index] = decodeNumber(str, index);
|
|
432
|
+
prevLat += dLat;
|
|
433
|
+
prevLng += dLng;
|
|
434
|
+
points.push({ lat: prevLat / factor, lng: prevLng / factor });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (includeTime) {
|
|
438
|
+
const presenceFlags: boolean[] = [];
|
|
439
|
+
const numPresenceInts = Math.ceil(ptCount / 30);
|
|
440
|
+
for (let i = 0; i < numPresenceInts; i++) {
|
|
441
|
+
let p = 0;
|
|
442
|
+
[p, index] = decodeNumber(str, index);
|
|
443
|
+
for (let b = 0; b < 30 && (i * 30 + b) < ptCount; b++) {
|
|
444
|
+
presenceFlags.push((p & (1 << b)) !== 0);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
let prevTime = 0;
|
|
448
|
+
for (let i = 0; i < ptCount; i++) {
|
|
449
|
+
if (presenceFlags[i]) {
|
|
450
|
+
let dTime = 0;
|
|
451
|
+
[dTime, index] = decodeNumber(str, index);
|
|
452
|
+
prevTime += dTime;
|
|
453
|
+
points[i].time = prevTime;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (includeAccuracy) {
|
|
459
|
+
const presenceFlags: boolean[] = [];
|
|
460
|
+
const numPresenceInts = Math.ceil(ptCount / 30);
|
|
461
|
+
for (let i = 0; i < numPresenceInts; i++) {
|
|
462
|
+
let p = 0;
|
|
463
|
+
[p, index] = decodeNumber(str, index);
|
|
464
|
+
for (let b = 0; b < 30 && (i * 30 + b) < ptCount; b++) {
|
|
465
|
+
presenceFlags.push((p & (1 << b)) !== 0);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
let prevAcc = 0;
|
|
469
|
+
for (let i = 0; i < ptCount; i++) {
|
|
470
|
+
if (presenceFlags[i]) {
|
|
471
|
+
let dAcc = 0;
|
|
472
|
+
[dAcc, index] = decodeNumber(str, index);
|
|
473
|
+
prevAcc += dAcc;
|
|
474
|
+
points[i].acc = prevAcc;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return points;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let prevLat = 0;
|
|
482
|
+
let prevLng = 0;
|
|
483
|
+
let prevTime = 0;
|
|
484
|
+
let prevAcc = 0;
|
|
485
|
+
|
|
486
|
+
while (index < str.length) {
|
|
487
|
+
let bitmask = 3;
|
|
488
|
+
if (sparseStrategy === 'bitmap' && (includeTime || includeAccuracy)) {
|
|
489
|
+
[bitmask, index] = decodeNumber(str, index);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
let dLat = 0;
|
|
493
|
+
let dLng = 0;
|
|
494
|
+
|
|
495
|
+
[dLat, index] = decodeNumber(str, index);
|
|
496
|
+
[dLng, index] = decodeNumber(str, index);
|
|
497
|
+
prevLat += dLat;
|
|
498
|
+
prevLng += dLng;
|
|
499
|
+
|
|
500
|
+
const point: Point = {
|
|
501
|
+
lat: prevLat / factor,
|
|
502
|
+
lng: prevLng / factor,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
if (sparseStrategy === 'bitmap') {
|
|
506
|
+
if (includeTime && (bitmask & 1)) {
|
|
507
|
+
let dTime = 0;
|
|
508
|
+
[dTime, index] = decodeNumber(str, index);
|
|
509
|
+
prevTime += dTime;
|
|
510
|
+
point.time = prevTime;
|
|
511
|
+
}
|
|
512
|
+
if (includeAccuracy && (bitmask & 2)) {
|
|
513
|
+
let dAcc = 0;
|
|
514
|
+
[dAcc, index] = decodeNumber(str, index);
|
|
515
|
+
prevAcc += dAcc;
|
|
516
|
+
point.acc = prevAcc;
|
|
517
|
+
}
|
|
518
|
+
} else {
|
|
519
|
+
if (includeTime) {
|
|
520
|
+
let dTime = 0;
|
|
521
|
+
[dTime, index] = decodeNumber(str, index);
|
|
522
|
+
if (dTime !== 0) {
|
|
523
|
+
prevTime += dTime;
|
|
524
|
+
point.time = prevTime;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (includeAccuracy) {
|
|
528
|
+
let dAcc = 0;
|
|
529
|
+
[dAcc, index] = decodeNumber(str, index);
|
|
530
|
+
if (dAcc !== 0) {
|
|
531
|
+
prevAcc += dAcc;
|
|
532
|
+
point.acc = prevAcc;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
points.push(point);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
return points;
|
|
541
|
+
}
|
|
542
|
+
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "monopolyline",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "An extended Google Polyline encoder/decoder supporting timestamps, accuracy, and sparse data strategies.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "bun test"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"polyline",
|
|
13
|
+
"gps",
|
|
14
|
+
"compression",
|
|
15
|
+
"encoding"
|
|
16
|
+
],
|
|
17
|
+
"author": "mizulu",
|
|
18
|
+
"license": "",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"typescript": "^5.0.0",
|
|
21
|
+
"bun-types": "latest"
|
|
22
|
+
}
|
|
23
|
+
}
|