giser-convert-crs 1.0.1 → 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.
@@ -11,178 +11,325 @@
11
11
  const EPSG3857_RADIUS = 6378137.0;
12
12
  const EPSG3857_MAX_LAT = 85.0511287798066;
13
13
 
14
- const GPS = {
15
- PI,
16
- x_pi: X_PI,
17
- EARTH_RADIUS: EPSG3857_RADIUS,
18
-
19
- delta(lat, lon) {
20
- let dLat = this.transformLat(lon - 105.0, lat - 35.0);
21
- let dLon = this.transformLon(lon - 105.0, lat - 35.0);
22
- const radLat = lat / 180.0 * PI;
23
- const sinLat = Math.sin(radLat);
24
- const magic = 1 - ECCENTRICITY_SQUARED * sinLat * sinLat;
25
- const sqrtMagic = Math.sqrt(magic);
26
-
27
- dLat = (dLat * 180.0) / ((SEMI_MAJOR_AXIS * (1 - ECCENTRICITY_SQUARED)) / (magic * sqrtMagic) * PI);
28
- dLon = (dLon * 180.0) / (SEMI_MAJOR_AXIS / sqrtMagic * Math.cos(radLat) * PI);
29
-
30
- return { lat: dLat, lon: dLon };
31
- },
32
-
33
- wgs84_gcj02(wgsLon, wgsLat) {
34
- this._assertLonLat(wgsLon, wgsLat);
35
- if (this.outOfChina(wgsLat, wgsLon)) return [wgsLon, wgsLat];
36
-
37
- const d = this.delta(wgsLat, wgsLon);
38
- return [wgsLon + d.lon, wgsLat + d.lat];
39
- },
40
-
41
- gcj02_wgs84(gcjLon, gcjLat) {
42
- this._assertLonLat(gcjLon, gcjLat);
43
- if (this.outOfChina(gcjLat, gcjLon)) return [gcjLon, gcjLat];
44
-
45
- const d = this.delta(gcjLat, gcjLon);
46
- return [gcjLon - d.lon, gcjLat - d.lat];
47
- },
48
-
49
- gcj02_wgs84_precise(gcjLon, gcjLat) {
50
- this._assertLonLat(gcjLon, gcjLat);
51
- if (this.outOfChina(gcjLat, gcjLon)) return [gcjLon, gcjLat];
52
-
53
- let wgsLon = gcjLon;
54
- let wgsLat = gcjLat;
55
-
56
- for (let i = 0; i < 5; i++) {
57
- const [currLon, currLat] = this.wgs84_gcj02(wgsLon, wgsLat);
58
- const dLon = currLon - gcjLon;
59
- const dLat = currLat - gcjLat;
60
- wgsLon -= dLon;
61
- wgsLat -= dLat;
62
- if (Math.abs(dLon) < 1e-11 && Math.abs(dLat) < 1e-11) break;
63
- }
64
-
65
- return [wgsLon, wgsLat];
66
- },
67
-
68
- gcj02_bd09ll(gcjLon, gcjLat) {
69
- this._assertLonLat(gcjLon, gcjLat);
70
- const z = Math.sqrt(gcjLon * gcjLon + gcjLat * gcjLat) + 0.00002 * Math.sin(gcjLat * X_PI);
71
- const theta = Math.atan2(gcjLat, gcjLon) + 0.000003 * Math.cos(gcjLon * X_PI);
72
- return [z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006];
73
- },
74
-
75
- bd09ll_gcj02(bdLon, bdLat) {
76
- this._assertLonLat(bdLon, bdLat);
77
- const x = bdLon - 0.0065;
78
- const y = bdLat - 0.006;
79
- const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);
80
- const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);
81
- return [z * Math.cos(theta), z * Math.sin(theta)];
82
- },
83
-
84
- bd09ll_gcj02_precise(bdLon, bdLat) {
85
- let [gLon, gLat] = this.bd09ll_gcj02(bdLon, bdLat);
86
- for (let i = 0; i < 2; i++) {
87
- const [tBdLon, tBdLat] = this.gcj02_bd09ll(gLon, gLat);
88
- gLon -= (tBdLon - bdLon);
89
- gLat -= (tBdLat - bdLat);
90
- }
91
- return [gLon, gLat];
92
- },
93
-
94
- wgs84_bd09ll(wgsLon, wgsLat) {
95
- const gcj = this.wgs84_gcj02(wgsLon, wgsLat);
96
- return this.gcj02_bd09ll(gcj[0], gcj[1]);
97
- },
98
-
99
- bd09ll_wgs84(bdLon, bdLat) {
100
- const gcj = this.bd09ll_gcj02(bdLon, bdLat);
101
- return this.gcj02_wgs84(gcj[0], gcj[1]);
102
- },
103
-
104
- bd09ll_wgs84_precise(bdLon, bdLat) {
105
- const gcj = this.bd09ll_gcj02_precise(bdLon, bdLat);
106
- return this.gcj02_wgs84_precise(gcj[0], gcj[1]);
107
- },
14
+ const ERR_LONLAT = 'Invalid coordinate: longitude and latitude must be finite numbers.';
15
+ const ERR_PLANE = 'Invalid coordinate: x and y must be finite numbers.';
16
+
17
+ /**
18
+ * Hot-path scratch buffers.
19
+ *
20
+ * Every write below is consumed within the same synchronous call frame
21
+ * (never across an await / callback boundary), so sharing these buffers is
22
+ * safe and removes the per-point allocations that dominated batch runs.
23
+ */
24
+ const _delta = [0, 0];
25
+ const _forward = [0, 0];
26
+ const _back = [0, 0];
27
+
28
+ function assertLonLat(lon, lat) {
29
+ if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
30
+ throw new Error(ERR_LONLAT);
31
+ }
32
+ }
108
33
 
109
- wgs84_epsg3857(lon, lat) {
110
- this._assertLonLat(lon, lat);
111
- const clampedLat = Math.max(-EPSG3857_MAX_LAT, Math.min(EPSG3857_MAX_LAT, lat));
112
- const x = lon * EPSG3857_RADIUS * PI / 180.0;
113
- const radLat = clampedLat * PI / 180.0;
114
- const y = EPSG3857_RADIUS * Math.log(Math.tan(PI / 4.0 + radLat / 2.0));
115
- return [x, y];
116
- },
34
+ function assertPlane(x, y) {
35
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
36
+ throw new Error(ERR_PLANE);
37
+ }
38
+ }
117
39
 
118
- epsg3857_wgs84(x, y) {
119
- if (!Number.isFinite(x) || !Number.isFinite(y)) {
120
- throw new Error('Invalid coordinate: x and y must be finite numbers.');
121
- }
122
- const lon = (x / (EPSG3857_RADIUS * PI)) * 180.0;
123
- const lat = (2.0 * Math.atan(Math.exp(y / EPSG3857_RADIUS)) - PI / 2.0) * 180.0 / PI;
124
- return [lon, lat];
125
- },
40
+ function outOfChina(lat, lon) {
41
+ if (Array.isArray(lat)) {
42
+ return outOfChina(lat[1], lat[0]);
43
+ }
44
+ return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
45
+ }
46
+
47
+ function transformLat(x, y) {
48
+ let ret = -100 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
49
+ ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
50
+ ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
51
+ ret += (160.0 * Math.sin(y / 12.0 * PI) + 320.0 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
52
+ return ret;
53
+ }
54
+
55
+ function transformLon(x, y) {
56
+ let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
57
+ ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
58
+ ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
59
+ ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
60
+ return ret;
61
+ }
62
+
63
+ /**
64
+ * Writes the GCJ-02 offset for (lat, lon) into `out` as [dLat, dLon].
65
+ * Allocation-free core of the public `delta()`.
66
+ */
67
+ function deltaInto(lat, lon, out) {
68
+ const x = lon - 105.0;
69
+ const y = lat - 35.0;
70
+ let dLat = transformLat(x, y);
71
+ let dLon = transformLon(x, y);
72
+ const radLat = lat / 180.0 * PI;
73
+ const sinLat = Math.sin(radLat);
74
+ const magic = 1 - ECCENTRICITY_SQUARED * sinLat * sinLat;
75
+ const sqrtMagic = Math.sqrt(magic);
76
+
77
+ out[0] = (dLat * 180.0) / ((SEMI_MAJOR_AXIS * (1 - ECCENTRICITY_SQUARED)) / (magic * sqrtMagic) * PI);
78
+ out[1] = (dLon * 180.0) / (SEMI_MAJOR_AXIS / sqrtMagic * Math.cos(radLat) * PI);
79
+ }
80
+
81
+ function delta(lat, lon) {
82
+ deltaInto(lat, lon, _delta);
83
+ return { lat: _delta[0], lon: _delta[1] };
84
+ }
85
+
86
+ /* ------------------------------------------------------------------ *
87
+ * Allocation-free transform cores: (lon, lat, out) => out
88
+ * ------------------------------------------------------------------ */
89
+
90
+ function wgs84_gcj02Into(wgsLon, wgsLat, out) {
91
+ if (outOfChina(wgsLat, wgsLon)) {
92
+ out[0] = wgsLon;
93
+ out[1] = wgsLat;
94
+ return out;
95
+ }
96
+ deltaInto(wgsLat, wgsLon, _delta);
97
+ out[0] = wgsLon + _delta[1];
98
+ out[1] = wgsLat + _delta[0];
99
+ return out;
100
+ }
101
+
102
+ function gcj02_wgs84Into(gcjLon, gcjLat, out) {
103
+ if (outOfChina(gcjLat, gcjLon)) {
104
+ out[0] = gcjLon;
105
+ out[1] = gcjLat;
106
+ return out;
107
+ }
108
+ deltaInto(gcjLat, gcjLon, _delta);
109
+ out[0] = gcjLon - _delta[1];
110
+ out[1] = gcjLat - _delta[0];
111
+ return out;
112
+ }
113
+
114
+ function gcj02_wgs84_preciseInto(gcjLon, gcjLat, out) {
115
+ if (outOfChina(gcjLat, gcjLon)) {
116
+ out[0] = gcjLon;
117
+ out[1] = gcjLat;
118
+ return out;
119
+ }
126
120
 
127
- gcj02_epsg3857(lon, lat) {
128
- const wgs = this.gcj02_wgs84_precise(lon, lat);
129
- return this.wgs84_epsg3857(wgs[0], wgs[1]);
130
- },
121
+ let wgsLon = gcjLon;
122
+ let wgsLat = gcjLat;
131
123
 
132
- epsg3857_gcj02(x, y) {
133
- const wgs = this.epsg3857_wgs84(x, y);
134
- return this.wgs84_gcj02(wgs[0], wgs[1]);
135
- },
124
+ for (let i = 0; i < 5; i++) {
125
+ wgs84_gcj02Into(wgsLon, wgsLat, _forward);
126
+ const dLon = _forward[0] - gcjLon;
127
+ const dLat = _forward[1] - gcjLat;
128
+ wgsLon -= dLon;
129
+ wgsLat -= dLat;
130
+ if (Math.abs(dLon) < 1e-11 && Math.abs(dLat) < 1e-11) break;
131
+ }
136
132
 
137
- bd09ll_epsg3857(lon, lat) {
138
- const wgs = this.bd09ll_wgs84_precise(lon, lat);
139
- return this.wgs84_epsg3857(wgs[0], wgs[1]);
140
- },
133
+ out[0] = wgsLon;
134
+ out[1] = wgsLat;
135
+ return out;
136
+ }
137
+
138
+ function gcj02_bd09llInto(gcjLon, gcjLat, out) {
139
+ const z = Math.sqrt(gcjLon * gcjLon + gcjLat * gcjLat) + 0.00002 * Math.sin(gcjLat * X_PI);
140
+ const theta = Math.atan2(gcjLat, gcjLon) + 0.000003 * Math.cos(gcjLon * X_PI);
141
+ out[0] = z * Math.cos(theta) + 0.0065;
142
+ out[1] = z * Math.sin(theta) + 0.006;
143
+ return out;
144
+ }
145
+
146
+ function bd09ll_gcj02Into(bdLon, bdLat, out) {
147
+ const x = bdLon - 0.0065;
148
+ const y = bdLat - 0.006;
149
+ const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);
150
+ const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);
151
+ out[0] = z * Math.cos(theta);
152
+ out[1] = z * Math.sin(theta);
153
+ return out;
154
+ }
155
+
156
+ function bd09ll_gcj02_preciseInto(bdLon, bdLat, out) {
157
+ bd09ll_gcj02Into(bdLon, bdLat, out);
158
+ let gLon = out[0];
159
+ let gLat = out[1];
160
+ for (let i = 0; i < 2; i++) {
161
+ gcj02_bd09llInto(gLon, gLat, _back);
162
+ gLon -= (_back[0] - bdLon);
163
+ gLat -= (_back[1] - bdLat);
164
+ }
165
+ out[0] = gLon;
166
+ out[1] = gLat;
167
+ return out;
168
+ }
169
+
170
+ function wgs84_epsg3857Into(lon, lat, out) {
171
+ const clampedLat = Math.max(-EPSG3857_MAX_LAT, Math.min(EPSG3857_MAX_LAT, lat));
172
+ const x = lon * EPSG3857_RADIUS * PI / 180.0;
173
+ const radLat = clampedLat * PI / 180.0;
174
+ out[0] = x;
175
+ out[1] = EPSG3857_RADIUS * Math.log(Math.tan(PI / 4.0 + radLat / 2.0));
176
+ return out;
177
+ }
178
+
179
+ function epsg3857_wgs84Into(x, y, out) {
180
+ out[0] = (x / (EPSG3857_RADIUS * PI)) * 180.0;
181
+ out[1] = (2.0 * Math.atan(Math.exp(y / EPSG3857_RADIUS)) - PI / 2.0) * 180.0 / PI;
182
+ return out;
183
+ }
184
+
185
+ /**
186
+ * GCJ-02 -> EPSG:3857. The datum shift is inverted before projecting, so
187
+ * `precise` selects the fixed-point inverse instead of the fast one-off
188
+ * approximation — mirroring `gcj02_wgs84_precise`. Defaults to `true`.
189
+ */
190
+ function gcj02_epsg3857Into(lon, lat, out, precise = true) {
191
+ (precise ? gcj02_wgs84_preciseInto : gcj02_wgs84Into)(lon, lat, _back);
192
+ return wgs84_epsg3857Into(_back[0], _back[1], out);
193
+ }
194
+
195
+ /** BD-09 -> EPSG:3857. `precise` behaves as in {@link gcj02_epsg3857Into}. */
196
+ function bd09ll_epsg3857Into(lon, lat, out, precise = true) {
197
+ if (precise) {
198
+ bd09ll_gcj02_preciseInto(lon, lat, _forward);
199
+ gcj02_wgs84_preciseInto(_forward[0], _forward[1], _back);
200
+ return wgs84_epsg3857Into(_back[0], _back[1], out);
201
+ }
202
+ bd09ll_gcj02Into(lon, lat, _forward);
203
+ gcj02_wgs84Into(_forward[0], _forward[1], _back);
204
+ return wgs84_epsg3857Into(_back[0], _back[1], out);
205
+ }
206
+
207
+ function wgs84_bd09llInto(wgsLon, wgsLat, out) {
208
+ wgs84_gcj02Into(wgsLon, wgsLat, _forward);
209
+ return gcj02_bd09llInto(_forward[0], _forward[1], out);
210
+ }
211
+
212
+ function bd09ll_wgs84Into(bdLon, bdLat, out) {
213
+ bd09ll_gcj02Into(bdLon, bdLat, _forward);
214
+ return gcj02_wgs84Into(_forward[0], _forward[1], out);
215
+ }
216
+
217
+ function bd09ll_wgs84_preciseInto(bdLon, bdLat, out) {
218
+ bd09ll_gcj02_preciseInto(bdLon, bdLat, _forward);
219
+ return gcj02_wgs84_preciseInto(_forward[0], _forward[1], out);
220
+ }
221
+
222
+ function epsg3857_gcj02Into(x, y, out) {
223
+ epsg3857_wgs84Into(x, y, _forward);
224
+ return wgs84_gcj02Into(_forward[0], _forward[1], out);
225
+ }
226
+
227
+ function epsg3857_bd09llInto(x, y, out) {
228
+ epsg3857_wgs84Into(x, y, _forward);
229
+ return wgs84_bd09llInto(_forward[0], _forward[1], out);
230
+ }
231
+
232
+ /* ------------------------------------------------------------------ *
233
+ * Public array-returning API (validated at the boundary)
234
+ * ------------------------------------------------------------------ */
235
+
236
+ /** Validates then delegates to an allocation-free core. */
237
+ const expose = (into, validate = assertLonLat) => (lon, lat) => {
238
+ validate(lon, lat);
239
+ return into(lon, lat, [0, 0]);
240
+ };
141
241
 
142
- epsg3857_bd09ll(x, y) {
143
- const wgs = this.epsg3857_wgs84(x, y);
144
- return this.wgs84_bd09ll(wgs[0], wgs[1]);
145
- },
242
+ const wgs84_gcj02 = expose(wgs84_gcj02Into);
243
+ const gcj02_wgs84 = expose(gcj02_wgs84Into);
244
+ const gcj02_wgs84_precise = expose(gcj02_wgs84_preciseInto);
245
+ const gcj02_bd09ll = expose(gcj02_bd09llInto);
246
+ const bd09ll_gcj02 = expose(bd09ll_gcj02Into);
247
+ const bd09ll_gcj02_precise = expose(bd09ll_gcj02_preciseInto);
248
+ const wgs84_bd09ll = expose(wgs84_bd09llInto);
249
+ const bd09ll_wgs84 = expose(bd09ll_wgs84Into);
250
+ const bd09ll_wgs84_precise = expose(bd09ll_wgs84_preciseInto);
251
+ const wgs84_epsg3857 = expose(wgs84_epsg3857Into);
252
+ const epsg3857_wgs84 = expose(epsg3857_wgs84Into, assertPlane);
253
+ const epsg3857_gcj02 = expose(epsg3857_gcj02Into, assertPlane);
254
+ const epsg3857_bd09ll = expose(epsg3857_bd09llInto, assertPlane);
255
+
256
+ const gcj02_epsg3857 = (lon, lat, precise = true) => {
257
+ assertLonLat(lon, lat);
258
+ return gcj02_epsg3857Into(lon, lat, [0, 0], precise);
259
+ };
146
260
 
147
- outOfChina(lat, lon) {
148
- if (Array.isArray(lat)) {
149
- return this.outOfChina(lat[1], lat[0]);
150
- }
151
- return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
152
- },
261
+ const bd09ll_epsg3857 = (lon, lat, precise = true) => {
262
+ assertLonLat(lon, lat);
263
+ return bd09ll_epsg3857Into(lon, lat, [0, 0], precise);
264
+ };
153
265
 
154
- transformLat(x, y) {
155
- let ret = -100 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
156
- ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
157
- ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
158
- ret += (160.0 * Math.sin(y / 12.0 * PI) + 320.0 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
159
- return ret;
160
- },
266
+ /**
267
+ * WGS-84 -> GCJ-02 as `[lon, lat]`, matching every other entry point.
268
+ * Previously took `(lat, lon)` and returned a `{ lat, lon }` object.
269
+ */
270
+ function gcj_encrypt(wgsLon, wgsLat) {
271
+ return wgs84_gcj02(wgsLon, wgsLat);
272
+ }
161
273
 
162
- transformLon(x, y) {
163
- let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
164
- ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
165
- ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
166
- ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
167
- return ret;
168
- },
274
+ const GPS = {
275
+ PI,
276
+ x_pi: X_PI,
277
+ EARTH_RADIUS: EPSG3857_RADIUS,
169
278
 
170
- gcj_encrypt(wgsLat, wgsLon) {
171
- const [lon, lat] = this.wgs84_gcj02(wgsLon, wgsLat);
172
- return { lat, lon };
173
- },
279
+ delta,
280
+ transformLat,
281
+ transformLon,
282
+ outOfChina,
283
+
284
+ wgs84_gcj02,
285
+ gcj02_wgs84,
286
+ gcj02_wgs84_precise,
287
+ gcj02_bd09ll,
288
+ bd09ll_gcj02,
289
+ bd09ll_gcj02_precise,
290
+ wgs84_bd09ll,
291
+ bd09ll_wgs84,
292
+ bd09ll_wgs84_precise,
293
+ wgs84_epsg3857,
294
+ epsg3857_wgs84,
295
+ gcj02_epsg3857,
296
+ epsg3857_gcj02,
297
+ bd09ll_epsg3857,
298
+ epsg3857_bd09ll,
299
+
300
+ gcj_encrypt,
301
+
302
+ /** @deprecated internal helper, kept so existing callers keep working. */
303
+ _assertLonLat: assertLonLat
304
+ };
174
305
 
175
- _assertLonLat(lon, lat) {
176
- if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
177
- throw new Error('Invalid coordinate: longitude and latitude must be finite numbers.');
178
- }
179
- }
306
+ /**
307
+ * Allocation-free cores keyed by `<from>_<to>`, consumed by
308
+ * ProjectionTransform so batch runs never allocate a result array.
309
+ */
310
+ GPS._into = {
311
+ wgs84_gcj02: wgs84_gcj02Into,
312
+ gcj02_wgs84: gcj02_wgs84Into,
313
+ gcj02_wgs84_precise: gcj02_wgs84_preciseInto,
314
+ gcj02_bd09ll: gcj02_bd09llInto,
315
+ bd09ll_gcj02: bd09ll_gcj02Into,
316
+ bd09ll_gcj02_precise: bd09ll_gcj02_preciseInto,
317
+ wgs84_bd09ll: wgs84_bd09llInto,
318
+ bd09ll_wgs84: bd09ll_wgs84Into,
319
+ bd09ll_wgs84_precise: bd09ll_wgs84_preciseInto,
320
+ wgs84_epsg3857: wgs84_epsg3857Into,
321
+ epsg3857_wgs84: epsg3857_wgs84Into,
322
+ gcj02_epsg3857: gcj02_epsg3857Into,
323
+ epsg3857_gcj02: epsg3857_gcj02Into,
324
+ bd09ll_epsg3857: bd09ll_epsg3857Into,
325
+ epsg3857_bd09ll: epsg3857_bd09llInto
180
326
  };
181
327
 
182
328
  const crs = {
183
329
  bd09ll: '+proj=longlat +datum=BD09',
184
330
  gcj02: '+proj=longlat +datum=GCJ02',
185
331
  wgs84: '+proj=longlat +datum=WGS84 +no_defs',
332
+ cgcs2000: '+proj=longlat +ellps=GRS80 +no_defs',
186
333
  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'
187
334
  };
188
335
 
@@ -196,6 +343,7 @@
196
343
  '4326': 'wgs84',
197
344
  'cgcs2000': 'wgs84',
198
345
  'cgcs-2000': 'wgs84',
346
+ 'cgcs 2000': 'wgs84',
199
347
  'epsg:4490': 'wgs84',
200
348
  'epsg4490': 'wgs84',
201
349
  '4490': 'wgs84',
@@ -213,6 +361,68 @@
213
361
  'webmercator': 'epsg3857'
214
362
  };
215
363
 
364
+ /**
365
+ * proj4 strings accepted as input, mapped to a `crs` key.
366
+ *
367
+ * Keys are matched after whitespace collapsing + lowercasing, so the many
368
+ * spellings of the same datum in the wild all resolve. Values are normalised
369
+ * through `CRS_ALIASES` before use (CGCS2000 folds onto WGS-84 at民用精度).
370
+ */
371
+ const PROJ4_TO_CRS = {
372
+ [crs.wgs84]: 'wgs84',
373
+ [crs.cgcs2000]: 'cgcs2000',
374
+ [crs.gcj02]: 'gcj02',
375
+ [crs.bd09ll]: 'bd09ll',
376
+ [crs.epsg3857]: 'epsg3857',
377
+
378
+ // common WGS-84 spellings
379
+ '+proj=longlat +datum=wgs84': 'wgs84',
380
+ '+proj=longlat +ellps=wgs84': 'wgs84',
381
+ '+proj=longlat +ellps=wgs84 +no_defs': 'wgs84',
382
+ '+proj=longlat +a=6378137 +b=6356752.314245 +no_defs': 'wgs84',
383
+ '+proj=longlat +a=6378137 +b=6356752.314245 +no_defs +towgs84=0,0,0': 'wgs84',
384
+
385
+ // common CGCS2000 / GRS80 spellings
386
+ '+proj=longlat +ellps=grs80': 'cgcs2000',
387
+ '+proj=longlat +ellps=grs80 +no_defs': 'cgcs2000',
388
+ '+proj=longlat +ellps=grs80 +towgs84=0,0,0,0,0,0,0 +no_defs': 'cgcs2000',
389
+ '+proj=longlat +a=6378137 +b=6356752.314140 +no_defs': 'cgcs2000',
390
+
391
+ // GCJ-02 / BD-09 are not standard EPSG datum codes, but these appear in configs
392
+ '+proj=longlat +ellps=wgs84 +nadgrids=@china': 'gcj02'
393
+ };
394
+
395
+ const ERR_COORD = 'Invalid coordinate: longitude and latitude must be finite numbers.';
396
+
397
+ /** ProjectionTransform scratch buffer, kept module-level to avoid per-point allocation. */
398
+ const _result = [0, 0];
399
+
400
+ /**
401
+ * Explicit `precise` mapping, keyed by `<from>_<to>`.
402
+ *
403
+ * Only pairs whose datum shift has to be *inverted* appear here; everything
404
+ * else is a forward formula that has no precision mode. Keeping this as a
405
+ * table (rather than scattered `if`s) is what stops a new CRS pair from
406
+ * silently inheriting the approximate inverse.
407
+ */
408
+ const PRECISE_CORES = {
409
+ gcj02_wgs84: GPS._into.gcj02_wgs84_precise,
410
+ bd09ll_wgs84: GPS._into.bd09ll_wgs84_precise,
411
+ bd09ll_gcj02: GPS._into.bd09ll_gcj02_precise
412
+ };
413
+
414
+ /** Fast counterparts used when `precise: false`. */
415
+ const FAST_CORES = {
416
+ gcj02_epsg3857: (lon, lat, out) => GPS._into.gcj02_epsg3857(lon, lat, out, false),
417
+ bd09ll_epsg3857: (lon, lat, out) => GPS._into.bd09ll_epsg3857(lon, lat, out, false)
418
+ };
419
+
420
+ function resolveInto(from, to, isPrecise) {
421
+ const key = `${from}_${to}`;
422
+ if (isPrecise) return PRECISE_CORES[key] || GPS._into[key];
423
+ return FAST_CORES[key] || GPS._into[key];
424
+ }
425
+
216
426
  const ProjectionTransform = {
217
427
  crs,
218
428
 
@@ -241,18 +451,13 @@
241
451
  if (from === to) {
242
452
  return isMutate
243
453
  ? coord => coord
244
- : coord => [Number(coord[0]), Number(coord[1]), ...coord.slice(2)];
245
- }
246
-
247
- let method;
248
- if (isPrecise && (from === 'gcj02' || from === 'bd09ll') && to === 'wgs84') {
249
- method = `${from}_wgs84_precise`;
250
- } else {
251
- method = `${from}_${to}`;
454
+ : coord => (coord.length > 2
455
+ ? [Number(coord[0]), Number(coord[1]), ...coord.slice(2)]
456
+ : [Number(coord[0]), Number(coord[1])]);
252
457
  }
253
458
 
254
- const fn = GPS[method];
255
- if (typeof fn !== 'function') {
459
+ const into = resolveInto(from, to, isPrecise);
460
+ if (typeof into !== 'function') {
256
461
  throw new Error(`Unsupported CRS transform: ${from} -> ${to}.`);
257
462
  }
258
463
 
@@ -260,12 +465,10 @@
260
465
  return coord => {
261
466
  const x = Number(coord[0]);
262
467
  const y = Number(coord[1]);
263
- if (!Number.isFinite(x) || !Number.isFinite(y)) {
264
- throw new Error('Invalid coordinate: longitude and latitude must be finite numbers.');
265
- }
266
- const [tx, ty] = fn.call(GPS, x, y);
267
- coord[0] = tx;
268
- coord[1] = ty;
468
+ if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error(ERR_COORD);
469
+ into(x, y, _result);
470
+ coord[0] = _result[0];
471
+ coord[1] = _result[1];
269
472
  return coord;
270
473
  };
271
474
  }
@@ -273,11 +476,11 @@
273
476
  return coord => {
274
477
  const x = Number(coord[0]);
275
478
  const y = Number(coord[1]);
276
- if (!Number.isFinite(x) || !Number.isFinite(y)) {
277
- throw new Error('Invalid coordinate: longitude and latitude must be finite numbers.');
278
- }
279
- const [tx, ty] = fn.call(GPS, x, y);
280
- return [tx, ty, ...coord.slice(2)];
479
+ if (!Number.isFinite(x) || !Number.isFinite(y)) throw new Error(ERR_COORD);
480
+ into(x, y, _result);
481
+ return coord.length > 2
482
+ ? [_result[0], _result[1], ...coord.slice(2)]
483
+ : [_result[0], _result[1]];
281
484
  };
282
485
  },
283
486
 
@@ -401,20 +604,41 @@
401
604
  return normalized;
402
605
  },
403
606
 
607
+ /**
608
+ * Resolves a proj4 definition to an internal CRS id, or `null`.
609
+ * Always returns an id already folded through `CRS_ALIASES`.
610
+ */
404
611
  _toCRS(proj) {
405
- for (const key in this.crs) {
406
- if (proj === this.crs[key]) return key;
612
+ if (typeof proj !== 'string') return null;
613
+
614
+ const key = proj.trim().replace(/\s+/g, ' ').toLowerCase();
615
+ let crsName = PROJ4_TO_CRS[key];
616
+
617
+ if (!crsName) {
618
+ // honour caller-extended `crs` entries
619
+ for (const name in this.crs) {
620
+ if (key === String(this.crs[name]).trim().replace(/\s+/g, ' ').toLowerCase()) {
621
+ crsName = name;
622
+ break;
623
+ }
624
+ }
407
625
  }
408
- return null;
626
+
627
+ return crsName ? (CRS_ALIASES[crsName] || crsName) : null;
409
628
  },
410
629
 
630
+ /**
631
+ * Structural position test only.
632
+ *
633
+ * Deliberately does *not* coerce with `Number()`: the coordinate
634
+ * transformer validates and coerces once, so validating here too would
635
+ * double the conversion cost on every point of a batch.
636
+ */
411
637
  _isCoord(coordinate) {
412
638
  return Array.isArray(coordinate) &&
413
639
  coordinate.length >= 2 &&
414
640
  !Array.isArray(coordinate[0]) &&
415
- !Array.isArray(coordinate[1]) &&
416
- Number.isFinite(Number(coordinate[0])) &&
417
- Number.isFinite(Number(coordinate[1]));
641
+ !Array.isArray(coordinate[1]);
418
642
  }
419
643
  };
420
644
 
@@ -633,9 +857,11 @@
633
857
  WKT,
634
858
 
635
859
  transform(input, fromCRS, toCRS, options) {
636
- if (input == null || input === '') return null;
860
+ if (input == null) return null;
637
861
 
638
862
  if (typeof input === 'string') {
863
+ // blank / whitespace-only WKT is treated as "no geometry", not an error
864
+ if (!input.trim()) return null;
639
865
  const geoJSON = WKT.parse(input);
640
866
  const transformed = ProjectionTransform.transform(geoJSON, fromCRS, toCRS, options);
641
867
  return WKT.stringify(transformed, options);