packages feed

linear-geo-0.1.0.0: src/Linear/Geo/Geodetic.hs

{-|
Module      : Linear.Geo.Geodetic
Copyright   : Travis Whitaker 2023
License     : MIT
Maintainer  : pi.boy.travis@gmail.com
Stability   : Provisional
Portability : Portable (Windows, POSIX)

Geodetic coordinates. The ellipsoid is not indexed explicitly, but conversion functions
for WGS84 are provided.

-}

{-# LANGUAGE BangPatterns
           , DeriveAnyClass
           , DeriveDataTypeable
           , DeriveFunctor
           , DeriveGeneric
           , DerivingStrategies
           #-}

module Linear.Geo.Geodetic (
    Geo(..)
  , normalizeGeo
  , fromLatLonAlt
  , toLatLonAlt
  , simpleEllipsoid
  , earthEllipsoid
  , ecefToGeoFerrariEllipsoid
  , ecefToGeoFerrariEarth
  , geoToECEF
  , ecefToGeo
  ) where

import Control.Applicative

import Control.DeepSeq (NFData)

import Control.Monad.Fix
import Control.Monad.Zip

import Data.Data (Data)

import GHC.Generics (Generic)

import qualified Linear.V3 as L

import Linear.Geo.ECEF
import Linear.Geo.PlaneAngle

-- | A point in some geodetic coordinate system, where 'geoLat' is the angle
--   between the normal at the specified point on the ellipsoid and the
--   equatorial plane (north positive, south negative), 'geoLon' is the angle
--   formed by the intersection of the parallel and the prime meridian and the
--   specified point on the parallel, and 'geoAlt' is the magnitude of the
--   position vector minus the magnitude of the unique vector colinear and
--   coordinal with the position vector impingent on the ellipsoid's surface
--   (i.e. height above ellipsoid). Angles are in radians.
data Geo a = Geo {
    geoLat :: !(Radians a)
  , geoLon :: !(Radians a)
  , geoAlt :: !a
  } deriving stock ( Eq
                   , Ord
                   , Show
                   , Generic
                   , Data
                   , Bounded
                   , Functor
                   )
    deriving anyclass (NFData)

instance Applicative Geo where
    pure x = Geo (pure x) (pure x) x
    (Geo pf lf hf) <*> (Geo p l h) = Geo (pf <*> p) (lf <*> l) (hf h)

instance Monad Geo where
    return = pure
    (Geo (Radians p) (Radians l) h) >>= f =
        let Geo p' _ _ = f p
            Geo _ l' _ = f l
            Geo _ _ h' = f h
        in Geo p' l' h'

instance MonadZip Geo where
    mzipWith = liftA2

instance MonadFix Geo where
    mfix f = Geo (let Geo (Radians p) _ _ = f p in Radians p)
                 (let Geo _ (Radians l) _ = f l in Radians l)
                 (let Geo _ _           h = f h in h)

instance Foldable Geo where
    foldMap f (Geo p l h) = foldMap f p <> foldMap f l <> f h

instance Traversable Geo where
    traverse f (Geo p l h) = Geo <$> traverse f p <*> traverse f l <*> f h

-- | Normalize the two angle components of a `Geo`.
normalizeGeo :: (Floating a, Real a) => Geo a -> Geo a
normalizeGeo (Geo p l a) = Geo (normalizeAngle p) (normalizeAngle l) a

-- | Convert a pair of angles and a height above the ellipsoid into a 'Geo'.
fromLatLonAlt :: (PlaneAngle lat, PlaneAngle lon, Floating a, Real a)
              => lat a -- ^ Latitude
              -> lon a -- ^ Longitude
              -> a     -- ^ Altitude
              -> Geo a
fromLatLonAlt lat lon alt = Geo (toRadians lat) (toRadians lon) alt

-- | Unpack a 'Geo' into latitude, longitude, and height above the ellipsoid.
toLatLonAlt :: (PlaneAngle lat, PlaneAngle lon, Floating a, Real a)
            => Geo a
            -> (lat a, lon a, a)
toLatLonAlt (Geo p l a) = (fromRadians p, fromRadians l, a)

-- | Convert from geodetic coordinates to ECEF by assuming the earth is an
--   ellipsoid.
simpleEllipsoid :: Floating a
                => a -- ^ Semi-major axis.
                -> a -- ^ Semi-minor axis.
                -> Geo a
                -> ECEF a
simpleEllipsoid a b =
    let -- coefficient for adjusted prime vertical radius
        dpvr  = (b ^ 2) / (a ^ 2)
        -- square of first eccentricity
        esqr  = 1 - dpvr
        -- prime vertical radius as function of latitude
        pvr p = a / (sqrt (1 - (esqr * ((sin p) ^ 2))))
        proj (Geo (Radians p) (Radians l) h) =
            let n  = pvr p
                nh = n + h
                nd = (dpvr * n) + h
            in ECEF (L.V3 (nh * cos p * cos l)
                          (nh * cos p * sin l)
                          (nd * sin p)
                    )
    in proj

-- | Standard WGS84 ellipsoid.
earthEllipsoid :: RealFloat a
               => Geo a
               -> ECEF a
earthEllipsoid = simpleEllipsoid 6378137 6356752.314245

-- | Conversion from ECEF to geodetic coordinates via a numerically stable
--   formulation of Ferrari's closed-form solution to the quartic polynomial.
--   See https://ieeexplore.ieee.org/document/303772/
ecefToGeoFerrariEllipsoid :: RealFloat a
                          => a -- ^ Semi-major axis.
                          -> a -- ^ Semi-minor axis.
                          -> ECEF a
                          -> Geo a
ecefToGeoFerrariEllipsoid a b (ECEF (L.V3 x y z)) =
    let r     = sqrt ((x ^ 2) + (y ^ 2))
        dpvr  = (b ^ 2) / (a ^ 2)
        esqr  = 1 - dpvr
        e'sqr = ((a ^ 2) - (b ^ 2)) / (b ^ 2)
        eesqr = (a ^ 2) - (b ^ 2)
        ff    = 54 * (b ^ 2) * (z ^ 2)
        gg    = (r ^ 2) + ((1 - esqr) * (z ^ 2)) - (esqr * eesqr)
        cc    = ((esqr ^ 2) * ff * (r ^ 2)) / (gg ^ 3)
        ss    = (1 + cc + sqrt ((cc ^ 2) + (2 * cc))) ** (1 / 3)
        pp    = ff / (3 * (((ss + (1 / ss) + 1)) ^ 2) * (gg ^ 2))
        qq    = sqrt (1 + (2 * (esqr ^ 2) * pp))
        r0    = ((-(pp * esqr * r)) / (1 + qq))
              + (sqrt ( ((1 / 2) * (a ^ 2) * (1 + (1 / qq)))
                      - ((pp * (1 - esqr) * (z ^ 2)) / (qq * (1 + qq)))
                      - ((1 / 2) * pp * (r ^ 2))
                      )
                )
        uu    = sqrt (((r - (esqr * r0)) ^ 2) + (z ^ 2))
        vv    = sqrt (((r - esqr * r0) ^ 2) + ((1 - esqr) * (z ^ 2)))
        zz0   = ((b ^ 2) * z) / (a * vv)
        h     = uu * (1 - ((b ^ 2) / (a * vv)))
        p     = atan ((z + (e'sqr * zz0)) / r)
        l     = atan2 y x
    in Geo (Radians p) (Radians l) h

-- | Standard WGS84 ellipsoid.
ecefToGeoFerrariEarth :: RealFloat a => ECEF a -> Geo a
ecefToGeoFerrariEarth = ecefToGeoFerrariEllipsoid 6378137 6356752.314245

-- | Synonym for 'earthEllipsoid'.
geoToECEF :: RealFloat a => Geo a -> ECEF a
geoToECEF = earthEllipsoid

-- | Synonym for 'ecefToGeoFerrariEarth'.
ecefToGeo ::  RealFloat a => ECEF a -> Geo a
ecefToGeo = ecefToGeoFerrariEarth