Pure MoonBit geodesy: Karney geodesics, rhumb lines, intersections, and LocalCartesian ENU (GeographicLib port), UTM/UPS/MGRS, 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/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/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 squaremoon 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| 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, with round-trip residuals |
| 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 that matches Python geographiclib exactly (the example aborts if it does not) |
moon doc --serve| Area | Reference | Result |
|---|---|---|
| Geodesic inverse | 1,000 cases from Karney's GeodTest-short.dat (CC0) | max distance error 7.5 nm |
| Geodesic direct | same data set | max position error 8.6 nm |
| Geodesic inverse/direct | 2,020 cases vs Python geographiclib 2.1 | relative difference < 1e-11 |
| GeodesicExact (\|f\| up to 0.5, oblate and prolate) | 111 cases vs GeographicLib 2.5 GeodSolve -E | max distance error 7.5 nm; max position error 6.4 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 | max distance error 3.73 nm |
| Rhumb direct | 296 cases vs the same RhumbSolve | max position error 10.5 nm |
| Rhumb area | those inverse and direct cases | < 4e-15 of the ellipsoid area |
| Geodesic intersection | 139 cases (closest, offset, next, segment) vs GeographicLib IntersectTool 2.5 | max |Δx|, |Δy| 0.45 µm; the curves meet to 8 nm |
| LocalCartesian / Geocentric | 48 geocentric + 39 local cases (WGS-84, sphere, prolate, GRS80); PROJ topocentric cross-check | forward max |Δ| < 1 nm |
| Transverse Mercator / UTM / UPS | 600 points vs PROJ 9 (pyproj) | max difference 5.5 nm |
| MGRS | 315 points vs NGA GeoTrans (mgrs package) | 315/315 identical strings |
| GCJ-02 / BD-09 | 200 points vs coord-convert | < 1e-11° |
| 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 | 3.0 µs | 3.6 µs |
| geodesic inverse, nearly antipodal | 3.0 µs | 3.0 µs |
| geodesic direct, 1 000 km | 0.66 µs | 2.3 µs |
| polygon area, 4 vertices | 4.6 µs | 7.8 µs |
| WGS-84 → GCJ-02 | 0.14 µs | 0.13 µs |
| GCJ-02 → WGS-84 (exact, iterative) | 0.80 µs | 0.62 µs |
| UTM from lat/lon | 0.86 µs | 0.83 µs |
| MGRS encode (1 m) | 1.5 µs | 2.3 µs |
| Part | Lines of MoonBit |
|---|---|
| Core library (geodesic, utm, cn, internal) | ~7 300 |
| Hand-written tests | ~1 750 |
| Generated reference data (*_data_test.mbt, vectors_test.mbt) | ~20 400 |
| Examples (demo, CLI, four scenarios) | ~1 450 |
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 天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.3975Install
Download zipPure MoonBit geodesy: Karney geodesics, rhumb lines, intersections, and LocalCartesian ENU (GeographicLib port), UTM/UPS/MGRS, and WGS-84/GCJ-02/BD-09 conversions