giser-convert-crs 1.0.0 → 1.0.2

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.
@@ -1,454 +1,848 @@
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
- }
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
+ const EPSG3857_RADIUS = 6378137.0;
6
+ const EPSG3857_MAX_LAT = 85.0511287798066;
7
+
8
+ const ERR_LONLAT = 'Invalid coordinate: longitude and latitude must be finite numbers.';
9
+ const ERR_PLANE = 'Invalid coordinate: x and y must be finite numbers.';
10
+
11
+ /**
12
+ * Hot-path scratch buffers.
13
+ *
14
+ * Every write below is consumed within the same synchronous call frame
15
+ * (never across an await / callback boundary), so sharing these buffers is
16
+ * safe and removes the per-point allocations that dominated batch runs.
17
+ */
18
+ const _delta = [0, 0];
19
+ const _forward = [0, 0];
20
+ const _back = [0, 0];
21
+
22
+ function assertLonLat(lon, lat) {
23
+ if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
24
+ throw new Error(ERR_LONLAT);
25
+ }
26
+ }
27
+
28
+ function assertPlane(x, y) {
29
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
30
+ throw new Error(ERR_PLANE);
31
+ }
32
+ }
33
+
34
+ function outOfChina(lat, lon) {
35
+ if (Array.isArray(lat)) {
36
+ return outOfChina(lat[1], lat[0]);
37
+ }
38
+ return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
39
+ }
40
+
41
+ function transformLat(x, y) {
42
+ let ret = -100 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
43
+ ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
44
+ ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
45
+ ret += (160.0 * Math.sin(y / 12.0 * PI) + 320.0 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
46
+ return ret;
47
+ }
48
+
49
+ function transformLon(x, y) {
50
+ let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
51
+ ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
52
+ ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
53
+ ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
54
+ return ret;
55
+ }
56
+
57
+ /**
58
+ * Writes the GCJ-02 offset for (lat, lon) into `out` as [dLat, dLon].
59
+ * Allocation-free core of the public `delta()`.
60
+ */
61
+ function deltaInto(lat, lon, out) {
62
+ const x = lon - 105.0;
63
+ const y = lat - 35.0;
64
+ let dLat = transformLat(x, y);
65
+ let dLon = transformLon(x, y);
66
+ const radLat = lat / 180.0 * PI;
67
+ const sinLat = Math.sin(radLat);
68
+ const magic = 1 - ECCENTRICITY_SQUARED * sinLat * sinLat;
69
+ const sqrtMagic = Math.sqrt(magic);
70
+
71
+ out[0] = (dLat * 180.0) / ((SEMI_MAJOR_AXIS * (1 - ECCENTRICITY_SQUARED)) / (magic * sqrtMagic) * PI);
72
+ out[1] = (dLon * 180.0) / (SEMI_MAJOR_AXIS / sqrtMagic * Math.cos(radLat) * PI);
73
+ }
74
+
75
+ function delta(lat, lon) {
76
+ deltaInto(lat, lon, _delta);
77
+ return { lat: _delta[0], lon: _delta[1] };
78
+ }
79
+
80
+ /* ------------------------------------------------------------------ *
81
+ * Allocation-free transform cores: (lon, lat, out) => out
82
+ * ------------------------------------------------------------------ */
83
+
84
+ function wgs84_gcj02Into(wgsLon, wgsLat, out) {
85
+ if (outOfChina(wgsLat, wgsLon)) {
86
+ out[0] = wgsLon;
87
+ out[1] = wgsLat;
88
+ return out;
89
+ }
90
+ deltaInto(wgsLat, wgsLon, _delta);
91
+ out[0] = wgsLon + _delta[1];
92
+ out[1] = wgsLat + _delta[0];
93
+ return out;
94
+ }
95
+
96
+ function gcj02_wgs84Into(gcjLon, gcjLat, out) {
97
+ if (outOfChina(gcjLat, gcjLon)) {
98
+ out[0] = gcjLon;
99
+ out[1] = gcjLat;
100
+ return out;
101
+ }
102
+ deltaInto(gcjLat, gcjLon, _delta);
103
+ out[0] = gcjLon - _delta[1];
104
+ out[1] = gcjLat - _delta[0];
105
+ return out;
106
+ }
107
+
108
+ function gcj02_wgs84_preciseInto(gcjLon, gcjLat, out) {
109
+ if (outOfChina(gcjLat, gcjLon)) {
110
+ out[0] = gcjLon;
111
+ out[1] = gcjLat;
112
+ return out;
113
+ }
114
+
115
+ let wgsLon = gcjLon;
116
+ let wgsLat = gcjLat;
117
+
118
+ for (let i = 0; i < 5; i++) {
119
+ wgs84_gcj02Into(wgsLon, wgsLat, _forward);
120
+ const dLon = _forward[0] - gcjLon;
121
+ const dLat = _forward[1] - gcjLat;
122
+ wgsLon -= dLon;
123
+ wgsLat -= dLat;
124
+ if (Math.abs(dLon) < 1e-11 && Math.abs(dLat) < 1e-11) break;
125
+ }
126
+
127
+ out[0] = wgsLon;
128
+ out[1] = wgsLat;
129
+ return out;
130
+ }
131
+
132
+ function gcj02_bd09llInto(gcjLon, gcjLat, out) {
133
+ const z = Math.sqrt(gcjLon * gcjLon + gcjLat * gcjLat) + 0.00002 * Math.sin(gcjLat * X_PI);
134
+ const theta = Math.atan2(gcjLat, gcjLon) + 0.000003 * Math.cos(gcjLon * X_PI);
135
+ out[0] = z * Math.cos(theta) + 0.0065;
136
+ out[1] = z * Math.sin(theta) + 0.006;
137
+ return out;
138
+ }
139
+
140
+ function bd09ll_gcj02Into(bdLon, bdLat, out) {
141
+ const x = bdLon - 0.0065;
142
+ const y = bdLat - 0.006;
143
+ const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);
144
+ const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);
145
+ out[0] = z * Math.cos(theta);
146
+ out[1] = z * Math.sin(theta);
147
+ return out;
148
+ }
149
+
150
+ function bd09ll_gcj02_preciseInto(bdLon, bdLat, out) {
151
+ bd09ll_gcj02Into(bdLon, bdLat, out);
152
+ let gLon = out[0];
153
+ let gLat = out[1];
154
+ for (let i = 0; i < 2; i++) {
155
+ gcj02_bd09llInto(gLon, gLat, _back);
156
+ gLon -= (_back[0] - bdLon);
157
+ gLat -= (_back[1] - bdLat);
158
+ }
159
+ out[0] = gLon;
160
+ out[1] = gLat;
161
+ return out;
162
+ }
163
+
164
+ function wgs84_epsg3857Into(lon, lat, out) {
165
+ const clampedLat = Math.max(-EPSG3857_MAX_LAT, Math.min(EPSG3857_MAX_LAT, lat));
166
+ const x = lon * EPSG3857_RADIUS * PI / 180.0;
167
+ const radLat = clampedLat * PI / 180.0;
168
+ out[0] = x;
169
+ out[1] = EPSG3857_RADIUS * Math.log(Math.tan(PI / 4.0 + radLat / 2.0));
170
+ return out;
171
+ }
172
+
173
+ function epsg3857_wgs84Into(x, y, out) {
174
+ out[0] = (x / (EPSG3857_RADIUS * PI)) * 180.0;
175
+ out[1] = (2.0 * Math.atan(Math.exp(y / EPSG3857_RADIUS)) - PI / 2.0) * 180.0 / PI;
176
+ return out;
177
+ }
178
+
179
+ /**
180
+ * GCJ-02 -> EPSG:3857. The datum shift is inverted before projecting, so
181
+ * `precise` selects the fixed-point inverse instead of the fast one-off
182
+ * approximation — mirroring `gcj02_wgs84_precise`. Defaults to `true`.
183
+ */
184
+ function gcj02_epsg3857Into(lon, lat, out, precise = true) {
185
+ (precise ? gcj02_wgs84_preciseInto : gcj02_wgs84Into)(lon, lat, _back);
186
+ return wgs84_epsg3857Into(_back[0], _back[1], out);
187
+ }
188
+
189
+ /** BD-09 -> EPSG:3857. `precise` behaves as in {@link gcj02_epsg3857Into}. */
190
+ function bd09ll_epsg3857Into(lon, lat, out, precise = true) {
191
+ if (precise) {
192
+ bd09ll_gcj02_preciseInto(lon, lat, _forward);
193
+ gcj02_wgs84_preciseInto(_forward[0], _forward[1], _back);
194
+ return wgs84_epsg3857Into(_back[0], _back[1], out);
195
+ }
196
+ bd09ll_gcj02Into(lon, lat, _forward);
197
+ gcj02_wgs84Into(_forward[0], _forward[1], _back);
198
+ return wgs84_epsg3857Into(_back[0], _back[1], out);
199
+ }
200
+
201
+ function wgs84_bd09llInto(wgsLon, wgsLat, out) {
202
+ wgs84_gcj02Into(wgsLon, wgsLat, _forward);
203
+ return gcj02_bd09llInto(_forward[0], _forward[1], out);
204
+ }
205
+
206
+ function bd09ll_wgs84Into(bdLon, bdLat, out) {
207
+ bd09ll_gcj02Into(bdLon, bdLat, _forward);
208
+ return gcj02_wgs84Into(_forward[0], _forward[1], out);
209
+ }
210
+
211
+ function bd09ll_wgs84_preciseInto(bdLon, bdLat, out) {
212
+ bd09ll_gcj02_preciseInto(bdLon, bdLat, _forward);
213
+ return gcj02_wgs84_preciseInto(_forward[0], _forward[1], out);
214
+ }
215
+
216
+ function epsg3857_gcj02Into(x, y, out) {
217
+ epsg3857_wgs84Into(x, y, _forward);
218
+ return wgs84_gcj02Into(_forward[0], _forward[1], out);
219
+ }
220
+
221
+ function epsg3857_bd09llInto(x, y, out) {
222
+ epsg3857_wgs84Into(x, y, _forward);
223
+ return wgs84_bd09llInto(_forward[0], _forward[1], out);
224
+ }
225
+
226
+ /* ------------------------------------------------------------------ *
227
+ * Public array-returning API (validated at the boundary)
228
+ * ------------------------------------------------------------------ */
229
+
230
+ /** Validates then delegates to an allocation-free core. */
231
+ const expose = (into, validate = assertLonLat) => (lon, lat) => {
232
+ validate(lon, lat);
233
+ return into(lon, lat, [0, 0]);
234
+ };
235
+
236
+ const wgs84_gcj02 = expose(wgs84_gcj02Into);
237
+ const gcj02_wgs84 = expose(gcj02_wgs84Into);
238
+ const gcj02_wgs84_precise = expose(gcj02_wgs84_preciseInto);
239
+ const gcj02_bd09ll = expose(gcj02_bd09llInto);
240
+ const bd09ll_gcj02 = expose(bd09ll_gcj02Into);
241
+ const bd09ll_gcj02_precise = expose(bd09ll_gcj02_preciseInto);
242
+ const wgs84_bd09ll = expose(wgs84_bd09llInto);
243
+ const bd09ll_wgs84 = expose(bd09ll_wgs84Into);
244
+ const bd09ll_wgs84_precise = expose(bd09ll_wgs84_preciseInto);
245
+ const wgs84_epsg3857 = expose(wgs84_epsg3857Into);
246
+ const epsg3857_wgs84 = expose(epsg3857_wgs84Into, assertPlane);
247
+ const epsg3857_gcj02 = expose(epsg3857_gcj02Into, assertPlane);
248
+ const epsg3857_bd09ll = expose(epsg3857_bd09llInto, assertPlane);
249
+
250
+ const gcj02_epsg3857 = (lon, lat, precise = true) => {
251
+ assertLonLat(lon, lat);
252
+ return gcj02_epsg3857Into(lon, lat, [0, 0], precise);
253
+ };
254
+
255
+ const bd09ll_epsg3857 = (lon, lat, precise = true) => {
256
+ assertLonLat(lon, lat);
257
+ return bd09ll_epsg3857Into(lon, lat, [0, 0], precise);
258
+ };
259
+
260
+ /**
261
+ * WGS-84 -> GCJ-02 as `[lon, lat]`, matching every other entry point.
262
+ * Previously took `(lat, lon)` and returned a `{ lat, lon }` object.
263
+ */
264
+ function gcj_encrypt(wgsLon, wgsLat) {
265
+ return wgs84_gcj02(wgsLon, wgsLat);
266
+ }
267
+
268
+ const GPS = {
269
+ PI,
270
+ x_pi: X_PI,
271
+ EARTH_RADIUS: EPSG3857_RADIUS,
272
+
273
+ delta,
274
+ transformLat,
275
+ transformLon,
276
+ outOfChina,
277
+
278
+ wgs84_gcj02,
279
+ gcj02_wgs84,
280
+ gcj02_wgs84_precise,
281
+ gcj02_bd09ll,
282
+ bd09ll_gcj02,
283
+ bd09ll_gcj02_precise,
284
+ wgs84_bd09ll,
285
+ bd09ll_wgs84,
286
+ bd09ll_wgs84_precise,
287
+ wgs84_epsg3857,
288
+ epsg3857_wgs84,
289
+ gcj02_epsg3857,
290
+ epsg3857_gcj02,
291
+ bd09ll_epsg3857,
292
+ epsg3857_bd09ll,
293
+
294
+ gcj_encrypt,
295
+
296
+ /** @deprecated internal helper, kept so existing callers keep working. */
297
+ _assertLonLat: assertLonLat
298
+ };
299
+
300
+ /**
301
+ * Allocation-free cores keyed by `<from>_<to>`, consumed by
302
+ * ProjectionTransform so batch runs never allocate a result array.
303
+ */
304
+ GPS._into = {
305
+ wgs84_gcj02: wgs84_gcj02Into,
306
+ gcj02_wgs84: gcj02_wgs84Into,
307
+ gcj02_wgs84_precise: gcj02_wgs84_preciseInto,
308
+ gcj02_bd09ll: gcj02_bd09llInto,
309
+ bd09ll_gcj02: bd09ll_gcj02Into,
310
+ bd09ll_gcj02_precise: bd09ll_gcj02_preciseInto,
311
+ wgs84_bd09ll: wgs84_bd09llInto,
312
+ bd09ll_wgs84: bd09ll_wgs84Into,
313
+ bd09ll_wgs84_precise: bd09ll_wgs84_preciseInto,
314
+ wgs84_epsg3857: wgs84_epsg3857Into,
315
+ epsg3857_wgs84: epsg3857_wgs84Into,
316
+ gcj02_epsg3857: gcj02_epsg3857Into,
317
+ epsg3857_gcj02: epsg3857_gcj02Into,
318
+ bd09ll_epsg3857: bd09ll_epsg3857Into,
319
+ epsg3857_bd09ll: epsg3857_bd09llInto
320
+ };
321
+
322
+ const crs = {
323
+ bd09ll: '+proj=longlat +datum=BD09',
324
+ gcj02: '+proj=longlat +datum=GCJ02',
325
+ wgs84: '+proj=longlat +datum=WGS84 +no_defs',
326
+ cgcs2000: '+proj=longlat +ellps=GRS80 +no_defs',
327
+ epsg3857: '+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs'
328
+ };
329
+
330
+ const CRS_ALIASES = {
331
+ 'bd-09': 'bd09ll',
332
+ 'bd09': 'bd09ll',
333
+ 'bd09ll': 'bd09ll',
334
+ 'bd-09ll': 'bd09ll',
335
+ 'epsg:4326': 'wgs84',
336
+ 'epsg4326': 'wgs84',
337
+ '4326': 'wgs84',
338
+ 'cgcs2000': 'wgs84',
339
+ 'cgcs-2000': 'wgs84',
340
+ 'cgcs 2000': 'wgs84',
341
+ 'epsg:4490': 'wgs84',
342
+ 'epsg4490': 'wgs84',
343
+ '4490': 'wgs84',
344
+ 'gcj-02': 'gcj02',
345
+ 'gcj02': 'gcj02',
346
+ 'wgs-84': 'wgs84',
347
+ 'wgs84': 'wgs84',
348
+ 'epsg:3857': 'epsg3857',
349
+ 'epsg3857': 'epsg3857',
350
+ '3857': 'epsg3857',
351
+ 'epsg:900913': 'epsg3857',
352
+ 'epsg900913': 'epsg3857',
353
+ '900913': 'epsg3857',
354
+ 'mercator': 'epsg3857',
355
+ 'webmercator': 'epsg3857'
356
+ };
357
+
358
+ /**
359
+ * proj4 strings accepted as input, mapped to a `crs` key.
360
+ *
361
+ * Keys are matched after whitespace collapsing + lowercasing, so the many
362
+ * spellings of the same datum in the wild all resolve. Values are normalised
363
+ * through `CRS_ALIASES` before use (CGCS2000 folds onto WGS-84 at民用精度).
364
+ */
365
+ const PROJ4_TO_CRS = {
366
+ [crs.wgs84]: 'wgs84',
367
+ [crs.cgcs2000]: 'cgcs2000',
368
+ [crs.gcj02]: 'gcj02',
369
+ [crs.bd09ll]: 'bd09ll',
370
+ [crs.epsg3857]: 'epsg3857',
371
+
372
+ // common WGS-84 spellings
373
+ '+proj=longlat +datum=wgs84': 'wgs84',
374
+ '+proj=longlat +ellps=wgs84': 'wgs84',
375
+ '+proj=longlat +ellps=wgs84 +no_defs': 'wgs84',
376
+ '+proj=longlat +a=6378137 +b=6356752.314245 +no_defs': 'wgs84',
377
+ '+proj=longlat +a=6378137 +b=6356752.314245 +no_defs +towgs84=0,0,0': 'wgs84',
378
+
379
+ // common CGCS2000 / GRS80 spellings
380
+ '+proj=longlat +ellps=grs80': 'cgcs2000',
381
+ '+proj=longlat +ellps=grs80 +no_defs': 'cgcs2000',
382
+ '+proj=longlat +ellps=grs80 +towgs84=0,0,0,0,0,0,0 +no_defs': 'cgcs2000',
383
+ '+proj=longlat +a=6378137 +b=6356752.314140 +no_defs': 'cgcs2000',
384
+
385
+ // GCJ-02 / BD-09 are not standard EPSG datum codes, but these appear in configs
386
+ '+proj=longlat +ellps=wgs84 +nadgrids=@china': 'gcj02'
387
+ };
388
+
389
+ const ERR_COORD = 'Invalid coordinate: longitude and latitude must be finite numbers.';
390
+
391
+ /** ProjectionTransform scratch buffer, kept module-level to avoid per-point allocation. */
392
+ const _result = [0, 0];
393
+
394
+ /**
395
+ * Explicit `precise` mapping, keyed by `<from>_<to>`.
396
+ *
397
+ * Only pairs whose datum shift has to be *inverted* appear here; everything
398
+ * else is a forward formula that has no precision mode. Keeping this as a
399
+ * table (rather than scattered `if`s) is what stops a new CRS pair from
400
+ * silently inheriting the approximate inverse.
401
+ */
402
+ const PRECISE_CORES = {
403
+ gcj02_wgs84: GPS._into.gcj02_wgs84_precise,
404
+ bd09ll_wgs84: GPS._into.bd09ll_wgs84_precise,
405
+ bd09ll_gcj02: GPS._into.bd09ll_gcj02_precise
130
406
  };
131
407
 
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
- }
408
+ /** Fast counterparts used when `precise: false`. */
409
+ const FAST_CORES = {
410
+ gcj02_epsg3857: (lon, lat, out) => GPS._into.gcj02_epsg3857(lon, lat, out, false),
411
+ bd09ll_epsg3857: (lon, lat, out) => GPS._into.bd09ll_epsg3857(lon, lat, out, false)
267
412
  };
268
413
 
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
- }
414
+ function resolveInto(from, to, isPrecise) {
415
+ const key = `${from}_${to}`;
416
+ if (isPrecise) return PRECISE_CORES[key] || GPS._into[key];
417
+ return FAST_CORES[key] || GPS._into[key];
418
+ }
419
+
420
+ const ProjectionTransform = {
421
+ crs,
422
+
423
+ transform(source, fromCRS, toCRS, options = {}) {
424
+ if (source == null) return null;
425
+
426
+ const from = this._normalizeCRS(fromCRS);
427
+ const to = this._normalizeCRS(toCRS);
428
+ const transformCoord = this._getCoordTransformer(from, to, options);
429
+
430
+ if (this._isCoord(source)) {
431
+ return transformCoord(source);
432
+ }
433
+
434
+ if (Array.isArray(source)) {
435
+ return this._transformCoordinateTree(source, transformCoord, options);
436
+ }
437
+
438
+ return this._transformGeoJSON(source, transformCoord, options);
439
+ },
440
+
441
+ _getCoordTransformer(from, to, options = {}) {
442
+ const isMutate = Boolean(options.mutate);
443
+ const isPrecise = options.precise !== false;
444
+
445
+ if (from === to) {
446
+ return isMutate
447
+ ? coord => coord
448
+ : coord => (coord.length > 2
449
+ ? [Number(coord[0]), Number(coord[1]), ...coord.slice(2)]
450
+ : [Number(coord[0]), Number(coord[1])]);
451
+ }
452
+
453
+ const into = resolveInto(from, to, isPrecise);
454
+ if (typeof into !== 'function') {
455
+ throw new Error(`Unsupported CRS transform: ${from} -> ${to}.`);
456
+ }
457
+
458
+ if (isMutate) {
459
+ return coord => {
460
+ const x = Number(coord[0]);
461
+ const y = Number(coord[1]);
462
+ if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error(ERR_COORD);
463
+ into(x, y, _result);
464
+ coord[0] = _result[0];
465
+ coord[1] = _result[1];
466
+ return coord;
467
+ };
468
+ }
469
+
470
+ return coord => {
471
+ const x = Number(coord[0]);
472
+ const y = Number(coord[1]);
473
+ if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error(ERR_COORD);
474
+ into(x, y, _result);
475
+ return coord.length > 2
476
+ ? [_result[0], _result[1], ...coord.slice(2)]
477
+ : [_result[0], _result[1]];
478
+ };
479
+ },
480
+
481
+ _transformGeoJSON(geoJSON, transformCoord, options = {}) {
482
+ if (!geoJSON || typeof geoJSON !== 'object') {
483
+ throw new Error('Invalid GeoJSON object.');
484
+ }
485
+
486
+ const result = options.mutate ? geoJSON : { ...geoJSON };
487
+
488
+ if (Array.isArray(geoJSON.bbox)) {
489
+ result.bbox = this._transformBbox(geoJSON.bbox, transformCoord);
490
+ }
491
+
492
+ if (geoJSON.type === 'Feature') {
493
+ result.geometry = geoJSON.geometry
494
+ ? this._transformGeoJSON(geoJSON.geometry, transformCoord, options)
495
+ : null;
496
+ return result;
497
+ }
498
+
499
+ if (geoJSON.type === 'FeatureCollection') {
500
+ if (!Array.isArray(geoJSON.features)) {
501
+ throw new Error('Invalid FeatureCollection: features must be an array.');
502
+ }
503
+ if (options.mutate) {
504
+ for (let i = 0; i < geoJSON.features.length; i++) {
505
+ geoJSON.features[i] = this._transformGeoJSON(geoJSON.features[i], transformCoord, options);
506
+ }
507
+ result.features = geoJSON.features;
508
+ } else {
509
+ result.features = geoJSON.features.map(f => this._transformGeoJSON(f, transformCoord, options));
510
+ }
511
+ return result;
512
+ }
513
+
514
+ if (geoJSON.type === 'GeometryCollection') {
515
+ if (!Array.isArray(geoJSON.geometries)) {
516
+ throw new Error('Invalid GeometryCollection: geometries must be an array.');
517
+ }
518
+ if (options.mutate) {
519
+ for (let i = 0; i < geoJSON.geometries.length; i++) {
520
+ geoJSON.geometries[i] = geoJSON.geometries[i]
521
+ ? this._transformGeoJSON(geoJSON.geometries[i], transformCoord, options)
522
+ : null;
523
+ }
524
+ result.geometries = geoJSON.geometries;
525
+ } else {
526
+ result.geometries = geoJSON.geometries.map(g => (g ? this._transformGeoJSON(g, transformCoord, options) : null));
527
+ }
528
+ return result;
529
+ }
530
+
531
+ if ('coordinates' in geoJSON) {
532
+ result.coordinates = this._transformCoordinateTree(geoJSON.coordinates, transformCoord, options);
533
+ return result;
534
+ }
535
+
536
+ throw new Error(`Unsupported GeoJSON type: ${geoJSON.type || 'unknown'}.`);
537
+ },
538
+
539
+ _transformCoordinateTree(coordinates, transformCoord, options = {}) {
540
+ if (coordinates == null) return coordinates;
541
+ if (this._isCoord(coordinates)) return transformCoord(coordinates);
542
+ if (!Array.isArray(coordinates)) throw new Error('Invalid coordinates: expected coordinate array.');
543
+
544
+ if (options.mutate) {
545
+ for (let i = 0; i < coordinates.length; i++) {
546
+ coordinates[i] = this._transformCoordinateTree(coordinates[i], transformCoord, options);
547
+ }
548
+ return coordinates;
549
+ }
550
+
551
+ return coordinates.map(item => this._transformCoordinateTree(item, transformCoord, options));
552
+ },
553
+
554
+ _transformBbox(bbox, transformCoord) {
555
+ if (!Array.isArray(bbox) || (bbox.length !== 4 && bbox.length !== 6)) {
556
+ return bbox;
557
+ }
558
+
559
+ if (bbox.length === 4) {
560
+ const [minX, minY, maxX, maxY] = bbox;
561
+ const p1 = transformCoord([minX, minY]);
562
+ const p2 = transformCoord([maxX, maxY]);
563
+ return [
564
+ Math.min(p1[0], p2[0]),
565
+ Math.min(p1[1], p2[1]),
566
+ Math.max(p1[0], p2[0]),
567
+ Math.max(p1[1], p2[1])
568
+ ];
569
+ }
570
+
571
+ const [minX, minY, minZ, maxX, maxY, maxZ] = bbox;
572
+ const p1 = transformCoord([minX, minY, minZ]);
573
+ const p2 = transformCoord([maxX, maxY, maxZ]);
574
+ return [
575
+ Math.min(p1[0], p2[0]),
576
+ Math.min(p1[1], p2[1]),
577
+ minZ,
578
+ Math.max(p1[0], p2[0]),
579
+ Math.max(p1[1], p2[1]),
580
+ maxZ
581
+ ];
582
+ },
583
+
584
+ _normalizeCRS(value) {
585
+ if (!value) throw new Error('must provide a valid fromCRS and toCRS.');
586
+
587
+ if (typeof value === 'object') {
588
+ if (value.type === 'proj4' && value.properties && value.properties.proj) {
589
+ const crsName = this._toCRS(value.properties.proj);
590
+ if (crsName) return crsName;
591
+ }
592
+ throw new Error('Unsupported CRS object.');
593
+ }
594
+
595
+ const key = String(value).trim().toLowerCase();
596
+ const normalized = CRS_ALIASES[key];
597
+ if (!normalized) throw new Error(`Unsupported CRS: ${value}.`);
598
+ return normalized;
599
+ },
600
+
601
+ /**
602
+ * Resolves a proj4 definition to an internal CRS id, or `null`.
603
+ * Always returns an id already folded through `CRS_ALIASES`.
604
+ */
605
+ _toCRS(proj) {
606
+ if (typeof proj !== 'string') return null;
607
+
608
+ const key = proj.trim().replace(/\s+/g, ' ').toLowerCase();
609
+ let crsName = PROJ4_TO_CRS[key];
610
+
611
+ if (!crsName) {
612
+ // honour caller-extended `crs` entries
613
+ for (const name in this.crs) {
614
+ if (key === String(this.crs[name]).trim().replace(/\s+/g, ' ').toLowerCase()) {
615
+ crsName = name;
616
+ break;
617
+ }
618
+ }
619
+ }
620
+
621
+ return crsName ? (CRS_ALIASES[crsName] || crsName) : null;
622
+ },
623
+
624
+ /**
625
+ * Structural position test only.
626
+ *
627
+ * Deliberately does *not* coerce with `Number()`: the coordinate
628
+ * transformer validates and coerces once, so validating here too would
629
+ * double the conversion cost on every point of a batch.
630
+ */
631
+ _isCoord(coordinate) {
632
+ return Array.isArray(coordinate) &&
633
+ coordinate.length >= 2 &&
634
+ !Array.isArray(coordinate[0]) &&
635
+ !Array.isArray(coordinate[1]);
636
+ }
637
+ };
638
+
639
+ const TYPE_MAP = {
640
+ POINT: 'Point',
641
+ LINESTRING: 'LineString',
642
+ POLYGON: 'Polygon',
643
+ MULTIPOINT: 'MultiPoint',
644
+ MULTILINESTRING: 'MultiLineString',
645
+ MULTIPOLYGON: 'MultiPolygon',
646
+ GEOMETRYCOLLECTION: 'GeometryCollection'
647
+ };
648
+
649
+ const REVERSE_TYPE_MAP = Object.fromEntries(
650
+ Object.entries(TYPE_MAP).map(([wktType, geoJSONType]) => [geoJSONType, wktType])
651
+ );
652
+
653
+ const WKT = {
654
+ parse(wkt) {
655
+ if (typeof wkt !== 'string' || !wkt.trim()) {
656
+ throw new Error('Invalid WKT string.');
657
+ }
658
+
659
+ const source = wkt.trim();
660
+ const header = source.match(/^([A-Za-z]+)(?:\s+(ZM|Z|M))?\s*/i);
661
+ if (!header) throw new Error('Invalid WKT string.');
662
+
663
+ const wktType = header[1].toUpperCase();
664
+ const geoJSONType = TYPE_MAP[wktType];
665
+ if (!geoJSONType) throw new Error(`Unsupported WKT type: ${wktType}.`);
666
+
667
+ const body = source.slice(header[0].length).trim();
668
+ if (/^EMPTY$/i.test(body)) {
669
+ if (geoJSONType === 'GeometryCollection') {
670
+ return { type: geoJSONType, geometries: [] };
671
+ }
672
+ return { type: geoJSONType, coordinates: this._emptyCoordinates(geoJSONType) };
673
+ }
674
+
675
+ if (!body.startsWith('(') || !body.endsWith(')')) {
676
+ throw new Error('Invalid WKT string: missing coordinate parentheses.');
677
+ }
678
+
679
+ const content = this._stripOuterParens(body);
680
+
681
+ if (geoJSONType === 'GeometryCollection') {
682
+ return {
683
+ type: 'GeometryCollection',
684
+ geometries: this._splitTopLevel(content).map(item => this.parse(item))
685
+ };
686
+ }
687
+
688
+ return {
689
+ type: geoJSONType,
690
+ coordinates: this._parseCoordinates(content, geoJSONType)
691
+ };
692
+ },
693
+
694
+ stringify(geoJSON, options = {}) {
695
+ if (!geoJSON || typeof geoJSON !== 'object' || !geoJSON.type) {
696
+ throw new Error('Invalid GeoJSON.');
697
+ }
698
+
699
+ const wktType = REVERSE_TYPE_MAP[geoJSON.type];
700
+ if (!wktType) throw new Error(`Unsupported geometry type: ${geoJSON.type}.`);
701
+
702
+ const precision = typeof options === 'number' ? options : options?.precision;
703
+
704
+ if (geoJSON.type === 'GeometryCollection') {
705
+ if (!Array.isArray(geoJSON.geometries) || geoJSON.geometries.length === 0) {
706
+ return 'GEOMETRYCOLLECTION EMPTY';
707
+ }
708
+ return `GEOMETRYCOLLECTION(${geoJSON.geometries.map(geom => this.stringify(geom, options)).join(', ')})`;
709
+ }
710
+
711
+ const coords = geoJSON.coordinates;
712
+ if (this._isEmpty(coords)) return `${wktType} EMPTY`;
713
+
714
+ return `${wktType}(${this._stringifyCoordinates(coords, geoJSON.type, precision)})`;
715
+ },
716
+
717
+ _parseCoordinates(content, type) {
718
+ switch (type) {
719
+ case 'Point':
720
+ return this._parsePoint(content);
721
+ case 'LineString':
722
+ return this._parsePointList(content);
723
+ case 'Polygon':
724
+ return this._parsePolygon(content);
725
+ case 'MultiPoint':
726
+ return this._parseMultiPoint(content);
727
+ case 'MultiLineString':
728
+ return this._splitTopLevel(content).map(line => this._parsePointList(this._stripOptionalOuterParens(line)));
729
+ case 'MultiPolygon':
730
+ return this._splitTopLevel(content).map(poly => this._parsePolygon(this._stripOptionalOuterParens(poly)));
731
+ default:
732
+ throw new Error(`Unsupported geometry type: ${type}.`);
733
+ }
734
+ },
735
+
736
+ _parsePoint(value) {
737
+ const numbers = value.trim().split(/\s+/).filter(Boolean).map(Number);
738
+ if (numbers.length < 2 || numbers.some(n => !Number.isFinite(n))) {
739
+ throw new Error(`Invalid WKT coordinate: ${value}.`);
740
+ }
741
+ return numbers;
742
+ },
743
+
744
+ _parsePointList(value) {
745
+ const points = this._splitTopLevel(value).map(part => this._parsePoint(this._stripOptionalOuterParens(part)));
746
+ if (!points.length) throw new Error('Invalid WKT coordinate list.');
747
+ return points;
748
+ },
749
+
750
+ _parsePolygon(value) {
751
+ const rings = this._splitTopLevel(value).map(ring => this._parsePointList(this._stripOptionalOuterParens(ring)));
752
+ if (!rings.length) throw new Error('Invalid WKT polygon.');
753
+ return rings;
754
+ },
755
+
756
+ _parseMultiPoint(value) {
757
+ return this._splitTopLevel(value).map(point => this._parsePoint(this._stripOptionalOuterParens(point)));
758
+ },
759
+
760
+ _splitTopLevel(value) {
761
+ const parts = [];
762
+ let depth = 0;
763
+ let start = 0;
764
+
765
+ for (let i = 0; i < value.length; i++) {
766
+ const char = value[i];
767
+ if (char === '(') depth++;
768
+ else if (char === ')') depth--;
769
+
770
+ if (depth < 0) throw new Error('Invalid WKT string: unbalanced parentheses.');
771
+
772
+ if (char === ',' && depth === 0) {
773
+ parts.push(value.slice(start, i).trim());
774
+ start = i + 1;
775
+ }
776
+ }
777
+
778
+ if (depth !== 0) throw new Error('Invalid WKT string: unbalanced parentheses.');
779
+ const last = value.slice(start).trim();
780
+ if (last) parts.push(last);
781
+ return parts;
782
+ },
783
+
784
+ _stripOuterParens(value) {
785
+ const trimmed = value.trim();
786
+ if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) {
787
+ throw new Error('Invalid WKT string: missing coordinate parentheses.');
788
+ }
789
+ return trimmed.slice(1, -1).trim();
790
+ },
791
+
792
+ _stripOptionalOuterParens(value) {
793
+ const trimmed = value.trim();
794
+ if (!trimmed.startsWith('(')) return trimmed;
795
+ return this._stripOuterParens(trimmed);
796
+ },
797
+
798
+ _stringifyCoordinates(coords, type, precision) {
799
+ switch (type) {
800
+ case 'Point':
801
+ return this._stringifyPoint(coords, precision);
802
+ case 'LineString':
803
+ return this._stringifyPointList(coords, precision);
804
+ case 'Polygon':
805
+ return this._stringifyPolygon(coords, precision);
806
+ case 'MultiPoint':
807
+ return coords.map(point => `(${this._stringifyPoint(point, precision)})`).join(', ');
808
+ case 'MultiLineString':
809
+ return coords.map(line => `(${this._stringifyPointList(line, precision)})`).join(', ');
810
+ case 'MultiPolygon':
811
+ return coords.map(poly => `(${this._stringifyPolygon(poly, precision)})`).join(', ');
812
+ default:
813
+ throw new Error(`Unsupported geometry type: ${type}.`);
814
+ }
815
+ },
816
+
817
+ _stringifyPoint(point, precision) {
818
+ if (!Array.isArray(point) || point.length < 2) throw new Error('Invalid coordinate.');
819
+ return point.map(value => {
820
+ const number = Number(value);
821
+ if (!Number.isFinite(number)) throw new Error('Invalid coordinate value.');
822
+ if (typeof precision === 'number' && precision >= 0) {
823
+ return Number(number.toFixed(precision)).toString();
824
+ }
825
+ return String(number);
826
+ }).join(' ');
827
+ },
828
+
829
+ _stringifyPointList(points, precision) {
830
+ if (!Array.isArray(points)) throw new Error('Invalid coordinate list.');
831
+ return points.map(point => this._stringifyPoint(point, precision)).join(', ');
832
+ },
833
+
834
+ _stringifyPolygon(rings, precision) {
835
+ if (!Array.isArray(rings)) throw new Error('Invalid polygon coordinates.');
836
+ return rings.map(ring => `(${this._stringifyPointList(ring, precision)})`).join(', ');
837
+ },
838
+
839
+ _emptyCoordinates(type) {
840
+ return [];
841
+ },
842
+
843
+ _isEmpty(coords) {
844
+ return Array.isArray(coords) && coords.length === 0;
845
+ }
452
846
  };
453
847
 
454
848
  const giserConvertCrs = {
@@ -456,32 +850,36 @@ const giserConvertCrs = {
456
850
  ProjectionTransform,
457
851
  WKT,
458
852
 
459
- transform(input, fromCRS, toCRS) {
460
- if (input == null || input === '') return null;
853
+ transform(input, fromCRS, toCRS, options) {
854
+ if (input == null) return null;
461
855
 
462
856
  if (typeof input === 'string') {
857
+ // blank / whitespace-only WKT is treated as "no geometry", not an error
858
+ if (!input.trim()) return null;
463
859
  const geoJSON = WKT.parse(input);
464
- return WKT.stringify(ProjectionTransform.transform(geoJSON, fromCRS, toCRS));
860
+ const transformed = ProjectionTransform.transform(geoJSON, fromCRS, toCRS, options);
861
+ return WKT.stringify(transformed, options);
465
862
  }
466
863
 
467
- return ProjectionTransform.transform(input, fromCRS, toCRS);
864
+ return ProjectionTransform.transform(input, fromCRS, toCRS, options);
468
865
  },
469
866
 
470
- transformPoint(coord, fromCRS, toCRS) {
471
- return this.transform(coord, fromCRS, toCRS);
867
+ transformPoint(coord, fromCRS, toCRS, options) {
868
+ return this.transform(coord, fromCRS, toCRS, options);
472
869
  },
473
870
 
474
- transformPoints(points, fromCRS, toCRS) {
475
- return this.transform(points, fromCRS, toCRS);
871
+ transformPoints(points, fromCRS, toCRS, options) {
872
+ return this.transform(points, fromCRS, toCRS, options);
476
873
  },
477
874
 
478
- transformGeoJSON(geoJSON, fromCRS, toCRS) {
479
- return this.transform(geoJSON, fromCRS, toCRS);
875
+ transformGeoJSON(geoJSON, fromCRS, toCRS, options) {
876
+ return this.transform(geoJSON, fromCRS, toCRS, options);
480
877
  },
481
878
 
482
- transformWKT(wkt, fromCRS, toCRS) {
483
- return this.transform(wkt, fromCRS, toCRS);
879
+ transformWKT(wkt, fromCRS, toCRS, options) {
880
+ return this.transform(wkt, fromCRS, toCRS, options);
484
881
  }
485
882
  };
486
883
 
487
884
  export { GPS, ProjectionTransform, WKT, giserConvertCrs as default };
885
+ //# sourceMappingURL=giserConvertCrs.esm.js.map