diff --git a/README.md b/README.md index 47f7d2e..4131a2f 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,10 @@ trans = ENUfromLLA(origin_lla, wgs84) point_enu = trans(point_lla) # Equivalently -point_enu = ENU(point_enu, point_origin, wgs84) +point_enu = ENU(point_lla, origin_lla, wgs84) ``` +The `NED` coordinate system is also available as an alternative to `ENU`, with +the same interface. Similarly, we could convert to UTM/UPS coordinates, and two types are provided for this - `UTM` stores 3D coordinates `x`, `y`, and `z` in an unspecified zone, @@ -252,7 +254,8 @@ as the geodetic coordinates mentioned above, it's common to see defining a terrestrial reference frame. * The east,north and up **ENU** components of a Cartesian coordinate frame at a particular point on the ellipsoid. This coordinate system is useful as a - local frame for navigation. + local frame for navigation. It is also common for navigation systems to use + north,east and down (**NED**) coordinates. * Easting,northing and vertical components of a **projected coordinate system** or [**map projection**](http://www.icsm.gov.au/mapping/about_projections.html). There's an entire zoo of these, designed to represent the curved surface of an @@ -325,10 +328,17 @@ projection about the corresponding pole, otherwise `zone` is an integer between ##### `ENU{T}` - east-north-up -The `ENU` type is a local Cartesian coordinate that encodes a point's distance +The `ENU` type stores local Cartesian coordinates that encode a point's distance towards east `e`, towards north `n` and upwards `u` with respect to an unspecified origin. Like `ECEF`, `ENU` is also a subtype of `StaticVector`. +##### `NED{T}` - north-east-down + +The `NED` type is an alternative convention to `ENU`. It stores local Cartesian +coordinates that encode a point's distance towards north `n`, towards east `e` +and downwards `d` with respect to an unspecified origin. Like `ECEF`, `NED` is +also a subtype of `StaticVector`. + ### Geodetic Datums Geodetic datums are modelled as subtypes of the abstract type `Datum`. The @@ -379,7 +389,7 @@ be pre-cached (for instance, the origin of an ENU transformation). The `LLAfromECEF` and `ECEFfromLLA` transformations require an ellipsoidal datum to perform the conversion. The exact transformation is performed in both directions, -using a port the ECEF → LLA transformation from *GeographicLib*. +using a port of the ECEF → LLA transformation from *GeographicLib*. Note that in some cases where points are very close to the centre of the ellipsoid, multiple equivalent `LLA` points are valid solutions to the transformation problem. @@ -402,22 +412,24 @@ native Julia port of that used in *GeographicLib*, and is accurate to nanometers for up to several UTM zones away from the reference meridian. However, the series expansion diverges at ±90° from the reference meridian. While the `UTMZ`-methods will automatically choose the canonical zone and hemisphere for the input, -extreme care must be taken to choose an appropriate zone for the `UTM` methods. -(In the future, we implement the exact UTM transformation as a fallback — -contributions welcome!) +extreme care must be taken to choose an appropriate zone for the `UTM` methods +(In the future, we will implement the exact UTM transformation as a fallback — +contributions welcome!). There is also `UTMfromUTMZ` and `UTMZfromUTM` transformations that are helpful for converting between these two formats and putting data into the same `UTM` zone. -#### To and from local `ENU` frames +#### To and from local `ENU` and `NED` frames The `ECEFfromENU` and `ENUfromECEF` transformations define the transformation around a specific origin. Both the origin coordinates as an `ECEF` as well as its corresponding latitude and longitude are stored in the transformation for maximal efficiency when performing multiple `transform`s. The transformation can be inverted with `inv` to perform the reverse transformation with respect to the -same origin. +same origin. Moreover, the `ENUfromNED` transformation and its inverse `NEDfromENU` +can be used to change a local frame's orientation convention between `ENU` and `NED`, +while keeping the same origin. #### Web Mercator support @@ -443,11 +455,17 @@ These include: * `ECEFfromUTM(zone, hemisphere, datum) = ECEFfromLLA(datum) ∘ LLAfromUTM(zone, hemisphere, datum)` * `ENUfromLLA(origin, datum) = ENUfromECEF(origin, datum) ∘ ECEFfromLLA(datum)` * `LLAfromENU(origin, datum) = LLAfromECEF(datum) ∘ ECEFfromENU(origin, datum)` +* `NEDfromLLA(origin, datum) = NEDfromENU() ∘ ENUfromECEF(origin, datum) ∘ ECEFfromLLA(datum)` +* `LLAfromNED(origin, datum) = LLAfromECEF(datum) ∘ ECEFfromENU(origin,datum) ∘ ENUfromNED()` * `ECEFfromUTMZ(datum) = ECEFfromLLA(datum) ∘ LLAfromUTMZ(datum)` * `ENUfromUTMZ(origin, datum) = ENUfromLLA(origin, datum) ∘ LLAfromUTMZ(datum` * `UTMZfromENU(origin, datum) = UTMZfromLLA(datum) ∘ LLAfromENU(origin, datum)` +* `NEDfromUTMZ(origin, datum) = NEDfromLLA(origin, datum) ∘ LLAfromUTMZ(datum` +* `UTMZfromNED(origin, datum) = UTMZfromLLA(datum) ∘ LLAfromNED(origin, datum)` * `UTMfromENU(origin, zone, hemisphere, datum) = UTMfromLLA(zone, hemisphere, datum) ∘ LLAfromENU(origin, datum)` * `ENUfromUTM(origin, zone, hemisphere, datum) = ENUfromLLA(origin, datum) ∘ LLAfromUTM(zone, hemisphere, datum)` +* `UTMfromNED(origin, zone, hemisphere, datum) = UTMfromLLA(zone, hemisphere, datum) ∘ LLAfromNED(origin, datum)` +* `NEDfromUTM(origin, zone, hemisphere, datum) = NEDfromLLA(origin, datum) ∘ LLAfromUTM(zone, hemisphere, datum)` Constructor-based transforms for these are also provided, such as `UTMZ(ecef, datum)` which converts to `LLA` as an intermediary, as above. When converting multiple diff --git a/src/Geodesy.jl b/src/Geodesy.jl index e2b73c9..235f999 100644 --- a/src/Geodesy.jl +++ b/src/Geodesy.jl @@ -13,6 +13,7 @@ export # Points ECEF, ENU, + NED, LLA, LatLon, UTM, @@ -38,9 +39,10 @@ export # transformation methods transform_deriv, transform_deriv_params, compose, ∘, - ECEFfromLLA, LLAfromECEF, ENUfromECEF, ECEFfromENU, ENUfromLLA, LLAfromENU, - UTMfromLLA, LLAfromUTM, UTMfromECEF, ECEFfromUTM, ENUfromUTM, UTMfromENU, - UTMZfromLLA, LLAfromUTMZ, UTMZfromECEF, ECEFfromUTMZ, ENUfromUTMZ, UTMZfromENU, + ECEFfromLLA, LLAfromECEF, ENUfromECEF, ECEFfromENU, ENUfromLLA, LLAfromENU, + ENUfromNED, NEDfromENU, NEDfromECEF, ECEFfromNED, NEDfromLLA, LLAfromNED, + UTMfromLLA, LLAfromUTM, UTMfromECEF, ECEFfromUTM, ENUfromUTM, UTMfromENU, NEDfromUTM, UTMfromNED, + UTMZfromLLA, LLAfromUTMZ, UTMZfromECEF, ECEFfromUTMZ, ENUfromUTMZ, UTMZfromENU, NEDfromUTMZ, UTMZfromNED, UTMZfromUTM, UTMfromUTMZ, WebMercatorfromLLA, LLAfromWebMercator, diff --git a/src/conversion.jl b/src/conversion.jl index bf8bc93..dd3f48c 100644 --- a/src/conversion.jl +++ b/src/conversion.jl @@ -1,4 +1,3 @@ - ############################ ### Identity conversions ### ############################ @@ -6,10 +5,19 @@ ECEF(ecef::ECEF, datum) = ecef LLA(lla::LLA, datum) = lla ENU(enu::ENU, datum) = enu +NED(ned::NED, datum) = ned UTM(utm::UTM, datum) = utm UTMZ(utmz::UTMZ, datum) = utmz +################################ +### ENU <-> NED coordinates ### +################################ + +ENU(ned::NED) = ENUfromNED()(ned) +NED(enu::ENU) = NEDfromENU()(enu) + + ################################ ### LLA <-> ECEF coordinates ### ################################ @@ -23,10 +31,17 @@ LLA(ecef::ECEF, datum) = LLAfromECEF(datum)(ecef) ################################ ENU(ecef::ECEF, origin, datum) = ENUfromECEF(origin, datum)(ecef) - ECEF(enu::ENU, origin, datum) = ECEFfromENU(origin, datum)(enu) +################################ +### ECEF <-> NED coordinates ### +################################ + +NED(ecef::ECEF, origin, datum) = NEDfromECEF(origin, datum)(ecef) +ECEF(ned::NED, origin, datum) = ECEFfromNED(origin, datum)(ned) + + ################################ ### LLA <-> ENU coordinates ### ################################ @@ -35,6 +50,14 @@ ENU(lla::LLA, origin, datum) = ENUfromLLA(origin, datum)(lla) LLA(enu::ENU, origin, datum) = LLAfromENU(origin, datum)(enu) +################################ +### LLA <-> NED coordinates ### +################################ + +NED(lla::LLA, origin, datum) = NEDfromLLA(origin, datum)(lla) +LLA(ned::NED, origin, datum) = LLAfromNED(origin, datum)(ned) + + ################################ ### LLA <-> UTMZ coordinates ### ################################ @@ -59,6 +82,14 @@ ENU(utm::UTMZ, origin, datum) = ENUfromUTMZ(origin, datum)(utm) UTMZ(enu::ENU, origin, datum) = UTMZfromENU(origin, datum)(enu) +################################ +### NED <-> UTMZ coordinates ### +################################ + +NED(utm::UTMZ, origin, datum) = NEDfromUTMZ(origin, datum)(utm) +UTMZ(ned::NED, origin, datum) = UTMZfromNED(origin, datum)(ned) + + ############################### ### LLA <-> UTM coordinates ### ############################### @@ -82,6 +113,15 @@ UTM(ecef::ECEF, zone::Integer, hemisphere::Bool, datum) = UTMfromECEF(zone, hemi ENU(utm::UTM, zone::Integer, hemisphere::Bool, origin, datum) = ENUfromUTM(origin, zone, hemisphere, datum)(utm) UTM(enu::ENU, zone::Integer, hemisphere::Bool, origin, datum) = UTMfromENU(origin, zone, hemisphere, datum)(enu) + +############################### +### NED <-> UTM coordinates ### +############################### + +NED(utm::UTM, zone::Integer, hemisphere::Bool, origin, datum) = NEDfromUTM(origin, zone, hemisphere, datum)(utm) +UTM(ned::NED, zone::Integer, hemisphere::Bool, origin, datum) = UTMfromNED(origin, zone, hemisphere, datum)(ned) + + ############################### ### UTMZ <-> UTM coordinates ### ############################### diff --git a/src/points.jl b/src/points.jl index 67bc303..da7f65c 100644 --- a/src/points.jl +++ b/src/points.jl @@ -82,6 +82,24 @@ end Base.show(io::IO, ::MIME"text/plain", enu::ENU) = print(io, "ENU($(enu.e), $(enu.n), $(enu.u))") +""" + NED(n, e, d = 0.0) + +North-East-Down (NED) coordinates. A local Cartesian coordinate system, linearized about a reference point. +""" +struct NED{T <: Number} <: FieldVector{T} + n::T + e::T + d::T +end +NED(x :: T, y :: T) where {T} = NED(x, y, zero(T)) +@inline function NED(x,y,z) + T = promote_type(promote_type(typeof(x),typeof(y)), typeof(z)) + NED{T}(x,y,z) +end +Base.show(io::IO, ::MIME"text/plain", ned::NED) = print(io, "NED($(ned.n), $(ned.e), $(ned.d))") + + """ UTM(x, y, z = 0.0) diff --git a/src/transformations.jl b/src/transformations.jl index 5e7e428..7ddda6f 100644 --- a/src/transformations.jl +++ b/src/transformations.jl @@ -45,7 +45,7 @@ end LLAfromECEF(datum::Datum) = LLAfromECEF(ellipsoid(datum)) - function (trans::LLAfromECEF)(ecef::ECEF) +function (trans::LLAfromECEF)(ecef::ECEF) # Ported to Julia by Andy Ferris, 2016 and re-released under MIT license. #/** # * \file Geocentric.cpp @@ -191,6 +191,44 @@ Base.inv(trans::LLAfromECEF) = ECEFfromLLA(trans.el) Base.inv(trans::ECEFfromLLA) = LLAfromECEF(trans.el) +################## +## ENU <-> NED ## +################## + +""" + ENUfromNED(ned::NED) + +Construct a `Transformation` object to convert from local `NED` coordinates +to local `ENU` coordinates centered at the same origin. This is a simple +permutation of coordinates and sign change for the altitude. +""" +struct ENUfromNED <: Transformation end # singleton type + +Base.show(io::IO, ::ENUfromNED) = print(io, "ENUfromNED()") + +function (::ENUfromNED)(ned::NED) + ENU(ned.e, ned.n, -ned.d) +end + +""" + NEDfromENU(enu::ENU) + +Construct a `Transformation` object to convert from local `ENU` coordinates +to local `NED` coordinates centered at the same origin. This is a simple +permutation of coordinates and sign change for the altitude. +""" +struct NEDfromENU <: Transformation end # singleton type + +Base.show(io::IO, ::NEDfromENU) = print(io, "NEDfromENU()") + +function (::NEDfromENU)(enu::ENU) + NED(enu.n, enu.e, -enu.u) +end + +Base.inv(::ENUfromNED) = NEDfromENU() +Base.inv(::NEDfromENU) = ENUfromNED() + + ################## ## ECEF <-> ENU ## ################## @@ -288,6 +326,34 @@ end Base.inv(trans::ECEFfromENU) = ENUfromECEF(trans.origin, trans.lat, trans.lon) Base.inv(trans::ENUfromECEF) = ECEFfromENU(trans.origin, trans.lat, trans.lon) + +################## +## ECEF <-> NED ## +################## + +""" + NEDfromECEF(origin, datum) + NEDfromECEF(origin::UTM, zone, isnorth, datum) + NEDfromECEF(origin::ECEF, lat, lon) + +Construct a composite transformation NEDfromENU() ∘ ENUfromECEF(origin, datum) +to convert from global `ECEF` coordinates to local `NED` coordinates centered at the `origin`. +This object pre-caches both the ECEF coordinates and latitude and longitude of the origin for maximal efficiency. +""" +NEDfromECEF(origin, datum) = NEDfromENU() ∘ ENUfromECEF(origin, datum) + +""" + ECEFfromNED(origin, datum) + ECEFfromNED(origin::UTM, zone, isnorth, datum) + ECEFfromNED(origin::ECEF, lat, lon) + +Construct a composite transformation ECEFfromENU(origin,datum) ∘ ENUfromNED() +to convert from local `NED` coordinates centred at `origin` to global `ECEF` coodinates. +This object pre-caches both the ECEF coordinates and latitude and longitude of the origin for maximal efficiency. +""" +ECEFfromNED(origin, datum) = ECEFfromENU(origin,datum) ∘ ENUfromNED() + + ################# ## LLA <-> ENU ## ################# @@ -306,6 +372,26 @@ Creates composite transformation `LLAfromECEF(datum) ∘ ECEFfromENU(origin, dat """ LLAfromENU(origin, datum) = LLAfromECEF(datum) ∘ ECEFfromENU(origin, datum) + +################# +## LLA <-> NED ## +################# + +""" + NEDfromLLA(origin, datum) + +Creates composite transformation `NEDfromECEF(origin, datum) ∘ ECEFfromLLA(datum)`. +""" +NEDfromLLA(origin, datum) = NEDfromECEF(origin, datum) ∘ ECEFfromLLA(datum) + +""" + LLAfromNED(origin, datum) + +Creates composite transformation `LLAfromECEF(datum) ∘ ECEFfromNED(origin, datum)`. +""" +LLAfromNED(origin, datum) = LLAfromECEF(datum) ∘ ECEFfromNED(origin, datum) + + ################# ## LLA <-> UTM ## ################# @@ -413,6 +499,7 @@ Creates composite transformation `ECEFfromLLA(datum) ∘ LLAfromUTM(zone, isnort """ ECEFfromUTM(zone, isnorth, datum) = ECEFfromLLA(datum) ∘ LLAfromUTM(zone, isnorth, datum) + ################## ## LLA <-> UTMZ ## ################## @@ -499,6 +586,7 @@ end Base.inv(trans::LLAfromUTMZ) = UTMZfromLLA(trans.tm, trans.datum) Base.inv(trans::UTMZfromLLA) = LLAfromUTMZ(trans.tm, trans.datum) + ################### ## UTM <-> UTMZ ## ################### @@ -546,6 +634,7 @@ end Base.inv(trans::UTMfromUTMZ) = UTMZfromUTM(trans.zone, trans.isnorth, trans.datum) Base.inv(trans::UTMZfromUTM) = UTMfromUTMZ(trans.zone, trans.isnorth, trans.datum) + ################### ## ECEF <-> UTMZ ## ################### @@ -564,6 +653,7 @@ Creates composite transformation `ECEFfromLLA(datum) ∘ LLAfromUTMZ(datum)`. """ ECEFfromUTMZ(datum) = ECEFfromLLA(datum) ∘ LLAfromUTMZ(datum) + ################## ## ENU <-> UTMZ ## ################## @@ -585,6 +675,7 @@ Creates composite transformation `UTMZfromLLA(datum) ∘ LLAfromENU(origin, datu """ UTMZfromENU(origin, datum) = UTMZfromLLA(datum) ∘ LLAfromENU(origin, datum) + ################# ## ENU <-> UTM ## ################# @@ -610,3 +701,51 @@ Creates composite transformation `UTMfromLLA(zone, isnorth, datum) ∘ LLAfromEN If `origin` is a `UTM` point, then it is assumed it is in the given specified zone and hemisphere. """ ENUfromUTM(origin, zone::Integer, isnorth::Bool, datum) = ENUfromLLA(origin, datum) ∘ LLAfromUTM(zone, isnorth, datum) + + +################## +## NED <-> UTMZ ## +################## + +NEDfromECEF(origin::UTMZ, datum) = NEDfromECEF(LLAfromUTMZ(datum)(origin), datum) +ECEFfromNED(origin::UTMZ, datum) = ECEFfromNED(LLAfromUTMZ(datum)(origin), datum) + +""" + NEDfromUTMZ(origin, datum) + +Creates composite transformation `ENUfromLLA(origin, datum) ∘ LLAfromUTMZ(datum)`. +""" +NEDfromUTMZ(origin, datum) = NEDfromLLA(origin, datum) ∘ LLAfromUTMZ(datum) + +""" + UTMZfromNED(origin, datum) + +Creates composite transformation `UTMZfromLLA(datum) ∘ LLAfromNED(origin, datum)`. +""" +UTMZfromNED(origin, datum) = UTMZfromLLA(datum) ∘ LLAfromNED(origin, datum) + +################# +## NED <-> UTM ## +################# +NEDfromECEF(origin::UTM, zone::Integer, isnorth::Bool, datum) = NEDfromECEF(LLAfromUTM(zone, isnorth, datum)(origin), datum) +ECEFfromNED(origin::UTM, zone::Integer, isnorth::Bool, datum) = ECEFfromNED(LLAfromUTM(zone, isnorth, datum)(origin), datum) + +# Assume origin and utm point share the same zone and hemisphere +UTMfromNED(origin::UTM, zone::Integer, isnorth::Bool, datum) = UTMfromLLA(zone, isnorth, datum) ∘ LLAfromNED(UTMZ(origin, zone, isnorth), datum) +NEDfromUTM(origin::UTM, zone::Integer, isnorth::Bool, datum) = NEDfromLLA(UTMZ(origin, zone, isnorth), datum) ∘ LLAfromUTM(zone, isnorth, datum) + +""" + UTMfromNED(origin, zone, isnorth, datum) + +Creates composite transformation `UTMfromLLA(zone, isnorth, datum) ∘ LLAfromNED(origin, datum)`. +If `origin` is a `UTM` point, then it is assumed it is in the given specified zone and hemisphere. +""" +UTMfromNED(origin, zone::Integer, isnorth::Bool, datum) = UTMfromLLA(zone, isnorth, datum) ∘ LLAfromNED(origin, datum) + +""" + NEDfromUTM(origin, zone, isnorth, datum) + +Creates composite transformation `UTMfromLLA(zone, isnorth, datum) ∘ LLAfromNED(origin, datum)`. +If `origin` is a `UTM` point, then it is assumed it is in the given specified zone and hemisphere. +""" +NEDfromUTM(origin, zone::Integer, isnorth::Bool, datum) = NEDfromLLA(origin, datum) ∘ LLAfromUTM(zone, isnorth, datum) \ No newline at end of file diff --git a/test/conversion.jl b/test/conversion.jl index f025ceb..750fd4a 100644 --- a/test/conversion.jl +++ b/test/conversion.jl @@ -21,6 +21,22 @@ @test ENU(ecef, lla_ref, wgs84) ≈ enu @test ECEF(enu, lla_ref, wgs84) ≈ ecef + # ENU <-> NED + ned = NED(478.764855466788, -343.493749083977, 0.027242885224325164) + @test NED(enu) == ned + @test ENU(ned) == enu + @test NED((ENU(ned))) == ned + @test ENU((NED(enu))) == enu + + # LLA <-> NED + ned = NED(enu.n, enu.e, -enu.u) + @test NED(lla, lla_ref, wgs84) ≈ ned + @test LLA(ned, lla_ref, wgs84) ≈ lla + + # ECEF <-> NED + @test NED(ecef, lla_ref, wgs84) ≈ ned + @test ECEF(ned, lla_ref, wgs84) ≈ ecef + # LLA <-> UTM (z, h) = (19, true) utm = UTM(327412.48528248386, 4.692686244318043e6, 0.0) @@ -35,6 +51,10 @@ @test UTM(enu, z, h, lla_ref, wgs84) ≈ utm @test ENU(utm, z, h, lla_ref, wgs84) ≈ enu + # NED <-> UTM + @test UTM(ned, z, h, lla_ref, wgs84) ≈ utm + @test NED(utm, z, h, lla_ref, wgs84) ≈ ned + # LLA <-> UTMZ utmz = UTMZ(327412.48528248386, 4.692686244318043e6, 0, z, h) @test UTMZ(lla, wgs84) ≈ utmz @@ -48,6 +68,10 @@ @test UTMZ(enu, lla_ref, wgs84) ≈ utmz @test ENU(utmz, lla_ref, wgs84) ≈ enu + # NED <-> UTMZ + @test UTMZ(ned, lla_ref, wgs84) ≈ utmz + @test NED(utmz, lla_ref, wgs84) ≈ ned + # UTM <-> UTMZ @test UTMZ(utm, z, h, wgs84) ≈ utmz @test UTM(utmz, z, h, wgs84) ≈ utm @@ -56,6 +80,7 @@ @test ECEF(ecef, wgs84) == ecef @test LLA(lla, wgs84) == lla @test ENU(enu, wgs84) == enu + @test NED(ned, wgs84) == ned @test UTM(utm, wgs84) == utm @test UTMZ(utmz, wgs84) == utmz end @@ -113,6 +138,11 @@ enu = ENU(ecef1, ecef2, wgs84) @test euclidean_distance(enu, enu000) ≈ d + + ned000 = NED(0.0, 0.0, 0.0) + ned = NED(ecef1, ecef2, wgs84) + + @test euclidean_distance(ned, ned000) ≈ d end @test number_of_utm_distance_tests > 0 end diff --git a/test/points.jl b/test/points.jl index 7defa64..61670ef 100644 --- a/test/points.jl +++ b/test/points.jl @@ -24,4 +24,10 @@ @test UTMZ(utm, 1, true) == utmz @test UTM(utmz) == utm + + enu = ENU(1.0, 1.0, 0.0) + @test ENU(1.0, 1.0) == enu + + ned = NED(1.0, 1.0, 0.0) + @test NED(1.0, 1.0) == ned end # @testset