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.
@@ -5,178 +5,325 @@ const ECCENTRICITY_SQUARED = 0.00669342162296594323;
5
5
  const EPSG3857_RADIUS = 6378137.0;
6
6
  const EPSG3857_MAX_LAT = 85.0511287798066;
7
7
 
8
- const GPS = {
9
- PI,
10
- x_pi: X_PI,
11
- EARTH_RADIUS: EPSG3857_RADIUS,
12
-
13
- delta(lat, lon) {
14
- let dLat = this.transformLat(lon - 105.0, lat - 35.0);
15
- let dLon = this.transformLon(lon - 105.0, lat - 35.0);
16
- const radLat = lat / 180.0 * PI;
17
- const sinLat = Math.sin(radLat);
18
- const magic = 1 - ECCENTRICITY_SQUARED * sinLat * sinLat;
19
- const sqrtMagic = Math.sqrt(magic);
20
-
21
- dLat = (dLat * 180.0) / ((SEMI_MAJOR_AXIS * (1 - ECCENTRICITY_SQUARED)) / (magic * sqrtMagic) * PI);
22
- dLon = (dLon * 180.0) / (SEMI_MAJOR_AXIS / sqrtMagic * Math.cos(radLat) * PI);
23
-
24
- return { lat: dLat, lon: dLon };
25
- },
26
-
27
- wgs84_gcj02(wgsLon, wgsLat) {
28
- this._assertLonLat(wgsLon, wgsLat);
29
- if (this.outOfChina(wgsLat, wgsLon)) return [wgsLon, wgsLat];
30
-
31
- const d = this.delta(wgsLat, wgsLon);
32
- return [wgsLon + d.lon, wgsLat + d.lat];
33
- },
34
-
35
- gcj02_wgs84(gcjLon, gcjLat) {
36
- this._assertLonLat(gcjLon, gcjLat);
37
- if (this.outOfChina(gcjLat, gcjLon)) return [gcjLon, gcjLat];
38
-
39
- const d = this.delta(gcjLat, gcjLon);
40
- return [gcjLon - d.lon, gcjLat - d.lat];
41
- },
42
-
43
- gcj02_wgs84_precise(gcjLon, gcjLat) {
44
- this._assertLonLat(gcjLon, gcjLat);
45
- if (this.outOfChina(gcjLat, gcjLon)) return [gcjLon, gcjLat];
46
-
47
- let wgsLon = gcjLon;
48
- let wgsLat = gcjLat;
49
-
50
- for (let i = 0; i < 5; i++) {
51
- const [currLon, currLat] = this.wgs84_gcj02(wgsLon, wgsLat);
52
- const dLon = currLon - gcjLon;
53
- const dLat = currLat - gcjLat;
54
- wgsLon -= dLon;
55
- wgsLat -= dLat;
56
- if (Math.abs(dLon) < 1e-11 && Math.abs(dLat) < 1e-11) break;
57
- }
58
-
59
- return [wgsLon, wgsLat];
60
- },
61
-
62
- gcj02_bd09ll(gcjLon, gcjLat) {
63
- this._assertLonLat(gcjLon, gcjLat);
64
- const z = Math.sqrt(gcjLon * gcjLon + gcjLat * gcjLat) + 0.00002 * Math.sin(gcjLat * X_PI);
65
- const theta = Math.atan2(gcjLat, gcjLon) + 0.000003 * Math.cos(gcjLon * X_PI);
66
- return [z * Math.cos(theta) + 0.0065, z * Math.sin(theta) + 0.006];
67
- },
68
-
69
- bd09ll_gcj02(bdLon, bdLat) {
70
- this._assertLonLat(bdLon, bdLat);
71
- const x = bdLon - 0.0065;
72
- const y = bdLat - 0.006;
73
- const z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * X_PI);
74
- const theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * X_PI);
75
- return [z * Math.cos(theta), z * Math.sin(theta)];
76
- },
77
-
78
- bd09ll_gcj02_precise(bdLon, bdLat) {
79
- let [gLon, gLat] = this.bd09ll_gcj02(bdLon, bdLat);
80
- for (let i = 0; i < 2; i++) {
81
- const [tBdLon, tBdLat] = this.gcj02_bd09ll(gLon, gLat);
82
- gLon -= (tBdLon - bdLon);
83
- gLat -= (tBdLat - bdLat);
84
- }
85
- return [gLon, gLat];
86
- },
87
-
88
- wgs84_bd09ll(wgsLon, wgsLat) {
89
- const gcj = this.wgs84_gcj02(wgsLon, wgsLat);
90
- return this.gcj02_bd09ll(gcj[0], gcj[1]);
91
- },
92
-
93
- bd09ll_wgs84(bdLon, bdLat) {
94
- const gcj = this.bd09ll_gcj02(bdLon, bdLat);
95
- return this.gcj02_wgs84(gcj[0], gcj[1]);
96
- },
97
-
98
- bd09ll_wgs84_precise(bdLon, bdLat) {
99
- const gcj = this.bd09ll_gcj02_precise(bdLon, bdLat);
100
- return this.gcj02_wgs84_precise(gcj[0], gcj[1]);
101
- },
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
+ }
102
27
 
103
- wgs84_epsg3857(lon, lat) {
104
- this._assertLonLat(lon, lat);
105
- const clampedLat = Math.max(-EPSG3857_MAX_LAT, Math.min(EPSG3857_MAX_LAT, lat));
106
- const x = lon * EPSG3857_RADIUS * PI / 180.0;
107
- const radLat = clampedLat * PI / 180.0;
108
- const y = EPSG3857_RADIUS * Math.log(Math.tan(PI / 4.0 + radLat / 2.0));
109
- return [x, y];
110
- },
28
+ function assertPlane(x, y) {
29
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
30
+ throw new Error(ERR_PLANE);
31
+ }
32
+ }
111
33
 
112
- epsg3857_wgs84(x, y) {
113
- if (!Number.isFinite(x) || !Number.isFinite(y)) {
114
- throw new Error('Invalid coordinate: x and y must be finite numbers.');
115
- }
116
- const lon = (x / (EPSG3857_RADIUS * PI)) * 180.0;
117
- const lat = (2.0 * Math.atan(Math.exp(y / EPSG3857_RADIUS)) - PI / 2.0) * 180.0 / PI;
118
- return [lon, lat];
119
- },
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
+ }
120
114
 
121
- gcj02_epsg3857(lon, lat) {
122
- const wgs = this.gcj02_wgs84_precise(lon, lat);
123
- return this.wgs84_epsg3857(wgs[0], wgs[1]);
124
- },
115
+ let wgsLon = gcjLon;
116
+ let wgsLat = gcjLat;
125
117
 
126
- epsg3857_gcj02(x, y) {
127
- const wgs = this.epsg3857_wgs84(x, y);
128
- return this.wgs84_gcj02(wgs[0], wgs[1]);
129
- },
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
+ }
130
126
 
131
- bd09ll_epsg3857(lon, lat) {
132
- const wgs = this.bd09ll_wgs84_precise(lon, lat);
133
- return this.wgs84_epsg3857(wgs[0], wgs[1]);
134
- },
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
+ };
135
235
 
136
- epsg3857_bd09ll(x, y) {
137
- const wgs = this.epsg3857_wgs84(x, y);
138
- return this.wgs84_bd09ll(wgs[0], wgs[1]);
139
- },
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
+ };
140
254
 
141
- outOfChina(lat, lon) {
142
- if (Array.isArray(lat)) {
143
- return this.outOfChina(lat[1], lat[0]);
144
- }
145
- return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
146
- },
255
+ const bd09ll_epsg3857 = (lon, lat, precise = true) => {
256
+ assertLonLat(lon, lat);
257
+ return bd09ll_epsg3857Into(lon, lat, [0, 0], precise);
258
+ };
147
259
 
148
- transformLat(x, y) {
149
- let ret = -100 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
150
- ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
151
- ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
152
- ret += (160.0 * Math.sin(y / 12.0 * PI) + 320.0 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
153
- return ret;
154
- },
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
+ }
155
267
 
156
- transformLon(x, y) {
157
- let ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
158
- ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
159
- ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
160
- ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
161
- return ret;
162
- },
268
+ const GPS = {
269
+ PI,
270
+ x_pi: X_PI,
271
+ EARTH_RADIUS: EPSG3857_RADIUS,
163
272
 
164
- gcj_encrypt(wgsLat, wgsLon) {
165
- const [lon, lat] = this.wgs84_gcj02(wgsLon, wgsLat);
166
- return { lat, lon };
167
- },
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
+ };
168
299
 
169
- _assertLonLat(lon, lat) {
170
- if (!Number.isFinite(lon) || !Number.isFinite(lat)) {
171
- throw new Error('Invalid coordinate: longitude and latitude must be finite numbers.');
172
- }
173
- }
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
174
320
  };
175
321
 
176
322
  const crs = {
177
323
  bd09ll: '+proj=longlat +datum=BD09',
178
324
  gcj02: '+proj=longlat +datum=GCJ02',
179
325
  wgs84: '+proj=longlat +datum=WGS84 +no_defs',
326
+ cgcs2000: '+proj=longlat +ellps=GRS80 +no_defs',
180
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'
181
328
  };
182
329
 
@@ -190,6 +337,7 @@ const CRS_ALIASES = {
190
337
  '4326': 'wgs84',
191
338
  'cgcs2000': 'wgs84',
192
339
  'cgcs-2000': 'wgs84',
340
+ 'cgcs 2000': 'wgs84',
193
341
  'epsg:4490': 'wgs84',
194
342
  'epsg4490': 'wgs84',
195
343
  '4490': 'wgs84',
@@ -207,6 +355,68 @@ const CRS_ALIASES = {
207
355
  'webmercator': 'epsg3857'
208
356
  };
209
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
406
+ };
407
+
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)
412
+ };
413
+
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
+
210
420
  const ProjectionTransform = {
211
421
  crs,
212
422
 
@@ -235,18 +445,13 @@ const ProjectionTransform = {
235
445
  if (from === to) {
236
446
  return isMutate
237
447
  ? coord => coord
238
- : coord => [Number(coord[0]), Number(coord[1]), ...coord.slice(2)];
239
- }
240
-
241
- let method;
242
- if (isPrecise && (from === 'gcj02' || from === 'bd09ll') && to === 'wgs84') {
243
- method = `${from}_wgs84_precise`;
244
- } else {
245
- method = `${from}_${to}`;
448
+ : coord => (coord.length > 2
449
+ ? [Number(coord[0]), Number(coord[1]), ...coord.slice(2)]
450
+ : [Number(coord[0]), Number(coord[1])]);
246
451
  }
247
452
 
248
- const fn = GPS[method];
249
- if (typeof fn !== 'function') {
453
+ const into = resolveInto(from, to, isPrecise);
454
+ if (typeof into !== 'function') {
250
455
  throw new Error(`Unsupported CRS transform: ${from} -> ${to}.`);
251
456
  }
252
457
 
@@ -254,12 +459,10 @@ const ProjectionTransform = {
254
459
  return coord => {
255
460
  const x = Number(coord[0]);
256
461
  const y = Number(coord[1]);
257
- if (!Number.isFinite(x) || !Number.isFinite(y)) {
258
- throw new Error('Invalid coordinate: longitude and latitude must be finite numbers.');
259
- }
260
- const [tx, ty] = fn.call(GPS, x, y);
261
- coord[0] = tx;
262
- coord[1] = ty;
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];
263
466
  return coord;
264
467
  };
265
468
  }
@@ -267,11 +470,11 @@ const ProjectionTransform = {
267
470
  return coord => {
268
471
  const x = Number(coord[0]);
269
472
  const y = Number(coord[1]);
270
- if (!Number.isFinite(x) || !Number.isFinite(y)) {
271
- throw new Error('Invalid coordinate: longitude and latitude must be finite numbers.');
272
- }
273
- const [tx, ty] = fn.call(GPS, x, y);
274
- return [tx, ty, ...coord.slice(2)];
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]];
275
478
  };
276
479
  },
277
480
 
@@ -395,20 +598,41 @@ const ProjectionTransform = {
395
598
  return normalized;
396
599
  },
397
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
+ */
398
605
  _toCRS(proj) {
399
- for (const key in this.crs) {
400
- if (proj === this.crs[key]) return key;
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
+ }
401
619
  }
402
- return null;
620
+
621
+ return crsName ? (CRS_ALIASES[crsName] || crsName) : null;
403
622
  },
404
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
+ */
405
631
  _isCoord(coordinate) {
406
632
  return Array.isArray(coordinate) &&
407
633
  coordinate.length >= 2 &&
408
634
  !Array.isArray(coordinate[0]) &&
409
- !Array.isArray(coordinate[1]) &&
410
- Number.isFinite(Number(coordinate[0])) &&
411
- Number.isFinite(Number(coordinate[1]));
635
+ !Array.isArray(coordinate[1]);
412
636
  }
413
637
  };
414
638
 
@@ -627,9 +851,11 @@ const giserConvertCrs = {
627
851
  WKT,
628
852
 
629
853
  transform(input, fromCRS, toCRS, options) {
630
- if (input == null || input === '') return null;
854
+ if (input == null) return null;
631
855
 
632
856
  if (typeof input === 'string') {
857
+ // blank / whitespace-only WKT is treated as "no geometry", not an error
858
+ if (!input.trim()) return null;
633
859
  const geoJSON = WKT.parse(input);
634
860
  const transformed = ProjectionTransform.transform(geoJSON, fromCRS, toCRS, options);
635
861
  return WKT.stringify(transformed, options);