linear-geo-0.1.0.0: src/Linear/Geo/ENU.hs
{-|
Module : Linear.Geo.ENU
Copyright : Travis Whitaker 2023
License : MIT
Maintainer : pi.boy.travis@gmail.com
Stability : Provisional
Portability : Portable (Windows, POSIX)
East-North-Up coordinates.
-}
{-# LANGUAGE DataKinds
, DeriveAnyClass
, DeriveDataTypeable
, DeriveGeneric
, DerivingStrategies
, MagicHash
, ScopedTypeVariables
, TypeFamilies
#-}
module Linear.Geo.ENU (
ENU(..)
, alignOrigin
, liftAO2
, liftAO2V
, rotNormToECEF
, rotNormToECEFFromENU
, enuToECEF
, rotECEFToNorm
, rotECEFToNormFromENU
, ecefToENU
, disp
, diff
, lerp
, dot
, quadrance
, norm
, distance
, normalize
, project
) where
import Control.DeepSeq (NFData)
import Data.Data (Data)
import GHC.Generics (Generic)
import GHC.Exts
import qualified Linear.Affine as L
import qualified Linear.Epsilon as L
import qualified Linear.Matrix as L
import qualified Linear.Metric as L
import qualified Linear.V2 as L
import qualified Linear.V3 as L
import qualified Linear.Vector as L
import Linear.Geo.ECEF
import Linear.Geo.Geodetic
import Linear.Geo.PlaneAngle
-- | R3 vector with the origin located at some arbitrary 'ECEF' position vector,
-- first basis pointing east at the origin, second basis vector pointing north
-- at the origin, and third basis vector normal to the plane tangent to the
-- ellipsoid at the origin.
--
-- Each value records both the ENU vector and the ENU origin. Most functions
-- of multiple ENU values will require the points to occupy coordinal frames.
-- Binary operations on ENU values should preserve the coordinate frame of the
-- /left/ value.
--
-- The 'Eq' and 'Ord' instances for this type implement structural equality,
-- i.e. ENU points with different 'enuOrigin' values will never be equal.
-- Floating point errors limit the usefulness of
-- exact-equality-as-coincidence.
--
-- Operations on ENU points use the uncorrected WGS84 geoid model.
data ENU a = ENU {
enuOrigin :: ECEF a
, enuPoint :: L.V3 a
} deriving stock ( Eq
, Ord
, Show
, Generic
, Data
, Bounded
)
deriving anyclass (NFData)
instance L.R1 ENU where
_x f (ENU o (L.V3 x y z)) = (\x' -> ENU o (L.V3 x' y z)) <$> f x
instance L.R2 ENU where
_y f (ENU o (L.V3 x y z)) = (\y' -> ENU o (L.V3 x y' z)) <$> f y
_xy f (ENU o (L.V3 x y z)) = (\(L.V2 x' y') -> ENU o (L.V3 x' y' z))
<$> f (L.V2 x y)
instance L.R3 ENU where
_z f (ENU o (L.V3 x y z)) = (\z' -> ENU o (L.V3 x y z')) <$> f z
_xyz f (ENU o v) = ENU o <$> f v
-- | Align the second argument with the coordinate system of the first.
alignOrigin :: RealFloat a => ENU a -> ENU a -> ENU a
alignOrigin (ENU xo _) y@(ENU yo _)
| isTrue# (reallyUnsafePtrEquality# xo yo) = y
| xo == yo = y
| otherwise = ecefToENU xo (enuToECEF y)
-- | Lift a function on vectors to a function on origin-aligned ENU points.
liftAO2 :: RealFloat a => (L.V3 a -> L.V3 a -> b) -> ENU a -> ENU a -> b
liftAO2 f x@(ENU _ xp) y = let (ENU _ y'p) = alignOrigin x y
in f xp y'p
-- | Lift a binary operation on vectors to a binary operation on origin-aligned
-- ENU points.
liftAO2V :: RealFloat a
=> (L.V3 a -> L.V3 a -> L.V3 a)
-> ENU a
-> ENU a
-> ENU a
liftAO2V f x@(ENU xo xp) y = let (ENU _ y'p) = alignOrigin x y
in ENU xo (f xp y'p)
-- | Rotation matrix that rotates the ENU coordinate frame at the provided
-- latitude and longitude to the ECEF coordinate frame.
rotNormToECEF :: Floating a
=> Radians a -- ^ lat
-> Radians a -- ^ lon
-> L.M33 a
rotNormToECEF (Radians po) (Radians lo) =
L.V3 (L.V3 (-(sin lo)) ((-(cos lo)) * (sin po)) ((cos lo) * (cos po)))
(L.V3 (cos lo) ((- (sin lo)) * (sin po)) ((sin lo) * (cos po)))
(L.V3 0 (cos po) (sin po) )
-- | Do 'rotNormToECEF', but get the lat and lon from some 'ENU's origin.
rotNormToECEFFromENU :: RealFloat a => ENU a -> L.M33 a
rotNormToECEFFromENU (ENU o _) =
let (Geo po lo _) = ecefToGeo o
in rotNormToECEF po lo
-- | Convert an 'ENU' to an 'ECEF' by adding the rotated position vector to the
-- origin.
enuToECEF :: RealFloat a => ENU a -> ECEF a
enuToECEF enu@(ENU o x) =
let rot = rotNormToECEFFromENU enu
in o L..+^ (rot L.!* x)
-- | Rotation matrix that rotates the ECEF coordinate frame to the ENU
-- coordinate frame at the provided latitude and longitude.
rotECEFToNorm :: Floating a
=> Radians a -- ^ lat
-> Radians a -- ^ lon
-> L.M33 a
rotECEFToNorm (Radians po) (Radians lo) =
L.V3 (L.V3 (-(sin lo)) (cos lo) 0 )
(L.V3 ((-(cos lo)) * (sin po)) ((-(sin lo)) * (sin po)) (cos po))
(L.V3 ((cos lo) * (cos po)) ((sin lo) * (cos po)) (sin po))
-- | Do 'rotECEFToNorm', but get the lat and lon from some 'ENU's origin.
rotECEFToNormFromENU :: RealFloat a => ENU a -> L.M33 a
rotECEFToNormFromENU (ENU o _) =
let (Geo po lo _) = ecefToGeo o
in rotECEFToNorm po lo
-- | Pack an 'ECEF' origin and point into an 'ENU'.
ecefToENU :: RealFloat a
=> ECEF a -- ^ Origin
-> ECEF a -- ^ Point
-> ENU a
ecefToENU o@(ECEF vo) (ECEF vp) =
let (Geo po lo _) = ecefToGeo o
rot = rotECEFToNorm po lo
x = rot L.!* (vp - vo)
in ENU o x
-- | Affine addition. Apply a displacement vector.
disp :: Num a => ENU a -> L.V3 a -> ENU a
disp (ENU o p) v = (ENU o (p + v))
-- | Affine subtraction. Get the vector from the first to the second ENU point.
diff :: RealFloat a => ENU a -> ENU a -> L.V3 a
diff x y = enuPoint $ liftAO2V (L..-.) x y
-- | Linearly interpolate between two points.
lerp :: RealFloat a => a -> ENU a -> ENU a -> ENU a
lerp f = liftAO2V (L.lerp f)
-- | Lifted dot.
dot :: RealFloat a => ENU a -> ENU a -> a
dot = liftAO2 L.dot
-- | Lifted quadrance.
quadrance :: Num a => ENU a -> a
quadrance = L.quadrance . enuPoint
-- | Lifted norm.
norm :: Floating a => ENU a -> a
norm = L.norm . enuPoint
-- | Lifted distance.
distance :: RealFloat a => ENU a -> ENU a -> a
distance = liftAO2 L.distance
-- | Lifted normalize.
normalize :: (Floating a, L.Epsilon a) => ENU a -> ENU a
normalize (ENU xo xp) = ENU xo (L.normalize xp)
-- | Lifted project.
project :: RealFloat a => ENU a -> ENU a -> ENU a
project = liftAO2V L.project