π geo3d
3D positions on Earth, made simple.
Give geo3d a GPS position, then ask everyday questions:
- π Where is that plane, seen from here?
- π‘ Which way should the antenna point?
- π How far apart are they?
- π Can they see each other?
- π₯ Are they going to collide?
It's pure Rust, with no dependencies.
use ;
let me = new; // latitude, longitude, height in metres
let plane = new;
let look = new.look_angles; // which way to point
// due north, 14.9Β° up, 11.5 km away
assert!;
β¨ Why geo3d?
- π― Accurate. Conversions are exact, and distances along the ground are good to 15 nanometres. Everything is tested against GeographicLib, the reference library.
- π‘οΈ Reliable. It handles the tricky places, like the poles and opposite sides of the Earth, without NaN or panics.
- π§© Easy to use. Degrees and metres everywhere, with named fields. Latitude always comes first.
- πͺΆ Light and fast. No dependencies, no
unsafecode, and nothing allocates. Works on Rust 1.85 and newer.
πΊοΈ The basics
Latitude and longitude are angles, so you can't subtract two of them to get a distance. geo3d turns positions into frames where normal 3D maths works.
| Type | What it holds | Units |
|---|---|---|
Geodetic |
latitude, longitude and height, like GPS gives you | degrees, metres |
Ecef |
x, y, z from the centre of the Earth | metres |
Enu / Ned |
how far east, north and up (or north, east and down) from you | metres |
Aer |
azimuth, elevation and range: which way to point | degrees, metres |
Eci |
x, y, z that don't spin with the Earth, for satellites | metres |
Vec3 |
a plain 3D vector, like a velocity | metres, m/s |
A few rules hold everywhere:
- Latitude comes first:
Geodetic::new(lat, lon, height). - Bearings go clockwise from north, from 0Β° to 360Β°.
- Heights are above the WGS84 ellipsoid, like GPS. They are not heights above sea level.
- WGS84 is the default, but any ellipsoid works:
GRS80.to_ecef(p).
New to these words? The glossary explains them.
π³ Recipes
Every example here runs as a test, so you can copy and paste it.
Convert between latitude/longitude and x, y, z
use ;
let sydney = new;
let xyz = geodetic_to_ecef; // x β4 646 032 m, y 2 553 122 m, z β3 534 494 m
let back = ecef_to_geodetic; // Sydney again
assert!;
let on_grs80 = GRS80.to_ecef; // other ellipsoids work the same way
Find where something is, relative to you
use ;
let radar = new;
let target = new;
let enu = radar.enu_of; // 11 131 m north, 2 960 m up
let ned = radar.ned_of; // the same, as north-east-down
let back = radar.geodetic_of; // back to latitude and longitude
assert!;
π‘ Build one LocalFrame per observer and reuse it. Building it is the slow part.
Work out which way to point
use ;
let station = new;
let aircraft = new;
let aer = station.look_angles; // 35.6Β° from north, 7.3Β° up, 19.3 km away
let again = station.destination; // and back to the aircraft's position
assert!;
Find where a line of sight hits the ground
use ;
let drone = new; // 120 m up
let spot = ground_strike; // looking east, 30Β° down: lands 208 m away
assert!;
assert!; // looking up: it never lands
Measure distance
There are three kinds of distance. Pick the one you need:
| You want | Use | Accuracy |
|---|---|---|
| A straight line through space | distance3d_m(a, b) |
exact |
| The distance along the ground | WGS84.geodesic_inverse(a, b) |
15 nm |
| A quick estimate along the ground | WGS84.great_circle_distance_m(a, b) |
within 0.6 % |
use ;
let lax = new;
let jfk = new;
let straight = distance3d_m; // 3 918 745 m, through the Earth
let ground = WGS84.geodesic_inverse.unwrap; // 3 983 005 m, setting off at 65.9Β°
let quick = WGS84.great_circle_distance_m; // 3 974 267 m
// Or the other way round: travel that far on that bearing, and you arrive at JFK.
let = WGS84.geodesic_direct;
assert!;
The distance along the ground works for any two points, even ones on opposite sides of the Earth.
Check if something is inside a zone
Every shape answers contains:
use ;
let site = new;
let p = new; // 2.2 km east, 500 m up
let bubble = new; // within 10 km
let camera = new; // facing east, Β±20Β°, 5 km reach
let zone = new; // 8 km wide, up to 2 km high
assert!;
π‘ AltitudeBand adds a height range to any 2D shape: band.within_prism(inside_polygon, height).
Check if two objects will collide
use ;
let a_pos = new;
let b_pos = new;
// Heading, ground speed (m/s) and climb (m/s), turned into a 3D velocity.
let a_vel = new.velocity_of_course; // south-west
let b_vel = new.velocity_of_course; // north
let a = new;
let b = new;
let cpa = a.closest_approach; // closest in 59 s, when they're 434 m apart
let alert = a.conflict; // Some(...) because they come within 1 km
let soon = a.at; // where `a` will be in a minute
assert!;
Check if two points can see each other
use ;
let plane = WGS84.to_ecef; // 10 km up
let near = WGS84.to_ecef; // 111 km away
let far = WGS84.to_ecef; // 1 113 km away
assert!;
assert!; // the Earth is in the way
let horizon = WGS84.horizon_distance_m; // you can see about 357 km
π‘ geo3d treats the Earth as smooth. Hills and the way air bends light aren't included.
Track satellites
use ;
let angle = gmst_from_unix; // how far the Earth has turned at that time
let fixed = new;
let in_space = fixed.to_eci;
let back = in_space.to_ecef;
assert!;
π‘ This is the TEME frame that SGP4 satellite models use, so their output plugs straight in. It is
not J2000. Unix time is UTC, which can be up to 0.9 s away from the Earth's true rotation. That's
up to 400 m at the equator. For better accuracy, pass a UT1 date to eci::gmst_rad.
Shift between datums
use ;
// WGS84 β OSGB36 (Great Britain), as published: metres, arc-seconds and parts per million.
let h = position_vector;
let p = new;
let shifted = h.apply;
let back = h.apply_inverse; // exactly reverses it
assert!;
π‘ Datums are published with one of two sign conventions. For the other one, use
Helmert7::coordinate_frame with the same numbers.
π Coming from MATLAB, pymap3d or map_3d?
Most functions have a familiar twin. Make a frame once with let frame = LocalFrame::new(observer);
and then:
| MATLAB / pymap3d / map_3d | geo3d |
|---|---|
geodetic2ecef / ecef2geodetic |
geodetic_to_ecef(g) / ecef_to_geodetic(e) |
geodetic2enu / enu2geodetic |
frame.enu_of(g) / frame.geodetic_of(enu) |
geodetic2ned / ned2geodetic |
frame.ned_of(g) / frame.geodetic_of_ned(ned) |
ecef2enu / enu2ecef |
frame.to_enu(e) / frame.to_ecef(enu) |
geodetic2aer / aer2geodetic |
frame.look_angles(g) / frame.destination(aer) |
ecef2aer / aer2ecef |
frame.look_angles_ecef(e) / frame.ecef_of_aer(aer) |
enu2aer / aer2enu |
enu.to_aer() / aer.to_enu() |
ecef2enuv / enu2ecefv |
frame.dir_to_enu(v) / frame.dir_to_ecef(enu) |
lookAtSpheroid |
frame.ground_strike(azimuth, elevation) |
vdist / distance |
WGS84.geodesic_inverse(a, b) |
vreckon / reckon |
WGS84.geodesic_direct(a, bearing, distance) |
ecef2eci / eci2ecef |
e.to_eci(gmst) / i.to_ecef(gmst) |
Everything is in degrees, so there's no deg flag to remember.
π How it compares
We tested geo3d against five other Rust crates, giving them all the same inputs. The full comparison has every number, and shows how to run it yourself.
π― Converting x, y, z back to latitude and longitude
geo3d keeps refining its answer until it stops changing, so it stays exact all the way out to satellite heights. map_3d stops after one step, and drifts 81 m that far out.
π Distance along the ground
geo3d measures distance on the Earth's real, slightly squashed shape, just like GeographicLib. The other crates assume a perfect sphere, which can be tens of kilometres out on a long trip.
π‘ Pointing straight up
geo3d uses a formula that stays precise in every direction. Some crates lose precision when pointing straight up, and end up 13 cm off at satellite distances.
π‘οΈ Tricky cases
geo3d gives a correct answer in every case we tried. Other crates return NaN at the Earth's centre or the poles, give a wrong answer far out in space, or panic.
π§° Features
Only geo3d has ground strikes, 3D zones, collision checks and datum shifts, and it does it with no dependencies. Other crates are ahead on n-vectors and attitude. Both are on our roadmap.
β‘ Speed
geo3d is the fastest, or level with the fastest, in five of the ten tests. It's slower in the other five, and we know why:
- x, y, z back to latitude and longitude (two of the tests). geo3d refines its answer until it's exact. A faster method that is just as exact is next on the list (#28).
- One-off ENU conversions. A
LocalFramecosts a little to build, and pays off from the second target onwards. - Quick great-circle distance. geo3d uses a formula that stays exact on the far side of the Earth.
- Distance along the ground. It's about 4 % behind geographiclib-rs.
π‘ To use your CPU's fused multiply-add, build with RUSTFLAGS="-C target-cpu=native".
π Accuracy
| What | How accurate |
|---|---|
| Latitude/longitude/height β x, y, z | exact |
| x, y, z β latitude/longitude/height | 0.01 mm or better, from 11 km underground to satellite heights |
| Local frames, pointing, ground strikes and zones | exact, even straight up |
| Distance and travel along the ground | 15 nm, for any two points |
| Great-circle estimates and horizon distance | within 0.6 % |
| Closest approach | exact for objects moving in straight lines |
| Datum shifts | exact for the published numbers |
Satellite frame (eci) |
exact rotation; up to 400 m from treating UTC as UT1 |
"Exact" means exact apart from normal floating-point rounding. The tests check every row against GeographicLib or an independent crate.
π§© Optional features
They're all off by default, so a plain cargo add geo3d pulls in nothing else.
| Feature | What it adds |
|---|---|
serde |
saving and loading every type |
mint |
conversions to mint types, and through them to cgmath and others |
glam |
conversions to glam::DVec3 |
nalgebra |
conversions to nalgebra vectors and points |
geo-types |
Geodetic β geo_types::Point (x is longitude, y is latitude) |
let v: DVec3 = new.into;
let p: Point = new.into; // x = lon, y = lat
π§ Not included (yet)
- 2D shapes like polygons: use
geo. - Map grids like UTM and MGRS: use
utmorprojfor now. UTM is on our roadmap. - Height above sea level, terrain, and the way air bends light.
- High-precision Earth rotation (precession and nutation).
π Glossary
- Ellipsoid: the smooth, slightly squashed ball we measure positions on. GPS uses WGS84.
- Geodetic: latitude, longitude and height.
- ECEF: x, y, z in metres from the centre of the Earth, spinning with it.
- ENU / NED: east-north-up or north-east-down, measured from where you stand.
- AER: azimuth (compass direction), elevation (angle above the horizon) and range (distance).
- ECI: like ECEF, but not spinning with the Earth. Used for satellites.
- TEME: the version of ECI that SGP4 satellite models use.
- Geodesic: the shortest path along the ground.
- Great circle: the shortest path on a perfect sphere. A quick stand-in for a geodesic.
- Datum: a reference frame for coordinates. Helmert numbers convert between two of them.
π€ Contributing
Contributions are very welcome! The contributing guide shows how to get started, and the roadmap lists what's planned. Issues marked good first issue are a great place to begin.