wkt-parse-and-geojson 1.0.2 → 1.0.4

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -21,6 +21,7 @@
21
21
  - [featureToWkt(feature)](#7-featuretowktfeature)
22
22
  - [featureCollectionToWkt(fc)](#8-featurecollectiontowktfc)
23
23
  - [GeoJSON 工厂方法](#9-geojson-工厂方法)
24
+ - [校验工具](#10-校验工具)
24
25
  - [类型定义](#类型定义)
25
26
  - [注意事项与边界行为](#注意事项与边界行为)
26
27
  - [本地调试](#本地调试)
@@ -480,6 +481,84 @@ createGeometryCollection([
480
481
 
481
482
  ---
482
483
 
484
+ ## 10. 校验工具
485
+
486
+ ### `validateWKT(wkt)`
487
+
488
+ 校验 WKT 字符串格式是否合法。
489
+
490
+ ```javascript
491
+ import { validateWKT } from 'wkt-parse-and-geojson';
492
+
493
+ validateWKT('POINT (30.5 40.5)')
494
+ // → { valid: true }
495
+
496
+ validateWKT('POINT EMPTY')
497
+ // → { valid: false, error: 'POINT EMPTY cannot be represented...' }
498
+
499
+ validateWKT('INVALID WKT')
500
+ // → { valid: false, error: 'Unknown geometry type: INVALID' }
501
+ ```
502
+
503
+ ### `validateGeoJSON(geojson)`
504
+
505
+ 校验 GeoJSON Geometry 对象是否合法。
506
+
507
+ ```javascript
508
+ import { validateGeoJSON } from 'wkt-parse-and-geojson';
509
+
510
+ validateGeoJSON({ type: 'Point', coordinates: [30.5, 40.5] })
511
+ // → { valid: true }
512
+
513
+ validateGeoJSON({ type: 'Point' })
514
+ // → { valid: false, error: 'Point must have "coordinates"' }
515
+
516
+ validateGeoJSON({ type: 'InvalidType', coordinates: [] })
517
+ // → { valid: false, error: 'Invalid geometry type: InvalidType...' }
518
+ ```
519
+
520
+ ### `tryFixWKT(wkt)`
521
+
522
+ 尝试修复不规范的 WKT 字符串(处理尾部多余字符)。
523
+
524
+ ```javascript
525
+ import { tryFixWKT } from 'wkt-parse-and-geojson';
526
+
527
+ tryFixWKT('POINT (30.5 40.5) garbage')
528
+ // → { fixed: 'POINT (30.5 40.5)', changed: true }
529
+ ```
530
+
531
+ ### `cloneGeometry(geometry)`
532
+
533
+ 深度克隆几何对象,避免意外修改原对象。
534
+
535
+ ```javascript
536
+ import { cloneGeometry } from 'wkt-parse-and-geojson';
537
+
538
+ const original = { type: 'Point', coordinates: [0, 0] };
539
+ const cloned = cloneGeometry(original);
540
+ cloned.coordinates[0] = 100;
541
+ // original.coordinates[0] === 0 (未改变)
542
+ ```
543
+
544
+ ### `geometryEquals(a, b)`
545
+
546
+ 判断两个几何对象是否相等。
547
+
548
+ ```javascript
549
+ import { geometryEquals } from 'wkt-parse-and-geojson';
550
+
551
+ const a = { type: 'Point', coordinates: [30.5, 40.5] };
552
+ const b = { type: 'Point', coordinates: [30.5, 40.5] };
553
+ geometryEquals(a, b)
554
+ // → true
555
+
556
+ geometryEquals(a, { type: 'Point', coordinates: [0, 0] })
557
+ // → false
558
+ ```
559
+
560
+ ---
561
+
483
562
  ## 类型定义
484
563
 
485
564
  ```typescript
package/dist/index.cjs.js CHANGED
@@ -1,5 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ // Precompiled regex for better performance
4
+ const RE_WHITESPACE = /\s/;
5
+ const RE_NUMBER_START = /[0-9\-]/;
6
+ const RE_NUMBER_BODY = /[0-9\.\-eE\+]/;
7
+ const RE_WORD_CHAR = /[a-zA-Z_]/;
3
8
  class Lexer {
4
9
  constructor(input) {
5
10
  this.pos = 0;
@@ -12,14 +17,13 @@ class Lexer {
12
17
  return this.input[this.pos++] || '';
13
18
  }
14
19
  isWhitespace(c) {
15
- return /\s/.test(c);
20
+ return RE_WHITESPACE.test(c);
16
21
  }
17
- /** 数字开头字符:数字 或 负号 */
18
22
  isNumberStart(c) {
19
- return /[0-9\-]/.test(c);
23
+ return RE_NUMBER_START.test(c);
20
24
  }
21
25
  isNumberBody(c) {
22
- return /[0-9\.\-eE\+]/.test(c);
26
+ return RE_NUMBER_BODY.test(c);
23
27
  }
24
28
  nextToken() {
25
29
  // skip whitespace
@@ -44,18 +48,18 @@ class Lexer {
44
48
  }
45
49
  // Number: starts with digit or minus
46
50
  if (this.isNumberStart(c)) {
47
- let num = '';
51
+ const start = this.pos;
48
52
  while (this.pos < this.input.length && this.isNumberBody(this.peek())) {
49
- num += this.advance();
53
+ this.pos++;
50
54
  }
51
- return { type: 'NUMBER', value: num };
55
+ return { type: 'NUMBER', value: this.input.slice(start, this.pos) };
52
56
  }
53
57
  // 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();
58
+ const start = this.pos;
59
+ while (this.pos < this.input.length && RE_WORD_CHAR.test(this.peek())) {
60
+ this.pos++;
57
61
  }
58
- return { type: 'WORD', value: word.toUpperCase() };
62
+ return { type: 'WORD', value: this.input.slice(start, this.pos).toUpperCase() };
59
63
  }
60
64
  }
61
65
  class WKTParser {
@@ -250,11 +254,9 @@ class WKTParser {
250
254
  }
251
255
  this.advance(); // consume (
252
256
  const geometries = [];
253
- while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
257
+ while (!this.isDone()) {
254
258
  geometries.push(this.parseGeometry());
255
- if (this.peek().type === 'COMMA') {
256
- this.advance();
257
- }
259
+ this.skipComma();
258
260
  }
259
261
  this.consume('RPAREN');
260
262
  return { type: 'GeometryCollection', geometries };
@@ -300,11 +302,9 @@ class WKTParser {
300
302
  return [];
301
303
  this.advance(); // consume outer (
302
304
  const lists = [];
303
- while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
305
+ while (!this.isDone()) {
304
306
  lists.push(this.parseCoordinatesList());
305
- if (this.peek().type === 'COMMA') {
306
- this.advance();
307
- }
307
+ this.skipComma();
308
308
  }
309
309
  this.consume('RPAREN');
310
310
  return lists;
@@ -653,10 +653,221 @@ function featureCollectionToWkt(fc) {
653
653
  });
654
654
  }
655
655
 
656
+ /**
657
+ * 校验 WKT 字符串格式是否合法
658
+ */
659
+ function validateWKT(wkt) {
660
+ if (!wkt || typeof wkt !== 'string') {
661
+ return { valid: false, error: 'WKT must be a non-empty string' };
662
+ }
663
+ const trimmed = wkt.trim();
664
+ if (trimmed.length === 0) {
665
+ return { valid: false, error: 'WKT cannot be empty' };
666
+ }
667
+ try {
668
+ parse(wkt);
669
+ return { valid: true };
670
+ }
671
+ catch (e) {
672
+ return { valid: false, error: e.message };
673
+ }
674
+ }
675
+ /**
676
+ * 校验 GeoJSON Geometry 对象是否合法
677
+ */
678
+ function validateGeoJSON(geojson) {
679
+ if (!geojson || typeof geojson !== 'object') {
680
+ return { valid: false, error: 'GeoJSON must be an object' };
681
+ }
682
+ const obj = geojson;
683
+ // 检查 type 字段
684
+ if (!obj.type || typeof obj.type !== 'string') {
685
+ return { valid: false, error: 'GeoJSON must have a "type" property' };
686
+ }
687
+ const type = obj.type;
688
+ const validTypes = [
689
+ 'Point', 'LineString', 'Polygon',
690
+ 'MultiPoint', 'MultiLineString', 'MultiPolygon',
691
+ 'GeometryCollection'
692
+ ];
693
+ if (!validTypes.includes(type)) {
694
+ return { valid: false, error: `Invalid geometry type: "${type}". Must be one of: ${validTypes.join(', ')}` };
695
+ }
696
+ // GeometryCollection 特殊处理
697
+ if (type === 'GeometryCollection') {
698
+ if (!obj.geometries || !Array.isArray(obj.geometries)) {
699
+ return { valid: false, error: 'GeometryCollection must have a "geometries" array' };
700
+ }
701
+ for (let i = 0; i < obj.geometries.length; i++) {
702
+ const result = validateGeoJSON(obj.geometries[i]);
703
+ if (!result.valid) {
704
+ return { valid: false, error: `GeometryCollection[${i}]: ${result.error}` };
705
+ }
706
+ }
707
+ return { valid: true };
708
+ }
709
+ // 其他几何类型必须要有 coordinates
710
+ if (obj.coordinates === undefined) {
711
+ return { valid: false, error: `${type} must have "coordinates"` };
712
+ }
713
+ // 校验坐标格式
714
+ return validateCoordinates(type, obj.coordinates);
715
+ }
716
+ function validateCoordinates(type, coords) {
717
+ switch (type) {
718
+ case 'Point':
719
+ return validatePosition(coords);
720
+ case 'LineString':
721
+ case 'MultiPoint':
722
+ if (!Array.isArray(coords)) {
723
+ return { valid: false, error: `${type} coordinates must be an array` };
724
+ }
725
+ for (let i = 0; i < coords.length; i++) {
726
+ const result = validatePosition(coords[i]);
727
+ if (!result.valid) {
728
+ return { valid: false, error: `${type}[${i}]: ${result.error}` };
729
+ }
730
+ }
731
+ return { valid: true };
732
+ case 'Polygon':
733
+ case 'MultiLineString':
734
+ if (!Array.isArray(coords)) {
735
+ return { valid: false, error: `${type} coordinates must be a nested array` };
736
+ }
737
+ for (let i = 0; i < coords.length; i++) {
738
+ if (!Array.isArray(coords[i])) {
739
+ return { valid: false, error: `${type}[${i}] must be an array of positions` };
740
+ }
741
+ for (let j = 0; j < coords[i].length; j++) {
742
+ const result = validatePosition(coords[i][j]);
743
+ if (!result.valid) {
744
+ return { valid: false, error: `${type}[${i}][${j}]: ${result.error}` };
745
+ }
746
+ }
747
+ }
748
+ return { valid: true };
749
+ case 'MultiPolygon':
750
+ if (!Array.isArray(coords)) {
751
+ return { valid: false, error: `${type} coordinates must be a deeply nested array` };
752
+ }
753
+ for (let i = 0; i < coords.length; i++) {
754
+ if (!Array.isArray(coords[i])) {
755
+ return { valid: false, error: `${type}[${i}] must be an array of rings` };
756
+ }
757
+ for (let j = 0; j < coords[i].length; j++) {
758
+ if (!Array.isArray(coords[i][j])) {
759
+ return { valid: false, error: `${type}[${i}][${j}] must be an array of positions` };
760
+ }
761
+ for (let k = 0; k < coords[i][j].length; k++) {
762
+ const result = validatePosition(coords[i][j][k]);
763
+ if (!result.valid) {
764
+ return { valid: false, error: `${type}[${i}][${j}][${k}]: ${result.error}` };
765
+ }
766
+ }
767
+ }
768
+ }
769
+ return { valid: true };
770
+ default:
771
+ return { valid: true };
772
+ }
773
+ }
774
+ function validatePosition(pos) {
775
+ if (!Array.isArray(pos)) {
776
+ return { valid: false, error: 'Position must be an array of numbers' };
777
+ }
778
+ if (pos.length < 2 || pos.length > 3) {
779
+ return { valid: false, error: `Position must have 2 or 3 coordinates, got ${pos.length}` };
780
+ }
781
+ for (let i = 0; i < pos.length; i++) {
782
+ if (typeof pos[i] !== 'number' || isNaN(pos[i])) {
783
+ return { valid: false, error: `Position[${i}] must be a valid number` };
784
+ }
785
+ }
786
+ return { valid: true };
787
+ }
788
+ /**
789
+ * 尝试从可能不规范的 WKT 中恢复出有效结果
790
+ * 主要处理尾部多余字符的情况
791
+ */
792
+ function tryFixWKT(wkt) {
793
+ const trimmed = wkt.trim();
794
+ if (!trimmed) {
795
+ return { fixed: wkt, changed: false };
796
+ }
797
+ // 检查是否有尾部多余字符
798
+ const result = validateWKT(trimmed);
799
+ if (result.valid) {
800
+ return { fixed: trimmed, changed: false };
801
+ }
802
+ // 尝试找到最后一个有效的 geometry 结束位置
803
+ const patterns = [
804
+ /\)\s*[A-Z]/i, // 括号后跟字母 (如 POLYGON ((...)) POINT )
805
+ /EMPTY\s+[A-Z]/i, // EMPTY 后跟字母
806
+ /\)\s*$/, // 括号结尾后有多余内容
807
+ ];
808
+ for (const pattern of patterns) {
809
+ const match = trimmed.match(pattern);
810
+ if (match) {
811
+ const fixed = trimmed.slice(0, match.index + (match[0].match(/\)/)?.[0].length || 0));
812
+ if (validateWKT(fixed).valid) {
813
+ return { fixed, changed: true };
814
+ }
815
+ }
816
+ }
817
+ // 尝试去除尾部垃圾字符
818
+ const lastValidIndex = findLastValidPosition(trimmed);
819
+ if (lastValidIndex > 0) {
820
+ const fixed = trimmed.slice(0, lastValidIndex + 1);
821
+ if (validateWKT(fixed).valid) {
822
+ return { fixed, changed: true };
823
+ }
824
+ }
825
+ return { fixed: wkt, changed: false };
826
+ }
827
+ function findLastValidPosition(wkt) {
828
+ // 从后往前找第一个有效的右括号位置
829
+ let depth = 0;
830
+ for (let i = wkt.length - 1; i >= 0; i--) {
831
+ const c = wkt[i];
832
+ if (c === ')')
833
+ depth++;
834
+ else if (c === '(')
835
+ depth--;
836
+ else if (c === ' ' && depth === 0 && i < wkt.length - 1) {
837
+ // 检查这个空格是否在有效位置
838
+ const afterSpace = wkt.slice(i + 1).trim();
839
+ if (!afterSpace)
840
+ continue;
841
+ if (!/^[A-Z]/.test(afterSpace))
842
+ continue;
843
+ // 如果空格后面是字母开头,可能是垃圾字符
844
+ if (i > 5 && /[A-Z]$/.test(wkt.slice(0, i).trim())) {
845
+ return i - 1;
846
+ }
847
+ }
848
+ }
849
+ return wkt.length - 1;
850
+ }
851
+ /**
852
+ * 深度克隆 GeoJSON 对象(用于避免意外修改原对象)
853
+ */
854
+ function cloneGeometry(geometry) {
855
+ return JSON.parse(JSON.stringify(geometry));
856
+ }
857
+ /**
858
+ * 判断两个几何对象是否相等(坐标对比)
859
+ */
860
+ function geometryEquals(a, b) {
861
+ if (a.type !== b.type)
862
+ return false;
863
+ return JSON.stringify(a) === JSON.stringify(b);
864
+ }
865
+
656
866
  exports.GeoJSONBuilder = GeoJSONBuilder;
657
867
  exports.WKTBuilder = WKTBuilder;
658
868
  exports.WKTParser = WKTParser;
659
869
  exports.build = build;
870
+ exports.cloneGeometry = cloneGeometry;
660
871
  exports.createGeometryCollection = createGeometryCollection;
661
872
  exports.createLineString = createLineString;
662
873
  exports.createMultiLineString = createMultiLineString;
@@ -667,7 +878,11 @@ exports.createPolygon = createPolygon;
667
878
  exports.featureCollectionToWkt = featureCollectionToWkt;
668
879
  exports.featureToWkt = featureToWkt;
669
880
  exports.geojsonToWkt = geojsonToWkt;
881
+ exports.geometryEquals = geometryEquals;
670
882
  exports.parse = parse;
883
+ exports.tryFixWKT = tryFixWKT;
884
+ exports.validateGeoJSON = validateGeoJSON;
885
+ exports.validateWKT = validateWKT;
671
886
  exports.wktToFeature = wktToFeature;
672
887
  exports.wktToFeatureCollection = wktToFeatureCollection;
673
888
  exports.wktToGeoJSON = wktToGeoJSON;
package/dist/index.d.ts CHANGED
@@ -4,3 +4,4 @@ export { build, WKTBuilder } from './wkt-builder';
4
4
  export { createPoint, createLineString, createPolygon, createMultiPoint, createMultiLineString, createMultiPolygon, createGeometryCollection, GeoJSONBuilder, } from './geojson-builder';
5
5
  export { wktToGeoJSON, wktToFeature, wktToFeatureCollection } from './wkt-to-geojson';
6
6
  export { geojsonToWkt, featureToWkt, featureCollectionToWkt } from './geojson-to-wkt';
7
+ export { validateWKT, validateGeoJSON, tryFixWKT, cloneGeometry, geometryEquals, type ValidationResult, } from './validate';
package/dist/index.esm.js CHANGED
@@ -1,3 +1,8 @@
1
+ // Precompiled regex for better performance
2
+ const RE_WHITESPACE = /\s/;
3
+ const RE_NUMBER_START = /[0-9\-]/;
4
+ const RE_NUMBER_BODY = /[0-9\.\-eE\+]/;
5
+ const RE_WORD_CHAR = /[a-zA-Z_]/;
1
6
  class Lexer {
2
7
  constructor(input) {
3
8
  this.pos = 0;
@@ -10,14 +15,13 @@ class Lexer {
10
15
  return this.input[this.pos++] || '';
11
16
  }
12
17
  isWhitespace(c) {
13
- return /\s/.test(c);
18
+ return RE_WHITESPACE.test(c);
14
19
  }
15
- /** 数字开头字符:数字 或 负号 */
16
20
  isNumberStart(c) {
17
- return /[0-9\-]/.test(c);
21
+ return RE_NUMBER_START.test(c);
18
22
  }
19
23
  isNumberBody(c) {
20
- return /[0-9\.\-eE\+]/.test(c);
24
+ return RE_NUMBER_BODY.test(c);
21
25
  }
22
26
  nextToken() {
23
27
  // skip whitespace
@@ -42,18 +46,18 @@ class Lexer {
42
46
  }
43
47
  // Number: starts with digit or minus
44
48
  if (this.isNumberStart(c)) {
45
- let num = '';
49
+ const start = this.pos;
46
50
  while (this.pos < this.input.length && this.isNumberBody(this.peek())) {
47
- num += this.advance();
51
+ this.pos++;
48
52
  }
49
- return { type: 'NUMBER', value: num };
53
+ return { type: 'NUMBER', value: this.input.slice(start, this.pos) };
50
54
  }
51
55
  // Word (geometry type or EMPTY/Z/M keyword)
52
- let word = '';
53
- while (this.pos < this.input.length && /[a-zA-Z_]/.test(this.peek())) {
54
- word += this.advance();
56
+ const start = this.pos;
57
+ while (this.pos < this.input.length && RE_WORD_CHAR.test(this.peek())) {
58
+ this.pos++;
55
59
  }
56
- return { type: 'WORD', value: word.toUpperCase() };
60
+ return { type: 'WORD', value: this.input.slice(start, this.pos).toUpperCase() };
57
61
  }
58
62
  }
59
63
  class WKTParser {
@@ -248,11 +252,9 @@ class WKTParser {
248
252
  }
249
253
  this.advance(); // consume (
250
254
  const geometries = [];
251
- while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
255
+ while (!this.isDone()) {
252
256
  geometries.push(this.parseGeometry());
253
- if (this.peek().type === 'COMMA') {
254
- this.advance();
255
- }
257
+ this.skipComma();
256
258
  }
257
259
  this.consume('RPAREN');
258
260
  return { type: 'GeometryCollection', geometries };
@@ -298,11 +300,9 @@ class WKTParser {
298
300
  return [];
299
301
  this.advance(); // consume outer (
300
302
  const lists = [];
301
- while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
303
+ while (!this.isDone()) {
302
304
  lists.push(this.parseCoordinatesList());
303
- if (this.peek().type === 'COMMA') {
304
- this.advance();
305
- }
305
+ this.skipComma();
306
306
  }
307
307
  this.consume('RPAREN');
308
308
  return lists;
@@ -651,4 +651,214 @@ function featureCollectionToWkt(fc) {
651
651
  });
652
652
  }
653
653
 
654
- export { GeoJSONBuilder, WKTBuilder, WKTParser, build, createGeometryCollection, createLineString, createMultiLineString, createMultiPoint, createMultiPolygon, createPoint, createPolygon, featureCollectionToWkt, featureToWkt, geojsonToWkt, parse, wktToFeature, wktToFeatureCollection, wktToGeoJSON };
654
+ /**
655
+ * 校验 WKT 字符串格式是否合法
656
+ */
657
+ function validateWKT(wkt) {
658
+ if (!wkt || typeof wkt !== 'string') {
659
+ return { valid: false, error: 'WKT must be a non-empty string' };
660
+ }
661
+ const trimmed = wkt.trim();
662
+ if (trimmed.length === 0) {
663
+ return { valid: false, error: 'WKT cannot be empty' };
664
+ }
665
+ try {
666
+ parse(wkt);
667
+ return { valid: true };
668
+ }
669
+ catch (e) {
670
+ return { valid: false, error: e.message };
671
+ }
672
+ }
673
+ /**
674
+ * 校验 GeoJSON Geometry 对象是否合法
675
+ */
676
+ function validateGeoJSON(geojson) {
677
+ if (!geojson || typeof geojson !== 'object') {
678
+ return { valid: false, error: 'GeoJSON must be an object' };
679
+ }
680
+ const obj = geojson;
681
+ // 检查 type 字段
682
+ if (!obj.type || typeof obj.type !== 'string') {
683
+ return { valid: false, error: 'GeoJSON must have a "type" property' };
684
+ }
685
+ const type = obj.type;
686
+ const validTypes = [
687
+ 'Point', 'LineString', 'Polygon',
688
+ 'MultiPoint', 'MultiLineString', 'MultiPolygon',
689
+ 'GeometryCollection'
690
+ ];
691
+ if (!validTypes.includes(type)) {
692
+ return { valid: false, error: `Invalid geometry type: "${type}". Must be one of: ${validTypes.join(', ')}` };
693
+ }
694
+ // GeometryCollection 特殊处理
695
+ if (type === 'GeometryCollection') {
696
+ if (!obj.geometries || !Array.isArray(obj.geometries)) {
697
+ return { valid: false, error: 'GeometryCollection must have a "geometries" array' };
698
+ }
699
+ for (let i = 0; i < obj.geometries.length; i++) {
700
+ const result = validateGeoJSON(obj.geometries[i]);
701
+ if (!result.valid) {
702
+ return { valid: false, error: `GeometryCollection[${i}]: ${result.error}` };
703
+ }
704
+ }
705
+ return { valid: true };
706
+ }
707
+ // 其他几何类型必须要有 coordinates
708
+ if (obj.coordinates === undefined) {
709
+ return { valid: false, error: `${type} must have "coordinates"` };
710
+ }
711
+ // 校验坐标格式
712
+ return validateCoordinates(type, obj.coordinates);
713
+ }
714
+ function validateCoordinates(type, coords) {
715
+ switch (type) {
716
+ case 'Point':
717
+ return validatePosition(coords);
718
+ case 'LineString':
719
+ case 'MultiPoint':
720
+ if (!Array.isArray(coords)) {
721
+ return { valid: false, error: `${type} coordinates must be an array` };
722
+ }
723
+ for (let i = 0; i < coords.length; i++) {
724
+ const result = validatePosition(coords[i]);
725
+ if (!result.valid) {
726
+ return { valid: false, error: `${type}[${i}]: ${result.error}` };
727
+ }
728
+ }
729
+ return { valid: true };
730
+ case 'Polygon':
731
+ case 'MultiLineString':
732
+ if (!Array.isArray(coords)) {
733
+ return { valid: false, error: `${type} coordinates must be a nested array` };
734
+ }
735
+ for (let i = 0; i < coords.length; i++) {
736
+ if (!Array.isArray(coords[i])) {
737
+ return { valid: false, error: `${type}[${i}] must be an array of positions` };
738
+ }
739
+ for (let j = 0; j < coords[i].length; j++) {
740
+ const result = validatePosition(coords[i][j]);
741
+ if (!result.valid) {
742
+ return { valid: false, error: `${type}[${i}][${j}]: ${result.error}` };
743
+ }
744
+ }
745
+ }
746
+ return { valid: true };
747
+ case 'MultiPolygon':
748
+ if (!Array.isArray(coords)) {
749
+ return { valid: false, error: `${type} coordinates must be a deeply nested array` };
750
+ }
751
+ for (let i = 0; i < coords.length; i++) {
752
+ if (!Array.isArray(coords[i])) {
753
+ return { valid: false, error: `${type}[${i}] must be an array of rings` };
754
+ }
755
+ for (let j = 0; j < coords[i].length; j++) {
756
+ if (!Array.isArray(coords[i][j])) {
757
+ return { valid: false, error: `${type}[${i}][${j}] must be an array of positions` };
758
+ }
759
+ for (let k = 0; k < coords[i][j].length; k++) {
760
+ const result = validatePosition(coords[i][j][k]);
761
+ if (!result.valid) {
762
+ return { valid: false, error: `${type}[${i}][${j}][${k}]: ${result.error}` };
763
+ }
764
+ }
765
+ }
766
+ }
767
+ return { valid: true };
768
+ default:
769
+ return { valid: true };
770
+ }
771
+ }
772
+ function validatePosition(pos) {
773
+ if (!Array.isArray(pos)) {
774
+ return { valid: false, error: 'Position must be an array of numbers' };
775
+ }
776
+ if (pos.length < 2 || pos.length > 3) {
777
+ return { valid: false, error: `Position must have 2 or 3 coordinates, got ${pos.length}` };
778
+ }
779
+ for (let i = 0; i < pos.length; i++) {
780
+ if (typeof pos[i] !== 'number' || isNaN(pos[i])) {
781
+ return { valid: false, error: `Position[${i}] must be a valid number` };
782
+ }
783
+ }
784
+ return { valid: true };
785
+ }
786
+ /**
787
+ * 尝试从可能不规范的 WKT 中恢复出有效结果
788
+ * 主要处理尾部多余字符的情况
789
+ */
790
+ function tryFixWKT(wkt) {
791
+ const trimmed = wkt.trim();
792
+ if (!trimmed) {
793
+ return { fixed: wkt, changed: false };
794
+ }
795
+ // 检查是否有尾部多余字符
796
+ const result = validateWKT(trimmed);
797
+ if (result.valid) {
798
+ return { fixed: trimmed, changed: false };
799
+ }
800
+ // 尝试找到最后一个有效的 geometry 结束位置
801
+ const patterns = [
802
+ /\)\s*[A-Z]/i, // 括号后跟字母 (如 POLYGON ((...)) POINT )
803
+ /EMPTY\s+[A-Z]/i, // EMPTY 后跟字母
804
+ /\)\s*$/, // 括号结尾后有多余内容
805
+ ];
806
+ for (const pattern of patterns) {
807
+ const match = trimmed.match(pattern);
808
+ if (match) {
809
+ const fixed = trimmed.slice(0, match.index + (match[0].match(/\)/)?.[0].length || 0));
810
+ if (validateWKT(fixed).valid) {
811
+ return { fixed, changed: true };
812
+ }
813
+ }
814
+ }
815
+ // 尝试去除尾部垃圾字符
816
+ const lastValidIndex = findLastValidPosition(trimmed);
817
+ if (lastValidIndex > 0) {
818
+ const fixed = trimmed.slice(0, lastValidIndex + 1);
819
+ if (validateWKT(fixed).valid) {
820
+ return { fixed, changed: true };
821
+ }
822
+ }
823
+ return { fixed: wkt, changed: false };
824
+ }
825
+ function findLastValidPosition(wkt) {
826
+ // 从后往前找第一个有效的右括号位置
827
+ let depth = 0;
828
+ for (let i = wkt.length - 1; i >= 0; i--) {
829
+ const c = wkt[i];
830
+ if (c === ')')
831
+ depth++;
832
+ else if (c === '(')
833
+ depth--;
834
+ else if (c === ' ' && depth === 0 && i < wkt.length - 1) {
835
+ // 检查这个空格是否在有效位置
836
+ const afterSpace = wkt.slice(i + 1).trim();
837
+ if (!afterSpace)
838
+ continue;
839
+ if (!/^[A-Z]/.test(afterSpace))
840
+ continue;
841
+ // 如果空格后面是字母开头,可能是垃圾字符
842
+ if (i > 5 && /[A-Z]$/.test(wkt.slice(0, i).trim())) {
843
+ return i - 1;
844
+ }
845
+ }
846
+ }
847
+ return wkt.length - 1;
848
+ }
849
+ /**
850
+ * 深度克隆 GeoJSON 对象(用于避免意外修改原对象)
851
+ */
852
+ function cloneGeometry(geometry) {
853
+ return JSON.parse(JSON.stringify(geometry));
854
+ }
855
+ /**
856
+ * 判断两个几何对象是否相等(坐标对比)
857
+ */
858
+ function geometryEquals(a, b) {
859
+ if (a.type !== b.type)
860
+ return false;
861
+ return JSON.stringify(a) === JSON.stringify(b);
862
+ }
863
+
864
+ export { GeoJSONBuilder, WKTBuilder, WKTParser, build, cloneGeometry, createGeometryCollection, createLineString, createMultiLineString, createMultiPoint, createMultiPolygon, createPoint, createPolygon, featureCollectionToWkt, featureToWkt, geojsonToWkt, geometryEquals, parse, tryFixWKT, validateGeoJSON, validateWKT, wktToFeature, wktToFeatureCollection, wktToGeoJSON };
package/dist/index.umd.js CHANGED
@@ -4,6 +4,11 @@
4
4
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.WKTGeoJSON = {}));
5
5
  })(this, (function (exports) { 'use strict';
6
6
 
7
+ // Precompiled regex for better performance
8
+ const RE_WHITESPACE = /\s/;
9
+ const RE_NUMBER_START = /[0-9\-]/;
10
+ const RE_NUMBER_BODY = /[0-9\.\-eE\+]/;
11
+ const RE_WORD_CHAR = /[a-zA-Z_]/;
7
12
  class Lexer {
8
13
  constructor(input) {
9
14
  this.pos = 0;
@@ -16,14 +21,13 @@
16
21
  return this.input[this.pos++] || '';
17
22
  }
18
23
  isWhitespace(c) {
19
- return /\s/.test(c);
24
+ return RE_WHITESPACE.test(c);
20
25
  }
21
- /** 数字开头字符:数字 或 负号 */
22
26
  isNumberStart(c) {
23
- return /[0-9\-]/.test(c);
27
+ return RE_NUMBER_START.test(c);
24
28
  }
25
29
  isNumberBody(c) {
26
- return /[0-9\.\-eE\+]/.test(c);
30
+ return RE_NUMBER_BODY.test(c);
27
31
  }
28
32
  nextToken() {
29
33
  // skip whitespace
@@ -48,18 +52,18 @@
48
52
  }
49
53
  // Number: starts with digit or minus
50
54
  if (this.isNumberStart(c)) {
51
- let num = '';
55
+ const start = this.pos;
52
56
  while (this.pos < this.input.length && this.isNumberBody(this.peek())) {
53
- num += this.advance();
57
+ this.pos++;
54
58
  }
55
- return { type: 'NUMBER', value: num };
59
+ return { type: 'NUMBER', value: this.input.slice(start, this.pos) };
56
60
  }
57
61
  // Word (geometry type or EMPTY/Z/M keyword)
58
- let word = '';
59
- while (this.pos < this.input.length && /[a-zA-Z_]/.test(this.peek())) {
60
- word += this.advance();
62
+ const start = this.pos;
63
+ while (this.pos < this.input.length && RE_WORD_CHAR.test(this.peek())) {
64
+ this.pos++;
61
65
  }
62
- return { type: 'WORD', value: word.toUpperCase() };
66
+ return { type: 'WORD', value: this.input.slice(start, this.pos).toUpperCase() };
63
67
  }
64
68
  }
65
69
  class WKTParser {
@@ -254,11 +258,9 @@
254
258
  }
255
259
  this.advance(); // consume (
256
260
  const geometries = [];
257
- while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
261
+ while (!this.isDone()) {
258
262
  geometries.push(this.parseGeometry());
259
- if (this.peek().type === 'COMMA') {
260
- this.advance();
261
- }
263
+ this.skipComma();
262
264
  }
263
265
  this.consume('RPAREN');
264
266
  return { type: 'GeometryCollection', geometries };
@@ -304,11 +306,9 @@
304
306
  return [];
305
307
  this.advance(); // consume outer (
306
308
  const lists = [];
307
- while (this.peek().type !== 'RPAREN' && this.peek().type !== 'EOF') {
309
+ while (!this.isDone()) {
308
310
  lists.push(this.parseCoordinatesList());
309
- if (this.peek().type === 'COMMA') {
310
- this.advance();
311
- }
311
+ this.skipComma();
312
312
  }
313
313
  this.consume('RPAREN');
314
314
  return lists;
@@ -657,10 +657,221 @@
657
657
  });
658
658
  }
659
659
 
660
+ /**
661
+ * 校验 WKT 字符串格式是否合法
662
+ */
663
+ function validateWKT(wkt) {
664
+ if (!wkt || typeof wkt !== 'string') {
665
+ return { valid: false, error: 'WKT must be a non-empty string' };
666
+ }
667
+ const trimmed = wkt.trim();
668
+ if (trimmed.length === 0) {
669
+ return { valid: false, error: 'WKT cannot be empty' };
670
+ }
671
+ try {
672
+ parse(wkt);
673
+ return { valid: true };
674
+ }
675
+ catch (e) {
676
+ return { valid: false, error: e.message };
677
+ }
678
+ }
679
+ /**
680
+ * 校验 GeoJSON Geometry 对象是否合法
681
+ */
682
+ function validateGeoJSON(geojson) {
683
+ if (!geojson || typeof geojson !== 'object') {
684
+ return { valid: false, error: 'GeoJSON must be an object' };
685
+ }
686
+ const obj = geojson;
687
+ // 检查 type 字段
688
+ if (!obj.type || typeof obj.type !== 'string') {
689
+ return { valid: false, error: 'GeoJSON must have a "type" property' };
690
+ }
691
+ const type = obj.type;
692
+ const validTypes = [
693
+ 'Point', 'LineString', 'Polygon',
694
+ 'MultiPoint', 'MultiLineString', 'MultiPolygon',
695
+ 'GeometryCollection'
696
+ ];
697
+ if (!validTypes.includes(type)) {
698
+ return { valid: false, error: `Invalid geometry type: "${type}". Must be one of: ${validTypes.join(', ')}` };
699
+ }
700
+ // GeometryCollection 特殊处理
701
+ if (type === 'GeometryCollection') {
702
+ if (!obj.geometries || !Array.isArray(obj.geometries)) {
703
+ return { valid: false, error: 'GeometryCollection must have a "geometries" array' };
704
+ }
705
+ for (let i = 0; i < obj.geometries.length; i++) {
706
+ const result = validateGeoJSON(obj.geometries[i]);
707
+ if (!result.valid) {
708
+ return { valid: false, error: `GeometryCollection[${i}]: ${result.error}` };
709
+ }
710
+ }
711
+ return { valid: true };
712
+ }
713
+ // 其他几何类型必须要有 coordinates
714
+ if (obj.coordinates === undefined) {
715
+ return { valid: false, error: `${type} must have "coordinates"` };
716
+ }
717
+ // 校验坐标格式
718
+ return validateCoordinates(type, obj.coordinates);
719
+ }
720
+ function validateCoordinates(type, coords) {
721
+ switch (type) {
722
+ case 'Point':
723
+ return validatePosition(coords);
724
+ case 'LineString':
725
+ case 'MultiPoint':
726
+ if (!Array.isArray(coords)) {
727
+ return { valid: false, error: `${type} coordinates must be an array` };
728
+ }
729
+ for (let i = 0; i < coords.length; i++) {
730
+ const result = validatePosition(coords[i]);
731
+ if (!result.valid) {
732
+ return { valid: false, error: `${type}[${i}]: ${result.error}` };
733
+ }
734
+ }
735
+ return { valid: true };
736
+ case 'Polygon':
737
+ case 'MultiLineString':
738
+ if (!Array.isArray(coords)) {
739
+ return { valid: false, error: `${type} coordinates must be a nested array` };
740
+ }
741
+ for (let i = 0; i < coords.length; i++) {
742
+ if (!Array.isArray(coords[i])) {
743
+ return { valid: false, error: `${type}[${i}] must be an array of positions` };
744
+ }
745
+ for (let j = 0; j < coords[i].length; j++) {
746
+ const result = validatePosition(coords[i][j]);
747
+ if (!result.valid) {
748
+ return { valid: false, error: `${type}[${i}][${j}]: ${result.error}` };
749
+ }
750
+ }
751
+ }
752
+ return { valid: true };
753
+ case 'MultiPolygon':
754
+ if (!Array.isArray(coords)) {
755
+ return { valid: false, error: `${type} coordinates must be a deeply nested array` };
756
+ }
757
+ for (let i = 0; i < coords.length; i++) {
758
+ if (!Array.isArray(coords[i])) {
759
+ return { valid: false, error: `${type}[${i}] must be an array of rings` };
760
+ }
761
+ for (let j = 0; j < coords[i].length; j++) {
762
+ if (!Array.isArray(coords[i][j])) {
763
+ return { valid: false, error: `${type}[${i}][${j}] must be an array of positions` };
764
+ }
765
+ for (let k = 0; k < coords[i][j].length; k++) {
766
+ const result = validatePosition(coords[i][j][k]);
767
+ if (!result.valid) {
768
+ return { valid: false, error: `${type}[${i}][${j}][${k}]: ${result.error}` };
769
+ }
770
+ }
771
+ }
772
+ }
773
+ return { valid: true };
774
+ default:
775
+ return { valid: true };
776
+ }
777
+ }
778
+ function validatePosition(pos) {
779
+ if (!Array.isArray(pos)) {
780
+ return { valid: false, error: 'Position must be an array of numbers' };
781
+ }
782
+ if (pos.length < 2 || pos.length > 3) {
783
+ return { valid: false, error: `Position must have 2 or 3 coordinates, got ${pos.length}` };
784
+ }
785
+ for (let i = 0; i < pos.length; i++) {
786
+ if (typeof pos[i] !== 'number' || isNaN(pos[i])) {
787
+ return { valid: false, error: `Position[${i}] must be a valid number` };
788
+ }
789
+ }
790
+ return { valid: true };
791
+ }
792
+ /**
793
+ * 尝试从可能不规范的 WKT 中恢复出有效结果
794
+ * 主要处理尾部多余字符的情况
795
+ */
796
+ function tryFixWKT(wkt) {
797
+ const trimmed = wkt.trim();
798
+ if (!trimmed) {
799
+ return { fixed: wkt, changed: false };
800
+ }
801
+ // 检查是否有尾部多余字符
802
+ const result = validateWKT(trimmed);
803
+ if (result.valid) {
804
+ return { fixed: trimmed, changed: false };
805
+ }
806
+ // 尝试找到最后一个有效的 geometry 结束位置
807
+ const patterns = [
808
+ /\)\s*[A-Z]/i, // 括号后跟字母 (如 POLYGON ((...)) POINT )
809
+ /EMPTY\s+[A-Z]/i, // EMPTY 后跟字母
810
+ /\)\s*$/, // 括号结尾后有多余内容
811
+ ];
812
+ for (const pattern of patterns) {
813
+ const match = trimmed.match(pattern);
814
+ if (match) {
815
+ const fixed = trimmed.slice(0, match.index + (match[0].match(/\)/)?.[0].length || 0));
816
+ if (validateWKT(fixed).valid) {
817
+ return { fixed, changed: true };
818
+ }
819
+ }
820
+ }
821
+ // 尝试去除尾部垃圾字符
822
+ const lastValidIndex = findLastValidPosition(trimmed);
823
+ if (lastValidIndex > 0) {
824
+ const fixed = trimmed.slice(0, lastValidIndex + 1);
825
+ if (validateWKT(fixed).valid) {
826
+ return { fixed, changed: true };
827
+ }
828
+ }
829
+ return { fixed: wkt, changed: false };
830
+ }
831
+ function findLastValidPosition(wkt) {
832
+ // 从后往前找第一个有效的右括号位置
833
+ let depth = 0;
834
+ for (let i = wkt.length - 1; i >= 0; i--) {
835
+ const c = wkt[i];
836
+ if (c === ')')
837
+ depth++;
838
+ else if (c === '(')
839
+ depth--;
840
+ else if (c === ' ' && depth === 0 && i < wkt.length - 1) {
841
+ // 检查这个空格是否在有效位置
842
+ const afterSpace = wkt.slice(i + 1).trim();
843
+ if (!afterSpace)
844
+ continue;
845
+ if (!/^[A-Z]/.test(afterSpace))
846
+ continue;
847
+ // 如果空格后面是字母开头,可能是垃圾字符
848
+ if (i > 5 && /[A-Z]$/.test(wkt.slice(0, i).trim())) {
849
+ return i - 1;
850
+ }
851
+ }
852
+ }
853
+ return wkt.length - 1;
854
+ }
855
+ /**
856
+ * 深度克隆 GeoJSON 对象(用于避免意外修改原对象)
857
+ */
858
+ function cloneGeometry(geometry) {
859
+ return JSON.parse(JSON.stringify(geometry));
860
+ }
861
+ /**
862
+ * 判断两个几何对象是否相等(坐标对比)
863
+ */
864
+ function geometryEquals(a, b) {
865
+ if (a.type !== b.type)
866
+ return false;
867
+ return JSON.stringify(a) === JSON.stringify(b);
868
+ }
869
+
660
870
  exports.GeoJSONBuilder = GeoJSONBuilder;
661
871
  exports.WKTBuilder = WKTBuilder;
662
872
  exports.WKTParser = WKTParser;
663
873
  exports.build = build;
874
+ exports.cloneGeometry = cloneGeometry;
664
875
  exports.createGeometryCollection = createGeometryCollection;
665
876
  exports.createLineString = createLineString;
666
877
  exports.createMultiLineString = createMultiLineString;
@@ -671,7 +882,11 @@
671
882
  exports.featureCollectionToWkt = featureCollectionToWkt;
672
883
  exports.featureToWkt = featureToWkt;
673
884
  exports.geojsonToWkt = geojsonToWkt;
885
+ exports.geometryEquals = geometryEquals;
674
886
  exports.parse = parse;
887
+ exports.tryFixWKT = tryFixWKT;
888
+ exports.validateGeoJSON = validateGeoJSON;
889
+ exports.validateWKT = validateWKT;
675
890
  exports.wktToFeature = wktToFeature;
676
891
  exports.wktToFeatureCollection = wktToFeatureCollection;
677
892
  exports.wktToGeoJSON = wktToGeoJSON;
@@ -0,0 +1,29 @@
1
+ import { Geometry } from './types';
2
+ export interface ValidationResult {
3
+ valid: boolean;
4
+ error?: string;
5
+ }
6
+ /**
7
+ * 校验 WKT 字符串格式是否合法
8
+ */
9
+ export declare function validateWKT(wkt: string): ValidationResult;
10
+ /**
11
+ * 校验 GeoJSON Geometry 对象是否合法
12
+ */
13
+ export declare function validateGeoJSON(geojson: unknown): ValidationResult;
14
+ /**
15
+ * 尝试从可能不规范的 WKT 中恢复出有效结果
16
+ * 主要处理尾部多余字符的情况
17
+ */
18
+ export declare function tryFixWKT(wkt: string): {
19
+ fixed: string;
20
+ changed: boolean;
21
+ };
22
+ /**
23
+ * 深度克隆 GeoJSON 对象(用于避免意外修改原对象)
24
+ */
25
+ export declare function cloneGeometry<G extends Geometry>(geometry: G): G;
26
+ /**
27
+ * 判断两个几何对象是否相等(坐标对比)
28
+ */
29
+ export declare function geometryEquals(a: Geometry, b: Geometry): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wkt-parse-and-geojson",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "type": "module",
5
5
  "description": "Zero-dependency WKT parser/builder and WKT↔GeoJSON converter",
6
6
  "main": "dist/index.cjs.js",
@@ -41,5 +41,8 @@
41
41
  "rollup": "^4.40.0",
42
42
  "tslib": "^2.8.1",
43
43
  "typescript": "^5.8.3"
44
+ },
45
+ "dependencies": {
46
+ "wkt-parse-and-geojson": "^1.0.3"
44
47
  }
45
48
  }