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/README.md ADDED
@@ -0,0 +1,752 @@
1
+ # giser-geometry-parse
2
+
3
+ 一个零依赖、轻量级的 TypeScript/JavaScript 库,用于:
4
+ - **WKT 解析**:将 WKT 字符串解析为 GeoJSON Geometry 对象
5
+ - **WKT 构建**:将 GeoJSON Geometry 对象序列化为 WKT 字符串
6
+ - **WKT ↔ GeoJSON 互转**:在 WKT 与 GeoJSON Geometry / Feature / FeatureCollection 之间自由转换
7
+ - **GeoJSON 工厂方法**:快速创建各种 GeoJSON 几何对象
8
+
9
+ ## 目录
10
+
11
+ - [支持的几何类型](#支持的几何类型)
12
+ - [安装与构建](#安装与构建)
13
+ - [快速上手](#快速上手)
14
+ - [API 文档](#api-文档)
15
+ - [parse(wkt)](#1-parsewkt)
16
+ - [build(geometry)](#2-buildgeometry)
17
+ - [wktToGeoJSON(wkt)](#3-wkttogeojsonwkt)
18
+ - [wktToFeature(wkt, properties?, id?)](#4-wkttofeaturewkt-properties-id)
19
+ - [wktToFeatureCollection(wkts, properties?)](#5-wkttofeaturecollectionwkts-properties)
20
+ - [geojsonToWkt(geometry)](#6-geojsontowktgeometry)
21
+ - [featureToWkt(feature)](#7-featuretowktfeature)
22
+ - [featureCollectionToWkt(fc)](#8-featurecollectiontowktfc)
23
+ - [GeoJSON 工厂方法](#9-geojson-工厂方法)
24
+ - [校验工具](#10-校验工具)
25
+ - [类型定义](#类型定义)
26
+ - [注意事项与边界行为](#注意事项与边界行为)
27
+ - [本地调试](#本地调试)
28
+
29
+ ---
30
+
31
+ ## 支持的几何类型
32
+
33
+ | WKT 类型 | GeoJSON 类型 | 说明 |
34
+ |---------|-------------|------|
35
+ | `POINT` | `Point` | 点 |
36
+ | `LINESTRING` | `LineString` | 线 |
37
+ | `POLYGON` | `Polygon` | 多边形(支持空洞) |
38
+ | `MULTIPOINT` | `MultiPoint` | 点集合 |
39
+ | `MULTILINESTRING` | `MultiLineString` | 线集合 |
40
+ | `MULTIPOLYGON` | `MultiPolygon` | 多边形集合 |
41
+ | `GEOMETRYCOLLECTION` | `GeometryCollection` | 几何集合 |
42
+
43
+ 支持以下 WKT 扩展语法:
44
+ - **Z 坐标**:`POINT (x y z)` / `POINT Z (x y z)`
45
+ - **维度修饰符**:`Z`、`M`、`ZM`(`M` 值会被忽略,仅保留 XYZ)
46
+ - **EMPTY**:`LINESTRING EMPTY`、`POLYGON EMPTY` 等(`POINT EMPTY` 会抛出错误,见[注意事项](#注意事项与边界行为))
47
+
48
+ ---
49
+
50
+ ## 安装与构建
51
+
52
+ ```bash
53
+ # 安装依赖
54
+ npm install
55
+
56
+ # 构建(生成 dist/ 目录)
57
+ npm run build
58
+
59
+ # 仅做 TypeScript 类型检查(不生成文件)
60
+ npm run typecheck
61
+ ```
62
+
63
+ 构建产物:
64
+
65
+ | 文件 | 格式 | 用途 |
66
+ |------|------|------|
67
+ | `dist/index.esm.js` | ES Module | 浏览器 / 现代打包工具 |
68
+ | `dist/index.cjs.js` | CommonJS | Node.js |
69
+ | `dist/index.umd.js` | UMD | `<script>` 标签直接引入 |
70
+
71
+ ---
72
+
73
+ ## 快速上手
74
+
75
+ ### Node.js
76
+
77
+ ```bash
78
+ npm install giser-geometry-parse
79
+ ```
80
+
81
+ ```javascript
82
+ // CommonJS
83
+ const { parse, build, wktToFeature } = require('giser-geometry-parse');
84
+
85
+ // ES Module - 按需导入
86
+ import { parse, build, wktToFeature } from 'giser-geometry-parse';
87
+
88
+ // ES Module - 命名空间导入(避免命名冲突)
89
+ import WKT from 'giser-geometry-parse';
90
+ const geom = WKT.parse('POINT (116.39 39.91)');
91
+ ```
92
+
93
+ ### 浏览器 (script 标签)
94
+
95
+ ```html
96
+ <!-- UMD 方式:通过 script 标签直接引入,全局变量 WKTGeoJSON -->
97
+ <script src="https://unpkg.com/giser-geometry-parse/dist/index.umd.js"></script>
98
+ <script>
99
+ // 方式一:使用命名空间(推荐,避免命名冲突)
100
+ const geom = WKTGeoJSON.parse('POINT (116.39 39.91)');
101
+ console.log(geom);
102
+
103
+ // 方式二:解构赋值(需注意命名冲突)
104
+ const { parse, build } = WKTGeoJSON;
105
+ const wkt = build(geom);
106
+ </script>
107
+ ```
108
+
109
+ ### 浏览器 (ES Module)
110
+
111
+ ```html
112
+ <script type="module">
113
+ import { parse, build } from 'giser-geometry-parse';
114
+
115
+ const geom = parse('POINT (116.39 39.91)');
116
+ console.log(geom);
117
+ // → { type: 'Point', coordinates: [116.39, 39.91] }
118
+
119
+ const wkt = build(geom);
120
+ console.log(wkt);
121
+ // → "POINT (116.39 39.91)"
122
+ </script>
123
+ ```
124
+
125
+ ---
126
+
127
+ ## API 文档
128
+
129
+ ### 1. `parse(wkt)`
130
+
131
+ 将 WKT 字符串解析为 GeoJSON Geometry 对象。
132
+
133
+ **参数:**
134
+ - `wkt` `string` — WKT 格式的字符串
135
+
136
+ **返回:** `Geometry`
137
+
138
+ **抛出:** 输入格式错误、未知几何类型、坐标值无效时抛出 `Error`
139
+
140
+ ```javascript
141
+ // ── POINT ─────────────────────────────────────────────────────────────
142
+ parse('POINT (116.39 39.91)')
143
+ // → { type: 'Point', coordinates: [116.39, 39.91] }
144
+
145
+ // 带 Z 坐标
146
+ parse('POINT (116.39 39.91 50)')
147
+ // → { type: 'Point', coordinates: [116.39, 39.91, 50] }
148
+
149
+ // 带维度修饰符(Z 关键字)
150
+ parse('POINT Z (116.39 39.91 50)')
151
+ // → { type: 'Point', coordinates: [116.39, 39.91, 50] }
152
+
153
+ // ── LINESTRING ────────────────────────────────────────────────────────
154
+ parse('LINESTRING (0 0, 1 1, 2 0)')
155
+ // → { type: 'LineString', coordinates: [[0,0],[1,1],[2,0]] }
156
+
157
+ // ── POLYGON ───────────────────────────────────────────────────────────
158
+ // 无空洞
159
+ parse('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))')
160
+ // → { type: 'Polygon', coordinates: [[[0,0],[10,0],[10,10],[0,10],[0,0]]] }
161
+
162
+ // 带空洞(外环 + 内环)
163
+ parse('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 4 2, 4 4, 2 4, 2 2))')
164
+ // → { type: 'Polygon', coordinates: [
165
+ // [[0,0],[10,0],[10,10],[0,10],[0,0]], ← 外环
166
+ // [[2,2],[4,2],[4,4],[2,4],[2,2]] ← 内环(空洞)
167
+ // ]}
168
+
169
+ // ── MULTIPOINT ────────────────────────────────────────────────────────
170
+ // 标准写法(每个点用括号包裹)
171
+ parse('MULTIPOINT ((0 0), (1 1), (2 2))')
172
+ // → { type: 'MultiPoint', coordinates: [[0,0],[1,1],[2,2]] }
173
+
174
+ // 非标准写法(兼容)
175
+ parse('MULTIPOINT (0 0, 1 1, 2 2)')
176
+ // → { type: 'MultiPoint', coordinates: [[0,0],[1,1],[2,2]] }
177
+
178
+ // ── MULTILINESTRING ───────────────────────────────────────────────────
179
+ parse('MULTILINESTRING ((0 0, 1 1), (2 2, 3 3))')
180
+ // → { type: 'MultiLineString', coordinates: [[[0,0],[1,1]], [[2,2],[3,3]]] }
181
+
182
+ // ── MULTIPOLYGON ──────────────────────────────────────────────────────
183
+ parse('MULTIPOLYGON (((0 0, 1 0, 1 1, 0 1, 0 0)), ((2 2, 3 2, 3 3, 2 3, 2 2)))')
184
+ // → { type: 'MultiPolygon', coordinates: [
185
+ // [[[0,0],[1,0],[1,1],[0,1],[0,0]]],
186
+ // [[[2,2],[3,2],[3,3],[2,3],[2,2]]]
187
+ // ]}
188
+
189
+ // ── GEOMETRYCOLLECTION ────────────────────────────────────────────────
190
+ parse('GEOMETRYCOLLECTION (POINT (0 0), LINESTRING (0 0, 1 1))')
191
+ // → { type: 'GeometryCollection', geometries: [
192
+ // { type: 'Point', coordinates: [0,0] },
193
+ // { type: 'LineString', coordinates: [[0,0],[1,1]] }
194
+ // ]}
195
+
196
+ // ── EMPTY ─────────────────────────────────────────────────────────────
197
+ parse('LINESTRING EMPTY')
198
+ // → { type: 'LineString', coordinates: [] }
199
+
200
+ parse('POLYGON EMPTY')
201
+ // → { type: 'Polygon', coordinates: [] }
202
+ ```
203
+
204
+ ---
205
+
206
+ ### 2. `build(geometry)`
207
+
208
+ 将 GeoJSON Geometry 对象转换为 WKT 字符串。
209
+
210
+ **参数:**
211
+ - `geometry` `Geometry` — GeoJSON Geometry 对象
212
+
213
+ **返回:** `string`
214
+
215
+ ```javascript
216
+ build({ type: 'Point', coordinates: [116.39, 39.91] })
217
+ // → 'POINT (116.39 39.91)'
218
+
219
+ build({ type: 'Point', coordinates: [116.39, 39.91, 50] })
220
+ // → 'POINT Z (116.39 39.91 50)'
221
+
222
+ build({ type: 'LineString', coordinates: [[0,0],[1,1],[2,0]] })
223
+ // → 'LINESTRING (0 0, 1 1, 2 0)'
224
+
225
+ build({ type: 'Polygon', coordinates: [[[0,0],[10,0],[10,10],[0,10],[0,0]]] })
226
+ // → 'POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))'
227
+
228
+ // 带空洞的多边形
229
+ build({
230
+ type: 'Polygon',
231
+ coordinates: [
232
+ [[0,0],[10,0],[10,10],[0,10],[0,0]],
233
+ [[2,2],[4,2],[4,4],[2,4],[2,2]]
234
+ ]
235
+ })
236
+ // → 'POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0), (2 2, 4 2, 4 4, 2 4, 2 2))'
237
+
238
+ // MULTIPOINT 输出符合 OGC 标准(每个点带括号)
239
+ build({ type: 'MultiPoint', coordinates: [[0,0],[1,1],[2,2]] })
240
+ // → 'MULTIPOINT ((0 0), (1 1), (2 2))'
241
+
242
+ // 空几何
243
+ build({ type: 'LineString', coordinates: [] })
244
+ // → 'LINESTRING EMPTY'
245
+
246
+ build({ type: 'GeometryCollection', geometries: [] })
247
+ // → 'GEOMETRYCOLLECTION EMPTY'
248
+ ```
249
+
250
+ ---
251
+
252
+ ### 3. `wktToGeoJSON(wkt)`
253
+
254
+ 将 WKT 字符串转换为 GeoJSON Geometry 对象(`parse` 的语义化别名)。
255
+
256
+ ```javascript
257
+ import { wktToGeoJSON } from 'giser-geometry-parse';
258
+
259
+ const geom = wktToGeoJSON('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))');
260
+ // → { type: 'Polygon', coordinates: [[[0,0],[1,0],[1,1],[0,1],[0,0]]] }
261
+ ```
262
+
263
+ ---
264
+
265
+ ### 4. `wktToFeature(wkt, properties?, id?)`
266
+
267
+ 将 WKT 字符串转换为 GeoJSON **Feature** 对象。
268
+
269
+ **参数:**
270
+ - `wkt` `string` — WKT 字符串
271
+ - `properties` `Record<string, unknown> | null`(可选)— Feature 属性,默认 `null`
272
+ - `id` `string | number`(可选)— Feature ID
273
+
274
+ **返回:** `Feature`
275
+
276
+ ```javascript
277
+ import { wktToFeature } from 'giser-geometry-parse';
278
+
279
+ // 带属性
280
+ wktToFeature('POINT (116.39 39.91)', { name: '北京', pop: 21540000 })
281
+ // → {
282
+ // type: 'Feature',
283
+ // geometry: { type: 'Point', coordinates: [116.39, 39.91] },
284
+ // properties: { name: '北京', pop: 21540000 }
285
+ // }
286
+
287
+ // 带 ID
288
+ wktToFeature('LINESTRING (0 0, 1 1)', null, 42)
289
+ // → { type: 'Feature', geometry: {...}, properties: null, id: 42 }
290
+
291
+ // 无属性
292
+ wktToFeature('POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))')
293
+ // → { type: 'Feature', geometry: {...}, properties: null }
294
+ ```
295
+
296
+ ---
297
+
298
+ ### 5. `wktToFeatureCollection(wkts, properties?)`
299
+
300
+ 将多个 WKT 字符串批量转换为 GeoJSON **FeatureCollection**。
301
+
302
+ **参数:**
303
+ - `wkts` `string[]` — WKT 字符串数组
304
+ - `properties` `Array<Record<string, unknown> | null>`(可选)— 每个 Feature 的属性数组
305
+
306
+ **返回:** `FeatureCollection`
307
+
308
+ ```javascript
309
+ import { wktToFeatureCollection } from 'giser-geometry-parse';
310
+
311
+ const fc = wktToFeatureCollection(
312
+ ['POINT (116.39 39.91)', 'POINT (121.47 31.23)', 'POINT (113.26 23.13)'],
313
+ [{ name: '北京' }, { name: '上海' }, { name: '广州' }]
314
+ );
315
+ // → {
316
+ // type: 'FeatureCollection',
317
+ // features: [
318
+ // { type: 'Feature', geometry: { type: 'Point', ... }, properties: { name: '北京' } },
319
+ // { type: 'Feature', geometry: { type: 'Point', ... }, properties: { name: '上海' } },
320
+ // { type: 'Feature', geometry: { type: 'Point', ... }, properties: { name: '广州' } }
321
+ // ]
322
+ // }
323
+
324
+ // 不传属性
325
+ const fc2 = wktToFeatureCollection(['POINT (0 0)', 'LINESTRING (0 0, 1 1)']);
326
+ ```
327
+
328
+ ---
329
+
330
+ ### 6. `geojsonToWkt(geometry)`
331
+
332
+ 将 GeoJSON Geometry 对象转换为 WKT 字符串(`build` 的语义化别名)。
333
+
334
+ ```javascript
335
+ import { geojsonToWkt } from 'giser-geometry-parse';
336
+
337
+ geojsonToWkt({ type: 'Point', coordinates: [116.39, 39.91] })
338
+ // → 'POINT (116.39 39.91)'
339
+ ```
340
+
341
+ ---
342
+
343
+ ### 7. `featureToWkt(feature)`
344
+
345
+ 将 GeoJSON **Feature** 转换为 WKT 字符串(取 `geometry` 部分)。
346
+
347
+ **抛出:** 若 `Feature.geometry` 为 `null`,则抛出 `Error`
348
+
349
+ ```javascript
350
+ import { featureToWkt } from 'giser-geometry-parse';
351
+
352
+ featureToWkt({
353
+ type: 'Feature',
354
+ geometry: { type: 'Point', coordinates: [116.39, 39.91] },
355
+ properties: { name: '北京' }
356
+ })
357
+ // → 'POINT (116.39 39.91)'
358
+ ```
359
+
360
+ ---
361
+
362
+ ### 8. `featureCollectionToWkt(fc)`
363
+
364
+ 将 GeoJSON **FeatureCollection** 中所有 Feature 转换为 WKT 字符串数组。`geometry` 为 `null` 的 Feature 对应位置返回 `null`。
365
+
366
+ **返回:** `Array<string | null>`
367
+
368
+ ```javascript
369
+ import { featureCollectionToWkt } from 'giser-geometry-parse';
370
+
371
+ featureCollectionToWkt({
372
+ type: 'FeatureCollection',
373
+ features: [
374
+ { type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0] }, properties: null },
375
+ { type: 'Feature', geometry: { type: 'LineString', coordinates: [[0,0],[1,1]] }, properties: null },
376
+ { type: 'Feature', geometry: null, properties: null } // ← null geometry
377
+ ]
378
+ })
379
+ // → ['POINT (0 0)', 'LINESTRING (0 0, 1 1)', null]
380
+ ```
381
+
382
+ ---
383
+
384
+ ### 9. GeoJSON 工厂方法
385
+
386
+ 快速创建 GeoJSON Geometry 对象。所有工厂方法均支持**简化输入**(自动包装)和**完整输入**两种形式。
387
+
388
+ #### `createPoint(x, y, z?)`
389
+
390
+ ```javascript
391
+ createPoint(116.39, 39.91)
392
+ // → { type: 'Point', coordinates: [116.39, 39.91] }
393
+
394
+ createPoint(116.39, 39.91, 50)
395
+ // → { type: 'Point', coordinates: [116.39, 39.91, 50] }
396
+ ```
397
+
398
+ #### `createLineString(coordinates)`
399
+
400
+ ```javascript
401
+ createLineString([[0,0],[1,1],[2,0]])
402
+ // → { type: 'LineString', coordinates: [[0,0],[1,1],[2,0]] }
403
+ ```
404
+
405
+ #### `createPolygon(coordinates)`
406
+
407
+ 支持两种输入:
408
+
409
+ ```javascript
410
+ // ① 传入单个外环 Position[](自动包装)
411
+ createPolygon([[0,0],[1,0],[1,1],[0,1],[0,0]])
412
+ // → { type: 'Polygon', coordinates: [[[0,0],[1,0],[1,1],[0,1],[0,0]]] }
413
+
414
+ // ② 传入完整环列表 Position[][](外环 + 内环/空洞)
415
+ createPolygon([
416
+ [[0,0],[10,0],[10,10],[0,10],[0,0]], // 外环
417
+ [[2,2],[4,2],[4,4],[2,4],[2,2]] // 内环(空洞)
418
+ ])
419
+ // → { type: 'Polygon', coordinates: [[...外环...], [...内环...]] }
420
+ ```
421
+
422
+ #### `createMultiPoint(coordinates)`
423
+
424
+ 支持两种输入:
425
+
426
+ ```javascript
427
+ // ① 传入单个点 Position(自动包装)
428
+ createMultiPoint([0, 0])
429
+ // → { type: 'MultiPoint', coordinates: [[0,0]] }
430
+
431
+ // ② 传入多个点 Position[]
432
+ createMultiPoint([[0,0],[1,1],[2,2]])
433
+ // → { type: 'MultiPoint', coordinates: [[0,0],[1,1],[2,2]] }
434
+ ```
435
+
436
+ #### `createMultiLineString(coordinates)`
437
+
438
+ 支持两种输入:
439
+
440
+ ```javascript
441
+ // ① 传入单条线 Position[](自动包装)
442
+ createMultiLineString([[0,0],[1,1],[2,0]])
443
+ // → { type: 'MultiLineString', coordinates: [[[0,0],[1,1],[2,0]]] }
444
+
445
+ // ② 传入多条线 Position[][]
446
+ createMultiLineString([[[0,0],[1,1]], [[2,2],[3,3]]])
447
+ // → { type: 'MultiLineString', coordinates: [[[0,0],[1,1]], [[2,2],[3,3]]] }
448
+ ```
449
+
450
+ #### `createMultiPolygon(coordinates)`
451
+
452
+ 支持两种输入:
453
+
454
+ ```javascript
455
+ // ① 传入单个多边形的环列表 Position[][](自动包装)
456
+ createMultiPolygon([[[0,0],[1,0],[1,1],[0,1],[0,0]]])
457
+ // → { type: 'MultiPolygon', coordinates: [[[[0,0],[1,0],[1,1],[0,1],[0,0]]]] }
458
+
459
+ // ② 传入多个多边形 Position[][][]
460
+ createMultiPolygon([
461
+ [[[0,0],[1,0],[1,1],[0,1],[0,0]]],
462
+ [[[2,2],[3,2],[3,3],[2,3],[2,2]]]
463
+ ])
464
+ // → { type: 'MultiPolygon', coordinates: [...] }
465
+ ```
466
+
467
+ #### `createGeometryCollection(geometries)`
468
+
469
+ 支持两种输入:
470
+
471
+ ```javascript
472
+ // ① 传入单个 Geometry(自动包装)
473
+ createGeometryCollection(createPoint(0, 0))
474
+ // → { type: 'GeometryCollection', geometries: [{ type: 'Point', coordinates: [0,0] }] }
475
+
476
+ // ② 传入 Geometry[]
477
+ createGeometryCollection([
478
+ createPoint(0, 0),
479
+ createLineString([[0,0],[1,1]]),
480
+ createPolygon([[0,0],[10,0],[10,10],[0,10],[0,0]])
481
+ ])
482
+ // → { type: 'GeometryCollection', geometries: [...] }
483
+ ```
484
+
485
+ ---
486
+
487
+ ## 10. 校验工具
488
+
489
+ ### `validateWKT(wkt)`
490
+
491
+ 校验 WKT 字符串格式是否合法。
492
+
493
+ ```javascript
494
+ import { validateWKT } from 'giser-geometry-parse';
495
+
496
+ validateWKT('POINT (30.5 40.5)')
497
+ // → { valid: true }
498
+
499
+ validateWKT('POINT EMPTY')
500
+ // → { valid: false, error: 'POINT EMPTY cannot be represented...' }
501
+
502
+ validateWKT('INVALID WKT')
503
+ // → { valid: false, error: 'Unknown geometry type: INVALID' }
504
+ ```
505
+
506
+ ### `validateGeoJSON(geojson)`
507
+
508
+ 校验 GeoJSON Geometry 对象是否合法。
509
+
510
+ ```javascript
511
+ import { validateGeoJSON } from 'giser-geometry-parse';
512
+
513
+ validateGeoJSON({ type: 'Point', coordinates: [30.5, 40.5] })
514
+ // → { valid: true }
515
+
516
+ validateGeoJSON({ type: 'Point' })
517
+ // → { valid: false, error: 'Point must have "coordinates"' }
518
+
519
+ validateGeoJSON({ type: 'InvalidType', coordinates: [] })
520
+ // → { valid: false, error: 'Invalid geometry type: InvalidType...' }
521
+ ```
522
+
523
+ ### `tryFixWKT(wkt)`
524
+
525
+ 尝试修复不规范的 WKT 字符串(处理尾部多余字符)。
526
+
527
+ ```javascript
528
+ import { tryFixWKT } from 'giser-geometry-parse';
529
+
530
+ tryFixWKT('POINT (30.5 40.5) garbage')
531
+ // → { fixed: 'POINT (30.5 40.5)', changed: true }
532
+ ```
533
+
534
+ ### `cloneGeometry(geometry)`
535
+
536
+ 深度克隆几何对象,避免意外修改原对象。
537
+
538
+ ```javascript
539
+ import { cloneGeometry } from 'giser-geometry-parse';
540
+
541
+ const original = { type: 'Point', coordinates: [0, 0] };
542
+ const cloned = cloneGeometry(original);
543
+ cloned.coordinates[0] = 100;
544
+ // original.coordinates[0] === 0 (未改变)
545
+ ```
546
+
547
+ ### `geometryEquals(a, b)`
548
+
549
+ 判断两个几何对象是否相等。
550
+
551
+ ```javascript
552
+ import { geometryEquals } from 'giser-geometry-parse';
553
+
554
+ const a = { type: 'Point', coordinates: [30.5, 40.5] };
555
+ const b = { type: 'Point', coordinates: [30.5, 40.5] };
556
+ geometryEquals(a, b)
557
+ // → true
558
+
559
+ geometryEquals(a, { type: 'Point', coordinates: [0, 0] })
560
+ // → false
561
+ ```
562
+
563
+ ---
564
+
565
+ ## 类型定义
566
+
567
+ ```typescript
568
+ // 坐标点(二维或三维)
569
+ type Position = [number, number] | [number, number, number];
570
+
571
+ // 几何类型
572
+ type Geometry =
573
+ | Point // { type: 'Point'; coordinates: Position }
574
+ | LineString // { type: 'LineString'; coordinates: Position[] }
575
+ | Polygon // { type: 'Polygon'; coordinates: Position[][] }
576
+ | MultiPoint // { type: 'MultiPoint'; coordinates: Position[] }
577
+ | MultiLineString // { type: 'MultiLineString'; coordinates: Position[][] }
578
+ | MultiPolygon // { type: 'MultiPolygon'; coordinates: Position[][][] }
579
+ | GeometryCollection // { type: 'GeometryCollection'; geometries: Geometry[] }
580
+
581
+ // Feature:包含一个 Geometry 和任意属性
582
+ interface Feature<G extends Geometry = Geometry> {
583
+ type: 'Feature';
584
+ geometry: G | null;
585
+ properties: Record<string, unknown> | null;
586
+ id?: string | number;
587
+ }
588
+
589
+ // FeatureCollection:包含多个 Feature
590
+ interface FeatureCollection {
591
+ type: 'FeatureCollection';
592
+ features: Feature[];
593
+ }
594
+
595
+ // 所有 GeoJSON 对象的联合类型
596
+ type GeoJSONObject = Geometry | Feature | FeatureCollection;
597
+ ```
598
+
599
+ ---
600
+
601
+ ## 注意事项与边界行为
602
+
603
+ ### POINT EMPTY
604
+
605
+ `POINT EMPTY` 在 GeoJSON 规范中没有对应表示(GeoJSON Point 不允许 `null` 坐标)。
606
+ 解析时会**抛出错误**,建议改用 `Feature` 的 `null geometry`:
607
+
608
+ ```javascript
609
+ // ❌ 会抛出错误
610
+ parse('POINT EMPTY');
611
+
612
+ // ✅ 推荐方式:用 null geometry Feature 表示空点
613
+ const emptyFeature = {
614
+ type: 'Feature',
615
+ geometry: null,
616
+ properties: null
617
+ };
618
+ ```
619
+
620
+ ### LINESTRING / POLYGON 等 EMPTY
621
+
622
+ 其他 EMPTY 几何会解析为空坐标数组,并在构建时输出 `EMPTY`:
623
+
624
+ ```javascript
625
+ parse('LINESTRING EMPTY')
626
+ // → { type: 'LineString', coordinates: [] }
627
+
628
+ build({ type: 'LineString', coordinates: [] })
629
+ // → 'LINESTRING EMPTY'
630
+ ```
631
+
632
+ ### 科学计数法坐标
633
+
634
+ WKT 标准不支持科学计数法(如 `1e-7`)。`build()` 内部已做格式化处理,确保输出为标准十进制:
635
+
636
+ ```javascript
637
+ build({ type: 'Point', coordinates: [0.0000001, 1.0000000] })
638
+ // → 'POINT (0.0000001 1)' 而非 'POINT (1e-7 1)'
639
+ ```
640
+
641
+ ### MULTIPOINT 标准格式
642
+
643
+ `build()` 输出符合 OGC/ISO 标准格式(每个点用括号包裹),可被 PostGIS、QGIS 等工具正确识别:
644
+
645
+ ```javascript
646
+ build({ type: 'MultiPoint', coordinates: [[0,0],[1,1]] })
647
+ // → 'MULTIPOINT ((0 0), (1 1))' ✅ 标准
648
+ // 不是:'MULTIPOINT (0 0, 1 1)' ❌ 非标准
649
+ ```
650
+
651
+ ### 尾部垃圾字符检测
652
+
653
+ 解析器会严格校验输入,发现几何体后的多余字符时抛出错误:
654
+
655
+ ```javascript
656
+ parse('POINT (0 0) garbage')
657
+ // → Error: Unexpected trailing token after geometry: "garbage"
658
+ ```
659
+
660
+ ---
661
+
662
+ ## 完整示例
663
+
664
+ ### WKT → GeoJSON Feature → 回写 WKT
665
+
666
+ ```javascript
667
+ import { wktToFeature, featureToWkt } from 'giser-geometry-parse';
668
+
669
+ const wkt = 'POLYGON ((116 39, 117 39, 117 40, 116 40, 116 39))';
670
+
671
+ // 解析为 Feature
672
+ const feature = wktToFeature(wkt, { name: '某区域', area: 12345 });
673
+
674
+ // 修改属性
675
+ feature.properties.verified = true;
676
+
677
+ // 取回 WKT
678
+ const outputWkt = featureToWkt(feature);
679
+ console.log(outputWkt);
680
+ // → 'POLYGON ((116 39, 117 39, 117 40, 116 40, 116 39))'
681
+ ```
682
+
683
+ ### 批量城市点构建 FeatureCollection
684
+
685
+ ```javascript
686
+ import { wktToFeatureCollection } from 'giser-geometry-parse';
687
+
688
+ const cities = [
689
+ { wkt: 'POINT (116.39 39.91)', props: { name: '北京', code: 'BJ' } },
690
+ { wkt: 'POINT (121.47 31.23)', props: { name: '上海', code: 'SH' } },
691
+ { wkt: 'POINT (113.26 23.13)', props: { name: '广州', code: 'GZ' } },
692
+ ];
693
+
694
+ const fc = wktToFeatureCollection(
695
+ cities.map(c => c.wkt),
696
+ cities.map(c => c.props)
697
+ );
698
+
699
+ // 直接输出为 GeoJSON 字符串
700
+ console.log(JSON.stringify(fc, null, 2));
701
+ ```
702
+
703
+ ### 使用工厂方法组合复杂几何
704
+
705
+ ```javascript
706
+ import { createPoint, createLineString, createPolygon, createGeometryCollection, build } from 'giser-geometry-parse';
707
+
708
+ const collection = createGeometryCollection([
709
+ createPoint(0, 0),
710
+ createPoint(10, 10),
711
+ createLineString([[0,0],[5,5],[10,0]]),
712
+ createPolygon([[20,0],[30,0],[30,10],[20,10],[20,0]]) // 单环,自动包装
713
+ ]);
714
+
715
+ console.log(build(collection));
716
+ // → 'GEOMETRYCOLLECTION (POINT (0 0), POINT (10 10), LINESTRING (0 0, 5 5, 10 0), POLYGON ((20 0, 30 0, 30 10, 20 10, 20 0)))'
717
+ ```
718
+
719
+ ---
720
+
721
+ ## 本地调试
722
+
723
+ 项目根目录提供两个测试页面:
724
+
725
+ | 文件 | 用途 | 加载方式 |
726
+ |------|------|----------|
727
+ | `index-local.html` | 本地版本测试 | 使用本地 `dist/index.umd.js` |
728
+ | `index-online.html` | 线上版本测试 | 使用 unpkg CDN 加载 npm 包 |
729
+
730
+ 启动本地 HTTP 服务后打开对应页面:
731
+
732
+ ```bash
733
+ # 方式一:npx serve(推荐)
734
+ npx serve .
735
+
736
+ # 方式二:Python
737
+ python -m http.server 8080
738
+
739
+ # 方式三:http-server
740
+ npx http-server -p 8080
741
+ ```
742
+
743
+ 访问 `http://localhost:8080/index-local.html` 测试本地构建版本。
744
+ 访问 `http://localhost:8080/index-online.html` 测试线上 npm 版本。
745
+
746
+ **提示**:本地测试前需先运行 `npm run build` 构建项目。
747
+
748
+ ---
749
+
750
+ ## License
751
+
752
+ MIT