giser-convert-crs 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,231 @@
1
+ # giserConvertCrs
2
+
3
+ `giserConvertCrs` 是一个几何坐标系转换工具库,支持 WGS-84、GCJ-02、BD-09 之间的点、线、面、数组、GeoJSON 和 WKT 转换。
4
+
5
+ > 注意:npm 包名需要保持小写,因此发布包名仍为 `giser-convert-crs`;代码变量、浏览器全局变量、构建文件名统一使用 `giserConvertCrs`。
6
+
7
+ ## 坐标系写法
8
+
9
+ 所有转换方法的 `fromCRS` 和 `toCRS` 都支持下面这些写法:
10
+
11
+ | 坐标系 | 可用写法 |
12
+ | --- | --- |
13
+ | WGS-84 | `wgs84`, `wgs-84`, `EPSG:4326`, `epsg:4326` |
14
+ | GCJ-02 | `gcj02`, `gcj-02` |
15
+ | BD-09 | `bd09ll`, `bd09`, `bd-09` |
16
+
17
+ ```js
18
+ giserConvertCrs.transform([116.397, 39.908], 'wgs84', 'gcj02');
19
+ giserConvertCrs.transform([116.397, 39.908], 'EPSG:4326', 'bd09');
20
+ giserConvertCrs.transform([116.397, 39.908], 'bd-09', 'wgs84');
21
+ ```
22
+
23
+ 坐标顺序固定为 `[lon, lat]`,即经度在前、纬度在后。三维坐标也支持,例如 `[lon, lat, height]`,转换后会保留高度值。
24
+
25
+ ## 支持的输入
26
+
27
+ ### 单个点
28
+
29
+ ```js
30
+ [116.397, 39.908]
31
+ [116.397, 39.908, 45]
32
+ ```
33
+
34
+ ### 点数组
35
+
36
+ ```js
37
+ [
38
+ [116.397, 39.908],
39
+ [117.2, 40.1]
40
+ ]
41
+ ```
42
+
43
+ ### GeoJSON
44
+
45
+ `giserConvertCrs.transformGeoJSON(geojson, 'wgs84', 'gcj02')` 里的 `geojson` 是标准 GeoJSON 几何对象、Feature 或 FeatureCollection。
46
+
47
+ Point:
48
+
49
+ ```js
50
+ const geojson = {
51
+ type: 'Point',
52
+ coordinates: [116.397, 39.908]
53
+ };
54
+
55
+ giserConvertCrs.transformGeoJSON(geojson, 'wgs84', 'gcj02');
56
+ ```
57
+
58
+ LineString:
59
+
60
+ ```js
61
+ const geojson = {
62
+ type: 'LineString',
63
+ coordinates: [
64
+ [116.397, 39.908],
65
+ [117.2, 40.1]
66
+ ]
67
+ };
68
+
69
+ giserConvertCrs.transformGeoJSON(geojson, 'wgs84', 'gcj02');
70
+ ```
71
+
72
+ Polygon:
73
+
74
+ ```js
75
+ const geojson = {
76
+ type: 'Polygon',
77
+ coordinates: [[
78
+ [116.0, 39.9],
79
+ [116.1, 39.9],
80
+ [116.1, 40.0],
81
+ [116.0, 39.9]
82
+ ]]
83
+ };
84
+
85
+ giserConvertCrs.transformGeoJSON(geojson, 'wgs84', 'gcj02');
86
+ ```
87
+
88
+ FeatureCollection:
89
+
90
+ ```js
91
+ const geojson = {
92
+ type: 'FeatureCollection',
93
+ features: [
94
+ {
95
+ type: 'Feature',
96
+ properties: { name: '点' },
97
+ geometry: {
98
+ type: 'Point',
99
+ coordinates: [116.397, 39.908]
100
+ }
101
+ },
102
+ {
103
+ type: 'Feature',
104
+ properties: { name: '线' },
105
+ geometry: {
106
+ type: 'LineString',
107
+ coordinates: [
108
+ [116.397, 39.908],
109
+ [117.2, 40.1]
110
+ ]
111
+ }
112
+ }
113
+ ]
114
+ };
115
+
116
+ giserConvertCrs.transformGeoJSON(geojson, 'wgs84', 'gcj02');
117
+ ```
118
+
119
+ 还支持 `MultiPoint`、`MultiLineString`、`MultiPolygon`、`GeometryCollection`。
120
+
121
+ ### WKT
122
+
123
+ ```js
124
+ 'POINT(116.397 39.908)'
125
+ 'LINESTRING(116 39, 117 40)'
126
+ 'POLYGON((116 39, 117 39, 117 40, 116 39))'
127
+ 'MULTIPOINT((116 39), (117 40))'
128
+ 'MULTILINESTRING((116 39, 117 40), (118 41, 119 42))'
129
+ 'MULTIPOLYGON(((116 39, 117 39, 117 40, 116 39)))'
130
+ ```
131
+
132
+ ## 安装
133
+
134
+ ```bash
135
+ npm install giser-convert-crs
136
+ ```
137
+
138
+ ## 引入方式
139
+
140
+ ### ES Module
141
+
142
+ ```js
143
+ import giserConvertCrs, { WKT, GPS } from 'giser-convert-crs';
144
+
145
+ const point = giserConvertCrs.transform([116.397, 39.908], 'wgs84', 'gcj02');
146
+ const geometry = WKT.parse('LINESTRING(116 39, 117 40)');
147
+ const bd09 = GPS.wgs84_bd09ll(116.397, 39.908);
148
+ ```
149
+
150
+ ### Script
151
+
152
+ ```html
153
+ <script src="https://unpkg.com/giser-convert-crs/dist/giserConvertCrs.umd.js"></script>
154
+ <script>
155
+ const point = giserConvertCrs.transform([116.397, 39.908], 'wgs84', 'gcj02');
156
+ </script>
157
+ ```
158
+
159
+ ## API
160
+
161
+ ### `giserConvertCrs.transform(input, fromCRS, toCRS)`
162
+
163
+ 统一转换入口,会根据输入类型自动处理:
164
+
165
+ - 输入坐标数组时,返回坐标数组。
166
+ - 输入 GeoJSON 时,返回同结构的新 GeoJSON。
167
+ - 输入 WKT 字符串时,返回 WKT 字符串。
168
+
169
+ ```js
170
+ giserConvertCrs.transform([116.397, 39.908], 'wgs84', 'gcj02');
171
+ giserConvertCrs.transform([[116.397, 39.908], [117.2, 40.1]], 'wgs84', 'bd09ll');
172
+ giserConvertCrs.transform('LINESTRING(116 39, 117 40)', 'wgs84', 'gcj02');
173
+ ```
174
+
175
+ ### `giserConvertCrs.transformPoint(coord, fromCRS, toCRS)`
176
+
177
+ ```js
178
+ giserConvertCrs.transformPoint([116.397, 39.908], 'wgs84', 'gcj02');
179
+ ```
180
+
181
+ ### `giserConvertCrs.transformPoints(points, fromCRS, toCRS)`
182
+
183
+ ```js
184
+ giserConvertCrs.transformPoints([[116.397, 39.908], [117.2, 40.1]], 'wgs84', 'gcj02');
185
+ ```
186
+
187
+ ### `giserConvertCrs.transformGeoJSON(geojson, fromCRS, toCRS)`
188
+
189
+ ```js
190
+ const result = giserConvertCrs.transformGeoJSON(geojson, 'wgs84', 'gcj02');
191
+ ```
192
+
193
+ ### `giserConvertCrs.transformWKT(wkt, fromCRS, toCRS)`
194
+
195
+ ```js
196
+ giserConvertCrs.transformWKT('POINT(116.397 39.908)', 'wgs84', 'gcj02');
197
+ giserConvertCrs.transformWKT('POLYGON((116 39, 117 39, 117 40, 116 39))', 'wgs84', 'gcj02');
198
+ ```
199
+
200
+ ### `giserConvertCrs.WKT`
201
+
202
+ ```js
203
+ const geojson = giserConvertCrs.WKT.parse('MULTIPOINT((116 39), (117 40))');
204
+ const wkt = giserConvertCrs.WKT.stringify({
205
+ type: 'LineString',
206
+ coordinates: [[116, 39, 10], [117, 40, 20]]
207
+ });
208
+ ```
209
+
210
+ ### `giserConvertCrs.GPS`
211
+
212
+ ```js
213
+ giserConvertCrs.GPS.wgs84_gcj02(116.397, 39.908);
214
+ giserConvertCrs.GPS.gcj02_wgs84(116.403243, 39.909403);
215
+ giserConvertCrs.GPS.gcj02_wgs84_precise(116.403243, 39.909403);
216
+ giserConvertCrs.GPS.gcj02_bd09ll(116.403243, 39.909403);
217
+ giserConvertCrs.GPS.bd09ll_gcj02(116.409, 39.915);
218
+ giserConvertCrs.GPS.wgs84_bd09ll(116.397, 39.908);
219
+ giserConvertCrs.GPS.bd09ll_wgs84(116.409, 39.915);
220
+ ```
221
+
222
+ ## 注意事项
223
+
224
+ 1. 坐标顺序固定为 `[lon, lat]`。
225
+ 2. WGS-84 和 GCJ-02 的加偏转换只对中国范围内坐标生效,中国境外坐标会原样返回。
226
+ 3. `gcj02_wgs84` 是常规近似反算,`gcj02_wgs84_precise` 是更高精度反算。
227
+ 4. 不支持的坐标系会抛出错误,例如 `epsg:3857`。
228
+
229
+ ## License
230
+
231
+ MIT
@@ -0,0 +1,487 @@
1
+ const PI = Math.PI;
2
+ const X_PI = PI * 3000.0 / 180.0;
3
+ const SEMI_MAJOR_AXIS = 6378245.0;
4
+ const ECCENTRICITY_SQUARED = 0.00669342162296594323;
5
+
6
+ const GPS = {
7
+ PI,
8
+ x_pi: X_PI,
9
+
10
+ delta(lat, lon) {
11
+ let dLat = this.transformLat(lon - 105.0, lat - 35.0);
12
+ let dLon = this.transformLon(lon - 105.0, lat - 35.0);
13
+ const radLat = lat / 180.0 * PI;
14
+ const sinLat = Math.sin(radLat);
15
+ const magic = 1 - ECCENTRICITY_SQUARED * sinLat * sinLat;
16
+ const sqrtMagic = Math.sqrt(magic);
17
+
18
+ dLat = (dLat * 180.0) / ((SEMI_MAJOR_AXIS * (1 - ECCENTRICITY_SQUARED)) / (magic * sqrtMagic) * PI);
19
+ dLon = (dLon * 180.0) / (SEMI_MAJOR_AXIS / sqrtMagic * Math.cos(radLat) * PI);
20
+
21
+ return { lat: dLat, lon: dLon };
22
+ },
23
+
24
+ wgs84_gcj02(wgsLon, wgsLat) {
25
+ this._assertLonLat(wgsLon, wgsLat);
26
+ if (this.outOfChina(wgsLat, wgsLon)) return [wgsLon, wgsLat];
27
+
28
+ const d = this.delta(wgsLat, wgsLon);
29
+ return [wgsLon + d.lon, wgsLat + d.lat];
30
+ },
31
+
32
+ gcj02_wgs84(gcjLon, gcjLat) {
33
+ this._assertLonLat(gcjLon, gcjLat);
34
+ if (this.outOfChina(gcjLat, gcjLon)) return [gcjLon, gcjLat];
35
+
36
+ const d = this.delta(gcjLat, gcjLon);
37
+ return [gcjLon - d.lon, gcjLat - d.lat];
38
+ },
39
+
40
+ gcj02_wgs84_precise(gcjLon, gcjLat) {
41
+ this._assertLonLat(gcjLon, gcjLat);
42
+ if (this.outOfChina(gcjLat, gcjLon)) return [gcjLon, gcjLat];
43
+
44
+ const threshold = 0.000000001;
45
+ let minLat = gcjLat - 0.01;
46
+ let minLon = gcjLon - 0.01;
47
+ let maxLat = gcjLat + 0.01;
48
+ let maxLon = gcjLon + 0.01;
49
+ let wgsLat = gcjLat;
50
+ let wgsLon = gcjLon;
51
+
52
+ for (let i = 0; i < 10000; i++) {
53
+ wgsLat = (minLat + maxLat) / 2;
54
+ wgsLon = (minLon + maxLon) / 2;
55
+ const [tmpLon, tmpLat] = this.wgs84_gcj02(wgsLon, wgsLat);
56
+ const dLat = tmpLat - gcjLat;
57
+ const dLon = tmpLon - gcjLon;
58
+
59
+ if (Math.abs(dLat) < threshold && Math.abs(dLon) < threshold) break;
60
+ if (dLat > 0) maxLat = wgsLat;
61
+ else minLat = wgsLat;
62
+ if (dLon > 0) maxLon = wgsLon;
63
+ else minLon = wgsLon;
64
+ }
65
+
66
+ return [wgsLon, wgsLat];
67
+ },
68
+
69
+ gcj02_bd09ll(gcjLon, gcjLat) {
70
+ this._assertLonLat(gcjLon, gcjLat);
71
+ const z = Math.sqrt(gcjLon * gcjLon + gcjLat * gcjLat) + 0.00002 * Math.sin(gcjLat * X_PI);
72
+ const theta = Math.atan2(gcjLat, gcjLon) + 0.000003 * Math.cos(gcjLon * X_PI);
73
+ return [z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006];
74
+ },
75
+
76
+ bd09ll_gcj02(bdLon, bdLat) {
77
+ this._assertLonLat(bdLon, bdLat);
78
+ const x = bdLon - 0.0065;
79
+ const y = bdLat - 0.006;
80
+ const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);
81
+ const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);
82
+ return [z * Math.cos(theta), z * Math.sin(theta)];
83
+ },
84
+
85
+ wgs84_bd09ll(wgsLon, wgsLat) {
86
+ const gcj = this.wgs84_gcj02(wgsLon, wgsLat);
87
+ return this.gcj02_bd09ll(gcj[0], gcj[1]);
88
+ },
89
+
90
+ bd09ll_wgs84(bdLon, bdLat) {
91
+ const gcj = this.bd09ll_gcj02(bdLon, bdLat);
92
+ return this.gcj02_wgs84(gcj[0], gcj[1]);
93
+ },
94
+
95
+ bd09ll_wgs84_precise(bdLon, bdLat) {
96
+ const gcj = this.bd09ll_gcj02(bdLon, bdLat);
97
+ return this.gcj02_wgs84_precise(gcj[0], gcj[1]);
98
+ },
99
+
100
+ outOfChina(lat, lon) {
101
+ return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
102
+ },
103
+
104
+ transformLat(x, y) {
105
+ let ret = -100 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
106
+ ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
107
+ ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
108
+ ret += (160.0 * Math.sin(y / 12.0 * PI) + 320.0 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
109
+ return ret;
110
+ },
111
+
112
+ transformLon(x, y) {
113
+ let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
114
+ ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
115
+ ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
116
+ ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
117
+ return ret;
118
+ },
119
+
120
+ gcj_encrypt(wgsLat, wgsLon) {
121
+ const [lon, lat] = this.wgs84_gcj02(wgsLon, wgsLat);
122
+ return { lat, lon };
123
+ },
124
+
125
+ _assertLonLat(lon, lat) {
126
+ if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
127
+ throw new Error('Invalid coordinate: longitude and latitude must be finite numbers.');
128
+ }
129
+ }
130
+ };
131
+
132
+ const crs = {
133
+ bd09ll: '+proj=longlat +datum=BD09',
134
+ gcj02: '+proj=longlat +datum=GCJ02',
135
+ wgs84: '+proj=longlat +datum=WGS84 +no_defs'
136
+ };
137
+
138
+ const CRS_ALIASES = {
139
+ 'bd-09': 'bd09ll',
140
+ 'bd09': 'bd09ll',
141
+ 'bd09ll': 'bd09ll',
142
+ 'epsg:4326': 'wgs84',
143
+ 'gcj-02': 'gcj02',
144
+ 'gcj02': 'gcj02',
145
+ 'wgs-84': 'wgs84',
146
+ 'wgs84': 'wgs84'
147
+ };
148
+
149
+ const ProjectionTransform = {
150
+ crs,
151
+
152
+ transform(source, fromCRS, toCRS) {
153
+ if (source == null) return null;
154
+
155
+ const from = this._normalizeCRS(fromCRS);
156
+ const to = this._normalizeCRS(toCRS);
157
+
158
+ if (this._isCoord(source)) {
159
+ return this._transformCoordinate(source, from, to);
160
+ }
161
+
162
+ if (Array.isArray(source)) {
163
+ return this._transformCoordinateTree(source, from, to);
164
+ }
165
+
166
+ return this._transformGeoJSON(source, from, to);
167
+ },
168
+
169
+ _transformGeoJSON(geoJSON, fromCRS, toCRS) {
170
+ if (!geoJSON || typeof geoJSON !== 'object') {
171
+ throw new Error('Invalid GeoJSON object.');
172
+ }
173
+
174
+ if (geoJSON.type === 'Feature') {
175
+ return {
176
+ ...geoJSON,
177
+ geometry: geoJSON.geometry ? this.transform(geoJSON.geometry, fromCRS, toCRS) : null
178
+ };
179
+ }
180
+
181
+ if (geoJSON.type === 'FeatureCollection') {
182
+ if (!Array.isArray(geoJSON.features)) throw new Error('Invalid FeatureCollection: features must be an array.');
183
+ return {
184
+ ...geoJSON,
185
+ features: geoJSON.features.map(feature => this.transform(feature, fromCRS, toCRS))
186
+ };
187
+ }
188
+
189
+ if (geoJSON.type === 'GeometryCollection') {
190
+ if (!Array.isArray(geoJSON.geometries)) throw new Error('Invalid GeometryCollection: geometries must be an array.');
191
+ return {
192
+ ...geoJSON,
193
+ geometries: geoJSON.geometries.map(geometry => this.transform(geometry, fromCRS, toCRS))
194
+ };
195
+ }
196
+
197
+ if ('coordinates' in geoJSON) {
198
+ return {
199
+ ...geoJSON,
200
+ coordinates: this._transformCoordinateTree(geoJSON.coordinates, fromCRS, toCRS)
201
+ };
202
+ }
203
+
204
+ throw new Error(`Unsupported GeoJSON type: ${geoJSON.type || 'unknown'}.`);
205
+ },
206
+
207
+ _transformCoordinateTree(coordinates, fromCRS, toCRS) {
208
+ if (coordinates == null) return coordinates;
209
+ if (this._isCoord(coordinates)) return this._transformCoordinate(coordinates, fromCRS, toCRS);
210
+ if (!Array.isArray(coordinates)) throw new Error('Invalid coordinates: expected coordinate array.');
211
+ return coordinates.map(item => this._transformCoordinateTree(item, fromCRS, toCRS));
212
+ },
213
+
214
+ _transformCoordinate(coordinate, fromCRS, toCRS) {
215
+ const from = this._normalizeCRS(fromCRS);
216
+ const to = this._normalizeCRS(toCRS);
217
+ const lon = Number(coordinate[0]);
218
+ const lat = Number(coordinate[1]);
219
+
220
+ if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
221
+ throw new Error('Invalid coordinate: longitude and latitude must be finite numbers.');
222
+ }
223
+
224
+ if (from === to) return [lon, lat, ...coordinate.slice(2)];
225
+
226
+ const method = `${from}_${to}`;
227
+ if (typeof GPS[method] !== 'function') {
228
+ throw new Error(`Unsupported CRS transform: ${fromCRS} -> ${toCRS}.`);
229
+ }
230
+
231
+ const transformed = GPS[method](lon, lat);
232
+ return [transformed[0], transformed[1], ...coordinate.slice(2)];
233
+ },
234
+
235
+ _normalizeCRS(value) {
236
+ if (!value) throw new Error('must provide a valid fromCRS and toCRS.');
237
+
238
+ if (typeof value === 'object') {
239
+ if (value.type === 'proj4' && value.properties && value.properties.proj) {
240
+ const crsName = this._toCRS(value.properties.proj);
241
+ if (crsName) return crsName;
242
+ }
243
+ throw new Error('Unsupported CRS object.');
244
+ }
245
+
246
+ const key = String(value).trim().toLowerCase();
247
+ const normalized = CRS_ALIASES[key];
248
+ if (!normalized) throw new Error(`Unsupported CRS: ${value}.`);
249
+ return normalized;
250
+ },
251
+
252
+ _toCRS(proj) {
253
+ for (const key in this.crs) {
254
+ if (proj === this.crs[key]) return key;
255
+ }
256
+ return null;
257
+ },
258
+
259
+ _isCoord(coordinate) {
260
+ return Array.isArray(coordinate) &&
261
+ coordinate.length >= 2 &&
262
+ Number.isFinite(Number(coordinate[0])) &&
263
+ Number.isFinite(Number(coordinate[1])) &&
264
+ !Array.isArray(coordinate[0]) &&
265
+ !Array.isArray(coordinate[1]);
266
+ }
267
+ };
268
+
269
+ const TYPE_MAP = {
270
+ POINT: 'Point',
271
+ LINESTRING: 'LineString',
272
+ POLYGON: 'Polygon',
273
+ MULTIPOINT: 'MultiPoint',
274
+ MULTILINESTRING: 'MultiLineString',
275
+ MULTIPOLYGON: 'MultiPolygon'
276
+ };
277
+
278
+ const REVERSE_TYPE_MAP = Object.fromEntries(
279
+ Object.entries(TYPE_MAP).map(([wktType, geoJSONType]) => [geoJSONType, wktType])
280
+ );
281
+
282
+ const WKT = {
283
+ parse(wkt) {
284
+ if (typeof wkt !== 'string' || !wkt.trim()) {
285
+ throw new Error('Invalid WKT string.');
286
+ }
287
+
288
+ const source = wkt.trim();
289
+ const header = source.match(/^([A-Za-z]+)(?:\s+(ZM|Z|M))?\s*/i);
290
+ if (!header) throw new Error('Invalid WKT string.');
291
+
292
+ const wktType = header[1].toUpperCase();
293
+ const geoJSONType = TYPE_MAP[wktType];
294
+ if (!geoJSONType) throw new Error(`Unsupported WKT type: ${wktType}.`);
295
+
296
+ const body = source.slice(header[0].length).trim();
297
+ if (/^EMPTY$/i.test(body)) {
298
+ return { type: geoJSONType, coordinates: this._emptyCoordinates(geoJSONType) };
299
+ }
300
+
301
+ if (!body.startsWith('(') || !body.endsWith(')')) {
302
+ throw new Error('Invalid WKT string: missing coordinate parentheses.');
303
+ }
304
+
305
+ const content = this._stripOuterParens(body);
306
+ return {
307
+ type: geoJSONType,
308
+ coordinates: this._parseCoordinates(content, geoJSONType)
309
+ };
310
+ },
311
+
312
+ stringify(geoJSON) {
313
+ if (!geoJSON || typeof geoJSON !== 'object' || !geoJSON.type) {
314
+ throw new Error('Invalid GeoJSON.');
315
+ }
316
+
317
+ const wktType = REVERSE_TYPE_MAP[geoJSON.type];
318
+ if (!wktType) throw new Error(`Unsupported geometry type: ${geoJSON.type}.`);
319
+
320
+ const coords = geoJSON.coordinates;
321
+ if (this._isEmpty(coords)) return `${wktType} EMPTY`;
322
+
323
+ return `${wktType}(${this._stringifyCoordinates(coords, geoJSON.type)})`;
324
+ },
325
+
326
+ _parseCoordinates(content, type) {
327
+ switch (type) {
328
+ case 'Point':
329
+ return this._parsePoint(content);
330
+ case 'LineString':
331
+ return this._parsePointList(content);
332
+ case 'Polygon':
333
+ return this._parsePolygon(content);
334
+ case 'MultiPoint':
335
+ return this._parseMultiPoint(content);
336
+ case 'MultiLineString':
337
+ return this._splitTopLevel(content).map(line => this._parsePointList(this._stripOptionalOuterParens(line)));
338
+ case 'MultiPolygon':
339
+ return this._splitTopLevel(content).map(poly => this._parsePolygon(this._stripOptionalOuterParens(poly)));
340
+ default:
341
+ throw new Error(`Unsupported geometry type: ${type}.`);
342
+ }
343
+ },
344
+
345
+ _parsePoint(value) {
346
+ const numbers = value.trim().split(/\s+/).filter(Boolean).map(Number);
347
+ if (numbers.length < 2 || numbers.some(n => !Number.isFinite(n))) {
348
+ throw new Error(`Invalid WKT coordinate: ${value}.`);
349
+ }
350
+ return numbers;
351
+ },
352
+
353
+ _parsePointList(value) {
354
+ const points = this._splitTopLevel(value).map(part => this._parsePoint(this._stripOptionalOuterParens(part)));
355
+ if (!points.length) throw new Error('Invalid WKT coordinate list.');
356
+ return points;
357
+ },
358
+
359
+ _parsePolygon(value) {
360
+ const rings = this._splitTopLevel(value).map(ring => this._parsePointList(this._stripOptionalOuterParens(ring)));
361
+ if (!rings.length) throw new Error('Invalid WKT polygon.');
362
+ return rings;
363
+ },
364
+
365
+ _parseMultiPoint(value) {
366
+ return this._splitTopLevel(value).map(point => this._parsePoint(this._stripOptionalOuterParens(point)));
367
+ },
368
+
369
+ _splitTopLevel(value) {
370
+ const parts = [];
371
+ let depth = 0;
372
+ let start = 0;
373
+
374
+ for (let i = 0; i < value.length; i++) {
375
+ const char = value[i];
376
+ if (char === '(') depth++;
377
+ else if (char === ')') depth--;
378
+
379
+ if (depth < 0) throw new Error('Invalid WKT string: unbalanced parentheses.');
380
+
381
+ if (char === ',' && depth === 0) {
382
+ parts.push(value.slice(start, i).trim());
383
+ start = i + 1;
384
+ }
385
+ }
386
+
387
+ if (depth !== 0) throw new Error('Invalid WKT string: unbalanced parentheses.');
388
+ const last = value.slice(start).trim();
389
+ if (last) parts.push(last);
390
+ return parts;
391
+ },
392
+
393
+ _stripOuterParens(value) {
394
+ const trimmed = value.trim();
395
+ if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) {
396
+ throw new Error('Invalid WKT string: missing coordinate parentheses.');
397
+ }
398
+ return trimmed.slice(1, -1).trim();
399
+ },
400
+
401
+ _stripOptionalOuterParens(value) {
402
+ const trimmed = value.trim();
403
+ if (!trimmed.startsWith('(')) return trimmed;
404
+ return this._stripOuterParens(trimmed);
405
+ },
406
+
407
+ _stringifyCoordinates(coords, type) {
408
+ switch (type) {
409
+ case 'Point':
410
+ return this._stringifyPoint(coords);
411
+ case 'LineString':
412
+ return this._stringifyPointList(coords);
413
+ case 'Polygon':
414
+ return this._stringifyPolygon(coords);
415
+ case 'MultiPoint':
416
+ return coords.map(point => `(${this._stringifyPoint(point)})`).join(', ');
417
+ case 'MultiLineString':
418
+ return coords.map(line => `(${this._stringifyPointList(line)})`).join(', ');
419
+ case 'MultiPolygon':
420
+ return coords.map(poly => `(${this._stringifyPolygon(poly)})`).join(', ');
421
+ default:
422
+ throw new Error(`Unsupported geometry type: ${type}.`);
423
+ }
424
+ },
425
+
426
+ _stringifyPoint(point) {
427
+ if (!Array.isArray(point) || point.length < 2) throw new Error('Invalid coordinate.');
428
+ return point.map(value => {
429
+ const number = Number(value);
430
+ if (!Number.isFinite(number)) throw new Error('Invalid coordinate value.');
431
+ return String(number);
432
+ }).join(' ');
433
+ },
434
+
435
+ _stringifyPointList(points) {
436
+ if (!Array.isArray(points)) throw new Error('Invalid coordinate list.');
437
+ return points.map(point => this._stringifyPoint(point)).join(', ');
438
+ },
439
+
440
+ _stringifyPolygon(rings) {
441
+ if (!Array.isArray(rings)) throw new Error('Invalid polygon coordinates.');
442
+ return rings.map(ring => `(${this._stringifyPointList(ring)})`).join(', ');
443
+ },
444
+
445
+ _emptyCoordinates(type) {
446
+ return type === 'Point' ? [] : [];
447
+ },
448
+
449
+ _isEmpty(coords) {
450
+ return Array.isArray(coords) && coords.length === 0;
451
+ }
452
+ };
453
+
454
+ const giserConvertCrs = {
455
+ GPS,
456
+ ProjectionTransform,
457
+ WKT,
458
+
459
+ transform(input, fromCRS, toCRS) {
460
+ if (input == null || input === '') return null;
461
+
462
+ if (typeof input === 'string') {
463
+ const geoJSON = WKT.parse(input);
464
+ return WKT.stringify(ProjectionTransform.transform(geoJSON, fromCRS, toCRS));
465
+ }
466
+
467
+ return ProjectionTransform.transform(input, fromCRS, toCRS);
468
+ },
469
+
470
+ transformPoint(coord, fromCRS, toCRS) {
471
+ return this.transform(coord, fromCRS, toCRS);
472
+ },
473
+
474
+ transformPoints(points, fromCRS, toCRS) {
475
+ return this.transform(points, fromCRS, toCRS);
476
+ },
477
+
478
+ transformGeoJSON(geoJSON, fromCRS, toCRS) {
479
+ return this.transform(geoJSON, fromCRS, toCRS);
480
+ },
481
+
482
+ transformWKT(wkt, fromCRS, toCRS) {
483
+ return this.transform(wkt, fromCRS, toCRS);
484
+ }
485
+ };
486
+
487
+ export { GPS, ProjectionTransform, WKT, giserConvertCrs as default };
@@ -0,0 +1,495 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3
+ typeof define === 'function' && define.amd ? define(factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.giserConvertCrs = factory());
5
+ })(this, (function () { 'use strict';
6
+
7
+ const PI = Math.PI;
8
+ const X_PI = PI * 3000.0 / 180.0;
9
+ const SEMI_MAJOR_AXIS = 6378245.0;
10
+ const ECCENTRICITY_SQUARED = 0.00669342162296594323;
11
+
12
+ const GPS = {
13
+ PI,
14
+ x_pi: X_PI,
15
+
16
+ delta(lat, lon) {
17
+ let dLat = this.transformLat(lon - 105.0, lat - 35.0);
18
+ let dLon = this.transformLon(lon - 105.0, lat - 35.0);
19
+ const radLat = lat / 180.0 * PI;
20
+ const sinLat = Math.sin(radLat);
21
+ const magic = 1 - ECCENTRICITY_SQUARED * sinLat * sinLat;
22
+ const sqrtMagic = Math.sqrt(magic);
23
+
24
+ dLat = (dLat * 180.0) / ((SEMI_MAJOR_AXIS * (1 - ECCENTRICITY_SQUARED)) / (magic * sqrtMagic) * PI);
25
+ dLon = (dLon * 180.0) / (SEMI_MAJOR_AXIS / sqrtMagic * Math.cos(radLat) * PI);
26
+
27
+ return { lat: dLat, lon: dLon };
28
+ },
29
+
30
+ wgs84_gcj02(wgsLon, wgsLat) {
31
+ this._assertLonLat(wgsLon, wgsLat);
32
+ if (this.outOfChina(wgsLat, wgsLon)) return [wgsLon, wgsLat];
33
+
34
+ const d = this.delta(wgsLat, wgsLon);
35
+ return [wgsLon + d.lon, wgsLat + d.lat];
36
+ },
37
+
38
+ gcj02_wgs84(gcjLon, gcjLat) {
39
+ this._assertLonLat(gcjLon, gcjLat);
40
+ if (this.outOfChina(gcjLat, gcjLon)) return [gcjLon, gcjLat];
41
+
42
+ const d = this.delta(gcjLat, gcjLon);
43
+ return [gcjLon - d.lon, gcjLat - d.lat];
44
+ },
45
+
46
+ gcj02_wgs84_precise(gcjLon, gcjLat) {
47
+ this._assertLonLat(gcjLon, gcjLat);
48
+ if (this.outOfChina(gcjLat, gcjLon)) return [gcjLon, gcjLat];
49
+
50
+ const threshold = 0.000000001;
51
+ let minLat = gcjLat - 0.01;
52
+ let minLon = gcjLon - 0.01;
53
+ let maxLat = gcjLat + 0.01;
54
+ let maxLon = gcjLon + 0.01;
55
+ let wgsLat = gcjLat;
56
+ let wgsLon = gcjLon;
57
+
58
+ for (let i = 0; i < 10000; i++) {
59
+ wgsLat = (minLat + maxLat) / 2;
60
+ wgsLon = (minLon + maxLon) / 2;
61
+ const [tmpLon, tmpLat] = this.wgs84_gcj02(wgsLon, wgsLat);
62
+ const dLat = tmpLat - gcjLat;
63
+ const dLon = tmpLon - gcjLon;
64
+
65
+ if (Math.abs(dLat) < threshold && Math.abs(dLon) < threshold) break;
66
+ if (dLat > 0) maxLat = wgsLat;
67
+ else minLat = wgsLat;
68
+ if (dLon > 0) maxLon = wgsLon;
69
+ else minLon = wgsLon;
70
+ }
71
+
72
+ return [wgsLon, wgsLat];
73
+ },
74
+
75
+ gcj02_bd09ll(gcjLon, gcjLat) {
76
+ this._assertLonLat(gcjLon, gcjLat);
77
+ const z = Math.sqrt(gcjLon * gcjLon + gcjLat * gcjLat) + 0.00002 * Math.sin(gcjLat * X_PI);
78
+ const theta = Math.atan2(gcjLat, gcjLon) + 0.000003 * Math.cos(gcjLon * X_PI);
79
+ return [z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006];
80
+ },
81
+
82
+ bd09ll_gcj02(bdLon, bdLat) {
83
+ this._assertLonLat(bdLon, bdLat);
84
+ const x = bdLon - 0.0065;
85
+ const y = bdLat - 0.006;
86
+ const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);
87
+ const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);
88
+ return [z * Math.cos(theta), z * Math.sin(theta)];
89
+ },
90
+
91
+ wgs84_bd09ll(wgsLon, wgsLat) {
92
+ const gcj = this.wgs84_gcj02(wgsLon, wgsLat);
93
+ return this.gcj02_bd09ll(gcj[0], gcj[1]);
94
+ },
95
+
96
+ bd09ll_wgs84(bdLon, bdLat) {
97
+ const gcj = this.bd09ll_gcj02(bdLon, bdLat);
98
+ return this.gcj02_wgs84(gcj[0], gcj[1]);
99
+ },
100
+
101
+ bd09ll_wgs84_precise(bdLon, bdLat) {
102
+ const gcj = this.bd09ll_gcj02(bdLon, bdLat);
103
+ return this.gcj02_wgs84_precise(gcj[0], gcj[1]);
104
+ },
105
+
106
+ outOfChina(lat, lon) {
107
+ return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
108
+ },
109
+
110
+ transformLat(x, y) {
111
+ let ret = -100 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
112
+ ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
113
+ ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
114
+ ret += (160.0 * Math.sin(y / 12.0 * PI) + 320.0 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
115
+ return ret;
116
+ },
117
+
118
+ transformLon(x, y) {
119
+ let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
120
+ ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
121
+ ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
122
+ ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
123
+ return ret;
124
+ },
125
+
126
+ gcj_encrypt(wgsLat, wgsLon) {
127
+ const [lon, lat] = this.wgs84_gcj02(wgsLon, wgsLat);
128
+ return { lat, lon };
129
+ },
130
+
131
+ _assertLonLat(lon, lat) {
132
+ if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
133
+ throw new Error('Invalid coordinate: longitude and latitude must be finite numbers.');
134
+ }
135
+ }
136
+ };
137
+
138
+ const crs = {
139
+ bd09ll: '+proj=longlat +datum=BD09',
140
+ gcj02: '+proj=longlat +datum=GCJ02',
141
+ wgs84: '+proj=longlat +datum=WGS84 +no_defs'
142
+ };
143
+
144
+ const CRS_ALIASES = {
145
+ 'bd-09': 'bd09ll',
146
+ 'bd09': 'bd09ll',
147
+ 'bd09ll': 'bd09ll',
148
+ 'epsg:4326': 'wgs84',
149
+ 'gcj-02': 'gcj02',
150
+ 'gcj02': 'gcj02',
151
+ 'wgs-84': 'wgs84',
152
+ 'wgs84': 'wgs84'
153
+ };
154
+
155
+ const ProjectionTransform = {
156
+ crs,
157
+
158
+ transform(source, fromCRS, toCRS) {
159
+ if (source == null) return null;
160
+
161
+ const from = this._normalizeCRS(fromCRS);
162
+ const to = this._normalizeCRS(toCRS);
163
+
164
+ if (this._isCoord(source)) {
165
+ return this._transformCoordinate(source, from, to);
166
+ }
167
+
168
+ if (Array.isArray(source)) {
169
+ return this._transformCoordinateTree(source, from, to);
170
+ }
171
+
172
+ return this._transformGeoJSON(source, from, to);
173
+ },
174
+
175
+ _transformGeoJSON(geoJSON, fromCRS, toCRS) {
176
+ if (!geoJSON || typeof geoJSON !== 'object') {
177
+ throw new Error('Invalid GeoJSON object.');
178
+ }
179
+
180
+ if (geoJSON.type === 'Feature') {
181
+ return {
182
+ ...geoJSON,
183
+ geometry: geoJSON.geometry ? this.transform(geoJSON.geometry, fromCRS, toCRS) : null
184
+ };
185
+ }
186
+
187
+ if (geoJSON.type === 'FeatureCollection') {
188
+ if (!Array.isArray(geoJSON.features)) throw new Error('Invalid FeatureCollection: features must be an array.');
189
+ return {
190
+ ...geoJSON,
191
+ features: geoJSON.features.map(feature => this.transform(feature, fromCRS, toCRS))
192
+ };
193
+ }
194
+
195
+ if (geoJSON.type === 'GeometryCollection') {
196
+ if (!Array.isArray(geoJSON.geometries)) throw new Error('Invalid GeometryCollection: geometries must be an array.');
197
+ return {
198
+ ...geoJSON,
199
+ geometries: geoJSON.geometries.map(geometry => this.transform(geometry, fromCRS, toCRS))
200
+ };
201
+ }
202
+
203
+ if ('coordinates' in geoJSON) {
204
+ return {
205
+ ...geoJSON,
206
+ coordinates: this._transformCoordinateTree(geoJSON.coordinates, fromCRS, toCRS)
207
+ };
208
+ }
209
+
210
+ throw new Error(`Unsupported GeoJSON type: ${geoJSON.type || 'unknown'}.`);
211
+ },
212
+
213
+ _transformCoordinateTree(coordinates, fromCRS, toCRS) {
214
+ if (coordinates == null) return coordinates;
215
+ if (this._isCoord(coordinates)) return this._transformCoordinate(coordinates, fromCRS, toCRS);
216
+ if (!Array.isArray(coordinates)) throw new Error('Invalid coordinates: expected coordinate array.');
217
+ return coordinates.map(item => this._transformCoordinateTree(item, fromCRS, toCRS));
218
+ },
219
+
220
+ _transformCoordinate(coordinate, fromCRS, toCRS) {
221
+ const from = this._normalizeCRS(fromCRS);
222
+ const to = this._normalizeCRS(toCRS);
223
+ const lon = Number(coordinate[0]);
224
+ const lat = Number(coordinate[1]);
225
+
226
+ if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
227
+ throw new Error('Invalid coordinate: longitude and latitude must be finite numbers.');
228
+ }
229
+
230
+ if (from === to) return [lon, lat, ...coordinate.slice(2)];
231
+
232
+ const method = `${from}_${to}`;
233
+ if (typeof GPS[method] !== 'function') {
234
+ throw new Error(`Unsupported CRS transform: ${fromCRS} -> ${toCRS}.`);
235
+ }
236
+
237
+ const transformed = GPS[method](lon, lat);
238
+ return [transformed[0], transformed[1], ...coordinate.slice(2)];
239
+ },
240
+
241
+ _normalizeCRS(value) {
242
+ if (!value) throw new Error('must provide a valid fromCRS and toCRS.');
243
+
244
+ if (typeof value === 'object') {
245
+ if (value.type === 'proj4' && value.properties && value.properties.proj) {
246
+ const crsName = this._toCRS(value.properties.proj);
247
+ if (crsName) return crsName;
248
+ }
249
+ throw new Error('Unsupported CRS object.');
250
+ }
251
+
252
+ const key = String(value).trim().toLowerCase();
253
+ const normalized = CRS_ALIASES[key];
254
+ if (!normalized) throw new Error(`Unsupported CRS: ${value}.`);
255
+ return normalized;
256
+ },
257
+
258
+ _toCRS(proj) {
259
+ for (const key in this.crs) {
260
+ if (proj === this.crs[key]) return key;
261
+ }
262
+ return null;
263
+ },
264
+
265
+ _isCoord(coordinate) {
266
+ return Array.isArray(coordinate) &&
267
+ coordinate.length >= 2 &&
268
+ Number.isFinite(Number(coordinate[0])) &&
269
+ Number.isFinite(Number(coordinate[1])) &&
270
+ !Array.isArray(coordinate[0]) &&
271
+ !Array.isArray(coordinate[1]);
272
+ }
273
+ };
274
+
275
+ const TYPE_MAP = {
276
+ POINT: 'Point',
277
+ LINESTRING: 'LineString',
278
+ POLYGON: 'Polygon',
279
+ MULTIPOINT: 'MultiPoint',
280
+ MULTILINESTRING: 'MultiLineString',
281
+ MULTIPOLYGON: 'MultiPolygon'
282
+ };
283
+
284
+ const REVERSE_TYPE_MAP = Object.fromEntries(
285
+ Object.entries(TYPE_MAP).map(([wktType, geoJSONType]) => [geoJSONType, wktType])
286
+ );
287
+
288
+ const WKT = {
289
+ parse(wkt) {
290
+ if (typeof wkt !== 'string' || !wkt.trim()) {
291
+ throw new Error('Invalid WKT string.');
292
+ }
293
+
294
+ const source = wkt.trim();
295
+ const header = source.match(/^([A-Za-z]+)(?:\s+(ZM|Z|M))?\s*/i);
296
+ if (!header) throw new Error('Invalid WKT string.');
297
+
298
+ const wktType = header[1].toUpperCase();
299
+ const geoJSONType = TYPE_MAP[wktType];
300
+ if (!geoJSONType) throw new Error(`Unsupported WKT type: ${wktType}.`);
301
+
302
+ const body = source.slice(header[0].length).trim();
303
+ if (/^EMPTY$/i.test(body)) {
304
+ return { type: geoJSONType, coordinates: this._emptyCoordinates(geoJSONType) };
305
+ }
306
+
307
+ if (!body.startsWith('(') || !body.endsWith(')')) {
308
+ throw new Error('Invalid WKT string: missing coordinate parentheses.');
309
+ }
310
+
311
+ const content = this._stripOuterParens(body);
312
+ return {
313
+ type: geoJSONType,
314
+ coordinates: this._parseCoordinates(content, geoJSONType)
315
+ };
316
+ },
317
+
318
+ stringify(geoJSON) {
319
+ if (!geoJSON || typeof geoJSON !== 'object' || !geoJSON.type) {
320
+ throw new Error('Invalid GeoJSON.');
321
+ }
322
+
323
+ const wktType = REVERSE_TYPE_MAP[geoJSON.type];
324
+ if (!wktType) throw new Error(`Unsupported geometry type: ${geoJSON.type}.`);
325
+
326
+ const coords = geoJSON.coordinates;
327
+ if (this._isEmpty(coords)) return `${wktType} EMPTY`;
328
+
329
+ return `${wktType}(${this._stringifyCoordinates(coords, geoJSON.type)})`;
330
+ },
331
+
332
+ _parseCoordinates(content, type) {
333
+ switch (type) {
334
+ case 'Point':
335
+ return this._parsePoint(content);
336
+ case 'LineString':
337
+ return this._parsePointList(content);
338
+ case 'Polygon':
339
+ return this._parsePolygon(content);
340
+ case 'MultiPoint':
341
+ return this._parseMultiPoint(content);
342
+ case 'MultiLineString':
343
+ return this._splitTopLevel(content).map(line => this._parsePointList(this._stripOptionalOuterParens(line)));
344
+ case 'MultiPolygon':
345
+ return this._splitTopLevel(content).map(poly => this._parsePolygon(this._stripOptionalOuterParens(poly)));
346
+ default:
347
+ throw new Error(`Unsupported geometry type: ${type}.`);
348
+ }
349
+ },
350
+
351
+ _parsePoint(value) {
352
+ const numbers = value.trim().split(/\s+/).filter(Boolean).map(Number);
353
+ if (numbers.length < 2 || numbers.some(n => !Number.isFinite(n))) {
354
+ throw new Error(`Invalid WKT coordinate: ${value}.`);
355
+ }
356
+ return numbers;
357
+ },
358
+
359
+ _parsePointList(value) {
360
+ const points = this._splitTopLevel(value).map(part => this._parsePoint(this._stripOptionalOuterParens(part)));
361
+ if (!points.length) throw new Error('Invalid WKT coordinate list.');
362
+ return points;
363
+ },
364
+
365
+ _parsePolygon(value) {
366
+ const rings = this._splitTopLevel(value).map(ring => this._parsePointList(this._stripOptionalOuterParens(ring)));
367
+ if (!rings.length) throw new Error('Invalid WKT polygon.');
368
+ return rings;
369
+ },
370
+
371
+ _parseMultiPoint(value) {
372
+ return this._splitTopLevel(value).map(point => this._parsePoint(this._stripOptionalOuterParens(point)));
373
+ },
374
+
375
+ _splitTopLevel(value) {
376
+ const parts = [];
377
+ let depth = 0;
378
+ let start = 0;
379
+
380
+ for (let i = 0; i < value.length; i++) {
381
+ const char = value[i];
382
+ if (char === '(') depth++;
383
+ else if (char === ')') depth--;
384
+
385
+ if (depth < 0) throw new Error('Invalid WKT string: unbalanced parentheses.');
386
+
387
+ if (char === ',' && depth === 0) {
388
+ parts.push(value.slice(start, i).trim());
389
+ start = i + 1;
390
+ }
391
+ }
392
+
393
+ if (depth !== 0) throw new Error('Invalid WKT string: unbalanced parentheses.');
394
+ const last = value.slice(start).trim();
395
+ if (last) parts.push(last);
396
+ return parts;
397
+ },
398
+
399
+ _stripOuterParens(value) {
400
+ const trimmed = value.trim();
401
+ if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) {
402
+ throw new Error('Invalid WKT string: missing coordinate parentheses.');
403
+ }
404
+ return trimmed.slice(1, -1).trim();
405
+ },
406
+
407
+ _stripOptionalOuterParens(value) {
408
+ const trimmed = value.trim();
409
+ if (!trimmed.startsWith('(')) return trimmed;
410
+ return this._stripOuterParens(trimmed);
411
+ },
412
+
413
+ _stringifyCoordinates(coords, type) {
414
+ switch (type) {
415
+ case 'Point':
416
+ return this._stringifyPoint(coords);
417
+ case 'LineString':
418
+ return this._stringifyPointList(coords);
419
+ case 'Polygon':
420
+ return this._stringifyPolygon(coords);
421
+ case 'MultiPoint':
422
+ return coords.map(point => `(${this._stringifyPoint(point)})`).join(', ');
423
+ case 'MultiLineString':
424
+ return coords.map(line => `(${this._stringifyPointList(line)})`).join(', ');
425
+ case 'MultiPolygon':
426
+ return coords.map(poly => `(${this._stringifyPolygon(poly)})`).join(', ');
427
+ default:
428
+ throw new Error(`Unsupported geometry type: ${type}.`);
429
+ }
430
+ },
431
+
432
+ _stringifyPoint(point) {
433
+ if (!Array.isArray(point) || point.length < 2) throw new Error('Invalid coordinate.');
434
+ return point.map(value => {
435
+ const number = Number(value);
436
+ if (!Number.isFinite(number)) throw new Error('Invalid coordinate value.');
437
+ return String(number);
438
+ }).join(' ');
439
+ },
440
+
441
+ _stringifyPointList(points) {
442
+ if (!Array.isArray(points)) throw new Error('Invalid coordinate list.');
443
+ return points.map(point => this._stringifyPoint(point)).join(', ');
444
+ },
445
+
446
+ _stringifyPolygon(rings) {
447
+ if (!Array.isArray(rings)) throw new Error('Invalid polygon coordinates.');
448
+ return rings.map(ring => `(${this._stringifyPointList(ring)})`).join(', ');
449
+ },
450
+
451
+ _emptyCoordinates(type) {
452
+ return type === 'Point' ? [] : [];
453
+ },
454
+
455
+ _isEmpty(coords) {
456
+ return Array.isArray(coords) && coords.length === 0;
457
+ }
458
+ };
459
+
460
+ const giserConvertCrs = {
461
+ GPS,
462
+ ProjectionTransform,
463
+ WKT,
464
+
465
+ transform(input, fromCRS, toCRS) {
466
+ if (input == null || input === '') return null;
467
+
468
+ if (typeof input === 'string') {
469
+ const geoJSON = WKT.parse(input);
470
+ return WKT.stringify(ProjectionTransform.transform(geoJSON, fromCRS, toCRS));
471
+ }
472
+
473
+ return ProjectionTransform.transform(input, fromCRS, toCRS);
474
+ },
475
+
476
+ transformPoint(coord, fromCRS, toCRS) {
477
+ return this.transform(coord, fromCRS, toCRS);
478
+ },
479
+
480
+ transformPoints(points, fromCRS, toCRS) {
481
+ return this.transform(points, fromCRS, toCRS);
482
+ },
483
+
484
+ transformGeoJSON(geoJSON, fromCRS, toCRS) {
485
+ return this.transform(geoJSON, fromCRS, toCRS);
486
+ },
487
+
488
+ transformWKT(wkt, fromCRS, toCRS) {
489
+ return this.transform(wkt, fromCRS, toCRS);
490
+ }
491
+ };
492
+
493
+ return giserConvertCrs;
494
+
495
+ }));
@@ -0,0 +1,117 @@
1
+ export type CRS =
2
+ | 'wgs84'
3
+ | 'wgs-84'
4
+ | 'EPSG:4326'
5
+ | 'epsg:4326'
6
+ | 'gcj02'
7
+ | 'gcj-02'
8
+ | 'bd09ll'
9
+ | 'bd09'
10
+ | 'bd-09'
11
+ | {
12
+ type: 'proj4';
13
+ properties: {
14
+ proj: string;
15
+ };
16
+ };
17
+
18
+ export type Position = [number, number] | [number, number, number] | number[];
19
+
20
+ export interface Point {
21
+ type: 'Point';
22
+ coordinates: Position;
23
+ }
24
+
25
+ export interface LineString {
26
+ type: 'LineString';
27
+ coordinates: Position[];
28
+ }
29
+
30
+ export interface Polygon {
31
+ type: 'Polygon';
32
+ coordinates: Position[][];
33
+ }
34
+
35
+ export interface MultiPoint {
36
+ type: 'MultiPoint';
37
+ coordinates: Position[];
38
+ }
39
+
40
+ export interface MultiLineString {
41
+ type: 'MultiLineString';
42
+ coordinates: Position[][];
43
+ }
44
+
45
+ export interface MultiPolygon {
46
+ type: 'MultiPolygon';
47
+ coordinates: Position[][][];
48
+ }
49
+
50
+ export interface GeometryCollection {
51
+ type: 'GeometryCollection';
52
+ geometries: Geometry[];
53
+ }
54
+
55
+ export type Geometry =
56
+ | Point
57
+ | LineString
58
+ | Polygon
59
+ | MultiPoint
60
+ | MultiLineString
61
+ | MultiPolygon
62
+ | GeometryCollection;
63
+
64
+ export interface Feature<G = Geometry> {
65
+ type: 'Feature';
66
+ geometry: G | null;
67
+ properties?: Record<string, unknown> | null;
68
+ id?: string | number;
69
+ }
70
+
71
+ export interface FeatureCollection<G = Geometry> {
72
+ type: 'FeatureCollection';
73
+ features: Feature<G>[];
74
+ }
75
+
76
+ export type GeoJSON = Geometry | Feature | FeatureCollection;
77
+
78
+ export interface GPSApi {
79
+ wgs84_gcj02(lon: number, lat: number): [number, number];
80
+ gcj02_wgs84(lon: number, lat: number): [number, number];
81
+ gcj02_wgs84_precise(lon: number, lat: number): [number, number];
82
+ gcj02_bd09ll(lon: number, lat: number): [number, number];
83
+ bd09ll_gcj02(lon: number, lat: number): [number, number];
84
+ wgs84_bd09ll(lon: number, lat: number): [number, number];
85
+ bd09ll_wgs84(lon: number, lat: number): [number, number];
86
+ bd09ll_wgs84_precise(lon: number, lat: number): [number, number];
87
+ outOfChina(lat: number, lon: number): boolean;
88
+ }
89
+
90
+ export interface ProjectionTransformApi {
91
+ crs: Record<'wgs84' | 'gcj02' | 'bd09ll', string>;
92
+ transform<T extends Position | Position[] | GeoJSON>(source: T, fromCRS: CRS, toCRS: CRS): T;
93
+ }
94
+
95
+ export interface WKTApi {
96
+ parse(wkt: string): Geometry;
97
+ stringify(geojson: Geometry): string;
98
+ }
99
+
100
+ export interface giserConvertCrs {
101
+ GPS: GPSApi;
102
+ ProjectionTransform: ProjectionTransformApi;
103
+ WKT: WKTApi;
104
+ transform(input: string, fromCRS: CRS, toCRS: CRS): string;
105
+ transform<T extends Position | Position[] | GeoJSON>(input: T, fromCRS: CRS, toCRS: CRS): T;
106
+ transformPoint<T extends Position>(coord: T, fromCRS: CRS, toCRS: CRS): T;
107
+ transformPoints<T extends Position[]>(points: T, fromCRS: CRS, toCRS: CRS): T;
108
+ transformGeoJSON<T extends GeoJSON>(geojson: T, fromCRS: CRS, toCRS: CRS): T;
109
+ transformWKT(wkt: string, fromCRS: CRS, toCRS: CRS): string;
110
+ }
111
+
112
+ export const GPS: GPSApi;
113
+ export const ProjectionTransform: ProjectionTransformApi;
114
+ export const WKT: WKTApi;
115
+
116
+ declare const giserConvertCrs: giserConvertCrs;
117
+ export default giserConvertCrs;
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "giser-convert-crs",
3
+ "version": "1.0.0",
4
+ "description": "China CRS geometry conversion library for WGS-84, GCJ-02, BD-09, GeoJSON and WKT.",
5
+ "main": "dist/giserConvertCrs.umd.js",
6
+ "module": "dist/giserConvertCrs.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "rollup -c && node scripts/copy-types.js",
13
+ "dev": "rollup -c -w",
14
+ "test": "node scripts/test.js"
15
+ },
16
+ "type": "module",
17
+ "devDependencies": {
18
+ "@rollup/plugin-commonjs": "^28.0.0",
19
+ "@rollup/plugin-node-resolve": "^16.0.0",
20
+ "rollup": "^4.28.0"
21
+ },
22
+ "keywords": [
23
+ "coordinate",
24
+ "transform",
25
+ "wgs84",
26
+ "gcj02",
27
+ "bd09",
28
+ "wkt",
29
+ "geojson"
30
+ ],
31
+ "author": "",
32
+ "license": "MIT"
33
+ }