Pure MoonBit geodesy: Karney geodesics, rhumb lines, intersections, LocalCartesian ENU (GeographicLib port), UTM/UPS/MGRS, Lambert and Albers conic grids, WMM2025 magnetic declination, and WGS-84/GCJ-02/BD-09 conversions
中文简介见下方。
| Package | What it does |
|---|---|
| ChrisTu117/moongeodesy/geodesic | Karney's geodesic algorithms (series and GeodesicExact elliptic integrals), rhumb lines, geodesic intersections, and local ENU (LocalCartesian): inverse & direct problems, geodesic lines, polygon area/perimeter, geocentric ECEF on any ellipsoid |
| ChrisTu117/moongeodesy/utm | Transverse Mercator (6th-order Krüger series), polar stereographic, UTM/UPS with standard zones, MGRS encode/decode |
| ChrisTu117/moongeodesy/conic | Lambert conformal conic and Albers equal-area conic (one or two standard parallels, any ellipsoid), EPSG-style grids with a false origin |
| ChrisTu117/moongeodesy/magnetic | World Magnetic Model 2025: declination, inclination, field components and their yearly change, grid variation, compass Blackout/Caution zones |
| ChrisTu117/moongeodesy/cn | WGS-84 → GCJ-02 (Gaode/Tencent), GCJ-02 → BD-09 (Baidu), with exact iterative inverses |
moon add ChrisTu117/moongeodesyimport {
"ChrisTu117/moongeodesy/geodesic",
"ChrisTu117/moongeodesy/utm",
"ChrisTu117/moongeodesy/conic",
"ChrisTu117/moongeodesy/magnetic",
"ChrisTu117/moongeodesy/cn",
}let g = @geodesic.Geodesic::wgs84()
// Inverse problem: distance and bearings between two points
let r = g.inverse(39.9087, 116.3975, 31.2304, 121.4737) // Beijing → Shanghai
// r.s12 = 1066676.4 m, r.azi1 = 152.94°, r.azi2, r.m12, r.area12 ...
// Direct problem: go 1000 km due east from Shenzhen
let d = g.direct(22.5431, 114.0579, 90.0, 1000000.0) // d.lat2, d.lon2, d.azi2
// Geodesic line: waypoints at most 100 km apart, e.g. for drawing a route
let line = g.inverse_line(31.2304, 121.4737, 37.7749, -122.4194)
let pts = line.densify(100000.0)
// Polygon area (m², counter-clockwise positive) and perimeter (m)
let a = g.polygon_area([(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0)])
// Rhumb line: constant-azimuth distance, London → Tokyo (longer than the geodesic)
let rh = @geodesic.Rhumb::wgs84()
let h = rh.inverse(51.4778, -0.0015, 35.6762, 139.6503) // h.s12, h.azi12, h.area12
// Intersection of two geodesics (signed metres from each start)
let ix = @geodesic.Intersect::wgs84()
let cross = ix.closest(0.0, 0.0, 45.0, 45.0, 10.0, 135.0) // cross.x, cross.y, cross.c
// Local East-North-Up relative to an origin (ellipsoidal height in metres)
let lc = @geodesic.LocalCartesian::wgs84(39.9087, 116.3975)
let enu = lc.forward(39.9097, 116.3985, 50.0) // enu.x east, enu.y north, enu.z up
let back = lc.reverse(enu.x, enu.y, enu.z) // back.lat, back.lon, back.h
// Other ellipsoids: CGCS2000 / GRS80 built in, or any (a, f)
let cgcs = @geodesic.Geodesic::cgcs2000()
let sphere = @geodesic.Geodesic::new(6371000.0, 0.0)
// High flattening (|f| above about 1/50): elliptic-integral geodesic
let flat = @geodesic.GeodesicExact::new(6378137.0, 0.1)
let hx = flat.inverse(40.6, -73.8, 1.5, 0.0) // hx.s12
// Chinese map coordinate systems
let gcj = @cn.wgs84_to_gcj02(@cn.LatLon::new(39.9087, 116.3975)) // GPS → Gaode
let bd = @cn.gcj02_to_bd09(gcj) // Gaode → Baidu
let wgs = @cn.bd09_to_wgs84(bd) // exact inverse (round trip < 1e-9°)
// UTM / UPS / MGRS (functions raise @utm.UtmError on bad input)
let u = @utm.from_latlon(39.9087, 116.3975) // zone 50N, easting/northing, convergence, scale
let m = @utm.to_mgrs(39.9087, 116.3975) // "50SMK4850217797"
let p = @utm.from_mgrs("50S MK 48502 17797") // centre of the 1 m square
// Conic grids: CGCS2000 Albers (equal area) with parallels 25°N/47°N, 105°E
let albers = @conic.ConicGrid::albers(
6378137.0,
1.0 / 298.257222101,
lat_origin=0.0,
lon_origin=105.0,
stdlat1=25.0,
stdlat2=47.0,
)
let gp = albers.forward(39.9087, 116.3975) // gp.easting = 956117.8 m, gp.northing, gp.gamma, gp.k
// Magnetic declination (World Magnetic Model 2025) for a compass
let wmm = @magnetic.MagneticModel::wmm2025()
let t = @magnetic.decimal_year(2026, 11, 10)
let mag = wmm.field(39.9087, 116.3975, 0.0, t) // mag.decl = -7.56° (west), mag.incl, mag.h ...
let compass = @magnetic.magnetic_bearing(r.azi1, mag.decl) // true bearing → compass bearingmoon run src/examples/demomoon run src/examples/cli -- geod inverse 39.9087 116.3975 31.2304 121.4737
moon run src/examples/cli -- geod direct 22.5431 114.0579 90 1000000
moon run src/examples/cli -- rhumb inverse 51.4778 -0.0015 35.6762 139.6503
moon run src/examples/cli -- intersect closest 0 0 45 45 10 135
moon run src/examples/cli -- enu forward 39.9087 116.3975 39.9097 116.3985 50
moon run src/examples/cli -- coord wgs2gcj 39.9087 116.3975
moon run src/examples/cli -- coord gcj2wgs 39.91010 116.40374
moon run src/examples/cli -- coord wgs2bd 39.9087 116.3975
moon run src/examples/cli -- coord bd2wgs 39.91645 116.41012
moon run src/examples/cli -- mag 39.9087 116.3975 2026-11-10
moon run src/examples/cli -- albers forward 25 47 0 105 39.9087 116.3975
moon run src/examples/cli -- lcc reverse 25 47 0 105 1000000 4000000| Scenario | Run | What it shows |
|---|---|---|
| Map APIs → true distance | moon run src/examples/s1_cn_distance | a Gaode (GCJ-02) shop and a Baidu (BD-09) customer converted to WGS-84, then the geodesic distance; mixing raw coordinates is off by ~764 m |
| UTM ↔ MGRS field survey | moon run src/examples/s2_utm_mgrs | UTM/UPS and MGRS for Beijing, Sydney, the Norway 32V and Svalbard 33X exceptions and the South Pole; UTM round trips below 1 µm, MGRS cell corners within √2 m |
| Accuracy vs GeographicLib | moon run src/examples/s3_accuracy | 200 cases of the official GeodTest set, max inverse/direct errors per distance band (table below) |
| Field and region area | moon run src/examples/s4_polygon_area | a field traced in GCJ-02 converted to WGS-84, its geodesic area in m², ha and 亩, incremental PolygonArea, and a continental triangle whose area and perimeter agree with Python geographiclib to 1e-12 (relative) |
| Compass bearing for a survey | moon run src/examples/s5_compass | GPS start, Gaode target: geodesic true bearing → compass bearing with the WMM2025 declination (263 m off over 2 km if ignored); the same bearing from a UTM map via grivation; declination from Mohe (−12.8°) to Kashgar (+4.1°) |
| Areas on conic grids | moon run src/examples/s6_conic_area | 0.5° cells from Sanya to Mohe measured on CGCS2000 Albers (agrees with the ellipsoid to 1e-8; measured 1e-9), Lambert (off by up to 6%) and Web Mercator (off by up to 176%); Lambert scale and convergence by latitude |
moon doc --serve| Area | Reference | Asserted bound (measured) |
|---|---|---|
| Geodesic inverse | 1,000 cases from Karney's GeodTest-short.dat (CC0) | distance < 8 nm (7.5 nm) |
| Geodesic direct | same data set | position < 10 nm (8.6–9.2 nm, by backend) |
| Geodesic inverse/direct | 2,020 cases vs Python geographiclib 2.1 | relative difference < 1e-11 |
| GeodesicExact | 111 cases vs GeographicLib 2.5 GeodSolve -E, oblate f up to 0.5, prolate down to −0.2 | distance < 8 nm (7.5 nm); position < 1e-13°, about 11 nm (6.3 nm) |
| Polygon area | 8 polygons (incl. pole-enclosing, antimeridian-crossing) vs geographiclib | < 1e-14 of the Earth's area |
| Rhumb inverse | 125 cases vs GeographicLib RhumbSolve 2.5 | distance < 4 nm (3.7 nm) |
| Rhumb direct | 296 cases vs the same RhumbSolve | position < 11 nm (10.5 nm); area < 5e-15 of the ellipsoid |
| Geodesic intersection | 139 cases (closest, offset, next, segment) vs GeographicLib IntersectTool 2.5; ties (two intersections equally close; 2–4 cases, depending on the backend's rounding) are compared by distance | |Δx|, |Δy| < 0.5 µm (0.45 µm); the curves meet to < 10 nm (7.9 nm) |
| Geocentric / LocalCartesian | 660 ECEF and 120 ENU cases (WGS-84, sphere, prolate, GRS80) vs 50-digit textbook formulas; 495 + 90 vs PROJ cart / topocentric | < 20 nm (7.5 nm) |
| Transverse Mercator / UTM / UPS | 600 points vs PROJ 9 (pyproj), up to ~930 km from the central meridian | < 10 nm (5.5 nm) |
| MGRS | 315 points vs NGA GeoTrans (mgrs package), precisions 0–5 | 315/315 identical strings |
| Lambert / Albers conic | 1792 points on 18 projections (oblate and a sphere) vs 50-digit evaluation of the EPSG GN 7-2 / Snyder formulas; PROJ 9.5; IOGP GIGS 5102/5103/5109; EPSG and Snyder worked examples | < 25 nm (19 nm); PROJ < 0.25 µm, except 6 mm on parallels 1e-4° apart, where PROJ's own formula loses digits; GIGS < 1 mm (Lambert) and < 5 mm (Albers), the same differences PROJ shows (GIGS allows 3–5 cm) |
| WMM2025 | NOAA's 100 official double-precision test points; report Tables 3b, 3d, 6; 119 points vs an independent 40-digit evaluation | Y within the file's rounding; Z < 2.5e-6 nT and X, H, F < 1e-3 nT (7.2e-4), differences that the independent evaluation shows are in the file (see notes); worked example to 10 decimals; independent < 1e-10 nT |
| GCJ-02 / BD-09 | 200 points vs coord-convert: GCJ-02 on the 140 inside China (the other 60 are foreign and left unchanged by design), BD-09 on all 200 | < 1e-11° |
| China / foreign boundary | every GeoNames town of 5000+ people in the GCJ-02 box (CC BY 4.0), except 39 within 3 km of China's land border and 11 disputed | 12 115 foreign towns not offset; 3 234 towns in China, Hong Kong, Macau, Taiwan offset; 3 known exceptions (islets missing from Natural Earth) |
| distance band | cases | max inverse |Δs12| (nm) | max direct position error (nm) |
|---|---|---|---|
| < 1 000 km | 21 | 0.96 | 1.77 |
| 1 000–10 000 km | 55 | 3.73 | 3.53 |
| > 10 000 km | 124 | 7.45 | 6.5 |
| all | 200 | 7.45 | 6.5 |
| Operation | native | js |
|---|---|---|
| geodesic inverse, Beijing → Shanghai | 1.37 µs | 1.86 µs |
| geodesic inverse, nearly antipodal | 1.44 µs | 2.05 µs |
| geodesic direct, 1 000 km | 0.45 µs | 1.01 µs |
| polygon area, 4 vertices | 2.99 µs | 5.16 µs |
| WGS-84 → GCJ-02 (with the border test) | 0.10 µs | 0.06 µs |
| GCJ-02 → WGS-84 (exact, iterative) | 0.46 µs | 0.47 µs |
| UTM from lat/lon | 0.64 µs | 0.52 µs |
| MGRS encode (1 m) | 0.78 µs | 0.91 µs |
| WMM2025 field, all elements and rates | 1.88 µs | 2.96 µs |
| Albers forward | 0.24 µs | 0.25 µs |
| Lambert forward | 0.27 µs | 0.24 µs |
| Part | Lines of MoonBit |
|---|---|
| Library code (geodesic, utm, conic, magnetic, cn, internal) | ~9 300 |
| Generated data inside the library (China boundary, WMM2025 coefficients) | ~3 500 |
| Hand-written tests | ~3 600 |
| Generated reference data (*_data_test.mbt, vectors_test.mbt) | ~43 500 |
| Package notes with checked examples (README.mbt.md) | ~860 |
| Examples (demo, CLI, six scenarios) | ~1 800 |
let g = @geodesic.Geodesic::wgs84()
let r = g.inverse(39.9087, 116.3975, 31.2304, 121.4737) // 北京 → 上海:r.s12 ≈ 1066.68 km
let gcj = @cn.wgs84_to_gcj02(@cn.LatLon::new(39.9087, 116.3975)) // GPS → 高德
let bd = @cn.gcj02_to_bd09(gcj) // 高德 → 百度
let mgrs = @utm.to_mgrs(39.9087, 116.3975) // "50SMK4850217797"
let rh = @geodesic.Rhumb::wgs84()
let h = rh.inverse(51.4778, -0.0015, 35.6762, 139.6503) // 伦敦 → 东京,恒向线
let ix = @geodesic.Intersect::wgs84()
let p = ix.closest(0.0, 0.0, 45.0, 45.0, 10.0, 135.0) // 两条测地线的最近交点
let lc = @geodesic.LocalCartesian::wgs84(39.9087, 116.3975) // 以天安门为原点的局部 ENU
let enu = lc.forward(39.9097, 116.3985, 50.0) // enu.x 东, enu.y 北, enu.z 天
let wmm = @magnetic.MagneticModel::wmm2025()
let mag = wmm.field(39.9087, 116.3975, 0.0, 2026.86) // 北京磁偏角 mag.decl ≈ -7.56°(偏西)moon run src/examples/cli -- geod inverse 39.9087 116.3975 31.2304 121.4737
moon run src/examples/cli -- rhumb inverse 51.4778 -0.0015 35.6762 139.6503
moon run src/examples/cli -- intersect closest 0 0 45 45 10 135
moon run src/examples/cli -- enu forward 39.9087 116.3975 39.9097 116.3985 50
moon run src/examples/cli -- coord wgs2gcj 39.9087 116.3975
moon run src/examples/cli -- mag 39.9087 116.3975 2026-11-10
moon run src/examples/cli -- albers forward 25 47 0 105 39.9087 116.3975Install
Download zipPure MoonBit geodesy: Karney geodesics, rhumb lines, intersections, LocalCartesian ENU (GeographicLib port), UTM/UPS/MGRS, Lambert and Albers conic grids, WMM2025 magnetic declination, and WGS-84/GCJ-02/BD-09 conversions