geodetics-1.1.0: src/Geodetics/Ellipsoids.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RoleAnnotations #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{- | An Ellipsoid is a reasonable best fit for the surface of the
Earth over some defined area. WGS84 is the standard used for the whole
of the Earth. Other Ellipsoids are considered a best fit for some
specific area.
-}
module Geodetics.Ellipsoids (
-- * Useful constants
degree,
arcminute,
arcsecond,
kilometer,
_2, _3, _4, _5, _6, _7,
-- * Helmert transform between geodetic reference systems
Helmert (..),
inverseHelmert,
ECEF,
applyHelmert,
-- * Ellipsoid models of the Geoid
Ellipsoid (..),
WGS84 (..),
LocalEllipsoid (..),
flattening,
minorRadius,
eccentricity2,
eccentricity'2,
-- * Auxiliary latitudes and related Values
normal,
latitudeRadius,
meridianRadius,
primeVerticalRadius,
isometricLatitude,
-- * Tiny linear algebra library for 3D vectors
Vec3,
Matrix3,
add3,
scale3,
negate3,
transform3,
invert3,
trans3,
dot3,
cross3
) where
-- | All angles in this library are in radians. This is one degree in radians.
degree :: Double
degree = pi/180
-- | One arc-minute in radians.
arcminute :: Double
arcminute = degree / 60
-- | One arc-second in radians.
arcsecond :: Double
arcsecond = arcminute / 60
-- | All distances in this library are in meters. This is one kilometer in meters.
kilometer :: Double
kilometer = 1000
-- | Lots of geodetic calculations involve integer powers. Writing e.g. @x ^ 2@ causes
-- GHC to complain that the @2@ has ambiguous type. @x ** 2@ doesn't complain
-- but is much slower. So for convenience, here are small integers with type @Int@.
_2, _3, _4, _5, _6, _7 :: Int
_2 = 2
_3 = 3
_4 = 4
_5 = 5
_6 = 6
_7 = 7
-- | 3d vector as @(X,Y,Z)@.
type Vec3 a = (a,a,a)
-- | 3x3 transform matrix for Vec3.
type Matrix3 a = Vec3 (Vec3 a)
-- | Multiply a vector by a scalar.
scale3 :: (Num a) => Vec3 a -> a -> Vec3 a
scale3 (x,y,z) s = (x*s, y*s, z*s)
-- | Negation of a vector.
negate3 :: (Num a) => Vec3 a -> Vec3 a
negate3 (x,y,z) = (negate x, negate y, negate z)
-- | Add two vectors
add3 :: (Num a) => Vec3 a -> Vec3 a -> Vec3 a
add3 (x1,y1,z1) (x2,y2,z2) = (x1+x2, y1+y2, z1+z2)
-- | Multiply a matrix by a vector in the Dimensional type system.
transform3 :: (Num a) =>
Matrix3 a -> Vec3 a -> Vec3 a
transform3 (tx,ty,tz) v = (t tx v, t ty v, t tz v)
where
t (x1,y1,z1) (x2,y2,z2) = x1*x2 + y1*y2 + z1*z2
-- | Inverse of a 3x3 matrix.
invert3 :: (Fractional a) => Matrix3 a -> Matrix3 a
invert3 ((x1,y1,z1),
(x2,y2,z2),
(x3,y3,z3)) =
((det2 y2 z2 y3 z3 / det, det2 z1 y1 z3 y3 / det, det2 y1 z1 y2 z2 / det),
(det2 z2 x2 z3 x3 / det, det2 x1 z1 x3 z3 / det, det2 z1 x1 z2 x2 / det),
(det2 x2 y2 x3 y3 / det, det2 y1 x1 y3 x3 / det, det2 x1 y1 x2 y2 / det))
where
det = (x1*y2*z3 + y1*z2*x3 + z1*x2*y3) - (z1*y2*x3 + y1*x2*z3 + x1*z2*y3)
det2 a b c d = a*d - b*c
-- | Transpose of a 3x3 matrix.
trans3 :: Matrix3 a -> Matrix3 a
trans3 ((x1,y1,z1),(x2,y2,z2),(x3,y3,z3)) = ((x1,x2,x3),(y1,y2,y3),(z1,z2,z3))
-- | Dot product of two vectors
dot3 :: (Num a) => Vec3 a -> Vec3 a -> a
dot3 (x1,y1,z1) (x2,y2,z2) = x1*x2 + y1*y2 + z1*z2
-- | Cross product of two vectors
cross3 :: (Num a) => Vec3 a -> Vec3 a -> Vec3 a
cross3 (x1,y1,z1) (x2,y2,z2) = (y1*z2 - z1*y2, z1*x2 - x1*z2, x1*y2 - y1*x2)
-- | The 7 parameter Helmert transformation. The monoid instance allows composition but
-- is only accurate for the small values used in practical ellipsoids.
data Helmert = Helmert {
cX, cY, cZ :: Double, -- ^ Offset in meters
helmertScale :: Double, -- ^ Parts per million
rX, rY, rZ :: Double -- ^ Rotation around each axis in radians.
} deriving (Eq, Show)
instance Semigroup Helmert where
h1 <> h2 = Helmert (cX h1 + cX h2) (cY h1 + cY h2) (cZ h1 + cZ h2)
(helmertScale h1 + helmertScale h2)
(rX h1 + rX h2) (rY h1 + rY h2) (rZ h1 + rZ h2)
instance Monoid Helmert where
mempty = Helmert 0 0 0 0 0 0 0
mappend = (<>)
-- | The inverse of a Helmert transformation.
inverseHelmert :: Helmert -> Helmert
inverseHelmert h = Helmert (negate $ cX h) (negate $ cY h) (negate $ cZ h)
(negate $ helmertScale h)
(negate $ rX h) (negate $ rY h) (negate $ rZ h)
-- | Earth-centred, Earth-fixed coordinates as a vector. The origin and axes are
-- not defined: use with caution.
type ECEF = Vec3 Double
-- | Apply a Helmert transformation to earth-centered coordinates.
applyHelmert:: Helmert -> ECEF -> ECEF
applyHelmert h (x,y,z) = (
cX h + s * ( x - rZ h * y + rY h * z),
cY h + s * ( rZ h * x + y - rX h * z),
cZ h + s * (negate (rY h) * x + rX h * y + z))
where
s = 1 + helmertScale h * 1e-6
-- | An Ellipsoid is defined by the major radius and the inverse flattening (which define its shape),
-- and its Helmert transform relative to WGS84 (which defines its position and orientation).
--
-- The inclusion of the Helmert parameters relative to WGS84 actually make this a Terrestrial
-- Reference Frame (TRF), but the term "Ellipsoid" will be used in this library for readability.
--
-- Minimum definition: @majorRadius@, @flatR@ & @helmert@.
--
-- Laws:
--
-- > helmertToWGS84 = applyHelmert . helmert
-- > helmertFromWGS84 e . helmertToWGS84 e = id
class (Show a, Eq a) => Ellipsoid a where
majorRadius :: a -> Double
flatR :: a -> Double
-- ^ Inverse of the flattening.
helmert :: a -> Helmert -- ^ The Helmert parameters relative to WGS84,
helmertToWGS84 :: a -> ECEF -> ECEF
-- ^ The Helmert transform that will convert a position wrt
-- this ellipsoid into a position wrt WGS84.
helmertToWGS84 e = applyHelmert (helmert e)
helmertFromWGS84 :: a -> ECEF -> ECEF
-- ^ And its inverse.
helmertFromWGS84 e = applyHelmert (inverseHelmert $ helmert e)
-- | The WGS84 geoid, major radius 6378137.0 meters, flattening = 1 / 298.257223563
-- as defined in \"Technical Manual DMA TM 8358.1 - Datums, Ellipsoids, Grids, and
-- Grid Reference Systems\" at the National Geospatial-Intelligence Agency (NGA).
--
-- The WGS84 has a special place in this library as the standard Ellipsoid against
-- which all others are defined.
data WGS84 = WGS84
deriving (Eq, Show)
instance Ellipsoid WGS84 where
majorRadius _ = 6378137.0
flatR _ = 298.257223563
helmert _ = mempty
helmertToWGS84 _ = id
helmertFromWGS84 _ = id
-- | Ellipsoids other than WGS84, used within a defined geographical area where
-- they are a better fit to the local geoid. Can also be used for historical ellipsoids.
--
-- The @Show@ instance just returns the name.
-- Creating two different local ellipsoids with the same name is a Bad Thing.
data LocalEllipsoid = LocalEllipsoid {
nameLocal :: String,
majorRadiusLocal :: Double,
flatRLocal :: Double,
helmertLocal :: Helmert
} deriving (Eq)
instance Show LocalEllipsoid where
show = nameLocal
instance Ellipsoid LocalEllipsoid where
majorRadius = majorRadiusLocal
flatR = flatRLocal
helmert = helmertLocal
-- | Flattening (f) of an ellipsoid.
flattening :: (Ellipsoid e) => e -> Double
flattening e = 1 / flatR e
-- | The minor radius of an ellipsoid in meters.
minorRadius :: (Ellipsoid e) => e -> Double
minorRadius e = majorRadius e * (1 - flattening e)
-- | The eccentricity squared of an ellipsoid.
eccentricity2 :: (Ellipsoid e) => e -> Double
eccentricity2 e = 2 * f - f^_2 where f = flattening e
-- | The second eccentricity squared of an ellipsoid.
eccentricity'2 :: (Ellipsoid e) => e -> Double
eccentricity'2 e = (f * (2 - f)) / (1 - f^_2) where f = flattening e
-- | Distance in meters from the surface at the specified latitude to the
-- axis of the Earth straight down. Also known as the radius of
-- curvature in the prime vertical, and often denoted @N@.
normal :: (Ellipsoid e) => e -> Double -> Double
normal e lat = majorRadius e / sqrt (1 - eccentricity2 e * sin lat ^ _2)
-- | Radius of the circle of latitude: the distance from a point
-- at that latitude to the axis of the Earth, in meters.
latitudeRadius :: (Ellipsoid e) => e -> Double -> Double
latitudeRadius e lat = normal e lat * cos lat
-- | Radius of curvature in the meridian at the specified latitude, in meters
-- Often denoted @M@.
meridianRadius :: (Ellipsoid e) => e -> Double -> Double
meridianRadius e lat =
majorRadius e * (1 - eccentricity2 e)
/ sqrt ((1 - eccentricity2 e * sin lat ^ _2) ^ _3)
-- | Radius of curvature of the ellipsoid perpendicular to the meridian at the specified latitude, in meters.
primeVerticalRadius :: (Ellipsoid e) => e -> Double -> Double
primeVerticalRadius e lat =
majorRadius e / sqrt (1 - eccentricity2 e * sin lat ^ _2)
-- | The isometric latitude. The isometric latitude is conventionally denoted by ψ
-- (not to be confused with the geocentric latitude): it is used in the development
-- of the ellipsoidal versions of the normal Mercator projection and the Transverse
-- Mercator projection. The name "isometric" arises from the fact that at any point
-- on the ellipsoid equal increments of ψ and longitude λ give rise to equal distance
-- displacements along the meridians and parallels respectively.
isometricLatitude :: (Ellipsoid e) => e -> Double -> Double
isometricLatitude ellipse lat = atanh sinLat - e * atanh (e * sinLat)
where
sinLat = sin lat
e = sqrt $ eccentricity2 ellipse