jord-1.0.0.0: src/Data/Geo/Jord/Position.hs
-- |
-- Module: Data.Geo.Jord.Position
-- Copyright: (c) 2020 Cedric Liegeois
-- License: BSD3
-- Maintainer: Cedric Liegeois <ofmooseandmen@yahoo.fr>
-- Stability: experimental
-- Portability: portable
--
-- Position of points in specified models (e.g. WGS84) and conversion functions between
-- coordinate system (geodetic to/from geocentric).
--
-- All functions are implemented using the vector-based approached described in
-- <http://www.navlab.net/Publications/A_Nonsingular_Horizontal_Point_Representation.pdf Gade, K. (2010). A Non-singular Horizontal Position Representation>
--
-- See <http://clynchg3c.com/Technote/geodesy/coorddef.pdf Earth Coordinates>
--
module Data.Geo.Jord.Position
(
-- * The 'Position' type
Position
, latitude
, longitude
, height
, nvec
, gcvec
, model
-- * /n/-vector
, NVector
, nx
, ny
, nz
, nvector
-- * Geocentric coordinates
, Geocentric
, gx
, gy
, gz
, geocentric
-- * Smart constructors
, latLongPos
, latLongHeightPos
, latLongPos'
, latLongHeightPos'
, wgs84Pos
, wgs84Pos'
, s84Pos
, s84Pos'
, nvectorPos
, nvectorHeightPos
, geocentricPos
, geocentricMetresPos
, nvh
-- * Read/Show points
, readPosition
, positionP
-- * Vector3d conversions
, nvectorFromLatLong
, nvectorToLatLong
, nvectorFromGeocentric
, nvectorToGeocentric
-- * Misc.
, antipode
, latLong
, latLong'
, northPole
, southPole
, nvNorthPole
, nvSouthPole
-- * re-exported for convenience
, module Data.Geo.Jord.Angle
, module Data.Geo.Jord.Ellipsoid
, module Data.Geo.Jord.Ellipsoids
, module Data.Geo.Jord.LatLong
, module Data.Geo.Jord.Length
, module Data.Geo.Jord.Model
, module Data.Geo.Jord.Models
, module Data.Geo.Jord.Quantity
, module Data.Geo.Jord.Vector3d
) where
import Text.ParserCombinators.ReadP (ReadP, option, readP_to_S, skipSpaces)
import Data.Geo.Jord.Angle
import Data.Geo.Jord.Ellipsoid
import Data.Geo.Jord.Ellipsoids
import Data.Geo.Jord.LatLong
import Data.Geo.Jord.Length
import Data.Geo.Jord.Model
import Data.Geo.Jord.Models
import Data.Geo.Jord.Quantity
import Data.Geo.Jord.Vector3d
-- | Coordinates of a position in a specified 'Model'.
-- A position provides both geodetic latitude & longitude, height and
-- geocentric coordinates. The horizontal position
-- (i.e. coordinates at the surface of the celestial body) is also provided
-- as /n/-vector.
--
-- The "show" instance gives position in degrees, minutes, seconds,
-- milliseconds ('Angle' "show" instance), height ('Length' "show" instance)
-- and the model ('Model' "show" instance).
--
-- The "eq" instance returns True if and only if, both positions have the same
-- horizontal position, height and model.
data Position a =
Position
{ latitude :: Angle -- ^ geodetic latitude
, longitude :: Angle -- ^ geodetic longitude
, height :: Length -- ^ height above the surface of the celestial body
, nvec :: !Vector3d -- ^ /n/-vector representing the horizontal coordinates of the position
, gcvec :: Vector3d -- ^ vector representing the geocentric coordinates of the position (metres)
, model :: !a -- ^ model (e.g. WGS84)
}
instance (Model a) => Show (Position a) where
show p = showLatLong (latitude p, longitude p) ++ " " ++ (show . height $ p) ++ " (" ++ (show . model $ p) ++ ")"
instance (Model a) => Eq (Position a)
-- model equality is ensure by @a@
where
p1 == p2 = latitude p1 == latitude p2 && longitude p1 == longitude p2 && height p1 == height p2
-- | normal vector to the surface of a celestial body.
--
-- Orientation: z-axis points to the North Pole along the body's rotation axis,
-- x-axis points towards the point where latitude = longitude = 0.
data NVector =
NVector Double Double Double
instance Show NVector where
show (NVector x y z) = "n-vector {" ++ show x ++ ", " ++ show y ++ ", " ++ show z ++ "}"
-- | x-component of the given /n/-vector.
nx :: NVector -> Double
nx (NVector x _ _) = x
-- | y-component of the given /n/-vector.
ny :: NVector -> Double
ny (NVector _ y _) = y
-- | z-component of the given /n/-vector.
nz :: NVector -> Double
nz (NVector _ _ z) = z
-- | Geocentric (cartesian) coordinates in the fixed-body coordinates system.
--
-- @x-y@ plane is the equatorial plane, @x@ is on the prime meridian, and @z@ on the polar axis.
--
-- On a spherical celestial body, an /n/-vector is equivalent to a normalised version of an
-- geocentric cartesian coordinate.
--
-- Note: For Earth, this is known as the Earth-Centred Earth Fixed coordinates system (ECEF).
--
data Geocentric =
Geocentric Length Length Length
instance Show Geocentric where
show (Geocentric x y z) = "geocentric {" ++ show x ++ ", " ++ show y ++ ", " ++ show z ++ "}"
-- | x-coordinate of the given 'Geocentric' coordinates.
gx :: Geocentric -> Length
gx (Geocentric x _ _) = x
-- | y-coordinate of the given 'Geocentric' coordinates.
gy :: Geocentric -> Length
gy (Geocentric _ y _) = y
-- | z-coordinate of the given 'Geocentric' coordinates.
gz :: Geocentric -> Length
gz (Geocentric _ _ z) = z
-- | @antipode p@ computes the antipodal position of @p@: the position which is diametrically
-- opposite to @p@.
antipode :: (Model a) => Position a -> Position a
antipode p = nvh nv h (model p)
where
h = height p
nv = vscale (nvec p) (-1.0)
-- | @nvector p@ returns the horizontal position of @p@ as a /n/-vector.
--
-- ==== __Examples__
--
-- >>> import Data.Geo.Jord.Position
-- >>>
-- >>> nvector (northPole S84)
-- n-vector {0.0, 0.0, 1.0}
--
-- >>> nvector (wgs84Pos 54 154 (metres 1000))
-- n-vector {-0.5282978852629286, 0.2576680951131586, 0.8090169943749475}
--
nvector :: (Model a) => Position a -> NVector
nvector p = NVector x y z
where
(Vector3d x y z) = nvec p
-- | @geocentric p@ returns the 'Geocentric' coordinates of position @p@.
--
-- ==== __Examples__
--
-- >>> import Data.Geo.Jord.Position
-- >>>
-- >>> geocentric (wgs84Pos 54 154 (metres 1000))
-- geocentric {-3377.4908375km, 1647.312349km, 5137.5528484km}
--
geocentric :: (Model a) => Position a -> Geocentric
geocentric p = Geocentric (metres x) (metres y) (metres z)
where
(Vector3d x y z) = gcvec p
-- | Horizontal position of the North Pole in the given model.
northPole :: (Model a) => a -> Position a
northPole = nvh nvNorthPole zero
-- | Horizontal position of the South Pole in the given model.
southPole :: (Model a) => a -> Position a
southPole = nvh nvSouthPole zero
-- | Horizontal position of the North Pole (/n/-vector).
nvNorthPole :: Vector3d
nvNorthPole = Vector3d 0.0 0.0 1.0
-- | Horizontal position of the South Pole (/n/-vector).
nvSouthPole :: Vector3d
nvSouthPole = Vector3d 0.0 0.0 (-1.0)
-- | Reads a 'Position' from the given string using 'positionP'.
readPosition :: (Model a) => String -> a -> Maybe (Position a)
readPosition s m =
case map fst $ filter (null . snd) $ readP_to_S (positionP m) s of
[] -> Nothing
p:_ -> Just p
-- | Parses and returns a 'Position'.
--
-- Supported formats:
--
-- * DD(MM)(SS)[N|S]DDD(MM)(SS)[E|W] - e.g. 553621N0130002E or 0116S03649E or 47N122W
--
-- * 'Angle'[N|S] 'Angle'[E|W] - e.g. 55°36'21''N 13°0'02''E or 11°16'S 36°49'E or 47°N 122°W
--
-- Additionally the string may end by a valid 'Length'.
--
-- ==== __Examples__
--
-- >>> import Data.Geo.Jord.Position
-- >>>
-- >>> readPosition "55°36'21''N 013°00'02''E" WGS84
-- Just 55°36'21.000"N,13°0'2.000"E 0.0m (WGS84)
-- >>>
-- >>> readPosition "55°36'21''N 013°00'02''E 1500m" WGS84
-- Just 55°36'21.000"N,13°0'2.000"E 1500.0m (WGS84)
--
positionP :: (Model a) => a -> ReadP (Position a)
positionP m = do
(lat, lon) <- latLongDmsP m
skipSpaces
h <- option zero lengthP
return (latLongHeightPos' lat lon h m)
-- | Ground 'Position' from given geodetic latitude & longitude in __decimal degrees__ in
-- the given model.
--
-- Latitude & longitude values are first converted to 'Angle' to ensure a consistent resolution
-- with the rest of the API, then wrapped to their respective range.
--
-- This is equivalent to:
--
-- @
-- 'latLongHeightPos' lat lon zero model
-- @
--
latLongPos :: (Model a) => Double -> Double -> a -> Position a
latLongPos lat lon = latLongHeightPos lat lon zero
-- | 'Position' from given geodetic latitude & longitude in __decimal degrees__ and height
-- in the given model
--
-- Latitude & longitude values are first converted to 'Angle' to ensure a consistent resolution
-- with the rest of the API, then wrapped to their respective range.
--
latLongHeightPos :: (Model a) => Double -> Double -> Length -> a -> Position a
latLongHeightPos lat lon = latLongHeightPos' (decimalDegrees lat) (decimalDegrees lon)
-- | Ground 'Position' from given geodetic latitude & longitude in
-- the given model.
-- Latitude & longitude values are wrapped to their respective range.
--
-- This is equivalent to:
--
-- @
-- 'latLongHeightPos'' lat lon zero model
-- @
--
latLongPos' :: (Model a) => Angle -> Angle -> a -> Position a
latLongPos' lat lon = latLongHeightPos' lat lon zero
-- | 'Position' from given geodetic latitude & longitude and height in the given model.
-- Latitude & longitude values are wrapped to their respective range.
latLongHeightPos' :: (Model a) => Angle -> Angle -> Length -> a -> Position a
latLongHeightPos' lat lon h m = Position lat' lon' h nv g m
where
nv = nvectorFromLatLong (lat, lon)
g = nvectorToGeocentric (nv, h) (surface m)
(lat', lon') = wrap lat lon nv m
-- | 'Position' from given geodetic latitude & longitude in __decimal degrees__ and height in
-- the WGS84 datum.
--
-- Latitude & longitude values are first converted to 'Angle' to ensure a consistent resolution
-- with the rest of the API, then wrapped to their respective range.
--
-- This is equivalent to:
--
-- @
-- 'latLongHeightPos' lat lon h 'WGS84'
-- @
--
wgs84Pos :: Double -> Double -> Length -> Position WGS84
wgs84Pos lat lon h = latLongHeightPos lat lon h WGS84
-- | 'Position' from given geodetic latitude & longitude and height in the WGS84 datum.
-- Latitude & longitude values are wrapped to their respective range.
--
-- This is equivalent to:
--
-- @
-- 'latLongHeightPos'' lat lon h 'WGS84'
-- @
--
wgs84Pos' :: Angle -> Angle -> Length -> Position WGS84
wgs84Pos' lat lon h = latLongHeightPos' lat lon h WGS84
-- | 'Position' from given latitude & longitude in __decimal degrees__ and height in the
-- spherical datum derived from WGS84.
--
-- Latitude & longitude values are first converted to 'Angle' to ensure a consistent resolution
-- with the rest of the API, then wrapped to their respective range.
--
-- This is equivalent to:
--
-- @
-- 'latLongHeightPos' lat lon h 'S84'
-- @
--
s84Pos :: Double -> Double -> Length -> Position S84
s84Pos lat lon h = latLongHeightPos lat lon h S84
-- | 'Position' from given latitude & longitude and height in the spherical datum derived
-- from WGS84. Latitude & longitude values are wrapped to their respective range.
--
-- This is equivalent to:
--
-- @
-- 'latLongHeightPos'' lat lon h 'S84'
-- @
--
s84Pos' :: Angle -> Angle -> Length -> Position S84
s84Pos' lat lon h = latLongHeightPos' lat lon h S84
-- | 'Position' from given geocentric coordinates x, y and z in the given model.
geocentricPos :: (Model a) => Length -> Length -> Length -> a -> Position a
geocentricPos x y z = geocentricMetresPos' (toMetres x) (toMetres y) (toMetres z)
-- | 'Position' from given geocentric coordinates x, y and z in __metres__ in the given model.
--
-- x, y, z lengths are first converted to 'Length' to ensure a consistent resolution with the rest of the API.
geocentricMetresPos :: (Model a) => Double -> Double -> Double -> a -> Position a
geocentricMetresPos x y z = geocentricMetresPos' (toMetres . metres $ x) (toMetres . metres $ y) (toMetres . metres $ z)
geocentricMetresPos' :: (Model a) => Double -> Double -> Double -> a -> Position a
geocentricMetresPos' x y z m = Position lat lon h nv ev m
where
ev = Vector3d x y z
(nv, h) = nvectorFromGeocentric ev (surface m)
(lat, lon) = nvectorToLatLong nv
-- | 'Position' from given /n/-vector x, y, z coordinates in the given model.
-- Vector (x, y, z) will be normalised to a unit vector to get a valid /n/-vector.
--
-- This is equivalent to:
--
-- @
-- 'nvectorHeightPos' lat lon zero model
-- @
--
nvectorPos :: (Model a) => Double -> Double -> Double -> a -> Position a
nvectorPos x y z = nvectorHeightPos x y z zero
-- | 'Position' from given /n/-vector x, y, z coordinates and height in the given model.
-- Vector (x, y, z) will be normalised to a unit vector to get a valid /n/-vector.
nvectorHeightPos :: (Model a) => Double -> Double -> Double -> Length -> a -> Position a
nvectorHeightPos x y z = nvh (vunit (Vector3d x y z))
-- | (latitude, longitude) pair in __decimal degrees__ from given position.
latLong :: (Model a) => Position a -> (Double, Double)
latLong p = (toDecimalDegrees . latitude $ p, toDecimalDegrees . longitude $ p)
-- | (latitude, longitude) pair from given position.
latLong' :: (Model a) => Position a -> (Angle, Angle)
latLong' p = (latitude p, longitude p)
-- given 'Vector3d' is a /n/-vector.
-- | position from /n/-vector, height and model; this method is to be used only if
nvh :: (Model a) => Vector3d -> Length -> a -> Position a
nvh nv h m = Position lat lon h nv g m
where
(lat, lon) = llWrapped nv (longitudeRange m)
g = nvectorToGeocentric (nv, h) (surface m)
-- | @nvectorToLatLong nv@ returns (latitude, longitude) pair equivalent to the given /n/-vector @nv@.
--
-- You should prefer using:
--
-- @
-- 'latLong' ('nvectorPos' x y z model)
-- @
--
-- Latitude is always in [-90°, 90°] and longitude in [-180°, 180°].
nvectorToLatLong :: Vector3d -> (Angle, Angle)
nvectorToLatLong nv = (lat, lon)
where
lat = atan2' (vz nv) (sqrt (vx nv * vx nv + vy nv * vy nv))
lon = atan2' (vy nv) (vx nv)
-- | @nvectorFromLatLong ll@ returns /n/-vector equivalent to the given (latitude, longitude) pair @ll@.
--
-- You should prefer using:
--
-- @
-- 'nvector' ('latLongPos' lat lon model)
-- @
nvectorFromLatLong :: (Angle, Angle) -> Vector3d
nvectorFromLatLong (lat, lon) = Vector3d x y z
where
cl = cos' lat
x = cl * cos' lon
y = cl * sin' lon
z = sin' lat
-- | @nvectorToGeocentric (nv, h) e@ returns the geocentric coordinates equivalent to the given
-- /n/-vector @nv@ and height @h@ using the ellispoid @e@.
--
-- You should prefer using:
--
-- @
-- 'geocentric' ('nvectorHeightPos' x y z h model)
-- @
--
nvectorToGeocentric :: (Vector3d, Length) -> Ellipsoid -> Vector3d
nvectorToGeocentric (nv, h) e
| isSphere e = nvectorToGeocentricS (nv, h) (equatorialRadius e)
| otherwise = nvectorToGeocentricE (nv, h) e
nvectorToGeocentricS :: (Vector3d, Length) -> Length -> Vector3d
nvectorToGeocentricS (nv, h) r = vscale nv (toMetres n)
where
n = add h r
nvectorToGeocentricE :: (Vector3d, Length) -> Ellipsoid -> Vector3d
nvectorToGeocentricE (nv, h) e = Vector3d gx' gy' gz'
where
a = toMetres . equatorialRadius $ e
b = toMetres . polarRadius $ e
nx' = vx nv
ny' = vy nv
nz' = vz nv
m = (a * a) / (b * b)
n = b / sqrt ((nx' * nx' * m) + (ny' * ny' * m) + (nz' * nz'))
h' = toMetres h
gx' = n * m * nx' + h' * nx'
gy' = n * m * ny' + h' * ny'
gz' = n * nz' + h' * nz'
-- | @nvectorFromGeocentric g e@ returns the /n/-vector equivalent to the geocentric
-- coordinates @g@ using the ellispoid @e@.
--
-- You should prefer using:
--
-- @
-- 'nvector' ('geocentricMetresPos' x y z model)
-- @
--
nvectorFromGeocentric :: Vector3d -> Ellipsoid -> (Vector3d, Length)
nvectorFromGeocentric g e
| isSphere e = nvectorFromGeocentricS g (equatorialRadius e)
| otherwise = nvectorFromGeocentricE g e
nvectorFromGeocentricS :: Vector3d -> Length -> (Vector3d, Length)
nvectorFromGeocentricS g r = (vunit g, h)
where
h = sub (metres (vnorm g)) r
nvectorFromGeocentricE :: Vector3d -> Ellipsoid -> (Vector3d, Length)
nvectorFromGeocentricE g e = (nvecEllipsoidal d e2 k px py pz, metres h)
where
e' = eccentricity e
e2 = e' * e'
e4 = e2 * e2
a = toMetres . equatorialRadius $ e
a2 = a * a
px = vx g
py = vy g
pz = vz g
p = (px * px + py * py) / a2
q = ((1 - e2) / a2) * (pz * pz)
r = (p + q - e4) / 6.0
s = (e4 * p * q) / (4.0 * r * r * r)
t = (1.0 + s + sqrt (s * (2.0 + s))) ** (1 / 3)
u = r * (1.0 + t + 1.0 / t)
v = sqrt (u * u + q * e4)
w = e2 * (u + v - q) / (2.0 * v)
k = sqrt (u + v + w * w) - w
d = k * sqrt (px * px + py * py) / (k + e2)
h = ((k + e2 - 1.0) / k) * sqrt (d * d + pz * pz)
nvecEllipsoidal :: Double -> Double -> Double -> Double -> Double -> Double -> Vector3d
nvecEllipsoidal d e2 k px py pz = Vector3d nx' ny' nz'
where
s = 1.0 / sqrt (d * d + pz * pz)
a = k / (k + e2)
nx' = s * a * px
ny' = s * a * py
nz' = s * pz
wrap :: (Model a) => Angle -> Angle -> Vector3d -> a -> (Angle, Angle)
wrap lat lon nv m =
if isValidLatLong lat lon m
then (lat, lon)
else llWrapped nv (longitudeRange m)
llWrapped :: Vector3d -> LongitudeRange -> (Angle, Angle)
llWrapped nv lr = (lat, lon')
where
(lat, lon) = nvectorToLatLong nv
lon' =
case lr of
L180 -> lon
L360 -> add lon (decimalDegrees 180)