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 +79 -0
- package/dist/index.cjs.js +215 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +211 -1
- package/dist/index.umd.js +215 -0
- package/dist/validate.d.ts +29 -0
- package/package.json +4 -1
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
|
-
|
|
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
|
+
"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
|
}
|