1#![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
106pub const MAX_LATITUDE: f64 = 85.051_128_779_806_6;
108
109pub const OSM_ATTRIBUTION: &str =
114 "Boundary mask derived from OpenStreetMap data (c) OpenStreetMap contributors, ODbL 1.0";
115
116const 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, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x00001f8000000000, 0x00001fc000000000, 0x00001fc000000000, 0x00001fe000000000, 0x00001fe000000000, 0x00001fc000000000, 0x00001fc000000000, 0x00001ff810000000, 0x00001ffcf0000000, 0x00003ffffc000000, 0x00007ffff8000000, 0x00007ffff8000000, 0x0003fffff8000000, 0x0003fffff800c000, 0x0007ffffff7ff000, 0x000ffffffffff800, 0x000fffffffffff00, 0x000fffffffffff00, 0x000fffffffffff80, 0x0007ffffffffff80, 0x0007ffffffffff80, 0x0007fffffffffff0, 0x0007fffffffffff8, 0x000ffffffffffff8, 0x000ffffffffffff8, 0x000fffffffffffe0, 0x003ffffc7fffff00, 0x00ffff8001fffe00, 0x07ffff00007ffe00, 0x07fff800001ffe00, 0x07ffe0000003f800, 0x1fff00000003f800, 0x23ff00000003e000, 0x01ffe00000000000, 0x007fc00000000000, 0x007f000000000000, 0x003e000000000000, 0x001e000000000000, 0x0000000000000000, ];
338
339const GCJ_BORDER_ROWS: [u64; 54] = [
340 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x00007fc000000000, 0x0000606000000000, 0x0000202000000000, 0x0000203000000000, 0x0000201000000000, 0x0000201000000000, 0x0000203000000000, 0x0000203c38000000, 0x00002007e8000000, 0x000060030e000000, 0x0000c00002000000, 0x0001800006000000, 0x0007800004000000, 0x00040000041be000, 0x000c000007ff3800, 0x0018000000800c00, 0x0030000000000780, 0x00100000000000c0, 0x00100000000000c0, 0x0010000000000040, 0x0018000000000040, 0x0008000000000078, 0x000800000000000c, 0x0018000000000004, 0x0010000000000006, 0x0010000000000006, 0x007000000000001c, 0x01c00003800000f0, 0x0f00007efe000180, 0x080000c003800100, 0x0800078000e00180, 0x38001d80003c0780, 0x6000f000000c0400, 0x5c00f00000041c00, 0x760010000007f000, 0x038030000000c000, 0x0080e00000000000, 0x00c1800000000000, 0x0061000000000000, 0x003f000000000000, ];
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#[inline]
429pub fn warm_up() {}
430
431#[inline]
450pub fn is_in_gcj02_region(lon: f64, lat: f64) -> bool {
451 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 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#[inline]
495pub fn is_in_mainland_china(lon: f64, lat: f64) -> bool {
496 is_in_gcj02_region(lon, lat)
497}
498
499#[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 ¤t 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#[inline]
546pub fn gcj02_to_bd09(lon: f64, lat: f64) -> (f64, f64) {
547 gcj02_to_bd09_unchecked(lon, lat)
548}
549
550#[inline]
569pub fn bd09_to_gcj02(lon: f64, lat: f64) -> (f64, f64) {
570 let mut gcj = bd09_to_gcj02_approx(lon, lat);
571
572 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#[inline]
589pub fn bd09_to_gcj02_fast(lon: f64, lat: f64) -> (f64, f64) {
590 bd09_to_gcj02_approx(lon, lat)
591}
592
593#[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#[inline]
623pub fn wgs84_to_gcj02_in_mainland(lon: f64, lat: f64) -> (f64, f64) {
624 wgs84_to_gcj02_unchecked(lon, lat)
625}
626
627#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[derive(Debug, Clone, Copy, PartialEq)]
754#[non_exhaustive]
755pub enum ProjectionError {
756 NonFiniteCoordinate,
758 LongitudeOutOfRange {
760 longitude: f64,
762 },
763 LatitudeOutOfRange {
765 latitude: f64,
767 },
768 LatitudeOutsideWebMercator {
770 latitude: f64,
772 },
773 XOutsideWebMercator {
775 x: f64,
777 },
778 YOutsideWebMercator {
780 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#[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#[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#[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#[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#[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#[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#[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#[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#[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 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 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 let sin_y_third = (y_pi / 3.0).sin();
1030 let sin_y = sin_triple(sin_y_third);
1031
1032 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), (121.4737, 31.2304), (106.5516, 29.5630), (111.7490, 40.8426), (113.2644, 23.1291), (87.6168, 43.8256), (91.1322, 29.6604), (109.5119, 18.2528), ];
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), (97.8550, 24.0128), (127.4990, 50.2496), (80.4208, 44.2017), (107.9718, 21.5478), (114.0579, 22.5431), (113.5767, 22.2707), (118.0894, 24.4798), ];
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), (113.5439, 22.1987), (121.5654, 25.0330), (139.6917, 35.6895), (126.9780, 37.5665), (106.9057, 47.8864), (85.3240, 27.7172), (105.8342, 21.0278), (103.8500, 22.4800), (124.4072, 40.1028), (112.3000, 16.5000), (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)); assert!(!is_in_gcj02_region(113.5439, 22.1987)); assert!(is_in_gcj02_region(114.0579, 22.5431)); assert!(is_in_gcj02_region(113.5767, 22.2707)); }
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}