wkt-parse-and-geojson 1.0.0
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 +656 -0
- package/dist/geojson-builder.d.ts +92 -0
- package/dist/geojson-to-wkt.d.ts +32 -0
- package/dist/index.cjs.js +673 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.esm.js +654 -0
- package/dist/index.umd.js +679 -0
- package/dist/types.d.ts +51 -0
- package/dist/wkt-builder.d.ts +17 -0
- package/dist/wkt-parser.d.ts +33 -0
- package/dist/wkt-to-geojson.d.ts +35 -0
- package/package.json +26 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class Lexer {
|
|
4
|
+
constructor(input) {
|
|
5
|
+
this.pos = 0;
|
|
6
|
+
this.input = input.trim();
|
|
7
|
+
}
|
|
8
|
+
peek() {
|
|
9
|
+
return this.input[this.pos] || '';
|
|
10
|
+
}
|
|
11
|
+
advance() {
|
|
12
|
+
return this.input[this.pos++] || '';
|
|
13
|
+
}
|
|
14
|
+
isWhitespace(c) {
|
|
15
|
+
return /\s/.test(c);
|
|
16
|
+
}
|
|
17
|
+
/** 数字开头字符:数字 或 负号 */
|
|
18
|
+
isNumberStart(c) {
|
|
19
|
+
return /[0-9\-]/.test(c);
|
|
20
|
+
}
|
|
21
|
+
isNumberBody(c) {
|
|
22
|
+
return /[0-9\.\-eE\+]/.test(c);
|
|
23
|
+
}
|
|
24
|
+
nextToken() {
|
|
25
|
+
// skip whitespace
|
|
26
|
+
while (this.pos < this.input.length && this.isWhitespace(this.peek())) {
|
|
27
|
+
this.advance();
|
|
28
|
+
}
|
|
29
|
+
if (this.pos >= this.input.length) {
|
|
30
|
+
return { type: 'EOF', value: '' };
|
|
31
|
+
}
|
|
32
|
+
const c = this.peek();
|
|
33
|
+
if (c === '(') {
|
|
34
|
+
this.advance();
|
|
35
|
+
return { type: 'LPAREN', value: '(' };
|
|
36
|
+
}
|
|
37
|
+
if (c === ')') {
|
|
38
|
+
this.advance();
|
|
39
|
+
return { type: 'RPAREN', value: ')' };
|
|
40
|
+
}
|
|
41
|
+
if (c === ',') {
|
|
42
|
+
this.advance();
|
|
43
|
+
return { type: 'COMMA', value: ',' };
|
|
44
|
+
}
|
|
45
|
+
// Number: starts with digit or minus
|
|
46
|
+
if (this.isNumberStart(c)) {
|
|
47
|
+
let num = '';
|
|
48
|
+
while (this.pos < this.input.length && this.isNumberBody(this.peek())) {
|
|
49
|
+
num += this.advance();
|
|
50
|
+
}
|
|
51
|
+
return { type: 'NUMBER', value: num };
|
|
52
|
+
}
|
|
53
|
+
// Word (geometry type or EMPTY/Z/M keyword)
|
|
54
|
+
let word = '';
|
|
55
|
+
while (this.pos < this.input.length && /[a-zA-Z_]/.test(this.peek())) {
|
|
56
|
+
word += this.advance();
|
|
57
|
+
}
|
|
58
|
+
return { type: 'WORD', value: word.toUpperCase() };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
class WKTParser {
|
|
62
|
+
constructor() {
|
|
63
|
+
this.tokens = [];
|
|
64
|
+
this.pos = 0;
|
|
65
|
+
}
|
|
66
|
+
parse(wkt) {
|
|
67
|
+
this.tokens = [];
|
|
68
|
+
this.pos = 0;
|
|
69
|
+
const lexer = new Lexer(wkt);
|
|
70
|
+
let token = lexer.nextToken();
|
|
71
|
+
while (token.type !== 'EOF') {
|
|
72
|
+
this.tokens.push(token);
|
|
73
|
+
token = lexer.nextToken();
|
|
74
|
+
}
|
|
75
|
+
this.tokens.push({ type: 'EOF', value: '' });
|
|
76
|
+
const geometry = this.parseGeometry();
|
|
77
|
+
// 校验尾部无多余字符(防止静默忽略垃圾输入)
|
|
78
|
+
if (this.peek().type !== 'EOF') {
|
|
79
|
+
throw new Error(`Unexpected trailing token after geometry: "${this.peek().value}"`);
|
|
80
|
+
}
|
|
81
|
+
return geometry;
|
|
82
|
+
}
|
|
83
|
+
peek() {
|
|
84
|
+
return this.tokens[this.pos] || { type: 'EOF', value: '' };
|
|
85
|
+
}
|
|
86
|
+
advance() {
|
|
87
|
+
return this.tokens[this.pos++];
|
|
88
|
+
}
|
|
89
|
+
/** 消费当前 token 并返回,若类型不匹配则抛出错误 */
|
|
90
|
+
consume(type) {
|
|
91
|
+
const token = this.peek();
|
|
92
|
+
if (token.type !== type) {
|
|
93
|
+
throw new Error(`Expected ${type}, got ${token.type}: "${token.value}"`);
|
|
94
|
+
}
|
|
95
|
+
return this.advance();
|
|
96
|
+
}
|
|
97
|
+
parseGeometry() {
|
|
98
|
+
const token = this.peek();
|
|
99
|
+
if (token.type !== 'WORD') {
|
|
100
|
+
throw new Error(`Expected geometry type keyword, got: "${token.value}"`);
|
|
101
|
+
}
|
|
102
|
+
switch (token.value) {
|
|
103
|
+
case 'POINT':
|
|
104
|
+
return this.parsePoint();
|
|
105
|
+
case 'LINESTRING':
|
|
106
|
+
return this.parseLineString();
|
|
107
|
+
case 'POLYGON':
|
|
108
|
+
return this.parsePolygon();
|
|
109
|
+
case 'MULTIPOINT':
|
|
110
|
+
return this.parseMultiPoint();
|
|
111
|
+
case 'MULTILINESTRING':
|
|
112
|
+
return this.parseMultiLineString();
|
|
113
|
+
case 'MULTIPOLYGON':
|
|
114
|
+
return this.parseMultiPolygon();
|
|
115
|
+
case 'GEOMETRYCOLLECTION':
|
|
116
|
+
return this.parseGeometryCollection();
|
|
117
|
+
default:
|
|
118
|
+
throw new Error(`Unknown geometry type: ${token.value}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// ── 消费可选的维度修饰符(Z / M / ZM)
|
|
122
|
+
skipDimensionKeyword() {
|
|
123
|
+
const t = this.peek();
|
|
124
|
+
if (t.type === 'WORD' && (t.value === 'Z' || t.value === 'M' || t.value === 'ZM')) {
|
|
125
|
+
this.advance();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ── 判断并消费 EMPTY 关键字
|
|
129
|
+
isEmptyGeometry() {
|
|
130
|
+
const t = this.peek();
|
|
131
|
+
if (t.type === 'WORD' && t.value === 'EMPTY') {
|
|
132
|
+
this.advance();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
// ── POINT ────────────────────────────────────────────────────────
|
|
138
|
+
parsePoint() {
|
|
139
|
+
this.advance(); // consume POINT
|
|
140
|
+
this.skipDimensionKeyword();
|
|
141
|
+
if (this.isEmptyGeometry()) {
|
|
142
|
+
// GeoJSON 无法表示空点:POINT EMPTY 在 GeoJSON 中应用 null geometry Feature,
|
|
143
|
+
// 此处直接抛出,由调用方决定如何处理。
|
|
144
|
+
throw new Error('POINT EMPTY cannot be represented as a GeoJSON Point. ' +
|
|
145
|
+
'Consider using wktToFeature() and checking Feature.geometry === null.');
|
|
146
|
+
}
|
|
147
|
+
this.consume('LPAREN');
|
|
148
|
+
const coords = this.parseCoordinates();
|
|
149
|
+
this.consume('RPAREN');
|
|
150
|
+
return { type: 'Point', coordinates: coords };
|
|
151
|
+
}
|
|
152
|
+
// ── LINESTRING ───────────────────────────────────────────────────
|
|
153
|
+
parseLineString() {
|
|
154
|
+
this.advance(); // consume LINESTRING
|
|
155
|
+
this.skipDimensionKeyword();
|
|
156
|
+
if (this.isEmptyGeometry()) {
|
|
157
|
+
return { type: 'LineString', coordinates: [] };
|
|
158
|
+
}
|
|
159
|
+
const coords = this.parseCoordinatesList();
|
|
160
|
+
return { type: 'LineString', coordinates: coords };
|
|
161
|
+
}
|
|
162
|
+
// ── POLYGON ──────────────────────────────────────────────────────
|
|
163
|
+
parsePolygon() {
|
|
164
|
+
this.advance(); // consume POLYGON
|
|
165
|
+
this.skipDimensionKeyword();
|
|
166
|
+
if (this.isEmptyGeometry()) {
|
|
167
|
+
return { type: 'Polygon', coordinates: [] };
|
|
168
|
+
}
|
|
169
|
+
const rings = this.parseCoordinateListList();
|
|
170
|
+
return { type: 'Polygon', coordinates: rings };
|
|
171
|
+
}
|
|
172
|
+
// ── MULTIPOINT ───────────────────────────────────────────────────
|
|
173
|
+
parseMultiPoint() {
|
|
174
|
+
this.advance(); // consume MULTIPOINT
|
|
175
|
+
this.skipDimensionKeyword();
|
|
176
|
+
if (this.isEmptyGeometry() || this.peek().type !== 'LPAREN') {
|
|
177
|
+
return { type: 'MultiPoint', coordinates: [] };
|
|
178
|
+
}
|
|
179
|
+
this.advance(); // consume outer (
|
|
180
|
+
const coords = [];
|
|
181
|
+
while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
|
|
182
|
+
if (this.peek().type === 'LPAREN') {
|
|
183
|
+
// 标准写法: MULTIPOINT ((x y), (x y))
|
|
184
|
+
this.advance(); // consume (
|
|
185
|
+
coords.push(this.parseCoordinates());
|
|
186
|
+
this.consume('RPAREN');
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
// 非标准写法: MULTIPOINT (x y, x y)
|
|
190
|
+
coords.push(this.parseCoordinates());
|
|
191
|
+
}
|
|
192
|
+
if (this.peek().type === 'COMMA') {
|
|
193
|
+
this.advance();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
this.consume('RPAREN');
|
|
197
|
+
return { type: 'MultiPoint', coordinates: coords };
|
|
198
|
+
}
|
|
199
|
+
// ── MULTILINESTRING ──────────────────────────────────────────────
|
|
200
|
+
parseMultiLineString() {
|
|
201
|
+
this.advance(); // consume MULTILINESTRING
|
|
202
|
+
this.skipDimensionKeyword();
|
|
203
|
+
if (this.isEmptyGeometry() || this.peek().type !== 'LPAREN') {
|
|
204
|
+
return { type: 'MultiLineString', coordinates: [] };
|
|
205
|
+
}
|
|
206
|
+
this.advance(); // consume outer (
|
|
207
|
+
const lines = [];
|
|
208
|
+
while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
|
|
209
|
+
lines.push(this.parseCoordinatesList());
|
|
210
|
+
if (this.peek().type === 'COMMA') {
|
|
211
|
+
this.advance();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
this.consume('RPAREN');
|
|
215
|
+
return { type: 'MultiLineString', coordinates: lines };
|
|
216
|
+
}
|
|
217
|
+
// ── MULTIPOLYGON ─────────────────────────────────────────────────
|
|
218
|
+
parseMultiPolygon() {
|
|
219
|
+
this.advance(); // consume MULTIPOLYGON
|
|
220
|
+
this.skipDimensionKeyword();
|
|
221
|
+
if (this.isEmptyGeometry()) {
|
|
222
|
+
return { type: 'MultiPolygon', coordinates: [] };
|
|
223
|
+
}
|
|
224
|
+
const polys = this.parseCoordinateListListList();
|
|
225
|
+
return { type: 'MultiPolygon', coordinates: polys };
|
|
226
|
+
}
|
|
227
|
+
// ── GEOMETRYCOLLECTION ───────────────────────────────────────────
|
|
228
|
+
parseGeometryCollection() {
|
|
229
|
+
this.advance(); // consume GEOMETRYCOLLECTION
|
|
230
|
+
this.skipDimensionKeyword();
|
|
231
|
+
if (this.isEmptyGeometry() || this.peek().type !== 'LPAREN') {
|
|
232
|
+
return { type: 'GeometryCollection', geometries: [] };
|
|
233
|
+
}
|
|
234
|
+
this.advance(); // consume (
|
|
235
|
+
const geometries = [];
|
|
236
|
+
while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
|
|
237
|
+
geometries.push(this.parseGeometry());
|
|
238
|
+
if (this.peek().type === 'COMMA') {
|
|
239
|
+
this.advance();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
this.consume('RPAREN');
|
|
243
|
+
return { type: 'GeometryCollection', geometries };
|
|
244
|
+
}
|
|
245
|
+
// ── 坐标解析辅助方法 ──────────────────────────────────────────────
|
|
246
|
+
/**
|
|
247
|
+
* 读取一个坐标点(自动检测维度:X Y 或 X Y Z)
|
|
248
|
+
* 读完 X、Y 后,若下一个 token 仍是 NUMBER,则继续读 Z
|
|
249
|
+
*/
|
|
250
|
+
parseCoordinates() {
|
|
251
|
+
const xStr = this.consume('NUMBER').value;
|
|
252
|
+
const yStr = this.consume('NUMBER').value;
|
|
253
|
+
const x = parseFloat(xStr);
|
|
254
|
+
const y = parseFloat(yStr);
|
|
255
|
+
if (isNaN(x))
|
|
256
|
+
throw new Error(`Invalid coordinate value: "${xStr}"`);
|
|
257
|
+
if (isNaN(y))
|
|
258
|
+
throw new Error(`Invalid coordinate value: "${yStr}"`);
|
|
259
|
+
// 动态检测 Z 坐标
|
|
260
|
+
if (this.peek().type === 'NUMBER') {
|
|
261
|
+
const zStr = this.advance().value;
|
|
262
|
+
const z = parseFloat(zStr);
|
|
263
|
+
if (isNaN(z))
|
|
264
|
+
throw new Error(`Invalid coordinate value: "${zStr}"`);
|
|
265
|
+
return [x, y, z];
|
|
266
|
+
}
|
|
267
|
+
return [x, y];
|
|
268
|
+
}
|
|
269
|
+
/** 解析带括号的坐标序列:( x y, x y, ... ) */
|
|
270
|
+
parseCoordinatesList() {
|
|
271
|
+
this.consume('LPAREN');
|
|
272
|
+
const coords = [];
|
|
273
|
+
while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
|
|
274
|
+
coords.push(this.parseCoordinates());
|
|
275
|
+
if (this.peek().type === 'COMMA') {
|
|
276
|
+
this.advance();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
this.consume('RPAREN');
|
|
280
|
+
return coords;
|
|
281
|
+
}
|
|
282
|
+
/** 解析环列表(Polygon 级别):( (...), (...) ) */
|
|
283
|
+
parseCoordinateListList() {
|
|
284
|
+
if (this.peek().type !== 'LPAREN')
|
|
285
|
+
return [];
|
|
286
|
+
this.advance(); // consume outer (
|
|
287
|
+
const lists = [];
|
|
288
|
+
while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
|
|
289
|
+
lists.push(this.parseCoordinatesList());
|
|
290
|
+
if (this.peek().type === 'COMMA') {
|
|
291
|
+
this.advance();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
this.consume('RPAREN');
|
|
295
|
+
return lists;
|
|
296
|
+
}
|
|
297
|
+
/** 解析多边形列表(MultiPolygon 级别):( ((...)), ((...)) ) */
|
|
298
|
+
parseCoordinateListListList() {
|
|
299
|
+
if (this.peek().type !== 'LPAREN')
|
|
300
|
+
return [];
|
|
301
|
+
this.advance(); // consume outer (
|
|
302
|
+
const lists = [];
|
|
303
|
+
while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
|
|
304
|
+
lists.push(this.parseCoordinateListList());
|
|
305
|
+
if (this.peek().type === 'COMMA') {
|
|
306
|
+
this.advance();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
this.consume('RPAREN');
|
|
310
|
+
return lists;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** 将 WKT 字符串解析为 GeoJSON Geometry 对象 */
|
|
314
|
+
function parse(wkt) {
|
|
315
|
+
const parser = new WKTParser();
|
|
316
|
+
return parser.parse(wkt);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 将坐标数值格式化为字符串,避免科学计数法(WKT 不支持)。
|
|
321
|
+
* 例:1e-7 → "0.0000001",1.50000 → "1.5",1.0 → "1"
|
|
322
|
+
*/
|
|
323
|
+
function formatNumber(v) {
|
|
324
|
+
// 有小数则最多保留 15 位有效位,再去掉尾零
|
|
325
|
+
if (v % 1 !== 0) {
|
|
326
|
+
return parseFloat(v.toFixed(15)).toString();
|
|
327
|
+
}
|
|
328
|
+
return v.toFixed(0);
|
|
329
|
+
}
|
|
330
|
+
function positionToWkt(pos) {
|
|
331
|
+
return pos.map(formatNumber).join(' ');
|
|
332
|
+
}
|
|
333
|
+
function coordsToWkt(coords) {
|
|
334
|
+
return coords.map(positionToWkt).join(', ');
|
|
335
|
+
}
|
|
336
|
+
class WKTBuilder {
|
|
337
|
+
build(geometry) {
|
|
338
|
+
switch (geometry.type) {
|
|
339
|
+
case 'Point':
|
|
340
|
+
return this.buildPoint(geometry);
|
|
341
|
+
case 'LineString':
|
|
342
|
+
return this.buildLineString(geometry);
|
|
343
|
+
case 'Polygon':
|
|
344
|
+
return this.buildPolygon(geometry);
|
|
345
|
+
case 'MultiPoint':
|
|
346
|
+
return this.buildMultiPoint(geometry);
|
|
347
|
+
case 'MultiLineString':
|
|
348
|
+
return this.buildMultiLineString(geometry);
|
|
349
|
+
case 'MultiPolygon':
|
|
350
|
+
return this.buildMultiPolygon(geometry);
|
|
351
|
+
case 'GeometryCollection':
|
|
352
|
+
return this.buildGeometryCollection(geometry);
|
|
353
|
+
default:
|
|
354
|
+
throw new Error(`Unknown geometry type: ${geometry.type}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
buildPoint(geom) {
|
|
358
|
+
const hasZ = geom.coordinates.length === 3;
|
|
359
|
+
return `POINT${hasZ ? ' Z' : ''} (${positionToWkt(geom.coordinates)})`;
|
|
360
|
+
}
|
|
361
|
+
buildLineString(geom) {
|
|
362
|
+
if (geom.coordinates.length === 0)
|
|
363
|
+
return 'LINESTRING EMPTY';
|
|
364
|
+
const hasZ = geom.coordinates[0].length === 3;
|
|
365
|
+
return `LINESTRING${hasZ ? ' Z' : ''} (${coordsToWkt(geom.coordinates)})`;
|
|
366
|
+
}
|
|
367
|
+
buildPolygon(geom) {
|
|
368
|
+
if (geom.coordinates.length === 0)
|
|
369
|
+
return 'POLYGON EMPTY';
|
|
370
|
+
const hasZ = geom.coordinates[0].length > 0 && geom.coordinates[0][0].length === 3;
|
|
371
|
+
const ringStr = geom.coordinates.map(ring => `(${coordsToWkt(ring)})`).join(', ');
|
|
372
|
+
return `POLYGON${hasZ ? ' Z' : ''} (${ringStr})`;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* 按 OGC/ISO WKT 标准,MULTIPOINT 每个点用括号包裹:
|
|
376
|
+
* MULTIPOINT ((0 0), (1 1), (2 2))
|
|
377
|
+
*/
|
|
378
|
+
buildMultiPoint(geom) {
|
|
379
|
+
if (geom.coordinates.length === 0)
|
|
380
|
+
return 'MULTIPOINT EMPTY';
|
|
381
|
+
const hasZ = geom.coordinates[0].length === 3;
|
|
382
|
+
const pts = geom.coordinates.map(p => `(${positionToWkt(p)})`).join(', ');
|
|
383
|
+
return `MULTIPOINT${hasZ ? ' Z' : ''} (${pts})`;
|
|
384
|
+
}
|
|
385
|
+
buildMultiLineString(geom) {
|
|
386
|
+
if (geom.coordinates.length === 0)
|
|
387
|
+
return 'MULTILINESTRING EMPTY';
|
|
388
|
+
const hasZ = geom.coordinates[0].length > 0 && geom.coordinates[0][0].length === 3;
|
|
389
|
+
const lines = geom.coordinates.map(line => `(${coordsToWkt(line)})`).join(', ');
|
|
390
|
+
return `MULTILINESTRING${hasZ ? ' Z' : ''} (${lines})`;
|
|
391
|
+
}
|
|
392
|
+
buildMultiPolygon(geom) {
|
|
393
|
+
if (geom.coordinates.length === 0)
|
|
394
|
+
return 'MULTIPOLYGON EMPTY';
|
|
395
|
+
const hasZ = geom.coordinates[0].length > 0 && geom.coordinates[0][0].length > 0 && geom.coordinates[0][0][0].length === 3;
|
|
396
|
+
const polys = geom.coordinates.map(poly => {
|
|
397
|
+
const rings = poly.map(ring => `(${coordsToWkt(ring)})`).join(', ');
|
|
398
|
+
return `(${rings})`;
|
|
399
|
+
}).join(', ');
|
|
400
|
+
return `MULTIPOLYGON${hasZ ? ' Z' : ''} (${polys})`;
|
|
401
|
+
}
|
|
402
|
+
buildGeometryCollection(geom) {
|
|
403
|
+
if (geom.geometries.length === 0)
|
|
404
|
+
return 'GEOMETRYCOLLECTION EMPTY';
|
|
405
|
+
const geoms = geom.geometries.map(g => this.build(g)).join(', ');
|
|
406
|
+
return `GEOMETRYCOLLECTION (${geoms})`;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** 将 GeoJSON Geometry 对象转换为 WKT 字符串 */
|
|
410
|
+
function build(geometry) {
|
|
411
|
+
return new WKTBuilder().build(geometry);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ─── 内部工具:判断是否为 Position([number, number] 或 [number, number, number])
|
|
415
|
+
function isPosition(v) {
|
|
416
|
+
return (Array.isArray(v) &&
|
|
417
|
+
(v.length === 2 || v.length === 3) &&
|
|
418
|
+
v.every((n) => typeof n === 'number'));
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* GeoJSON 几何对象构建器(类形式,方便组合使用)
|
|
422
|
+
*/
|
|
423
|
+
class GeoJSONBuilder {
|
|
424
|
+
createPoint(x, y, z) {
|
|
425
|
+
return z !== undefined
|
|
426
|
+
? { type: 'Point', coordinates: [x, y, z] }
|
|
427
|
+
: { type: 'Point', coordinates: [x, y] };
|
|
428
|
+
}
|
|
429
|
+
createLineString(coordinates) {
|
|
430
|
+
return { type: 'LineString', coordinates };
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* 创建 Polygon。
|
|
434
|
+
* - 传入 `Position[]`:视为单个外环,自动包装为 `[ring]`
|
|
435
|
+
* - 传入 `Position[][]`:视为完整的环列表(外环 + 内环/空洞)
|
|
436
|
+
*/
|
|
437
|
+
createPolygon(coordinates) {
|
|
438
|
+
const rings = isPosition(coordinates[0])
|
|
439
|
+
? [coordinates] // 单环:Position[] → Position[][]
|
|
440
|
+
: coordinates; // 多环:已是 Position[][]
|
|
441
|
+
return { type: 'Polygon', coordinates: rings };
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* 创建 MultiPoint。
|
|
445
|
+
* - 传入 `Position`:视为单个点,自动包装为 `[point]`
|
|
446
|
+
* - 传入 `Position[]`:视为多个点
|
|
447
|
+
*/
|
|
448
|
+
createMultiPoint(coordinates) {
|
|
449
|
+
const pts = isPosition(coordinates)
|
|
450
|
+
? [coordinates] // 单点:Position → Position[]
|
|
451
|
+
: coordinates; // 多点:已是 Position[]
|
|
452
|
+
return { type: 'MultiPoint', coordinates: pts };
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* 创建 MultiLineString。
|
|
456
|
+
* - 传入 `Position[]`:视为单条线,自动包装为 `[line]`
|
|
457
|
+
* - 传入 `Position[][]`:视为多条线
|
|
458
|
+
*/
|
|
459
|
+
createMultiLineString(coordinates) {
|
|
460
|
+
const lines = isPosition(coordinates[0])
|
|
461
|
+
? [coordinates] // 单线:Position[] → Position[][]
|
|
462
|
+
: coordinates; // 多线:已是 Position[][]
|
|
463
|
+
return { type: 'MultiLineString', coordinates: lines };
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* 创建 MultiPolygon。
|
|
467
|
+
* - 传入 `Position[][]`:视为单个多边形(环列表),自动包装为 `[polygon]`
|
|
468
|
+
* - 传入 `Position[][][]`:视为多个多边形
|
|
469
|
+
*/
|
|
470
|
+
createMultiPolygon(coordinates) {
|
|
471
|
+
// 判断:若第一个元素是 Position[](环),则整体是单个 polygon
|
|
472
|
+
const firstElem = coordinates[0];
|
|
473
|
+
const isSinglePolygon = Array.isArray(firstElem) && Array.isArray(firstElem[0]) && isPosition(firstElem[0]);
|
|
474
|
+
const polys = isSinglePolygon
|
|
475
|
+
? [coordinates] // 单多边形:Position[][] → Position[][][]
|
|
476
|
+
: coordinates; // 多多边形:已是 Position[][][]
|
|
477
|
+
return { type: 'MultiPolygon', coordinates: polys };
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* 创建 GeometryCollection。
|
|
481
|
+
* - 传入单个 `Geometry`:自动包装为 `[geometry]`
|
|
482
|
+
* - 传入 `Geometry[]`:直接使用
|
|
483
|
+
*/
|
|
484
|
+
createGeometryCollection(geometries) {
|
|
485
|
+
const geoms = Array.isArray(geometries) ? geometries : [geometries];
|
|
486
|
+
return { type: 'GeometryCollection', geometries: geoms };
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
// 单例,避免重复实例化
|
|
490
|
+
const _builder = new GeoJSONBuilder();
|
|
491
|
+
/** 创建 Point */
|
|
492
|
+
function createPoint(x, y, z) {
|
|
493
|
+
return _builder.createPoint(x, y, z);
|
|
494
|
+
}
|
|
495
|
+
/** 创建 LineString */
|
|
496
|
+
function createLineString(coordinates) {
|
|
497
|
+
return _builder.createLineString(coordinates);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* 创建 Polygon。
|
|
501
|
+
* - 传入 `Position[]`:单个外环,自动包装
|
|
502
|
+
* - 传入 `Position[][]`:外环 + 内环(空洞)
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* createPolygon([[0,0],[1,0],[1,1],[0,1],[0,0]])
|
|
506
|
+
* createPolygon([[[0,0],[10,0],[10,10],[0,10],[0,0]], [[2,2],[4,2],[4,4],[2,4],[2,2]]])
|
|
507
|
+
*/
|
|
508
|
+
function createPolygon(coordinates) {
|
|
509
|
+
return _builder.createPolygon(coordinates);
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* 创建 MultiPoint。
|
|
513
|
+
* - 传入 `Position`:单个点
|
|
514
|
+
* - 传入 `Position[]`:多个点
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* createMultiPoint([0, 0])
|
|
518
|
+
* createMultiPoint([[0,0],[1,1],[2,2]])
|
|
519
|
+
*/
|
|
520
|
+
function createMultiPoint(coordinates) {
|
|
521
|
+
return _builder.createMultiPoint(coordinates);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* 创建 MultiLineString。
|
|
525
|
+
* - 传入 `Position[]`:单条线
|
|
526
|
+
* - 传入 `Position[][]`:多条线
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* createMultiLineString([[0,0],[1,1]])
|
|
530
|
+
* createMultiLineString([[[0,0],[1,1]], [[2,2],[3,3]]])
|
|
531
|
+
*/
|
|
532
|
+
function createMultiLineString(coordinates) {
|
|
533
|
+
return _builder.createMultiLineString(coordinates);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* 创建 MultiPolygon。
|
|
537
|
+
* - 传入 `Position[][]`:单个多边形(环列表)
|
|
538
|
+
* - 传入 `Position[][][]`:多个多边形
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* createMultiPolygon([[[0,0],[1,0],[1,1],[0,1],[0,0]]])
|
|
542
|
+
* createMultiPolygon([[[[0,0],[1,0],[1,1],[0,1],[0,0]]], [[[2,2],[3,2],[3,3],[2,3],[2,2]]]])
|
|
543
|
+
*/
|
|
544
|
+
function createMultiPolygon(coordinates) {
|
|
545
|
+
return _builder.createMultiPolygon(coordinates);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* 创建 GeometryCollection。
|
|
549
|
+
* - 传入单个 `Geometry`:自动包装
|
|
550
|
+
* - 传入 `Geometry[]`:直接使用
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* createGeometryCollection(createPoint(0, 0))
|
|
554
|
+
* createGeometryCollection([createPoint(0,0), createLineString([[0,0],[1,1]])])
|
|
555
|
+
*/
|
|
556
|
+
function createGeometryCollection(geometries) {
|
|
557
|
+
return _builder.createGeometryCollection(geometries);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* 将 WKT 字符串转换为 GeoJSON Geometry 对象。
|
|
562
|
+
*
|
|
563
|
+
* @example
|
|
564
|
+
* wktToGeoJSON('POINT (30.5 40.5)')
|
|
565
|
+
* // → { type: 'Point', coordinates: [30.5, 40.5] }
|
|
566
|
+
*
|
|
567
|
+
* wktToGeoJSON('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))')
|
|
568
|
+
* // → { type: 'Polygon', coordinates: [[[0,0],[1,0],[1,1],[0,1],[0,0]]] }
|
|
569
|
+
*/
|
|
570
|
+
function wktToGeoJSON(wkt) {
|
|
571
|
+
return parse(wkt);
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* 将 WKT 字符串转换为 GeoJSON Feature 对象。
|
|
575
|
+
*
|
|
576
|
+
* @param wkt WKT 字符串
|
|
577
|
+
* @param properties 可选的 Feature 属性对象
|
|
578
|
+
* @param id 可选的 Feature ID
|
|
579
|
+
*
|
|
580
|
+
* @example
|
|
581
|
+
* wktToFeature('POINT (30.5 40.5)', { name: '北京' })
|
|
582
|
+
* // → { type: 'Feature', geometry: { type: 'Point', ... }, properties: { name: '北京' } }
|
|
583
|
+
*/
|
|
584
|
+
function wktToFeature(wkt, properties = null, id) {
|
|
585
|
+
const geometry = parse(wkt);
|
|
586
|
+
const feature = {
|
|
587
|
+
type: 'Feature',
|
|
588
|
+
geometry,
|
|
589
|
+
properties,
|
|
590
|
+
};
|
|
591
|
+
if (id !== undefined) {
|
|
592
|
+
feature.id = id;
|
|
593
|
+
}
|
|
594
|
+
return feature;
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* 将多个 WKT 字符串批量转换为 GeoJSON FeatureCollection。
|
|
598
|
+
*
|
|
599
|
+
* @param wkts WKT 字符串数组
|
|
600
|
+
* @param properties 可选,每个 Feature 的属性数组(长度应与 wkts 一致)
|
|
601
|
+
*
|
|
602
|
+
* @example
|
|
603
|
+
* wktToFeatureCollection(['POINT (0 0)', 'POINT (1 1)'])
|
|
604
|
+
* // → { type: 'FeatureCollection', features: [...] }
|
|
605
|
+
*/
|
|
606
|
+
function wktToFeatureCollection(wkts, properties) {
|
|
607
|
+
const features = wkts.map((wkt, i) => wktToFeature(wkt, properties ? (properties[i] ?? null) : null));
|
|
608
|
+
return { type: 'FeatureCollection', features };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 将 GeoJSON Geometry 对象转换为 WKT 字符串。
|
|
613
|
+
*
|
|
614
|
+
* @example
|
|
615
|
+
* geojsonToWkt({ type: 'Point', coordinates: [30.5, 40.5] })
|
|
616
|
+
* // → 'POINT (30.5 40.5)'
|
|
617
|
+
*
|
|
618
|
+
* geojsonToWkt({ type: 'Polygon', coordinates: [[[0,0],[1,0],[1,1],[0,1],[0,0]]] })
|
|
619
|
+
* // → 'POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))'
|
|
620
|
+
*/
|
|
621
|
+
function geojsonToWkt(geojson) {
|
|
622
|
+
return build(geojson);
|
|
623
|
+
}
|
|
624
|
+
/**
|
|
625
|
+
* 将 GeoJSON Feature 对象转换为 WKT 字符串(取 geometry 部分)。
|
|
626
|
+
*
|
|
627
|
+
* @throws 若 Feature.geometry 为 null,则抛出错误
|
|
628
|
+
*
|
|
629
|
+
* @example
|
|
630
|
+
* featureToWkt({ type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] }, properties: null })
|
|
631
|
+
* // → 'POINT (0 0)'
|
|
632
|
+
*/
|
|
633
|
+
function featureToWkt(feature) {
|
|
634
|
+
if (!feature.geometry) {
|
|
635
|
+
throw new Error('Feature.geometry is null, cannot convert to WKT');
|
|
636
|
+
}
|
|
637
|
+
return build(feature.geometry);
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* 将 GeoJSON FeatureCollection 中所有 Feature 转换为 WKT 字符串数组。
|
|
641
|
+
*
|
|
642
|
+
* geometry 为 null 的 Feature 会被跳过(返回数组中对应位置为 null)。
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* featureCollectionToWkt({ type: 'FeatureCollection', features: [...] })
|
|
646
|
+
* // → ['POINT (0 0)', 'LINESTRING (0 0, 1 1)', ...]
|
|
647
|
+
*/
|
|
648
|
+
function featureCollectionToWkt(fc) {
|
|
649
|
+
return fc.features.map((f) => {
|
|
650
|
+
if (!f.geometry)
|
|
651
|
+
return null;
|
|
652
|
+
return build(f.geometry);
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
exports.GeoJSONBuilder = GeoJSONBuilder;
|
|
657
|
+
exports.WKTBuilder = WKTBuilder;
|
|
658
|
+
exports.WKTParser = WKTParser;
|
|
659
|
+
exports.build = build;
|
|
660
|
+
exports.createGeometryCollection = createGeometryCollection;
|
|
661
|
+
exports.createLineString = createLineString;
|
|
662
|
+
exports.createMultiLineString = createMultiLineString;
|
|
663
|
+
exports.createMultiPoint = createMultiPoint;
|
|
664
|
+
exports.createMultiPolygon = createMultiPolygon;
|
|
665
|
+
exports.createPoint = createPoint;
|
|
666
|
+
exports.createPolygon = createPolygon;
|
|
667
|
+
exports.featureCollectionToWkt = featureCollectionToWkt;
|
|
668
|
+
exports.featureToWkt = featureToWkt;
|
|
669
|
+
exports.geojsonToWkt = geojsonToWkt;
|
|
670
|
+
exports.parse = parse;
|
|
671
|
+
exports.wktToFeature = wktToFeature;
|
|
672
|
+
exports.wktToFeatureCollection = wktToFeatureCollection;
|
|
673
|
+
exports.wktToGeoJSON = wktToGeoJSON;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export { parse, WKTParser } from './wkt-parser';
|
|
3
|
+
export { build, WKTBuilder } from './wkt-builder';
|
|
4
|
+
export { createPoint, createLineString, createPolygon, createMultiPoint, createMultiLineString, createMultiPolygon, createGeometryCollection, GeoJSONBuilder, } from './geojson-builder';
|
|
5
|
+
export { wktToGeoJSON, wktToFeature, wktToFeatureCollection } from './wkt-to-geojson';
|
|
6
|
+
export { geojsonToWkt, featureToWkt, featureCollectionToWkt } from './geojson-to-wkt';
|