packages feed

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