giser-geometry-parse 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/LICENSE +21 -0
- package/README.md +752 -0
- package/dist/default-export.d.ts +29 -0
- package/dist/geojson-builder.d.ts +92 -0
- package/dist/geojson-to-wkt.d.ts +32 -0
- package/dist/index.cjs.js +959 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.esm.js +932 -0
- package/dist/index.umd.js +965 -0
- package/dist/types.d.ts +51 -0
- package/dist/validate.d.ts +29 -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 +44 -0
|
@@ -0,0 +1,932 @@
|
|
|
1
|
+
var types = /*#__PURE__*/Object.freeze({
|
|
2
|
+
__proto__: null
|
|
3
|
+
});
|
|
4
|
+
|
|
5
|
+
// Precompiled regex for better performance
|
|
6
|
+
const RE_WHITESPACE = /\s/;
|
|
7
|
+
const RE_NUMBER_START = /[0-9\-]/;
|
|
8
|
+
const RE_NUMBER_BODY = /[0-9\.\-eE\+]/;
|
|
9
|
+
const RE_WORD_CHAR = /[a-zA-Z_]/;
|
|
10
|
+
class Lexer {
|
|
11
|
+
constructor(input) {
|
|
12
|
+
this.pos = 0;
|
|
13
|
+
this.input = input.trim();
|
|
14
|
+
}
|
|
15
|
+
peek() {
|
|
16
|
+
return this.input[this.pos] || '';
|
|
17
|
+
}
|
|
18
|
+
advance() {
|
|
19
|
+
return this.input[this.pos++] || '';
|
|
20
|
+
}
|
|
21
|
+
isWhitespace(c) {
|
|
22
|
+
return RE_WHITESPACE.test(c);
|
|
23
|
+
}
|
|
24
|
+
isNumberStart(c) {
|
|
25
|
+
return RE_NUMBER_START.test(c);
|
|
26
|
+
}
|
|
27
|
+
isNumberBody(c) {
|
|
28
|
+
return RE_NUMBER_BODY.test(c);
|
|
29
|
+
}
|
|
30
|
+
nextToken() {
|
|
31
|
+
// skip whitespace
|
|
32
|
+
while (this.pos < this.input.length && this.isWhitespace(this.peek())) {
|
|
33
|
+
this.advance();
|
|
34
|
+
}
|
|
35
|
+
if (this.pos >= this.input.length) {
|
|
36
|
+
return { type: 'EOF', value: '' };
|
|
37
|
+
}
|
|
38
|
+
const c = this.peek();
|
|
39
|
+
if (c === '(') {
|
|
40
|
+
this.advance();
|
|
41
|
+
return { type: 'LPAREN', value: '(' };
|
|
42
|
+
}
|
|
43
|
+
if (c === ')') {
|
|
44
|
+
this.advance();
|
|
45
|
+
return { type: 'RPAREN', value: ')' };
|
|
46
|
+
}
|
|
47
|
+
if (c === ',') {
|
|
48
|
+
this.advance();
|
|
49
|
+
return { type: 'COMMA', value: ',' };
|
|
50
|
+
}
|
|
51
|
+
// Number: starts with digit or minus
|
|
52
|
+
if (this.isNumberStart(c)) {
|
|
53
|
+
const start = this.pos;
|
|
54
|
+
while (this.pos < this.input.length && this.isNumberBody(this.peek())) {
|
|
55
|
+
this.pos++;
|
|
56
|
+
}
|
|
57
|
+
return { type: 'NUMBER', value: this.input.slice(start, this.pos) };
|
|
58
|
+
}
|
|
59
|
+
// Word (geometry type or EMPTY/Z/M keyword)
|
|
60
|
+
const start = this.pos;
|
|
61
|
+
while (this.pos < this.input.length && RE_WORD_CHAR.test(this.peek())) {
|
|
62
|
+
this.pos++;
|
|
63
|
+
}
|
|
64
|
+
return { type: 'WORD', value: this.input.slice(start, this.pos).toUpperCase() };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
class WKTParser {
|
|
68
|
+
constructor() {
|
|
69
|
+
this.tokens = [];
|
|
70
|
+
this.pos = 0;
|
|
71
|
+
}
|
|
72
|
+
parse(wkt) {
|
|
73
|
+
this.tokens = [];
|
|
74
|
+
this.pos = 0;
|
|
75
|
+
const lexer = new Lexer(wkt);
|
|
76
|
+
let token = lexer.nextToken();
|
|
77
|
+
while (token.type !== 'EOF') {
|
|
78
|
+
this.tokens.push(token);
|
|
79
|
+
token = lexer.nextToken();
|
|
80
|
+
}
|
|
81
|
+
this.tokens.push({ type: 'EOF', value: '' });
|
|
82
|
+
const geometry = this.parseGeometry();
|
|
83
|
+
// 校验尾部无多余字符(防止静默忽略垃圾输入)
|
|
84
|
+
if (this.peek().type !== 'EOF') {
|
|
85
|
+
throw new Error(`Unexpected trailing token after geometry: "${this.peek().value}"`);
|
|
86
|
+
}
|
|
87
|
+
return geometry;
|
|
88
|
+
}
|
|
89
|
+
peek() {
|
|
90
|
+
return this.tokens[this.pos] || { type: 'EOF', value: '' };
|
|
91
|
+
}
|
|
92
|
+
advance() {
|
|
93
|
+
return this.tokens[this.pos++];
|
|
94
|
+
}
|
|
95
|
+
/** 消费当前 token 并返回,若类型不匹配则抛出错误 */
|
|
96
|
+
consume(type) {
|
|
97
|
+
const token = this.peek();
|
|
98
|
+
if (token.type !== type) {
|
|
99
|
+
throw new Error(`Expected ${type}, got ${token.type}: "${token.value}"`);
|
|
100
|
+
}
|
|
101
|
+
return this.advance();
|
|
102
|
+
}
|
|
103
|
+
skipComma() {
|
|
104
|
+
if (this.peek().type === 'COMMA') {
|
|
105
|
+
this.advance();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
isDone() {
|
|
109
|
+
const t = this.peek();
|
|
110
|
+
return t.type === 'RPAREN' || t.type === 'EOF';
|
|
111
|
+
}
|
|
112
|
+
parseGeometry() {
|
|
113
|
+
const token = this.peek();
|
|
114
|
+
if (token.type !== 'WORD') {
|
|
115
|
+
throw new Error(`Expected geometry type keyword, got: "${token.value}"`);
|
|
116
|
+
}
|
|
117
|
+
switch (token.value) {
|
|
118
|
+
case 'POINT':
|
|
119
|
+
return this.parsePoint();
|
|
120
|
+
case 'LINESTRING':
|
|
121
|
+
return this.parseLineString();
|
|
122
|
+
case 'POLYGON':
|
|
123
|
+
return this.parsePolygon();
|
|
124
|
+
case 'MULTIPOINT':
|
|
125
|
+
return this.parseMultiPoint();
|
|
126
|
+
case 'MULTILINESTRING':
|
|
127
|
+
return this.parseMultiLineString();
|
|
128
|
+
case 'MULTIPOLYGON':
|
|
129
|
+
return this.parseMultiPolygon();
|
|
130
|
+
case 'GEOMETRYCOLLECTION':
|
|
131
|
+
return this.parseGeometryCollection();
|
|
132
|
+
default:
|
|
133
|
+
throw new Error(`Unknown geometry type: ${token.value}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// ── 消费可选的维度修饰符(Z / M / ZM)
|
|
137
|
+
skipDimensionKeyword() {
|
|
138
|
+
const t = this.peek();
|
|
139
|
+
if (t.type === 'WORD' && (t.value === 'Z' || t.value === 'M' || t.value === 'ZM')) {
|
|
140
|
+
this.advance();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ── 判断并消费 EMPTY 关键字
|
|
144
|
+
isEmptyGeometry() {
|
|
145
|
+
const t = this.peek();
|
|
146
|
+
if (t.type === 'WORD' && t.value === 'EMPTY') {
|
|
147
|
+
this.advance();
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
// ── POINT ────────────────────────────────────────────────────────
|
|
153
|
+
parsePoint() {
|
|
154
|
+
this.advance(); // consume POINT
|
|
155
|
+
this.skipDimensionKeyword();
|
|
156
|
+
if (this.isEmptyGeometry()) {
|
|
157
|
+
// GeoJSON 无法表示空点:POINT EMPTY 在 GeoJSON 中应用 null geometry Feature,
|
|
158
|
+
// 此处直接抛出,由调用方决定如何处理。
|
|
159
|
+
throw new Error('POINT EMPTY cannot be represented as a GeoJSON Point. ' +
|
|
160
|
+
'Consider using wktToFeature() and checking Feature.geometry === null.');
|
|
161
|
+
}
|
|
162
|
+
this.consume('LPAREN');
|
|
163
|
+
const coords = this.parseCoordinates();
|
|
164
|
+
this.consume('RPAREN');
|
|
165
|
+
return { type: 'Point', coordinates: coords };
|
|
166
|
+
}
|
|
167
|
+
// ── LINESTRING ───────────────────────────────────────────────────
|
|
168
|
+
parseLineString() {
|
|
169
|
+
this.advance(); // consume LINESTRING
|
|
170
|
+
this.skipDimensionKeyword();
|
|
171
|
+
if (this.isEmptyGeometry()) {
|
|
172
|
+
return { type: 'LineString', coordinates: [] };
|
|
173
|
+
}
|
|
174
|
+
const coords = this.parseCoordinatesList();
|
|
175
|
+
return { type: 'LineString', coordinates: coords };
|
|
176
|
+
}
|
|
177
|
+
// ── POLYGON ──────────────────────────────────────────────────────
|
|
178
|
+
parsePolygon() {
|
|
179
|
+
this.advance(); // consume POLYGON
|
|
180
|
+
this.skipDimensionKeyword();
|
|
181
|
+
if (this.isEmptyGeometry()) {
|
|
182
|
+
return { type: 'Polygon', coordinates: [] };
|
|
183
|
+
}
|
|
184
|
+
this.consume('LPAREN');
|
|
185
|
+
const rings = [];
|
|
186
|
+
while (!this.isDone()) {
|
|
187
|
+
rings.push(this.parseCoordinatesList());
|
|
188
|
+
this.skipComma();
|
|
189
|
+
}
|
|
190
|
+
this.consume('RPAREN');
|
|
191
|
+
return { type: 'Polygon', coordinates: rings };
|
|
192
|
+
}
|
|
193
|
+
// ── MULTIPOINT ───────────────────────────────────────────────────
|
|
194
|
+
parseMultiPoint() {
|
|
195
|
+
this.advance(); // consume MULTIPOINT
|
|
196
|
+
this.skipDimensionKeyword();
|
|
197
|
+
if (this.isEmptyGeometry() || this.peek().type !== 'LPAREN') {
|
|
198
|
+
return { type: 'MultiPoint', coordinates: [] };
|
|
199
|
+
}
|
|
200
|
+
this.advance(); // consume outer (
|
|
201
|
+
const coords = [];
|
|
202
|
+
while (!this.isDone()) {
|
|
203
|
+
if (this.peek().type === 'LPAREN') {
|
|
204
|
+
// 标准写法: MULTIPOINT ((x y), (x y))
|
|
205
|
+
this.advance(); // consume (
|
|
206
|
+
coords.push(this.parseCoordinates());
|
|
207
|
+
this.consume('RPAREN');
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
// 非标准写法: MULTIPOINT (x y, x y)
|
|
211
|
+
coords.push(this.parseCoordinates());
|
|
212
|
+
}
|
|
213
|
+
this.skipComma();
|
|
214
|
+
}
|
|
215
|
+
this.consume('RPAREN');
|
|
216
|
+
return { type: 'MultiPoint', coordinates: coords };
|
|
217
|
+
}
|
|
218
|
+
// ── MULTILINESTRING ──────────────────────────────────────────────
|
|
219
|
+
parseMultiLineString() {
|
|
220
|
+
this.advance(); // consume MULTILINESTRING
|
|
221
|
+
this.skipDimensionKeyword();
|
|
222
|
+
if (this.isEmptyGeometry() || this.peek().type !== 'LPAREN') {
|
|
223
|
+
return { type: 'MultiLineString', coordinates: [] };
|
|
224
|
+
}
|
|
225
|
+
this.advance(); // consume outer (
|
|
226
|
+
const lines = [];
|
|
227
|
+
while (!this.isDone()) {
|
|
228
|
+
lines.push(this.parseCoordinatesList());
|
|
229
|
+
this.skipComma();
|
|
230
|
+
}
|
|
231
|
+
this.consume('RPAREN');
|
|
232
|
+
return { type: 'MultiLineString', coordinates: lines };
|
|
233
|
+
}
|
|
234
|
+
// ── MULTIPOLYGON ─────────────────────────────────────────────────
|
|
235
|
+
parseMultiPolygon() {
|
|
236
|
+
this.advance(); // consume MULTIPOLYGON
|
|
237
|
+
this.skipDimensionKeyword();
|
|
238
|
+
if (this.isEmptyGeometry()) {
|
|
239
|
+
return { type: 'MultiPolygon', coordinates: [] };
|
|
240
|
+
}
|
|
241
|
+
this.consume('LPAREN');
|
|
242
|
+
const polys = [];
|
|
243
|
+
while (!this.isDone()) {
|
|
244
|
+
polys.push(this.parseCoordinateListList());
|
|
245
|
+
this.skipComma();
|
|
246
|
+
}
|
|
247
|
+
this.consume('RPAREN');
|
|
248
|
+
return { type: 'MultiPolygon', coordinates: polys };
|
|
249
|
+
}
|
|
250
|
+
// ── GEOMETRYCOLLECTION ───────────────────────────────────────────
|
|
251
|
+
parseGeometryCollection() {
|
|
252
|
+
this.advance(); // consume GEOMETRYCOLLECTION
|
|
253
|
+
this.skipDimensionKeyword();
|
|
254
|
+
if (this.isEmptyGeometry() || this.peek().type !== 'LPAREN') {
|
|
255
|
+
return { type: 'GeometryCollection', geometries: [] };
|
|
256
|
+
}
|
|
257
|
+
this.advance(); // consume (
|
|
258
|
+
const geometries = [];
|
|
259
|
+
while (!this.isDone()) {
|
|
260
|
+
geometries.push(this.parseGeometry());
|
|
261
|
+
this.skipComma();
|
|
262
|
+
}
|
|
263
|
+
this.consume('RPAREN');
|
|
264
|
+
return { type: 'GeometryCollection', geometries };
|
|
265
|
+
}
|
|
266
|
+
// ── 坐标解析辅助方法 ──────────────────────────────────────────────
|
|
267
|
+
/**
|
|
268
|
+
* 读取一个坐标点(自动检测维度:X Y 或 X Y Z)
|
|
269
|
+
* 读完 X、Y 后,若下一个 token 仍是 NUMBER,则继续读 Z
|
|
270
|
+
*/
|
|
271
|
+
parseCoordinates() {
|
|
272
|
+
const xStr = this.consume('NUMBER').value;
|
|
273
|
+
const yStr = this.consume('NUMBER').value;
|
|
274
|
+
const x = parseFloat(xStr);
|
|
275
|
+
const y = parseFloat(yStr);
|
|
276
|
+
if (isNaN(x))
|
|
277
|
+
throw new Error(`Invalid coordinate value: "${xStr}"`);
|
|
278
|
+
if (isNaN(y))
|
|
279
|
+
throw new Error(`Invalid coordinate value: "${yStr}"`);
|
|
280
|
+
// 动态检测 Z 坐标
|
|
281
|
+
if (this.peek().type === 'NUMBER') {
|
|
282
|
+
const zStr = this.advance().value;
|
|
283
|
+
const z = parseFloat(zStr);
|
|
284
|
+
if (isNaN(z))
|
|
285
|
+
throw new Error(`Invalid coordinate value: "${zStr}"`);
|
|
286
|
+
return [x, y, z];
|
|
287
|
+
}
|
|
288
|
+
return [x, y];
|
|
289
|
+
}
|
|
290
|
+
/** 解析带括号的坐标序列:( x y, x y, ... ) */
|
|
291
|
+
parseCoordinatesList() {
|
|
292
|
+
this.consume('LPAREN');
|
|
293
|
+
const coords = [];
|
|
294
|
+
while (!this.isDone()) {
|
|
295
|
+
coords.push(this.parseCoordinates());
|
|
296
|
+
this.skipComma();
|
|
297
|
+
}
|
|
298
|
+
this.consume('RPAREN');
|
|
299
|
+
return coords;
|
|
300
|
+
}
|
|
301
|
+
/** 解析环列表(Polygon 级别):( (...), (...) ) */
|
|
302
|
+
parseCoordinateListList() {
|
|
303
|
+
if (this.peek().type !== 'LPAREN')
|
|
304
|
+
return [];
|
|
305
|
+
this.advance(); // consume outer (
|
|
306
|
+
const lists = [];
|
|
307
|
+
while (!this.isDone()) {
|
|
308
|
+
lists.push(this.parseCoordinatesList());
|
|
309
|
+
this.skipComma();
|
|
310
|
+
}
|
|
311
|
+
this.consume('RPAREN');
|
|
312
|
+
return lists;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/** 将 WKT 字符串解析为 GeoJSON Geometry 对象 */
|
|
316
|
+
function parse(wkt) {
|
|
317
|
+
return WKT_PARSER.parse(wkt);
|
|
318
|
+
}
|
|
319
|
+
// 单例实例
|
|
320
|
+
const WKT_PARSER = new WKTParser();
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* 将坐标数值格式化为字符串,避免科学计数法(WKT 不支持)。
|
|
324
|
+
* 例:1e-7 → "0.0000001",1.50000 → "1.5",1.0 → "1"
|
|
325
|
+
*/
|
|
326
|
+
function formatNumber(v) {
|
|
327
|
+
if (v % 1 !== 0) {
|
|
328
|
+
return Number(v.toFixed(15)).toString();
|
|
329
|
+
}
|
|
330
|
+
return String(v);
|
|
331
|
+
}
|
|
332
|
+
function positionToWkt(pos) {
|
|
333
|
+
return pos.map(formatNumber).join(' ');
|
|
334
|
+
}
|
|
335
|
+
function coordsToWkt(coords) {
|
|
336
|
+
return coords.map(positionToWkt).join(', ');
|
|
337
|
+
}
|
|
338
|
+
// 检查坐标是否包含 Z(3个分量)
|
|
339
|
+
function hasZ(coordinates) {
|
|
340
|
+
if (!Array.isArray(coordinates))
|
|
341
|
+
return false;
|
|
342
|
+
if (coordinates.length === 0)
|
|
343
|
+
return false;
|
|
344
|
+
const first = coordinates[0];
|
|
345
|
+
// Point: coordinates[0] 是数字,不是数组
|
|
346
|
+
if (typeof first === 'number') {
|
|
347
|
+
return coordinates.length === 3;
|
|
348
|
+
}
|
|
349
|
+
// LineString/MultiPoint: coordinates[0] 是 Position(数字数组)
|
|
350
|
+
if (Array.isArray(first) && typeof first[0] === 'number') {
|
|
351
|
+
return first.length === 3;
|
|
352
|
+
}
|
|
353
|
+
// Polygon/MultiLineString: coordinates[0] 是 Position[](线的数组)
|
|
354
|
+
if (Array.isArray(first) && Array.isArray(first[0])) {
|
|
355
|
+
const firstRing = first;
|
|
356
|
+
return firstRing.length > 0 && firstRing[0].length === 3;
|
|
357
|
+
}
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
// 获取 Z 后缀字符串
|
|
361
|
+
function zSuffix(coordinates) {
|
|
362
|
+
return hasZ(coordinates) ? ' Z' : '';
|
|
363
|
+
}
|
|
364
|
+
class WKTBuilder {
|
|
365
|
+
build(geometry) {
|
|
366
|
+
switch (geometry.type) {
|
|
367
|
+
case 'Point':
|
|
368
|
+
return this.buildPoint(geometry);
|
|
369
|
+
case 'LineString':
|
|
370
|
+
return this.buildLineString(geometry);
|
|
371
|
+
case 'Polygon':
|
|
372
|
+
return this.buildPolygon(geometry);
|
|
373
|
+
case 'MultiPoint':
|
|
374
|
+
return this.buildMultiPoint(geometry);
|
|
375
|
+
case 'MultiLineString':
|
|
376
|
+
return this.buildMultiLineString(geometry);
|
|
377
|
+
case 'MultiPolygon':
|
|
378
|
+
return this.buildMultiPolygon(geometry);
|
|
379
|
+
case 'GeometryCollection':
|
|
380
|
+
return this.buildGeometryCollection(geometry);
|
|
381
|
+
default:
|
|
382
|
+
throw new Error(`Unknown geometry type: ${geometry.type}`);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
buildPoint(geom) {
|
|
386
|
+
return `POINT${zSuffix(geom.coordinates)} (${positionToWkt(geom.coordinates)})`;
|
|
387
|
+
}
|
|
388
|
+
buildLineString(geom) {
|
|
389
|
+
if (geom.coordinates.length === 0)
|
|
390
|
+
return 'LINESTRING EMPTY';
|
|
391
|
+
return `LINESTRING${zSuffix(geom.coordinates)} (${coordsToWkt(geom.coordinates)})`;
|
|
392
|
+
}
|
|
393
|
+
buildPolygon(geom) {
|
|
394
|
+
if (geom.coordinates.length === 0)
|
|
395
|
+
return 'POLYGON EMPTY';
|
|
396
|
+
const ringStr = geom.coordinates.map(ring => `(${coordsToWkt(ring)})`).join(', ');
|
|
397
|
+
return `POLYGON${zSuffix(geom.coordinates)} (${ringStr})`;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* 按 OGC/ISO WKT 标准,MULTIPOINT 每个点用括号包裹:
|
|
401
|
+
* MULTIPOINT ((0 0), (1 1), (2 2))
|
|
402
|
+
*/
|
|
403
|
+
buildMultiPoint(geom) {
|
|
404
|
+
if (geom.coordinates.length === 0)
|
|
405
|
+
return 'MULTIPOINT EMPTY';
|
|
406
|
+
const pts = geom.coordinates.map(p => `(${positionToWkt(p)})`).join(', ');
|
|
407
|
+
return `MULTIPOINT${zSuffix(geom.coordinates)} (${pts})`;
|
|
408
|
+
}
|
|
409
|
+
buildMultiLineString(geom) {
|
|
410
|
+
if (geom.coordinates.length === 0)
|
|
411
|
+
return 'MULTILINESTRING EMPTY';
|
|
412
|
+
const lines = geom.coordinates.map(line => `(${coordsToWkt(line)})`).join(', ');
|
|
413
|
+
return `MULTILINESTRING${zSuffix(geom.coordinates)} (${lines})`;
|
|
414
|
+
}
|
|
415
|
+
buildMultiPolygon(geom) {
|
|
416
|
+
if (geom.coordinates.length === 0)
|
|
417
|
+
return 'MULTIPOLYGON EMPTY';
|
|
418
|
+
const polys = geom.coordinates.map(poly => {
|
|
419
|
+
const rings = poly.map(ring => `(${coordsToWkt(ring)})`).join(', ');
|
|
420
|
+
return `(${rings})`;
|
|
421
|
+
}).join(', ');
|
|
422
|
+
return `MULTIPOLYGON${zSuffix(geom.coordinates)} (${polys})`;
|
|
423
|
+
}
|
|
424
|
+
buildGeometryCollection(geom) {
|
|
425
|
+
if (geom.geometries.length === 0)
|
|
426
|
+
return 'GEOMETRYCOLLECTION EMPTY';
|
|
427
|
+
const geoms = geom.geometries.map(g => this.build(g)).join(', ');
|
|
428
|
+
return `GEOMETRYCOLLECTION (${geoms})`;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/** 将 GeoJSON Geometry 对象转换为 WKT 字符串 */
|
|
432
|
+
function build(geometry) {
|
|
433
|
+
return WKT_BUILDER.build(geometry);
|
|
434
|
+
}
|
|
435
|
+
// 单例实例,避免重复创建
|
|
436
|
+
const WKT_BUILDER = new WKTBuilder();
|
|
437
|
+
|
|
438
|
+
// ─── 内部工具:判断是否为 Position([number, number] 或 [number, number, number])
|
|
439
|
+
function isPosition(v) {
|
|
440
|
+
return (Array.isArray(v) &&
|
|
441
|
+
(v.length === 2 || v.length === 3) &&
|
|
442
|
+
v.every((n) => typeof n === 'number'));
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* GeoJSON 几何对象构建器(类形式,方便组合使用)
|
|
446
|
+
*/
|
|
447
|
+
class GeoJSONBuilder {
|
|
448
|
+
createPoint(x, y, z) {
|
|
449
|
+
return z !== undefined
|
|
450
|
+
? { type: 'Point', coordinates: [x, y, z] }
|
|
451
|
+
: { type: 'Point', coordinates: [x, y] };
|
|
452
|
+
}
|
|
453
|
+
createLineString(coordinates) {
|
|
454
|
+
return { type: 'LineString', coordinates };
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* 创建 Polygon。
|
|
458
|
+
* - 传入 `Position[]`:视为单个外环,自动包装为 `[ring]`
|
|
459
|
+
* - 传入 `Position[][]`:视为完整的环列表(外环 + 内环/空洞)
|
|
460
|
+
*/
|
|
461
|
+
createPolygon(coordinates) {
|
|
462
|
+
const rings = isPosition(coordinates[0])
|
|
463
|
+
? [coordinates] // 单环:Position[] → Position[][]
|
|
464
|
+
: coordinates; // 多环:已是 Position[][]
|
|
465
|
+
return { type: 'Polygon', coordinates: rings };
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* 创建 MultiPoint。
|
|
469
|
+
* - 传入 `Position`:视为单个点,自动包装为 `[point]`
|
|
470
|
+
* - 传入 `Position[]`:视为多个点
|
|
471
|
+
*/
|
|
472
|
+
createMultiPoint(coordinates) {
|
|
473
|
+
const pts = isPosition(coordinates)
|
|
474
|
+
? [coordinates] // 单点:Position → Position[]
|
|
475
|
+
: coordinates; // 多点:已是 Position[]
|
|
476
|
+
return { type: 'MultiPoint', coordinates: pts };
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* 创建 MultiLineString。
|
|
480
|
+
* - 传入 `Position[]`:视为单条线,自动包装为 `[line]`
|
|
481
|
+
* - 传入 `Position[][]`:视为多条线
|
|
482
|
+
*/
|
|
483
|
+
createMultiLineString(coordinates) {
|
|
484
|
+
const lines = isPosition(coordinates[0])
|
|
485
|
+
? [coordinates] // 单线:Position[] → Position[][]
|
|
486
|
+
: coordinates; // 多线:已是 Position[][]
|
|
487
|
+
return { type: 'MultiLineString', coordinates: lines };
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* 创建 MultiPolygon。
|
|
491
|
+
* - 传入 `Position[][]`:视为单个多边形(环列表),自动包装为 `[polygon]`
|
|
492
|
+
* - 传入 `Position[][][]`:视为多个多边形
|
|
493
|
+
*/
|
|
494
|
+
createMultiPolygon(coordinates) {
|
|
495
|
+
// 判断:若第一个元素是 Position[](环),则整体是单个 polygon
|
|
496
|
+
const firstElem = coordinates[0];
|
|
497
|
+
const isSinglePolygon = Array.isArray(firstElem) && Array.isArray(firstElem[0]) && isPosition(firstElem[0]);
|
|
498
|
+
const polys = isSinglePolygon
|
|
499
|
+
? [coordinates] // 单多边形:Position[][] → Position[][][]
|
|
500
|
+
: coordinates; // 多多边形:已是 Position[][][]
|
|
501
|
+
return { type: 'MultiPolygon', coordinates: polys };
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* 创建 GeometryCollection。
|
|
505
|
+
* - 传入单个 `Geometry`:自动包装为 `[geometry]`
|
|
506
|
+
* - 传入 `Geometry[]`:直接使用
|
|
507
|
+
*/
|
|
508
|
+
createGeometryCollection(geometries) {
|
|
509
|
+
const geoms = Array.isArray(geometries) ? geometries : [geometries];
|
|
510
|
+
return { type: 'GeometryCollection', geometries: geoms };
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// 单例,避免重复实例化
|
|
514
|
+
const _builder = new GeoJSONBuilder();
|
|
515
|
+
/** 创建 Point */
|
|
516
|
+
function createPoint(x, y, z) {
|
|
517
|
+
return _builder.createPoint(x, y, z);
|
|
518
|
+
}
|
|
519
|
+
/** 创建 LineString */
|
|
520
|
+
function createLineString(coordinates) {
|
|
521
|
+
return _builder.createLineString(coordinates);
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* 创建 Polygon。
|
|
525
|
+
* - 传入 `Position[]`:单个外环,自动包装
|
|
526
|
+
* - 传入 `Position[][]`:外环 + 内环(空洞)
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* createPolygon([[0,0],[1,0],[1,1],[0,1],[0,0]])
|
|
530
|
+
* createPolygon([[[0,0],[10,0],[10,10],[0,10],[0,0]], [[2,2],[4,2],[4,4],[2,4],[2,2]]])
|
|
531
|
+
*/
|
|
532
|
+
function createPolygon(coordinates) {
|
|
533
|
+
return _builder.createPolygon(coordinates);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* 创建 MultiPoint。
|
|
537
|
+
* - 传入 `Position`:单个点
|
|
538
|
+
* - 传入 `Position[]`:多个点
|
|
539
|
+
*
|
|
540
|
+
* @example
|
|
541
|
+
* createMultiPoint([0, 0])
|
|
542
|
+
* createMultiPoint([[0,0],[1,1],[2,2]])
|
|
543
|
+
*/
|
|
544
|
+
function createMultiPoint(coordinates) {
|
|
545
|
+
return _builder.createMultiPoint(coordinates);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* 创建 MultiLineString。
|
|
549
|
+
* - 传入 `Position[]`:单条线
|
|
550
|
+
* - 传入 `Position[][]`:多条线
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* createMultiLineString([[0,0],[1,1]])
|
|
554
|
+
* createMultiLineString([[[0,0],[1,1]], [[2,2],[3,3]]])
|
|
555
|
+
*/
|
|
556
|
+
function createMultiLineString(coordinates) {
|
|
557
|
+
return _builder.createMultiLineString(coordinates);
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* 创建 MultiPolygon。
|
|
561
|
+
* - 传入 `Position[][]`:单个多边形(环列表)
|
|
562
|
+
* - 传入 `Position[][][]`:多个多边形
|
|
563
|
+
*
|
|
564
|
+
* @example
|
|
565
|
+
* createMultiPolygon([[[0,0],[1,0],[1,1],[0,1],[0,0]]])
|
|
566
|
+
* createMultiPolygon([[[[0,0],[1,0],[1,1],[0,1],[0,0]]], [[[2,2],[3,2],[3,3],[2,3],[2,2]]]])
|
|
567
|
+
*/
|
|
568
|
+
function createMultiPolygon(coordinates) {
|
|
569
|
+
return _builder.createMultiPolygon(coordinates);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* 创建 GeometryCollection。
|
|
573
|
+
* - 传入单个 `Geometry`:自动包装
|
|
574
|
+
* - 传入 `Geometry[]`:直接使用
|
|
575
|
+
*
|
|
576
|
+
* @example
|
|
577
|
+
* createGeometryCollection(createPoint(0, 0))
|
|
578
|
+
* createGeometryCollection([createPoint(0,0), createLineString([[0,0],[1,1]])])
|
|
579
|
+
*/
|
|
580
|
+
function createGeometryCollection(geometries) {
|
|
581
|
+
return _builder.createGeometryCollection(geometries);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* 将 WKT 字符串转换为 GeoJSON Geometry 对象。
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* wktToGeoJSON('POINT (30.5 40.5)')
|
|
589
|
+
* // → { type: 'Point', coordinates: [30.5, 40.5] }
|
|
590
|
+
*
|
|
591
|
+
* wktToGeoJSON('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))')
|
|
592
|
+
* // → { type: 'Polygon', coordinates: [[[0,0],[1,0],[1,1],[0,1],[0,0]]] }
|
|
593
|
+
*/
|
|
594
|
+
function wktToGeoJSON(wkt) {
|
|
595
|
+
return parse(wkt);
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* 将 WKT 字符串转换为 GeoJSON Feature 对象。
|
|
599
|
+
*
|
|
600
|
+
* @param wkt WKT 字符串
|
|
601
|
+
* @param properties 可选的 Feature 属性对象
|
|
602
|
+
* @param id 可选的 Feature ID
|
|
603
|
+
*
|
|
604
|
+
* @example
|
|
605
|
+
* wktToFeature('POINT (30.5 40.5)', { name: '北京' })
|
|
606
|
+
* // → { type: 'Feature', geometry: { type: 'Point', ... }, properties: { name: '北京' } }
|
|
607
|
+
*/
|
|
608
|
+
function wktToFeature(wkt, properties = null, id) {
|
|
609
|
+
const geometry = parse(wkt);
|
|
610
|
+
const feature = {
|
|
611
|
+
type: 'Feature',
|
|
612
|
+
geometry,
|
|
613
|
+
properties,
|
|
614
|
+
};
|
|
615
|
+
if (id !== undefined) {
|
|
616
|
+
feature.id = id;
|
|
617
|
+
}
|
|
618
|
+
return feature;
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* 将多个 WKT 字符串批量转换为 GeoJSON FeatureCollection。
|
|
622
|
+
*
|
|
623
|
+
* @param wkts WKT 字符串数组
|
|
624
|
+
* @param properties 可选,每个 Feature 的属性数组(长度应与 wkts 一致)
|
|
625
|
+
*
|
|
626
|
+
* @example
|
|
627
|
+
* wktToFeatureCollection(['POINT (0 0)', 'POINT (1 1)'])
|
|
628
|
+
* // → { type: 'FeatureCollection', features: [...] }
|
|
629
|
+
*/
|
|
630
|
+
function wktToFeatureCollection(wkts, properties) {
|
|
631
|
+
const features = wkts.map((wkt, i) => wktToFeature(wkt, properties ? (properties[i] ?? null) : null));
|
|
632
|
+
return { type: 'FeatureCollection', features };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* 将 GeoJSON Geometry 对象转换为 WKT 字符串。
|
|
637
|
+
*
|
|
638
|
+
* @example
|
|
639
|
+
* geojsonToWkt({ type: 'Point', coordinates: [30.5, 40.5] })
|
|
640
|
+
* // → 'POINT (30.5 40.5)'
|
|
641
|
+
*
|
|
642
|
+
* geojsonToWkt({ type: 'Polygon', coordinates: [[[0,0],[1,0],[1,1],[0,1],[0,0]]] })
|
|
643
|
+
* // → 'POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))'
|
|
644
|
+
*/
|
|
645
|
+
function geojsonToWkt(geojson) {
|
|
646
|
+
return build(geojson);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* 将 GeoJSON Feature 对象转换为 WKT 字符串(取 geometry 部分)。
|
|
650
|
+
*
|
|
651
|
+
* @throws 若 Feature.geometry 为 null,则抛出错误
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* featureToWkt({ type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] }, properties: null })
|
|
655
|
+
* // → 'POINT (0 0)'
|
|
656
|
+
*/
|
|
657
|
+
function featureToWkt(feature) {
|
|
658
|
+
if (!feature.geometry) {
|
|
659
|
+
throw new Error('Feature.geometry is null, cannot convert to WKT');
|
|
660
|
+
}
|
|
661
|
+
return build(feature.geometry);
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* 将 GeoJSON FeatureCollection 中所有 Feature 转换为 WKT 字符串数组。
|
|
665
|
+
*
|
|
666
|
+
* geometry 为 null 的 Feature 会被跳过(返回数组中对应位置为 null)。
|
|
667
|
+
*
|
|
668
|
+
* @example
|
|
669
|
+
* featureCollectionToWkt({ type: 'FeatureCollection', features: [...] })
|
|
670
|
+
* // → ['POINT (0 0)', 'LINESTRING (0 0, 1 1)', ...]
|
|
671
|
+
*/
|
|
672
|
+
function featureCollectionToWkt(fc) {
|
|
673
|
+
return fc.features.map((f) => {
|
|
674
|
+
if (!f.geometry)
|
|
675
|
+
return null;
|
|
676
|
+
return build(f.geometry);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// 预定义常量,避免重复创建
|
|
681
|
+
const VALID_GEOMETRY_TYPES = [
|
|
682
|
+
'Point', 'LineString', 'Polygon',
|
|
683
|
+
'MultiPoint', 'MultiLineString', 'MultiPolygon',
|
|
684
|
+
'GeometryCollection'
|
|
685
|
+
];
|
|
686
|
+
// 预计算错误消息字符串
|
|
687
|
+
const VALID_TYPES_MESSAGE = `Invalid geometry type. Must be one of: ${VALID_GEOMETRY_TYPES.join(', ')}`;
|
|
688
|
+
/**
|
|
689
|
+
* 校验 WKT 字符串格式是否合法
|
|
690
|
+
*/
|
|
691
|
+
function validateWKT(wkt) {
|
|
692
|
+
if (!wkt || typeof wkt !== 'string') {
|
|
693
|
+
return { valid: false, error: 'WKT must be a non-empty string' };
|
|
694
|
+
}
|
|
695
|
+
const trimmed = wkt.trim();
|
|
696
|
+
if (trimmed.length === 0) {
|
|
697
|
+
return { valid: false, error: 'WKT cannot be empty' };
|
|
698
|
+
}
|
|
699
|
+
try {
|
|
700
|
+
parse(wkt);
|
|
701
|
+
return { valid: true };
|
|
702
|
+
}
|
|
703
|
+
catch (e) {
|
|
704
|
+
return { valid: false, error: e.message };
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* 校验 GeoJSON Geometry 对象是否合法
|
|
709
|
+
*/
|
|
710
|
+
function validateGeoJSON(geojson) {
|
|
711
|
+
if (!geojson || typeof geojson !== 'object') {
|
|
712
|
+
return { valid: false, error: 'GeoJSON must be an object' };
|
|
713
|
+
}
|
|
714
|
+
const obj = geojson;
|
|
715
|
+
// 检查 type 字段
|
|
716
|
+
if (!obj.type || typeof obj.type !== 'string') {
|
|
717
|
+
return { valid: false, error: 'GeoJSON must have a "type" property' };
|
|
718
|
+
}
|
|
719
|
+
const type = obj.type;
|
|
720
|
+
if (!VALID_GEOMETRY_TYPES.includes(type)) {
|
|
721
|
+
return { valid: false, error: VALID_TYPES_MESSAGE };
|
|
722
|
+
}
|
|
723
|
+
// GeometryCollection 特殊处理
|
|
724
|
+
if (type === 'GeometryCollection') {
|
|
725
|
+
if (!obj.geometries || !Array.isArray(obj.geometries)) {
|
|
726
|
+
return { valid: false, error: 'GeometryCollection must have a "geometries" array' };
|
|
727
|
+
}
|
|
728
|
+
for (let i = 0; i < obj.geometries.length; i++) {
|
|
729
|
+
const result = validateGeoJSON(obj.geometries[i]);
|
|
730
|
+
if (!result.valid) {
|
|
731
|
+
return { valid: false, error: `GeometryCollection[${i}]: ${result.error}` };
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
return { valid: true };
|
|
735
|
+
}
|
|
736
|
+
// 其他几何类型必须要有 coordinates
|
|
737
|
+
if (obj.coordinates === undefined) {
|
|
738
|
+
return { valid: false, error: `${type} must have "coordinates"` };
|
|
739
|
+
}
|
|
740
|
+
// 校验坐标格式
|
|
741
|
+
return validateCoordinates(type, obj.coordinates);
|
|
742
|
+
}
|
|
743
|
+
function validateCoordinates(type, coords) {
|
|
744
|
+
switch (type) {
|
|
745
|
+
case 'Point':
|
|
746
|
+
return validatePosition(coords);
|
|
747
|
+
case 'LineString':
|
|
748
|
+
case 'MultiPoint':
|
|
749
|
+
if (!Array.isArray(coords)) {
|
|
750
|
+
return { valid: false, error: `${type} coordinates must be an array` };
|
|
751
|
+
}
|
|
752
|
+
for (let i = 0; i < coords.length; i++) {
|
|
753
|
+
const result = validatePosition(coords[i]);
|
|
754
|
+
if (!result.valid) {
|
|
755
|
+
return { valid: false, error: `${type}[${i}]: ${result.error}` };
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return { valid: true };
|
|
759
|
+
case 'Polygon':
|
|
760
|
+
case 'MultiLineString':
|
|
761
|
+
if (!Array.isArray(coords)) {
|
|
762
|
+
return { valid: false, error: `${type} coordinates must be a nested array` };
|
|
763
|
+
}
|
|
764
|
+
for (let i = 0; i < coords.length; i++) {
|
|
765
|
+
if (!Array.isArray(coords[i])) {
|
|
766
|
+
return { valid: false, error: `${type}[${i}] must be an array of positions` };
|
|
767
|
+
}
|
|
768
|
+
for (let j = 0; j < coords[i].length; j++) {
|
|
769
|
+
const result = validatePosition(coords[i][j]);
|
|
770
|
+
if (!result.valid) {
|
|
771
|
+
return { valid: false, error: `${type}[${i}][${j}]: ${result.error}` };
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return { valid: true };
|
|
776
|
+
case 'MultiPolygon':
|
|
777
|
+
if (!Array.isArray(coords)) {
|
|
778
|
+
return { valid: false, error: `${type} coordinates must be a deeply nested array` };
|
|
779
|
+
}
|
|
780
|
+
for (let i = 0; i < coords.length; i++) {
|
|
781
|
+
if (!Array.isArray(coords[i])) {
|
|
782
|
+
return { valid: false, error: `${type}[${i}] must be an array of rings` };
|
|
783
|
+
}
|
|
784
|
+
for (let j = 0; j < coords[i].length; j++) {
|
|
785
|
+
if (!Array.isArray(coords[i][j])) {
|
|
786
|
+
return { valid: false, error: `${type}[${i}][${j}] must be an array of positions` };
|
|
787
|
+
}
|
|
788
|
+
for (let k = 0; k < coords[i][j].length; k++) {
|
|
789
|
+
const result = validatePosition(coords[i][j][k]);
|
|
790
|
+
if (!result.valid) {
|
|
791
|
+
return { valid: false, error: `${type}[${i}][${j}][${k}]: ${result.error}` };
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
return { valid: true };
|
|
797
|
+
default:
|
|
798
|
+
return { valid: true };
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
function validatePosition(pos) {
|
|
802
|
+
if (!Array.isArray(pos)) {
|
|
803
|
+
return { valid: false, error: 'Position must be an array of numbers' };
|
|
804
|
+
}
|
|
805
|
+
if (pos.length < 2 || pos.length > 3) {
|
|
806
|
+
return { valid: false, error: `Position must have 2 or 3 coordinates, got ${pos.length}` };
|
|
807
|
+
}
|
|
808
|
+
for (let i = 0; i < pos.length; i++) {
|
|
809
|
+
if (typeof pos[i] !== 'number' || !Number.isFinite(pos[i])) {
|
|
810
|
+
return { valid: false, error: `Position[${i}] must be a valid number` };
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return { valid: true };
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* 尝试从可能不规范的 WKT 中恢复出有效结果
|
|
817
|
+
* 主要处理尾部多余字符的情况
|
|
818
|
+
*/
|
|
819
|
+
function tryFixWKT(wkt) {
|
|
820
|
+
const trimmed = wkt.trim();
|
|
821
|
+
if (!trimmed) {
|
|
822
|
+
return { fixed: wkt, changed: false };
|
|
823
|
+
}
|
|
824
|
+
// 先尝试直接解析,如果成功则不需要修复
|
|
825
|
+
try {
|
|
826
|
+
parse(trimmed);
|
|
827
|
+
return { fixed: trimmed, changed: false };
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
// 解析失败,尝试修复
|
|
831
|
+
}
|
|
832
|
+
// 尝试找到最后一个有效的 geometry 结束位置
|
|
833
|
+
const patterns = [
|
|
834
|
+
/\)\s*[A-Z]/i, // 括号后跟字母 (如 POLYGON ((...)) POINT )
|
|
835
|
+
/EMPTY\s+[A-Z]/i, // EMPTY 后跟字母
|
|
836
|
+
/\)\s*$/, // 括号结尾后有多余内容
|
|
837
|
+
];
|
|
838
|
+
for (const pattern of patterns) {
|
|
839
|
+
const match = trimmed.match(pattern);
|
|
840
|
+
if (match) {
|
|
841
|
+
const fixed = trimmed.slice(0, match.index + (match[0].match(/\)/)?.[0].length || 0));
|
|
842
|
+
try {
|
|
843
|
+
parse(fixed);
|
|
844
|
+
return { fixed, changed: true };
|
|
845
|
+
}
|
|
846
|
+
catch {
|
|
847
|
+
// 这个修复方案不行,尝试下一个
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
// 尝试去除尾部垃圾字符
|
|
852
|
+
const lastValidIndex = findLastValidPosition(trimmed);
|
|
853
|
+
if (lastValidIndex > 0) {
|
|
854
|
+
const fixed = trimmed.slice(0, lastValidIndex + 1);
|
|
855
|
+
try {
|
|
856
|
+
parse(fixed);
|
|
857
|
+
return { fixed, changed: true };
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
// 修复失败
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return { fixed: wkt, changed: false };
|
|
864
|
+
}
|
|
865
|
+
function findLastValidPosition(wkt) {
|
|
866
|
+
let depth = 0;
|
|
867
|
+
for (let i = wkt.length - 1; i >= 0; i--) {
|
|
868
|
+
const c = wkt[i];
|
|
869
|
+
if (c === ')')
|
|
870
|
+
depth++;
|
|
871
|
+
else if (c === '(')
|
|
872
|
+
depth--;
|
|
873
|
+
else if (c === ' ' && depth === 0 && /[A-Z]/.test(wkt.slice(i + 1))) {
|
|
874
|
+
// 如果空格后面是字母开头,可能是垃圾字符的起点
|
|
875
|
+
if (wkt.slice(0, i).trimEnd().match(/[A-Z]\s*$/)) {
|
|
876
|
+
return i - 1;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return wkt.length - 1;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* 深度克隆 GeoJSON 对象(用于避免意外修改原对象)
|
|
884
|
+
*/
|
|
885
|
+
function cloneGeometry(geometry) {
|
|
886
|
+
return JSON.parse(JSON.stringify(geometry));
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* 判断两个几何对象是否相等(坐标对比)
|
|
890
|
+
*/
|
|
891
|
+
function geometryEquals(a, b) {
|
|
892
|
+
if (a.type !== b.type)
|
|
893
|
+
return false;
|
|
894
|
+
// Point 比较最常见,单独优化
|
|
895
|
+
if (a.type === 'Point') {
|
|
896
|
+
const aCoords = a.coordinates;
|
|
897
|
+
const bCoords = b.coordinates;
|
|
898
|
+
return aCoords.length === bCoords.length &&
|
|
899
|
+
aCoords[0] === bCoords[0] &&
|
|
900
|
+
aCoords[1] === bCoords[1] &&
|
|
901
|
+
(aCoords.length === 2 || aCoords[2] === bCoords[2]);
|
|
902
|
+
}
|
|
903
|
+
// 其他类型使用 JSON.stringify 比较
|
|
904
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// WKT 命名空间 - 用于默认导出
|
|
908
|
+
const WKT = {
|
|
909
|
+
...types,
|
|
910
|
+
parse,
|
|
911
|
+
build,
|
|
912
|
+
createPoint,
|
|
913
|
+
createLineString,
|
|
914
|
+
createPolygon,
|
|
915
|
+
createMultiPoint,
|
|
916
|
+
createMultiLineString,
|
|
917
|
+
createMultiPolygon,
|
|
918
|
+
createGeometryCollection,
|
|
919
|
+
wktToGeoJSON,
|
|
920
|
+
wktToFeature,
|
|
921
|
+
wktToFeatureCollection,
|
|
922
|
+
geojsonToWkt,
|
|
923
|
+
featureToWkt,
|
|
924
|
+
featureCollectionToWkt,
|
|
925
|
+
validateWKT,
|
|
926
|
+
validateGeoJSON,
|
|
927
|
+
tryFixWKT,
|
|
928
|
+
cloneGeometry,
|
|
929
|
+
geometryEquals,
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
export { GeoJSONBuilder, WKTBuilder, WKTParser, build, cloneGeometry, createGeometryCollection, createLineString, createMultiLineString, createMultiPoint, createMultiPolygon, createPoint, createPolygon, WKT as default, featureCollectionToWkt, featureToWkt, geojsonToWkt, geometryEquals, parse, tryFixWKT, validateGeoJSON, validateWKT, wktToFeature, wktToFeatureCollection, wktToGeoJSON };
|