Skip to main content

coordtransform/
lib.rs

1//! High-accuracy, dependency-free coordinate transforms for WGS 84, GCJ-02,
2//! BD-09, and Web Mercator (EPSG:3857).
3//!
4//! The crate is designed for mobile applications, navigation, track processing,
5//! and other high-frequency workloads. It has no third-party crate dependencies
6//! and performs no heap allocation on the scalar conversion hot path.
7//!
8//! # Coordinate systems
9//!
10//! - **WGS 84**: global longitude/latitude coordinates used by GNSS and most GIS data.
11//! - **GCJ-02**: the coordinate system commonly used by map services in mainland China.
12//! - **BD-09**: Baidu's coordinate system, derived from GCJ-02.
13//! - **EPSG:3857**: Web Mercator coordinates in metres.
14//!
15//! # GCJ-02 transform region
16//!
17//! The traditional implementation only checks a large rectangle and therefore
18//! applies GCJ-02 offsets to locations in nearby countries. This implementation
19//! instead embeds a small, simplified, OSM-derived geographic mask directly in
20//! the binary. No boundary crate, parser, file I/O, lazy initialization, or heap
21//! allocation is needed at runtime.
22//!
23//! The embedded mask is deliberately approximate. It is intended only to decide
24//! whether the GCJ-02 transform should be dispatched for ordinary land-based map
25//! and navigation positions. It is not a legal, cadastral, maritime, or surveying
26//! boundary. Hong Kong and Macau are carved out of the mask, and Taiwan is outside
27//! the main ring.
28//!
29//! The embedded ring is a simplified and quantized transform-dispatch mask based
30//! on the OpenStreetMap China boundary relation (relation 270056), simplified and
31//! quantized offline for transform dispatch. Remote offshore geometry south of 18 N
32//! is intentionally ignored because this crate targets ordinary mainland/Hainan app
33//! navigation rather than maritime boundary classification. The mask is intentionally
34//! approximate near borders and must not be used for administrative decisions.
35//! The embedded geographic data remains subject to the Open Database License (ODbL);
36//! attribution: (c) OpenStreetMap contributors. See
37//! <https://www.openstreetmap.org/copyright>.
38//!
39//! # Accuracy
40//!
41//! The commonly published GCJ-02 formula does not provide an exact inverse.
42//! [`gcj02_to_wgs84`] uses a fast inverse seed followed by fixed numerical
43//! refinement. [`bd09_to_gcj02`] similarly refines the conventional analytical
44//! approximation against the forward BD-09 formula.
45//!
46//! Fast single-pass variants are also provided when throughput is more important
47//! than the smallest possible round-trip residual.
48//!
49//! # Performance
50//!
51//! Region lookup uses three stages:
52//!
53//! 1. A broad bounding-box rejection.
54//! 2. A precomputed 1-degree raster. Interior and exterior cells return in O(1).
55//! 3. Only cells touched by the simplified boundary run a point-in-polygon test.
56//!
57//! The polygon itself is stored as quantized `u16` coordinates at 0.001-degree
58//! resolution. The region data is static, requires no initialization, and is only
59//! a few kilobytes. Callers that already know a stream stays inside the transform
60//! region can use the `*_in_mainland` functions and skip region lookup entirely.
61//!
62//! # Quick start
63//!
64//! ```
65//! use coordtransform::{gcj02_to_wgs84, wgs84_to_gcj02};
66//!
67//! let wgs = (116.404, 39.915);
68//! let gcj = wgs84_to_gcj02(wgs.0, wgs.1);
69//! let restored = gcj02_to_wgs84(gcj.0, gcj.1);
70//!
71//! assert!((restored.0 - wgs.0).abs() < 1e-9);
72//! assert!((restored.1 - wgs.1).abs() < 1e-9);
73//! ```
74
75#![forbid(unsafe_code)]
76#![warn(missing_docs)]
77
78use std::f64::consts::PI;
79use std::fmt;
80
81const DEG_TO_RAD: f64 = PI / 180.0;
82const RAD_TO_DEG: f64 = 180.0 / PI;
83
84const GCJ_AXIS: f64 = 6_378_245.0;
85const GCJ_ECCENTRICITY_SQUARED: f64 = 0.006_693_421_622_965_943;
86const GCJ_DEGREE_SCALE: f64 = RAD_TO_DEG / GCJ_AXIS;
87const GCJ_INVERSE_REFINEMENTS: usize = 2;
88
89const X_PI: f64 = PI * 3000.0 / 180.0;
90const BD_LON_OFFSET: f64 = 0.0065;
91const BD_LAT_OFFSET: f64 = 0.006;
92const BD_RADIAL_EPSILON: f64 = 0.00002;
93const BD_ANGULAR_EPSILON: f64 = 0.000003;
94const BD_INVERSE_REFINEMENTS: usize = 2;
95
96const WEB_MERCATOR_RADIUS: f64 = 6_378_137.0;
97const WEB_MERCATOR_MAX_COORDINATE: f64 = PI * WEB_MERCATOR_RADIUS;
98
99const GCJ_REGION_MIN_LON: f64 = 73.0;
100const GCJ_REGION_MAX_LON: f64 = 135.2;
101const GCJ_REGION_MIN_LAT: f64 = 18.0;
102const GCJ_REGION_MAX_LAT: f64 = 54.0;
103const GCJ_RING_LON_ORIGIN: f64 = 70.0;
104const GCJ_RING_SCALE: f64 = 1000.0;
105
106/// Maximum absolute latitude representable in the canonical finite EPSG:3857 world.
107pub const MAX_LATITUDE: f64 = 85.051_128_779_806_6;
108
109/// Attribution text for the embedded OpenStreetMap-derived transform-region mask.
110///
111/// Applications redistributing the embedded boundary data should retain appropriate
112/// OpenStreetMap attribution and make the ODbL terms available to users.
113pub const OSM_ATTRIBUTION: &str =
114    "Boundary mask derived from OpenStreetMap data (c) OpenStreetMap contributors, ODbL 1.0";
115
116// Boundary snapshot prepared 2026-09-21.
117// Embedded geographic data: simplified/quantized from OpenStreetMap relation 270056.
118// Data attribution: (c) OpenStreetMap contributors, ODbL 1.0.
119// This is a transform-dispatch mask, not a legal administrative boundary.
120// Coordinates are stored as ((longitude - 70) * 1000, latitude * 1000).
121const GCJ_REGION_RING: [(u16, u16); 158] = [
122    (47935, 23341),
123    (47402, 22960),
124    (47291, 15644),
125    (48437, 15328),
126    (47476, 14385),
127    (40667, 14274),
128    (38926, 18023),
129    (38405, 18382),
130    (38315, 19326),
131    (39070, 20139),
132    (39424, 20681),
133    (38774, 21293),
134    (38111, 21178),
135    (37996, 21544),
136    (37542, 21581),
137    (36995, 21827),
138    (36536, 22354),
139    (36591, 22609),
140    (36317, 22843),
141    (35867, 22901),
142    (35334, 23299),
143    (34873, 23086),
144    (34239, 22748),
145    (33650, 22738),
146    (33207, 22556),
147    (32518, 22618),
148    (31771, 22339),
149    (31939, 21188),
150    (31181, 21127),
151    (30606, 21355),
152    (29100, 22076),
153    (29363, 23004),
154    (28806, 23122),
155    (28138, 23996),
156    (27649, 23745),
157    (27429, 23921),
158    (27629, 24175),
159    (27629, 25125),
160    (28000, 25408),
161    (28671, 26621),
162    (28587, 27470),
163    (28045, 28081),
164    (27354, 28166),
165    (26904, 28327),
166    (26121, 28960),
167    (25271, 29017),
168    (24334, 28926),
169    (23281, 28453),
170    (22226, 27822),
171    (21337, 28030),
172    (20029, 28119),
173    (19139, 27623),
174    (19070, 27236),
175    (18756, 27466),
176    (18705, 28053),
177    (17126, 27731),
178    (15952, 27887),
179    (15099, 28309),
180    (14162, 28816),
181    (13168, 29493),
182    (12106, 29990),
183    (11660, 30327),
184    (10167, 30495),
185    (9375, 30938),
186    (8486, 31979),
187    (8722, 32794),
188    (9185, 32621),
189    (8621, 33596),
190    (8562, 34107),
191    (7921, 35363),
192    (6477, 35767),
193    (5849, 36035),
194    (5412, 36632),
195    (4429, 36990),
196    (4481, 37944),
197    (3733, 38541),
198    (3418, 39439),
199    (4337, 40209),
200    (5189, 40554),
201    (5814, 40427),
202    (6823, 41113),
203    (8116, 41473),
204    (10034, 42149),
205    (10340, 43131),
206    (10301, 44076),
207    (9809, 44979),
208    (12485, 45285),
209    (12969, 47301),
210    (15508, 48253),
211    (16887, 49234),
212    (18693, 48278),
213    (20080, 47983),
214    (21148, 46732),
215    (20974, 45311),
216    (23520, 45066),
217    (25448, 44104),
218    (27170, 42895),
219    (30878, 42771),
220    (34621, 41750),
221    (37424, 42552),
222    (40362, 42855),
223    (41310, 44354),
224    (41939, 45178),
225    (43616, 44847),
226    (46125, 45780),
227    (47381, 46673),
228    (49805, 46782),
229    (48511, 47895),
230    (46266, 47773),
231    (46663, 49929),
232    (49200, 50160),
233    (50015, 51753),
234    (50854, 53390),
235    (53521, 53656),
236    (55666, 53163),
237    (56637, 52186),
238    (57007, 51321),
239    (57312, 50751),
240    (57694, 49765),
241    (58608, 49612),
242    (60250, 48886),
243    (61052, 47784),
244    (64174, 48398),
245    (64803, 48393),
246    (64683, 48114),
247    (64073, 46781),
248    (63422, 45560),
249    (63167, 45114),
250    (61873, 45262),
251    (61401, 44055),
252    (61062, 42845),
253    (60644, 42423),
254    (60056, 42962),
255    (59716, 42424),
256    (58500, 41991),
257    (58265, 41678),
258    (57349, 41454),
259    (56603, 41563),
260    (56030, 40885),
261    (55416, 40618),
262    (54735, 40317),
263    (54488, 40170),
264    (54400, 40114),
265    (54386, 40041),
266    (54378, 39985),
267    (54152, 39532),
268    (53849, 35084),
269    (55013, 30505),
270    (51553, 26249),
271    (50705, 26631),
272    (49787, 26205),
273    (49894, 25782),
274    (50047, 25388),
275    (48602, 24461),
276    (48442, 24553),
277    (48228, 24495),
278    (48104, 24362),
279    (48198, 24345),
280];
281
282const GCJ_INTERIOR_ROWS: [u64; 54] = [
283    0x0000000000000000, // lat 00..01
284    0x0000000000000000, // lat 01..02
285    0x0000000000000000, // lat 02..03
286    0x0000000000000000, // lat 03..04
287    0x0000000000000000, // lat 04..05
288    0x0000000000000000, // lat 05..06
289    0x0000000000000000, // lat 06..07
290    0x0000000000000000, // lat 07..08
291    0x0000000000000000, // lat 08..09
292    0x0000000000000000, // lat 09..10
293    0x0000000000000000, // lat 10..11
294    0x0000000000000000, // lat 11..12
295    0x0000000000000000, // lat 12..13
296    0x0000000000000000, // lat 13..14
297    0x0000000000000000, // lat 14..15
298    0x00001f8000000000, // lat 15..16
299    0x00001fc000000000, // lat 16..17
300    0x00001fc000000000, // lat 17..18
301    0x00001fe000000000, // lat 18..19
302    0x00001fe000000000, // lat 19..20
303    0x00001fc000000000, // lat 20..21
304    0x00001fc000000000, // lat 21..22
305    0x00001ff810000000, // lat 22..23
306    0x00001ffcf0000000, // lat 23..24
307    0x00003ffffc000000, // lat 24..25
308    0x00007ffff8000000, // lat 25..26
309    0x00007ffff8000000, // lat 26..27
310    0x0003fffff8000000, // lat 27..28
311    0x0003fffff800c000, // lat 28..29
312    0x0007ffffff7ff000, // lat 29..30
313    0x000ffffffffff800, // lat 30..31
314    0x000fffffffffff00, // lat 31..32
315    0x000fffffffffff00, // lat 32..33
316    0x000fffffffffff80, // lat 33..34
317    0x0007ffffffffff80, // lat 34..35
318    0x0007ffffffffff80, // lat 35..36
319    0x0007fffffffffff0, // lat 36..37
320    0x0007fffffffffff8, // lat 37..38
321    0x000ffffffffffff8, // lat 38..39
322    0x000ffffffffffff8, // lat 39..40
323    0x000fffffffffffe0, // lat 40..41
324    0x003ffffc7fffff00, // lat 41..42
325    0x00ffff8001fffe00, // lat 42..43
326    0x07ffff00007ffe00, // lat 43..44
327    0x07fff800001ffe00, // lat 44..45
328    0x07ffe0000003f800, // lat 45..46
329    0x1fff00000003f800, // lat 46..47
330    0x23ff00000003e000, // lat 47..48
331    0x01ffe00000000000, // lat 48..49
332    0x007fc00000000000, // lat 49..50
333    0x007f000000000000, // lat 50..51
334    0x003e000000000000, // lat 51..52
335    0x001e000000000000, // lat 52..53
336    0x0000000000000000, // lat 53..54
337];
338
339const GCJ_BORDER_ROWS: [u64; 54] = [
340    0x0000000000000000, // lat 00..01
341    0x0000000000000000, // lat 01..02
342    0x0000000000000000, // lat 02..03
343    0x0000000000000000, // lat 03..04
344    0x0000000000000000, // lat 04..05
345    0x0000000000000000, // lat 05..06
346    0x0000000000000000, // lat 06..07
347    0x0000000000000000, // lat 07..08
348    0x0000000000000000, // lat 08..09
349    0x0000000000000000, // lat 09..10
350    0x0000000000000000, // lat 10..11
351    0x0000000000000000, // lat 11..12
352    0x0000000000000000, // lat 12..13
353    0x0000000000000000, // lat 13..14
354    0x00007fc000000000, // lat 14..15
355    0x0000606000000000, // lat 15..16
356    0x0000202000000000, // lat 16..17
357    0x0000203000000000, // lat 17..18
358    0x0000201000000000, // lat 18..19
359    0x0000201000000000, // lat 19..20
360    0x0000203000000000, // lat 20..21
361    0x0000203c38000000, // lat 21..22
362    0x00002007e8000000, // lat 22..23
363    0x000060030e000000, // lat 23..24
364    0x0000c00002000000, // lat 24..25
365    0x0001800006000000, // lat 25..26
366    0x0007800004000000, // lat 26..27
367    0x00040000041be000, // lat 27..28
368    0x000c000007ff3800, // lat 28..29
369    0x0018000000800c00, // lat 29..30
370    0x0030000000000780, // lat 30..31
371    0x00100000000000c0, // lat 31..32
372    0x00100000000000c0, // lat 32..33
373    0x0010000000000040, // lat 33..34
374    0x0018000000000040, // lat 34..35
375    0x0008000000000078, // lat 35..36
376    0x000800000000000c, // lat 36..37
377    0x0018000000000004, // lat 37..38
378    0x0010000000000006, // lat 38..39
379    0x0010000000000006, // lat 39..40
380    0x007000000000001c, // lat 40..41
381    0x01c00003800000f0, // lat 41..42
382    0x0f00007efe000180, // lat 42..43
383    0x080000c003800100, // lat 43..44
384    0x0800078000e00180, // lat 44..45
385    0x38001d80003c0780, // lat 45..46
386    0x6000f000000c0400, // lat 46..47
387    0x5c00f00000041c00, // lat 47..48
388    0x760010000007f000, // lat 48..49
389    0x038030000000c000, // lat 49..50
390    0x0080e00000000000, // lat 50..51
391    0x00c1800000000000, // lat 51..52
392    0x0061000000000000, // lat 52..53
393    0x003f000000000000, // lat 53..54
394];
395
396const HONG_KONG_EXCLUSION_RING: [(u16, u16); 14] = [
397    (43830, 22540),
398    (43920, 22540),
399    (44030, 22525),
400    (44130, 22530),
401    (44220, 22550),
402    (44310, 22560),
403    (44430, 22500),
404    (44470, 22380),
405    (44400, 22280),
406    (44320, 22200),
407    (44180, 22170),
408    (44050, 22200),
409    (43910, 22180),
410    (43840, 22270),
411];
412
413const MACAU_EXCLUSION_RING: [(u16, u16); 8] = [
414    (43527, 22215),
415    (43550, 22222),
416    (43575, 22218),
417    (43603, 22194),
418    (43595, 22145),
419    (43584, 22111),
420    (43548, 22108),
421    (43531, 22151),
422];
423
424/// Compatibility no-op retained from versions that lazily initialized a boundary index.
425///
426/// The current implementation embeds all region data as static constants, so no
427/// warm-up is necessary and this function has no runtime effect.
428#[inline]
429pub fn warm_up() {}
430
431/// Returns `true` when a WGS 84 coordinate belongs to the approximate GCJ-02
432/// transform region used by this crate.
433///
434/// The check is optimized for high-frequency use. Most points are classified by
435/// a precomputed one-degree raster; only boundary cells require a polygon test.
436/// Hong Kong and Macau are explicitly excluded. The mask is suitable for ordinary
437/// application dispatch, not legal or surveying boundary decisions.
438///
439/// # Examples
440///
441/// ```
442/// use coordtransform::is_in_gcj02_region;
443///
444/// assert!(is_in_gcj02_region(116.4074, 39.9042));
445/// assert!(is_in_gcj02_region(121.4737, 31.2304));
446/// assert!(!is_in_gcj02_region(114.1694, 22.3193));
447/// assert!(!is_in_gcj02_region(126.9780, 37.5665));
448/// ```
449#[inline]
450pub fn is_in_gcj02_region(lon: f64, lat: f64) -> bool {
451    // This comparison form rejects NaN as well as coordinates outside the mask.
452    if !(lon >= GCJ_REGION_MIN_LON
453        && lon < GCJ_REGION_MAX_LON
454        && lat >= GCJ_REGION_MIN_LAT
455        && lat < GCJ_REGION_MAX_LAT)
456    {
457        return false;
458    }
459
460    // These small exclusions are checked first because their surrounding raster
461    // cells are otherwise well inside the broad China land mask.
462    if lon >= 113.80
463        && lon <= 114.50
464        && lat >= 22.05
465        && lat <= 22.60
466        && point_in_quantized_ring(lon, lat, &HONG_KONG_EXCLUSION_RING)
467    {
468        return false;
469    }
470    if lon >= 113.50
471        && lon <= 113.62
472        && lat >= 22.08
473        && lat <= 22.24
474        && point_in_quantized_ring(lon, lat, &MACAU_EXCLUSION_RING)
475    {
476        return false;
477    }
478
479    let row = lat as usize;
480    let col = lon as usize - 72;
481    let bit = 1_u64 << col;
482
483    if GCJ_BORDER_ROWS[row] & bit != 0 {
484        point_in_quantized_ring(lon, lat, &GCJ_REGION_RING)
485    } else {
486        GCJ_INTERIOR_ROWS[row] & bit != 0
487    }
488}
489
490/// Returns `true` when a WGS 84 coordinate is inside the approximate mainland
491/// GCJ-02 transform region.
492///
493/// This is a compatibility alias for [`is_in_gcj02_region`].
494#[inline]
495pub fn is_in_mainland_china(lon: f64, lat: f64) -> bool {
496    is_in_gcj02_region(lon, lat)
497}
498
499/// Returns `true` when a coordinate is outside this crate's GCJ-02 transform region.
500#[inline]
501pub fn is_out_of_china(lon: f64, lat: f64) -> bool {
502    !is_in_gcj02_region(lon, lat)
503}
504
505#[inline]
506fn point_in_quantized_ring(lon: f64, lat: f64, ring: &[(u16, u16)]) -> bool {
507    let x = (lon - GCJ_RING_LON_ORIGIN) * GCJ_RING_SCALE;
508    let y = lat * GCJ_RING_SCALE;
509    let mut inside = false;
510    let mut previous = ring[ring.len() - 1];
511
512    for &current in ring {
513        let xi = current.0 as f64;
514        let yi = current.1 as f64;
515        let xj = previous.0 as f64;
516        let yj = previous.1 as f64;
517
518        if (yi > y) != (yj > y) {
519            let crossing_x = (xj - xi) * (y - yi) / (yj - yi) + xi;
520            if x < crossing_x {
521                inside = !inside;
522            }
523        }
524
525        previous = current;
526    }
527
528    inside
529}
530
531/// Converts GCJ-02 longitude/latitude to BD-09 longitude/latitude.
532///
533/// This is a low-level coordinate-system transform and does not perform a
534/// geographic region check.
535///
536/// # Examples
537///
538/// ```
539/// use coordtransform::gcj02_to_bd09;
540///
541/// let (lon, lat) = gcj02_to_bd09(116.404, 39.915);
542/// assert!((lon - 116.41036949371029).abs() < 1e-12);
543/// assert!((lat - 39.92133699351022).abs() < 1e-12);
544/// ```
545#[inline]
546pub fn gcj02_to_bd09(lon: f64, lat: f64) -> (f64, f64) {
547    gcj02_to_bd09_unchecked(lon, lat)
548}
549
550/// Converts BD-09 longitude/latitude to GCJ-02 longitude/latitude with numerical
551/// refinement for tight forward/reverse consistency.
552///
553/// This is a low-level coordinate-system transform and does not perform a
554/// geographic region check. For the traditional single-pass approximation, use
555/// [`bd09_to_gcj02_fast`].
556///
557/// # Examples
558///
559/// ```
560/// use coordtransform::{bd09_to_gcj02, gcj02_to_bd09};
561///
562/// let original = (116.404, 39.915);
563/// let bd = gcj02_to_bd09(original.0, original.1);
564/// let restored = bd09_to_gcj02(bd.0, bd.1);
565/// assert!((restored.0 - original.0).abs() < 1e-9);
566/// assert!((restored.1 - original.1).abs() < 1e-9);
567/// ```
568#[inline]
569pub fn bd09_to_gcj02(lon: f64, lat: f64) -> (f64, f64) {
570    let mut gcj = bd09_to_gcj02_approx(lon, lat);
571
572    // Two fixed refinements reduce the inverse residual to far below practical
573    // mapping accuracy while keeping runtime predictable for batch workloads.
574    for _ in 0..BD_INVERSE_REFINEMENTS {
575        let forward = gcj02_to_bd09_unchecked(gcj.0, gcj.1);
576        gcj.0 -= forward.0 - lon;
577        gcj.1 -= forward.1 - lat;
578    }
579
580    gcj
581}
582
583/// Converts BD-09 longitude/latitude to GCJ-02 using the traditional single-pass
584/// inverse approximation.
585///
586/// This function is faster and matches the output style of many existing GCJ/BD
587/// implementations, but its round-trip residual is larger than [`bd09_to_gcj02`].
588#[inline]
589pub fn bd09_to_gcj02_fast(lon: f64, lat: f64) -> (f64, f64) {
590    bd09_to_gcj02_approx(lon, lat)
591}
592
593/// Converts WGS 84 longitude/latitude to GCJ-02 longitude/latitude.
594///
595/// The GCJ-02 offset is only applied when the WGS 84 position is inside the
596/// embedded GCJ-02 transform region. Outside that region, the input is
597/// returned unchanged.
598///
599/// # Examples
600///
601/// ```
602/// use coordtransform::wgs84_to_gcj02;
603///
604/// let (lon, lat) = wgs84_to_gcj02(116.404, 39.915);
605/// assert!((lon - 116.41024449916938).abs() < 1e-12);
606/// assert!((lat - 39.91640428150164).abs() < 1e-12);
607/// ```
608#[inline]
609pub fn wgs84_to_gcj02(lon: f64, lat: f64) -> (f64, f64) {
610    if !is_in_mainland_china(lon, lat) {
611        return (lon, lat);
612    }
613    wgs84_to_gcj02_unchecked(lon, lat)
614}
615
616/// Converts a WGS 84 position known to be in the mainland transform region to
617/// GCJ-02 without performing the region lookup.
618///
619/// Use this only when the caller already guarantees that the coordinate belongs
620/// to the transform region. It is useful for high-frequency navigation streams
621/// that never leave mainland China.
622#[inline]
623pub fn wgs84_to_gcj02_in_mainland(lon: f64, lat: f64) -> (f64, f64) {
624    wgs84_to_gcj02_unchecked(lon, lat)
625}
626
627/// Converts GCJ-02 longitude/latitude to WGS 84 longitude/latitude using a
628/// refined numerical inverse.
629///
630/// A rough WGS 84 candidate is recovered first and used for the embedded
631/// region decision. Coordinates outside the mainland transform region are
632/// returned unchanged.
633///
634/// # Examples
635///
636/// ```
637/// use coordtransform::{gcj02_to_wgs84, wgs84_to_gcj02};
638///
639/// let original = (121.4737, 31.2304);
640/// let gcj = wgs84_to_gcj02(original.0, original.1);
641/// let restored = gcj02_to_wgs84(gcj.0, gcj.1);
642/// assert!((restored.0 - original.0).abs() < 1e-9);
643/// assert!((restored.1 - original.1).abs() < 1e-9);
644/// ```
645#[inline]
646pub fn gcj02_to_wgs84(lon: f64, lat: f64) -> (f64, f64) {
647    gcj02_to_wgs84_if_mainland(lon, lat, GCJ_INVERSE_REFINEMENTS).unwrap_or((lon, lat))
648}
649
650/// Converts GCJ-02 longitude/latitude to WGS 84 using only the traditional
651/// one-step inverse approximation after the region check.
652///
653/// This is useful when throughput matters more than sub-metre inverse accuracy.
654/// For the high-accuracy default, use [`gcj02_to_wgs84`].
655#[inline]
656pub fn gcj02_to_wgs84_fast(lon: f64, lat: f64) -> (f64, f64) {
657    gcj02_to_wgs84_if_mainland(lon, lat, 0).unwrap_or((lon, lat))
658}
659
660/// Converts a GCJ-02 position known to belong to the mainland transform region
661/// to WGS 84 without performing the region lookup.
662///
663/// This uses the same refined inverse as [`gcj02_to_wgs84`].
664#[inline]
665pub fn gcj02_to_wgs84_in_mainland(lon: f64, lat: f64) -> (f64, f64) {
666    let seed = gcj02_inverse_seed(lon, lat);
667    refine_gcj_inverse(lon, lat, seed, GCJ_INVERSE_REFINEMENTS)
668}
669
670/// Converts WGS 84 longitude/latitude to BD-09 longitude/latitude.
671///
672/// The transform is only applied inside the embedded GCJ-02 transform
673/// region. Outside that region, the input is returned unchanged.
674#[inline]
675pub fn wgs84_to_bd09(lon: f64, lat: f64) -> (f64, f64) {
676    if !is_in_mainland_china(lon, lat) {
677        return (lon, lat);
678    }
679
680    let gcj = wgs84_to_gcj02_unchecked(lon, lat);
681    gcj02_to_bd09_unchecked(gcj.0, gcj.1)
682}
683
684/// Converts BD-09 longitude/latitude to WGS 84 longitude/latitude.
685///
686/// The BD-09 coordinate is first converted to GCJ-02, then a WGS 84 candidate
687/// is recovered and checked against the embedded GCJ-02 transform region.
688/// Outside that region, the original BD-09 input is returned unchanged.
689#[inline]
690pub fn bd09_to_wgs84(lon: f64, lat: f64) -> (f64, f64) {
691    if !lon.is_finite() || !lat.is_finite() {
692        return (lon, lat);
693    }
694
695    let gcj = bd09_to_gcj02(lon, lat);
696    match gcj02_to_wgs84_if_mainland(gcj.0, gcj.1, GCJ_INVERSE_REFINEMENTS) {
697        Some(wgs) => wgs,
698        None => (lon, lat),
699    }
700}
701
702/// Converts WGS 84 longitude/latitude to Web Mercator (EPSG:3857) metres.
703///
704/// Latitude is clamped to [`MAX_LATITUDE`], matching common slippy-map behavior.
705/// Longitude is not clamped so callers can intentionally use wrapped worlds.
706/// For strict validation, use [`try_wgs84_to_epsg3857`].
707#[inline]
708pub fn wgs84_to_epsg3857(lon: f64, lat: f64) -> (f64, f64) {
709    let lat = lat.clamp(-MAX_LATITUDE, MAX_LATITUDE);
710    web_mercator_forward(lon, lat)
711}
712
713/// Converts Web Mercator (EPSG:3857) metres to WGS 84 longitude/latitude.
714///
715/// Northing is clamped to the canonical finite Web Mercator world extent.
716/// Easting is not clamped so wrapped worlds remain representable. For strict
717/// validation, use [`try_epsg3857_to_wgs84`].
718#[inline]
719pub fn epsg3857_to_wgs84(x: f64, y: f64) -> (f64, f64) {
720    let y = y.clamp(-WEB_MERCATOR_MAX_COORDINATE, WEB_MERCATOR_MAX_COORDINATE);
721    web_mercator_inverse(x, y)
722}
723
724/// Converts GCJ-02 longitude/latitude to Web Mercator metres through WGS 84.
725#[inline]
726pub fn gcj02_to_epsg3857(lon: f64, lat: f64) -> (f64, f64) {
727    let wgs = gcj02_to_wgs84(lon, lat);
728    wgs84_to_epsg3857(wgs.0, wgs.1)
729}
730
731/// Converts Web Mercator metres to GCJ-02 longitude/latitude through WGS 84.
732#[inline]
733pub fn epsg3857_to_gcj02(x: f64, y: f64) -> (f64, f64) {
734    let wgs = epsg3857_to_wgs84(x, y);
735    wgs84_to_gcj02(wgs.0, wgs.1)
736}
737
738/// Converts BD-09 longitude/latitude to Web Mercator metres through WGS 84.
739#[inline]
740pub fn bd09_to_epsg3857(lon: f64, lat: f64) -> (f64, f64) {
741    let wgs = bd09_to_wgs84(lon, lat);
742    wgs84_to_epsg3857(wgs.0, wgs.1)
743}
744
745/// Converts Web Mercator metres to BD-09 longitude/latitude through WGS 84.
746#[inline]
747pub fn epsg3857_to_bd09(x: f64, y: f64) -> (f64, f64) {
748    let wgs = epsg3857_to_wgs84(x, y);
749    wgs84_to_bd09(wgs.0, wgs.1)
750}
751
752/// Errors returned by strict Web Mercator conversion functions.
753#[derive(Debug, Clone, Copy, PartialEq)]
754#[non_exhaustive]
755pub enum ProjectionError {
756    /// At least one supplied coordinate is not finite.
757    NonFiniteCoordinate,
758    /// Longitude is outside `[-180, 180]` degrees.
759    LongitudeOutOfRange {
760        /// The invalid longitude.
761        longitude: f64,
762    },
763    /// Latitude is outside `[-90, 90]` degrees.
764    LatitudeOutOfRange {
765        /// The invalid latitude.
766        latitude: f64,
767    },
768    /// Latitude is valid WGS 84 latitude but outside the finite Web Mercator domain.
769    LatitudeOutsideWebMercator {
770        /// The unsupported latitude.
771        latitude: f64,
772    },
773    /// Easting is outside the canonical finite Web Mercator world extent.
774    XOutsideWebMercator {
775        /// The unsupported easting in metres.
776        x: f64,
777    },
778    /// Northing is outside the canonical finite Web Mercator world extent.
779    YOutsideWebMercator {
780        /// The unsupported northing in metres.
781        y: f64,
782    },
783}
784
785impl fmt::Display for ProjectionError {
786    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
787        match self {
788            Self::NonFiniteCoordinate => write!(f, "coordinate must be finite"),
789            Self::LongitudeOutOfRange { longitude } => {
790                write!(f, "longitude {longitude} is outside [-180, 180] degrees")
791            }
792            Self::LatitudeOutOfRange { latitude } => {
793                write!(f, "latitude {latitude} is outside [-90, 90] degrees")
794            }
795            Self::LatitudeOutsideWebMercator { latitude } => write!(
796                f,
797                "latitude {latitude} is outside the finite EPSG:3857 latitude domain"
798            ),
799            Self::XOutsideWebMercator { x } => write!(
800                f,
801                "Web Mercator x coordinate {x} is outside the canonical world extent"
802            ),
803            Self::YOutsideWebMercator { y } => write!(
804                f,
805                "Web Mercator y coordinate {y} is outside the canonical world extent"
806            ),
807        }
808    }
809}
810
811impl std::error::Error for ProjectionError {}
812
813/// Strictly converts WGS 84 longitude/latitude to Web Mercator metres.
814///
815/// Unlike [`wgs84_to_epsg3857`], this function never clamps. It rejects
816/// non-finite coordinates, longitude outside `[-180, 180]`, latitude outside
817/// `[-90, 90]`, and latitude outside the finite EPSG:3857 domain.
818///
819/// # Errors
820///
821/// Returns [`ProjectionError`] when the input is outside the accepted domain.
822#[inline]
823pub fn try_wgs84_to_epsg3857(lon: f64, lat: f64) -> Result<(f64, f64), ProjectionError> {
824    validate_wgs84(lon, lat)?;
825    if lat.abs() > MAX_LATITUDE {
826        return Err(ProjectionError::LatitudeOutsideWebMercator { latitude: lat });
827    }
828    Ok(web_mercator_forward(lon, lat))
829}
830
831/// Validates a WGS 84 coordinate and converts it to Web Mercator with explicit
832/// latitude clamping.
833///
834/// This preserves the validated clamping behavior used by earlier versions while
835/// keeping the compatibility [`wgs84_to_epsg3857`] and strict
836/// [`try_wgs84_to_epsg3857`] APIs separate.
837///
838/// # Errors
839///
840/// Returns [`ProjectionError`] for non-finite coordinates or longitude/latitude
841/// outside the WGS 84 degree ranges.
842#[inline]
843pub fn wgs84_to_epsg3857_clamped(lon: f64, lat: f64) -> Result<(f64, f64), ProjectionError> {
844    validate_wgs84(lon, lat)?;
845    Ok(web_mercator_forward(
846        lon,
847        lat.clamp(-MAX_LATITUDE, MAX_LATITUDE),
848    ))
849}
850
851/// Strictly converts Web Mercator metres to WGS 84 longitude/latitude.
852///
853/// # Errors
854///
855/// Returns [`ProjectionError`] for non-finite values or coordinates outside the
856/// canonical finite EPSG:3857 world extent.
857#[inline]
858pub fn try_epsg3857_to_wgs84(x: f64, y: f64) -> Result<(f64, f64), ProjectionError> {
859    if !x.is_finite() || !y.is_finite() {
860        return Err(ProjectionError::NonFiniteCoordinate);
861    }
862    if x.abs() > WEB_MERCATOR_MAX_COORDINATE {
863        return Err(ProjectionError::XOutsideWebMercator { x });
864    }
865    if y.abs() > WEB_MERCATOR_MAX_COORDINATE {
866        return Err(ProjectionError::YOutsideWebMercator { y });
867    }
868    Ok(web_mercator_inverse(x, y))
869}
870
871/// Converts a mutable slice of WGS 84 points to GCJ-02 in place.
872///
873/// No allocation is performed. Each point still receives the normal OSM-derived
874/// transform-region check.
875#[inline]
876pub fn wgs84_to_gcj02_in_place(points: &mut [(f64, f64)]) {
877    for point in points {
878        *point = wgs84_to_gcj02(point.0, point.1);
879    }
880}
881
882/// Converts a mutable slice of GCJ-02 points to WGS 84 in place.
883#[inline]
884pub fn gcj02_to_wgs84_in_place(points: &mut [(f64, f64)]) {
885    for point in points {
886        *point = gcj02_to_wgs84(point.0, point.1);
887    }
888}
889
890/// Converts WGS 84 points known to be inside the mainland transform region to
891/// GCJ-02 in place without running region lookup for each point.
892///
893/// This is the highest-throughput batch path for tracks that are already known
894/// to stay inside the transform region. No allocation is performed.
895#[inline]
896pub fn wgs84_to_gcj02_in_mainland_in_place(points: &mut [(f64, f64)]) {
897    for point in points {
898        *point = wgs84_to_gcj02_unchecked(point.0, point.1);
899    }
900}
901
902/// Converts GCJ-02 points known to belong to the mainland transform region to
903/// WGS 84 in place without running region lookup for each point.
904///
905/// The same refined inverse as [`gcj02_to_wgs84`] is used. No allocation is
906/// performed.
907#[inline]
908pub fn gcj02_to_wgs84_in_mainland_in_place(points: &mut [(f64, f64)]) {
909    for point in points {
910        *point = gcj02_to_wgs84_in_mainland(point.0, point.1);
911    }
912}
913
914/// Converts a mutable slice of WGS 84 points to BD-09 in place.
915#[inline]
916pub fn wgs84_to_bd09_in_place(points: &mut [(f64, f64)]) {
917    for point in points {
918        *point = wgs84_to_bd09(point.0, point.1);
919    }
920}
921
922/// Converts a mutable slice of BD-09 points to WGS 84 in place.
923#[inline]
924pub fn bd09_to_wgs84_in_place(points: &mut [(f64, f64)]) {
925    for point in points {
926        *point = bd09_to_wgs84(point.0, point.1);
927    }
928}
929
930#[inline]
931fn gcj02_to_bd09_unchecked(lon: f64, lat: f64) -> (f64, f64) {
932    let z = (lon * lon + lat * lat).sqrt() + BD_RADIAL_EPSILON * (lat * X_PI).sin();
933    let theta = lat.atan2(lon) + BD_ANGULAR_EPSILON * (lon * X_PI).cos();
934    let (sin_theta, cos_theta) = theta.sin_cos();
935
936    (z * cos_theta + BD_LON_OFFSET, z * sin_theta + BD_LAT_OFFSET)
937}
938
939#[inline]
940fn bd09_to_gcj02_approx(lon: f64, lat: f64) -> (f64, f64) {
941    let x = lon - BD_LON_OFFSET;
942    let y = lat - BD_LAT_OFFSET;
943    let z = (x * x + y * y).sqrt() - BD_RADIAL_EPSILON * (y * X_PI).sin();
944    let theta = y.atan2(x) - BD_ANGULAR_EPSILON * (x * X_PI).cos();
945    let (sin_theta, cos_theta) = theta.sin_cos();
946
947    (z * cos_theta, z * sin_theta)
948}
949
950#[inline]
951fn gcj02_to_wgs84_if_mainland(
952    lon: f64,
953    lat: f64,
954    refinements_after_seed: usize,
955) -> Option<(f64, f64)> {
956    if !lon.is_finite() || !lat.is_finite() {
957        return None;
958    }
959
960    let seed = gcj02_inverse_seed(lon, lat);
961
962    // Near the transform boundary, either the observed GCJ-02 coordinate or the
963    // recovered WGS 84 seed can fall on the opposite side by a small offset. The
964    // two-way check makes dispatch more stable without adding a second lookup for
965    // ordinary interior points.
966    if !is_in_mainland_china(seed.0, seed.1) && !is_in_mainland_china(lon, lat) {
967        return None;
968    }
969
970    Some(refine_gcj_inverse(lon, lat, seed, refinements_after_seed))
971}
972
973#[inline]
974fn gcj02_inverse_seed(lon: f64, lat: f64) -> (f64, f64) {
975    let forward = wgs84_to_gcj02_unchecked(lon, lat);
976    (lon * 2.0 - forward.0, lat * 2.0 - forward.1)
977}
978
979#[inline]
980fn refine_gcj_inverse(
981    gcj_lon: f64,
982    gcj_lat: f64,
983    mut wgs: (f64, f64),
984    refinements: usize,
985) -> (f64, f64) {
986    for _ in 0..refinements {
987        let forward = wgs84_to_gcj02_unchecked(wgs.0, wgs.1);
988        wgs.0 -= forward.0 - gcj_lon;
989        wgs.1 -= forward.1 - gcj_lat;
990    }
991    wgs
992}
993
994#[inline]
995fn wgs84_to_gcj02_unchecked(lon: f64, lat: f64) -> (f64, f64) {
996    let x = lon - 105.0;
997    let y = lat - 35.0;
998    let (raw_lat, raw_lon) = transform_delta(x, y);
999
1000    let rad_lat = lat * DEG_TO_RAD;
1001    let (sin_lat, cos_lat) = rad_lat.sin_cos();
1002    let magic = 1.0 - GCJ_ECCENTRICITY_SQUARED * sin_lat * sin_lat;
1003    let sqrt_magic = magic.sqrt();
1004
1005    let delta_lat =
1006        raw_lat * GCJ_DEGREE_SCALE * magic * sqrt_magic / (1.0 - GCJ_ECCENTRICITY_SQUARED);
1007    let delta_lon = raw_lon * GCJ_DEGREE_SCALE * sqrt_magic / cos_lat;
1008
1009    (lon + delta_lon, lat + delta_lat)
1010}
1011
1012#[inline]
1013fn transform_delta(x: f64, y: f64) -> (f64, f64) {
1014    let xy = x * y;
1015    let sqrt_abs_x = x.abs().sqrt();
1016    let x_pi = x * PI;
1017    let y_pi = y * PI;
1018
1019    // One sin_cos at x*pi/3 yields the x*pi, 2*x*pi, and 6*x*pi terms
1020    // through angle-multiplication identities.
1021    let (sin_x_third, cos_x_third) = (x_pi / 3.0).sin_cos();
1022    let sin_x = sin_triple(sin_x_third);
1023    let cos_x = cos_triple(cos_x_third);
1024    let sin_2x = 2.0 * sin_x * cos_x;
1025    let sin_6x = sin_triple(sin_2x);
1026    let shared = 20.0 * sin_6x + 20.0 * sin_2x;
1027
1028    // The y*pi term is the triple-angle value of y*pi/3.
1029    let sin_y_third = (y_pi / 3.0).sin();
1030    let sin_y = sin_triple(sin_y_third);
1031
1032    // Using a base angle of pi/60 gives both /30 and /12 terms from one
1033    // sin_cos call for each axis.
1034    let (sin_x_thirtieth, sin_x_twelfth) = sin_double_and_quintuple(x_pi / 60.0);
1035    let (sin_y_thirtieth, sin_y_twelfth) = sin_double_and_quintuple(y_pi / 60.0);
1036
1037    let delta_lat = -100.0
1038        + 2.0 * x
1039        + 3.0 * y
1040        + 0.2 * y * y
1041        + 0.1 * xy
1042        + 0.2 * sqrt_abs_x
1043        + (shared
1044            + 20.0 * sin_y
1045            + 40.0 * sin_y_third
1046            + 160.0 * sin_y_twelfth
1047            + 320.0 * sin_y_thirtieth)
1048            * (2.0 / 3.0);
1049
1050    let delta_lon = 300.0
1051        + x
1052        + 2.0 * y
1053        + 0.1 * x * x
1054        + 0.1 * xy
1055        + 0.1 * sqrt_abs_x
1056        + (shared
1057            + 20.0 * sin_x
1058            + 40.0 * sin_x_third
1059            + 150.0 * sin_x_twelfth
1060            + 300.0 * sin_x_thirtieth)
1061            * (2.0 / 3.0);
1062
1063    (delta_lat, delta_lon)
1064}
1065
1066#[inline]
1067fn sin_triple(sin_angle: f64) -> f64 {
1068    sin_angle * (3.0 - 4.0 * sin_angle * sin_angle)
1069}
1070
1071#[inline]
1072fn cos_triple(cos_angle: f64) -> f64 {
1073    cos_angle * (4.0 * cos_angle * cos_angle - 3.0)
1074}
1075
1076#[inline]
1077fn sin_double_and_quintuple(angle: f64) -> (f64, f64) {
1078    let (sin_a, cos_a) = angle.sin_cos();
1079    let sin_2a = 2.0 * sin_a * cos_a;
1080    let cos_2a = cos_a * cos_a - sin_a * sin_a;
1081    let sin_4a = 2.0 * sin_2a * cos_2a;
1082    let cos_4a = cos_2a * cos_2a - sin_2a * sin_2a;
1083    let sin_5a = sin_4a * cos_a + cos_4a * sin_a;
1084    (sin_2a, sin_5a)
1085}
1086
1087#[inline]
1088fn validate_wgs84(lon: f64, lat: f64) -> Result<(), ProjectionError> {
1089    if !lon.is_finite() || !lat.is_finite() {
1090        return Err(ProjectionError::NonFiniteCoordinate);
1091    }
1092    if !(-180.0..=180.0).contains(&lon) {
1093        return Err(ProjectionError::LongitudeOutOfRange { longitude: lon });
1094    }
1095    if !(-90.0..=90.0).contains(&lat) {
1096        return Err(ProjectionError::LatitudeOutOfRange { latitude: lat });
1097    }
1098    Ok(())
1099}
1100
1101#[inline]
1102fn web_mercator_forward(lon: f64, lat: f64) -> (f64, f64) {
1103    let lon_rad = lon * DEG_TO_RAD;
1104    let lat_rad = lat * DEG_TO_RAD;
1105    let x = lon_rad * WEB_MERCATOR_RADIUS;
1106    let y = lat_rad.tan().asinh() * WEB_MERCATOR_RADIUS;
1107    (x, y)
1108}
1109
1110#[inline]
1111fn web_mercator_inverse(x: f64, y: f64) -> (f64, f64) {
1112    let lon = x / WEB_MERCATOR_RADIUS * RAD_TO_DEG;
1113    let lat = (y / WEB_MERCATOR_RADIUS).sinh().atan() * RAD_TO_DEG;
1114    (lon, lat)
1115}
1116
1117#[cfg(test)]
1118mod tests {
1119    use super::*;
1120
1121    fn assert_close(actual: f64, expected: f64, tolerance: f64) {
1122        assert!(
1123            (actual - expected).abs() <= tolerance,
1124            "actual={actual:.15}, expected={expected:.15}, tolerance={tolerance:e}"
1125        );
1126    }
1127
1128    fn assert_point_close(actual: (f64, f64), expected: (f64, f64), tolerance: f64) {
1129        assert_close(actual.0, expected.0, tolerance);
1130        assert_close(actual.1, expected.1, tolerance);
1131    }
1132
1133    #[test]
1134    fn mainland_region_accepts_representative_locations() {
1135        let inside = [
1136            (116.4074, 39.9042), // Beijing
1137            (121.4737, 31.2304), // Shanghai
1138            (106.5516, 29.5630), // Chongqing
1139            (111.7490, 40.8426), // Hohhot, Inner Mongolia
1140            (113.2644, 23.1291), // Guangzhou
1141            (87.6168, 43.8256),  // Urumqi
1142            (91.1322, 29.6604),  // Lhasa
1143            (109.5119, 18.2528), // Sanya
1144        ];
1145
1146        for point in inside {
1147            assert!(
1148                is_in_gcj02_region(point.0, point.1),
1149                "expected mainland transform region: {point:?}"
1150            );
1151        }
1152    }
1153
1154    #[test]
1155    fn mainland_region_accepts_border_area_chinese_cities() {
1156        let inside = [
1157            (124.3947, 40.1253), // Dandong
1158            (97.8550, 24.0128),  // Ruili
1159            (127.4990, 50.2496), // Heihe
1160            (80.4208, 44.2017),  // Khorgos
1161            (107.9718, 21.5478), // Dongxing
1162            (114.0579, 22.5431), // Shenzhen
1163            (113.5767, 22.2707), // Zhuhai
1164            (118.0894, 24.4798), // Xiamen
1165        ];
1166
1167        for point in inside {
1168            assert!(
1169                is_in_gcj02_region(point.0, point.1),
1170                "expected mainland transform region near boundary: {point:?}"
1171            );
1172        }
1173    }
1174
1175    #[test]
1176    fn mainland_region_rejects_special_regions_and_nearby_countries() {
1177        let outside = [
1178            (114.1694, 22.3193), // Hong Kong
1179            (113.5439, 22.1987), // Macau
1180            (121.5654, 25.0330), // Taipei
1181            (139.6917, 35.6895), // Tokyo
1182            (126.9780, 37.5665), // Seoul
1183            (106.9057, 47.8864), // Ulaanbaatar
1184            (85.3240, 27.7172),  // Kathmandu
1185            (105.8342, 21.0278), // Hanoi
1186            (103.8500, 22.4800), // Lao Cai, Vietnam
1187            (124.4072, 40.1028), // Sinuiju, North Korea
1188            (112.3000, 16.5000), // Remote offshore point below the app dispatch mask
1189            (172.6362, -43.5321),
1190        ];
1191
1192        for point in outside {
1193            assert!(
1194                !is_in_gcj02_region(point.0, point.1),
1195                "expected outside transform region: {point:?}"
1196            );
1197        }
1198    }
1199
1200    #[test]
1201    fn hong_kong_and_macau_exclusions_do_not_swallow_nearby_mainland_centres() {
1202        assert!(!is_in_gcj02_region(114.1694, 22.3193)); // Hong Kong
1203        assert!(!is_in_gcj02_region(113.5439, 22.1987)); // Macau
1204        assert!(is_in_gcj02_region(114.0579, 22.5431)); // Shenzhen
1205        assert!(is_in_gcj02_region(113.5767, 22.2707)); // Zhuhai
1206    }
1207
1208    #[test]
1209    fn invalid_region_inputs_are_rejected() {
1210        assert!(!is_in_gcj02_region(f64::NAN, 30.0));
1211        assert!(!is_in_gcj02_region(110.0, f64::NAN));
1212        assert!(!is_in_gcj02_region(f64::INFINITY, 30.0));
1213        assert!(!is_in_gcj02_region(110.0, f64::NEG_INFINITY));
1214    }
1215
1216    fn reference_wgs84_to_gcj02_unchecked(lon: f64, lat: f64) -> (f64, f64) {
1217        let x = lon - 105.0;
1218        let y = lat - 35.0;
1219        let xy = x * y;
1220        let sqrt_abs_x = x.abs().sqrt();
1221        let x_pi = x * PI;
1222        let y_pi = y * PI;
1223
1224        let shared = 20.0 * (6.0 * x_pi).sin() + 20.0 * (2.0 * x_pi).sin();
1225
1226        let raw_lat = -100.0
1227            + 2.0 * x
1228            + 3.0 * y
1229            + 0.2 * y * y
1230            + 0.1 * xy
1231            + 0.2 * sqrt_abs_x
1232            + (shared
1233                + 20.0 * y_pi.sin()
1234                + 40.0 * (y_pi / 3.0).sin()
1235                + 160.0 * (y_pi / 12.0).sin()
1236                + 320.0 * (y_pi / 30.0).sin())
1237                * (2.0 / 3.0);
1238
1239        let raw_lon = 300.0
1240            + x
1241            + 2.0 * y
1242            + 0.1 * x * x
1243            + 0.1 * xy
1244            + 0.1 * sqrt_abs_x
1245            + (shared
1246                + 20.0 * x_pi.sin()
1247                + 40.0 * (x_pi / 3.0).sin()
1248                + 150.0 * (x_pi / 12.0).sin()
1249                + 300.0 * (x_pi / 30.0).sin())
1250                * (2.0 / 3.0);
1251
1252        let rad_lat = lat * DEG_TO_RAD;
1253        let sin_lat = rad_lat.sin();
1254        let magic = 1.0 - GCJ_ECCENTRICITY_SQUARED * sin_lat * sin_lat;
1255        let sqrt_magic = magic.sqrt();
1256
1257        let delta_lat =
1258            raw_lat * GCJ_DEGREE_SCALE * magic * sqrt_magic / (1.0 - GCJ_ECCENTRICITY_SQUARED);
1259        let delta_lon = raw_lon * GCJ_DEGREE_SCALE * sqrt_magic / rad_lat.cos();
1260
1261        (lon + delta_lon, lat + delta_lat)
1262    }
1263
1264    #[test]
1265    fn optimized_gcj_forward_matches_straightforward_reference_formula() {
1266        let longitudes = [74.0, 87.6, 104.1, 113.3, 116.4, 121.5, 126.6, 134.0];
1267        let latitudes = [18.3, 23.1, 29.6, 31.2, 39.9, 43.8, 45.8, 52.0];
1268
1269        for lon in longitudes {
1270            for lat in latitudes {
1271                let optimized = wgs84_to_gcj02_unchecked(lon, lat);
1272                let reference = reference_wgs84_to_gcj02_unchecked(lon, lat);
1273                assert_point_close(optimized, reference, 1e-12);
1274            }
1275        }
1276    }
1277
1278    #[test]
1279    fn beijing_wgs84_to_gcj02_matches_reference_formula() {
1280        let actual = wgs84_to_gcj02(116.404, 39.915);
1281        assert_point_close(
1282            actual,
1283            (116.410_244_499_169_38, 39.916_404_281_501_64),
1284            1e-12,
1285        );
1286    }
1287
1288    #[test]
1289    fn shanghai_wgs84_to_gcj02_is_not_accidentally_identity() {
1290        let original = (121.4737, 31.2304);
1291        let converted = wgs84_to_gcj02(original.0, original.1);
1292        assert_ne!(converted, original);
1293        assert_point_close(
1294            converted,
1295            (121.478_223_059_276_93, 31.228_457_737_577_27),
1296            1e-12,
1297        );
1298    }
1299
1300    #[test]
1301    fn chongqing_and_inner_mongolia_are_not_accidentally_identity() {
1302        for original in [(106.5516, 29.5630), (111.7490, 40.8426)] {
1303            assert_ne!(
1304                wgs84_to_gcj02(original.0, original.1),
1305                original,
1306                "point={original:?}"
1307            );
1308        }
1309    }
1310
1311    #[test]
1312    fn gcj_wgs_round_trip_is_tight_across_mainland() {
1313        let samples = [
1314            (116.4040, 39.9150),
1315            (121.4737, 31.2304),
1316            (106.5516, 29.5630),
1317            (113.2644, 23.1291),
1318            (104.0665, 30.5728),
1319            (87.6168, 43.8256),
1320            (126.6424, 45.7567),
1321            (109.5119, 18.2528),
1322        ];
1323
1324        for original in samples {
1325            let gcj = wgs84_to_gcj02(original.0, original.1);
1326            let restored = gcj02_to_wgs84(gcj.0, gcj.1);
1327            assert_point_close(restored, original, 5e-10);
1328        }
1329    }
1330
1331    #[test]
1332    fn refined_gcj_inverse_is_more_accurate_than_fast_inverse() {
1333        let original = (116.404, 39.915);
1334        let gcj = wgs84_to_gcj02(original.0, original.1);
1335        let fast = gcj02_to_wgs84_fast(gcj.0, gcj.1);
1336        let precise = gcj02_to_wgs84(gcj.0, gcj.1);
1337
1338        let fast_error = (fast.0 - original.0).abs().max((fast.1 - original.1).abs());
1339        let precise_error = (precise.0 - original.0)
1340            .abs()
1341            .max((precise.1 - original.1).abs());
1342
1343        assert!(precise_error < fast_error);
1344        assert!(precise_error < 5e-10);
1345    }
1346
1347    #[test]
1348    fn gcj_bd_reference_forward_value_is_preserved() {
1349        let actual = gcj02_to_bd09(116.404, 39.915);
1350        assert_point_close(
1351            actual,
1352            (116.410_369_493_710_29, 39.921_336_993_510_22),
1353            1e-12,
1354        );
1355    }
1356
1357    #[test]
1358    fn fast_bd_inverse_preserves_traditional_reference_value() {
1359        let actual = bd09_to_gcj02_fast(116.404, 39.915);
1360        assert_point_close(
1361            actual,
1362            (116.397_627_291_193_15, 39.908_656_739_576_31),
1363            1e-12,
1364        );
1365    }
1366
1367    #[test]
1368    fn refined_bd_inverse_round_trip_is_tight() {
1369        let samples = [
1370            (116.4040, 39.9150),
1371            (121.4737, 31.2304),
1372            (113.2644, 23.1291),
1373            (104.0665, 30.5728),
1374            (87.6168, 43.8256),
1375        ];
1376
1377        for original in samples {
1378            let bd = gcj02_to_bd09(original.0, original.1);
1379            let restored = bd09_to_gcj02(bd.0, bd.1);
1380            assert_point_close(restored, original, 5e-9);
1381        }
1382    }
1383
1384    #[test]
1385    fn refined_bd_inverse_beats_single_pass_approximation() {
1386        let original = (113.2644, 23.1291);
1387        let bd = gcj02_to_bd09(original.0, original.1);
1388        let fast = bd09_to_gcj02_fast(bd.0, bd.1);
1389        let precise = bd09_to_gcj02(bd.0, bd.1);
1390
1391        let fast_error = (fast.0 - original.0).abs().max((fast.1 - original.1).abs());
1392        let precise_error = (precise.0 - original.0)
1393            .abs()
1394            .max((precise.1 - original.1).abs());
1395
1396        assert!(precise_error < fast_error);
1397        assert!(precise_error < 5e-9);
1398    }
1399
1400    #[test]
1401    fn wgs_bd_round_trip_is_tight_inside_region() {
1402        let samples = [
1403            (116.4040, 39.9150),
1404            (121.4737, 31.2304),
1405            (113.2644, 23.1291),
1406            (104.0665, 30.5728),
1407        ];
1408
1409        for original in samples {
1410            let bd = wgs84_to_bd09(original.0, original.1);
1411            let restored = bd09_to_wgs84(bd.0, bd.1);
1412            assert_point_close(restored, original, 5e-9);
1413        }
1414    }
1415
1416    #[test]
1417    fn guarded_china_transforms_are_identity_outside_region() {
1418        let samples = [
1419            (0.0, 0.0),
1420            (-73.9857, 40.7484),
1421            (2.3522, 48.8566),
1422            (139.6917, 35.6895),
1423            (114.1694, 22.3193),
1424            (113.5439, 22.1987),
1425            (121.5654, 25.0330),
1426        ];
1427
1428        for original in samples {
1429            assert_eq!(wgs84_to_gcj02(original.0, original.1), original);
1430            assert_eq!(gcj02_to_wgs84(original.0, original.1), original);
1431            assert_eq!(wgs84_to_bd09(original.0, original.1), original);
1432            assert_eq!(bd09_to_wgs84(original.0, original.1), original);
1433        }
1434    }
1435
1436    #[test]
1437    fn direct_gcj_bd_functions_remain_low_level_math_transforms() {
1438        let outside = (2.3522, 48.8566);
1439        assert_ne!(gcj02_to_bd09(outside.0, outside.1), outside);
1440        assert_ne!(bd09_to_gcj02(outside.0, outside.1), outside);
1441    }
1442
1443    #[test]
1444    fn web_mercator_known_value_matches_reference() {
1445        let (x, y) = wgs84_to_epsg3857(116.404, 39.915);
1446        assert_close(x, 12_958_034.006_300_215, 1e-6);
1447        assert_close(y, 4_853_597.988_299_838, 1e-6);
1448    }
1449
1450    #[test]
1451    fn web_mercator_round_trip_is_tight_worldwide() {
1452        let samples = [
1453            (0.0, 0.0),
1454            (116.404, 39.915),
1455            (-73.9857, 40.7484),
1456            (151.2093, -33.8688),
1457            (172.6362, -43.5321),
1458            (-0.1276, 51.5072),
1459        ];
1460
1461        for original in samples {
1462            let projected = wgs84_to_epsg3857(original.0, original.1);
1463            let restored = epsg3857_to_wgs84(projected.0, projected.1);
1464            assert_point_close(restored, original, 1e-12);
1465        }
1466    }
1467
1468    #[test]
1469    fn web_mercator_clamps_only_in_compatibility_api() {
1470        let (_, north) = wgs84_to_epsg3857(0.0, 90.0);
1471        assert_close(north, WEB_MERCATOR_MAX_COORDINATE, 1e-6);
1472
1473        assert!(matches!(
1474            try_wgs84_to_epsg3857(0.0, 90.0),
1475            Err(ProjectionError::LatitudeOutsideWebMercator { .. })
1476        ));
1477    }
1478
1479    #[test]
1480    fn strict_web_mercator_rejects_invalid_inputs() {
1481        assert!(matches!(
1482            try_wgs84_to_epsg3857(181.0, 0.0),
1483            Err(ProjectionError::LongitudeOutOfRange { .. })
1484        ));
1485        assert!(matches!(
1486            try_wgs84_to_epsg3857(0.0, f64::NAN),
1487            Err(ProjectionError::NonFiniteCoordinate)
1488        ));
1489        assert!(matches!(
1490            try_epsg3857_to_wgs84(WEB_MERCATOR_MAX_COORDINATE + 1.0, 0.0),
1491            Err(ProjectionError::XOutsideWebMercator { .. })
1492        ));
1493        assert!(matches!(
1494            try_epsg3857_to_wgs84(0.0, WEB_MERCATOR_MAX_COORDINATE + 1.0),
1495            Err(ProjectionError::YOutsideWebMercator { .. })
1496        ));
1497    }
1498
1499    #[test]
1500    fn strict_and_compatibility_mercator_match_for_valid_inputs() {
1501        let samples = [
1502            (0.0, 0.0),
1503            (116.404, 39.915),
1504            (-73.9857, 40.7484),
1505            (151.2093, -33.8688),
1506        ];
1507
1508        for point in samples {
1509            assert_eq!(
1510                try_wgs84_to_epsg3857(point.0, point.1).unwrap(),
1511                wgs84_to_epsg3857(point.0, point.1)
1512            );
1513        }
1514    }
1515
1516    #[test]
1517    fn in_place_gcj_batch_matches_scalar_conversion() {
1518        let original = [(116.404, 39.915), (121.4737, 31.2304), (172.6362, -43.5321)];
1519        let expected = original.map(|point| wgs84_to_gcj02(point.0, point.1));
1520        let mut actual = original;
1521        wgs84_to_gcj02_in_place(&mut actual);
1522        assert_eq!(actual, expected);
1523    }
1524
1525    #[test]
1526    fn in_place_bd_batch_round_trips() {
1527        let original = [(116.404, 39.915), (121.4737, 31.2304), (172.6362, -43.5321)];
1528        let mut points = original;
1529        wgs84_to_bd09_in_place(&mut points);
1530        bd09_to_wgs84_in_place(&mut points);
1531
1532        for (actual, expected) in points.into_iter().zip(original) {
1533            assert_point_close(actual, expected, 5e-9);
1534        }
1535    }
1536
1537    #[test]
1538    fn mainland_in_place_fast_path_matches_scalar_fast_path() {
1539        let original = [(116.404, 39.915), (121.4737, 31.2304), (113.2644, 23.1291)];
1540
1541        let mut gcj = original;
1542        wgs84_to_gcj02_in_mainland_in_place(&mut gcj);
1543        for (actual, point) in gcj.into_iter().zip(original) {
1544            assert_eq!(actual, wgs84_to_gcj02_in_mainland(point.0, point.1));
1545        }
1546
1547        let mut restored = original.map(|point| wgs84_to_gcj02_in_mainland(point.0, point.1));
1548        gcj02_to_wgs84_in_mainland_in_place(&mut restored);
1549        for (actual, expected) in restored.into_iter().zip(original) {
1550            assert_point_close(actual, expected, 5e-10);
1551        }
1552    }
1553
1554    #[test]
1555    fn mainland_fast_path_matches_guarded_path_for_known_inside_points() {
1556        for point in [(116.404, 39.915), (121.4737, 31.2304), (113.2644, 23.1291)] {
1557            assert_eq!(
1558                wgs84_to_gcj02_in_mainland(point.0, point.1),
1559                wgs84_to_gcj02(point.0, point.1)
1560            );
1561        }
1562    }
1563}