wkt-parse-and-geojson 1.0.3 → 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/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
@@ -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
@@ -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
@@ -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.3",
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
  }