@math.gl/wkb 5.0.0-alpha.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/LICENSE +140 -0
- package/README.md +232 -0
- package/dist/index.cjs +947 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +30 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +34 -0
- package/dist/types.js.map +1 -0
- package/dist/wkb-builder.d.ts +87 -0
- package/dist/wkb-builder.d.ts.map +1 -0
- package/dist/wkb-builder.js +259 -0
- package/dist/wkb-builder.js.map +1 -0
- package/dist/wkb-reader.d.ts +74 -0
- package/dist/wkb-reader.d.ts.map +1 -0
- package/dist/wkb-reader.js +249 -0
- package/dist/wkb-reader.js.map +1 -0
- package/dist/wkb.d.ts +21 -0
- package/dist/wkb.d.ts.map +1 -0
- package/dist/wkb.js +204 -0
- package/dist/wkb.js.map +1 -0
- package/dist/wkt.d.ts +6 -0
- package/dist/wkt.d.ts.map +1 -0
- package/dist/wkt.js +224 -0
- package/dist/wkt.js.map +1 -0
- package/package.json +37 -0
- package/src/index.ts +37 -0
- package/src/types.ts +54 -0
- package/src/wkb-builder.ts +367 -0
- package/src/wkb-reader.ts +400 -0
- package/src/wkb.ts +253 -0
- package/src/wkt.ts +233 -0
package/src/wkt.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// math.gl
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
// Copyright (c) vis.gl contributors
|
|
4
|
+
|
|
5
|
+
import type {WellKnownDimension, WellKnownGeometry} from './types';
|
|
6
|
+
import {getWellKnownDimensionSize, inferWellKnownGeometryDimension} from './types';
|
|
7
|
+
|
|
8
|
+
/** Parses one WKT geometry, including Z/M/ZM, collections, alternate MultiPoint, and empties. */
|
|
9
|
+
export function parseWKT(text: string): WellKnownGeometry {
|
|
10
|
+
const parser = new WKTParser(text);
|
|
11
|
+
const geometry = parser.parseGeometry(2);
|
|
12
|
+
parser.assertComplete();
|
|
13
|
+
return geometry;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Formats one geometry as WKT using an explicit or tuple-inferred semantic dimension. */
|
|
17
|
+
export function formatWKT(
|
|
18
|
+
geometry: WellKnownGeometry,
|
|
19
|
+
dimension: WellKnownDimension = inferWellKnownGeometryDimension(geometry)
|
|
20
|
+
): string {
|
|
21
|
+
const dimensionToken = dimension === 'xy' ? '' : ` ${dimension.slice(2).toUpperCase()}`;
|
|
22
|
+
if (geometry.type === 'GeometryCollection') {
|
|
23
|
+
if (geometry.geometries.length === 0) return `GEOMETRYCOLLECTION${dimensionToken} EMPTY`;
|
|
24
|
+
return `GEOMETRYCOLLECTION${dimensionToken} (${geometry.geometries.map(child => formatWKT(child, dimension)).join(', ')})`;
|
|
25
|
+
}
|
|
26
|
+
const type = geometry.type.toUpperCase();
|
|
27
|
+
if (isEmptyCoordinates(geometry.coordinates)) return `${type}${dimensionToken} EMPTY`;
|
|
28
|
+
return `${type}${dimensionToken} ${formatCoordinateNesting(geometry.coordinates, getGeometryDepth(geometry.type))}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class WKTParser {
|
|
32
|
+
private readonly tokens: string[];
|
|
33
|
+
private index = 0;
|
|
34
|
+
|
|
35
|
+
constructor(text: string) {
|
|
36
|
+
this.tokens = tokenizeWKT(text);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
parseGeometry(inheritedDimensionSize: number): WellKnownGeometry {
|
|
40
|
+
const type = this.takeWord().toUpperCase();
|
|
41
|
+
let dimensionSize = inheritedDimensionSize;
|
|
42
|
+
if (['Z', 'M', 'ZM'].includes(this.peek().toUpperCase())) {
|
|
43
|
+
const dimension = this.take().toUpperCase();
|
|
44
|
+
dimensionSize = dimension === 'ZM' ? 4 : 3;
|
|
45
|
+
}
|
|
46
|
+
if (this.peek().toUpperCase() === 'EMPTY') {
|
|
47
|
+
this.take();
|
|
48
|
+
return makeEmptyGeometry(type, dimensionSize);
|
|
49
|
+
}
|
|
50
|
+
if (type === 'GEOMETRYCOLLECTION') {
|
|
51
|
+
this.expect('(');
|
|
52
|
+
const geometries: WellKnownGeometry[] = [];
|
|
53
|
+
if (this.peek() !== ')') {
|
|
54
|
+
do geometries.push(this.parseGeometry(dimensionSize));
|
|
55
|
+
while (this.takeIf(','));
|
|
56
|
+
}
|
|
57
|
+
this.expect(')');
|
|
58
|
+
return {type: 'GeometryCollection', geometries};
|
|
59
|
+
}
|
|
60
|
+
const coordinates = this.parseCoordinateNesting(getWKTDepth(type), dimensionSize);
|
|
61
|
+
return makeGeometry(type, coordinates);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
assertComplete(): void {
|
|
65
|
+
if (this.index !== this.tokens.length) throw new Error(`Unexpected WKT token ${this.peek()}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private parseCoordinateNesting(depth: number, dimensionSize: number): unknown {
|
|
69
|
+
this.expect('(');
|
|
70
|
+
if (depth === 0) {
|
|
71
|
+
const coordinate = this.readCoordinate(dimensionSize);
|
|
72
|
+
this.expect(')');
|
|
73
|
+
return coordinate;
|
|
74
|
+
}
|
|
75
|
+
const values: unknown[] = [];
|
|
76
|
+
if (this.peek() !== ')') {
|
|
77
|
+
do {
|
|
78
|
+
if (depth === 1 && this.peek() !== '(') {
|
|
79
|
+
values.push(this.readCoordinate(dimensionSize));
|
|
80
|
+
} else {
|
|
81
|
+
values.push(this.parseCoordinateNesting(depth - 1, dimensionSize));
|
|
82
|
+
}
|
|
83
|
+
} while (this.takeIf(','));
|
|
84
|
+
}
|
|
85
|
+
this.expect(')');
|
|
86
|
+
return values;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private readCoordinate(dimensionSize: number): number[] {
|
|
90
|
+
const values: number[] = [];
|
|
91
|
+
while (values.length < dimensionSize && isNumberToken(this.peek())) {
|
|
92
|
+
values.push(Number(this.take()));
|
|
93
|
+
}
|
|
94
|
+
if (values.length < 2) throw new Error('WKT coordinate requires at least two numbers');
|
|
95
|
+
return values;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private peek(): string {
|
|
99
|
+
return this.tokens[this.index] || '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private take(): string {
|
|
103
|
+
if (this.index >= this.tokens.length) throw new Error('Unexpected end of WKT');
|
|
104
|
+
return this.tokens[this.index++];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private takeWord(): string {
|
|
108
|
+
const token = this.take();
|
|
109
|
+
if (!/^[A-Za-z_]+$/.test(token)) throw new Error(`Expected WKT geometry type, found ${token}`);
|
|
110
|
+
return token;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private takeIf(token: string): boolean {
|
|
114
|
+
if (this.peek() !== token) return false;
|
|
115
|
+
this.index++;
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private expect(token: string): void {
|
|
120
|
+
const actual = this.take();
|
|
121
|
+
if (actual !== token) throw new Error(`Expected WKT token ${token}, found ${actual}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function tokenizeWKT(text: string): string[] {
|
|
126
|
+
const tokens: string[] = [];
|
|
127
|
+
const tokenPattern = /[A-Za-z_]+|[(),]|[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?/gy;
|
|
128
|
+
let index = 0;
|
|
129
|
+
while (index < text.length) {
|
|
130
|
+
while (index < text.length && /\s/.test(text[index])) index++;
|
|
131
|
+
if (index === text.length) break;
|
|
132
|
+
tokenPattern.lastIndex = index;
|
|
133
|
+
const match = tokenPattern.exec(text);
|
|
134
|
+
if (!match) {
|
|
135
|
+
throw new Error(`Unexpected WKT character ${JSON.stringify(text[index])} at ${index}`);
|
|
136
|
+
}
|
|
137
|
+
tokens.push(match[0]);
|
|
138
|
+
index = tokenPattern.lastIndex;
|
|
139
|
+
}
|
|
140
|
+
return tokens;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function formatCoordinateNesting(value: readonly unknown[], depth: number): string {
|
|
144
|
+
if (depth === 0) return `(${(value as readonly number[]).map(formatNumber).join(' ')})`;
|
|
145
|
+
return `(${value
|
|
146
|
+
.map(child => {
|
|
147
|
+
if (depth === 1) return (child as readonly number[]).map(formatNumber).join(' ');
|
|
148
|
+
return formatCoordinateNesting(child as readonly unknown[], depth - 1);
|
|
149
|
+
})
|
|
150
|
+
.join(', ')})`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function formatNumber(value: number): string {
|
|
154
|
+
if (!Number.isFinite(value)) return 'NaN';
|
|
155
|
+
return String(value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getGeometryDepth(type: Exclude<WellKnownGeometry['type'], 'GeometryCollection'>): number {
|
|
159
|
+
switch (type) {
|
|
160
|
+
case 'Point':
|
|
161
|
+
return 0;
|
|
162
|
+
case 'LineString':
|
|
163
|
+
case 'MultiPoint':
|
|
164
|
+
return 1;
|
|
165
|
+
case 'Polygon':
|
|
166
|
+
case 'MultiLineString':
|
|
167
|
+
return 2;
|
|
168
|
+
case 'MultiPolygon':
|
|
169
|
+
return 3;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function getWKTDepth(type: string): number {
|
|
174
|
+
switch (type) {
|
|
175
|
+
case 'POINT':
|
|
176
|
+
return 0;
|
|
177
|
+
case 'LINESTRING':
|
|
178
|
+
return 1;
|
|
179
|
+
case 'POLYGON':
|
|
180
|
+
return 2;
|
|
181
|
+
case 'MULTIPOINT':
|
|
182
|
+
return 1;
|
|
183
|
+
case 'MULTILINESTRING':
|
|
184
|
+
return 2;
|
|
185
|
+
case 'MULTIPOLYGON':
|
|
186
|
+
return 3;
|
|
187
|
+
default:
|
|
188
|
+
throw new Error(`Unsupported WKT geometry type ${type}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function makeGeometry(type: string, coordinates: unknown): WellKnownGeometry {
|
|
193
|
+
const canonical = type[0] + type.slice(1).toLowerCase();
|
|
194
|
+
const names: Record<string, WellKnownGeometry['type']> = {
|
|
195
|
+
Point: 'Point',
|
|
196
|
+
Linestring: 'LineString',
|
|
197
|
+
Polygon: 'Polygon',
|
|
198
|
+
Multipoint: 'MultiPoint',
|
|
199
|
+
Multilinestring: 'MultiLineString',
|
|
200
|
+
Multipolygon: 'MultiPolygon'
|
|
201
|
+
};
|
|
202
|
+
const geometryType = names[canonical];
|
|
203
|
+
if (!geometryType) throw new Error(`Unsupported WKT geometry type ${type}`);
|
|
204
|
+
return {type: geometryType, coordinates} as WellKnownGeometry;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function makeEmptyGeometry(type: string, dimensionSize: number): WellKnownGeometry {
|
|
208
|
+
if (type === 'GEOMETRYCOLLECTION') return {type: 'GeometryCollection', geometries: []};
|
|
209
|
+
if (type === 'POINT') {
|
|
210
|
+
return {
|
|
211
|
+
type: 'Point',
|
|
212
|
+
coordinates: new Array(getValidDimensionSize(dimensionSize)).fill(Number.NaN)
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return makeGeometry(type, []);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getValidDimensionSize(size: number): 2 | 3 | 4 {
|
|
219
|
+
if (size === 2 || size === 3 || size === 4) return size;
|
|
220
|
+
return getWellKnownDimensionSize('xy');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isEmptyCoordinates(value: readonly unknown[]): boolean {
|
|
224
|
+
if (value.length === 0) return true;
|
|
225
|
+
if (typeof value[0] === 'number') {
|
|
226
|
+
return (value as readonly number[]).every(component => !Number.isFinite(component));
|
|
227
|
+
}
|
|
228
|
+
return value.every(child => isEmptyCoordinates(child as readonly unknown[]));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function isNumberToken(token: string): boolean {
|
|
232
|
+
return token !== '' && Number.isFinite(Number(token));
|
|
233
|
+
}
|