jord-2.0.0.0: src/Data/Geo/Jord/Geodetic.hs
-- |
-- Module: Data.Geo.Jord.Geodetic
-- Copyright: (c) 2020 Cedric Liegeois
-- License: BSD3
-- Maintainer: Cedric Liegeois <ofmooseandmen@yahoo.fr>
-- Stability: experimental
-- Portability: portable
--
-- Geodetic coordinates of points in specified models (e.g. WGS84) and conversion functions between
-- /n/-vectors and latitude/longitude.
--
-- 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>
--
-- In order to use this module you should start with the following imports:
--
-- @
-- import qualified Data.Geo.Jord.Geodetic as Geodetic
-- import qualified Data.Geo.Jord.Length as Length
-- import Data.Geo.Jord.Models
-- @
module Data.Geo.Jord.Geodetic
(
-- * positions types
HorizontalPosition
, Position
, HasCoordinates(..)
, height
, model
, model'
-- * Smart constructors
, latLongPos
, latLongPos'
, latLongHeightPos
, latLongHeightPos'
, wgs84Pos
, wgs84Pos'
, s84Pos
, s84Pos'
, nvectorPos
, nvectorPos'
, nvectorHeightPos
, nvectorHeightPos'
, atHeight
, atSurface
-- * Read/Show positions
, readHorizontalPosition
, horizontalPosition
, readPosition
, position
-- * /n/-vector conversions
, nvectorFromLatLong
, nvectorToLatLong
-- * Misc.
, antipode
, antipode'
, northPole
, southPole
) where
import Prelude hiding (read)
import Text.ParserCombinators.ReadP (ReadP, option, readP_to_S, skipSpaces)
import Data.Geo.Jord.Angle (Angle)
import qualified Data.Geo.Jord.Angle as Angle
import qualified Data.Geo.Jord.LatLong as LL (isValidLatLong, isValidLong, latLongDms, showLatLong)
import Data.Geo.Jord.Length (Length)
import qualified Data.Geo.Jord.Length as Length (length, zero)
import qualified Data.Geo.Jord.Math3d as Math3d (V3, scale, v3x, v3y, v3z, vec3)
import Data.Geo.Jord.Model
import Data.Geo.Jord.Models (S84(..), WGS84(..))
-- | Horizontal coordinates: geodetic latitude, longitude & /n/-vector.
data HCoords =
HCoords Angle Angle !Math3d.V3
-- | Geodetic coordinates (geodetic latitude, longitude as 'Angle's) of an horizontal position
-- in a specified 'Model'.
--
-- The coordinates are also given as a /n/-vector: the normal vector to the surface.
-- /n/-vector orientation:
-- * z-axis points to the North Pole along the body's rotation axis,
-- * x-axis points towards the point where latitude = longitude = 0
--
-- Note: at the poles all longitudes are equal, therefore a position with a latitude of 90° or -90° will have
-- its longitude forcibly set to 0°.
--
-- The "show" instance gives position in degrees, minutes, seconds, milliseconds ('Angle' "show" instance), and the
-- model ('Model' "show" instance).
--
-- The "eq" instance returns True if and only if, both positions have the same latitude, longitude and model.
-- Note: two positions in different models may represent the same location but are not considered equal.
data HorizontalPosition a =
HorizontalPosition HCoords a
-- | model of given 'HorizontalPosition' (e.g. WGS84).
model :: (Model a) => HorizontalPosition a -> a
model (HorizontalPosition _ m) = m
-- | Geodetic coordinates (geodetic latitude, longitude as 'Angle's and height as 'Length') of a position
-- in a specified model.
--
-- The "show" instance gives position in degrees, minutes, seconds, milliseconds (HorizontalPosition "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 coordinates and height.
--
-- see 'HorizontalPosition'.
data Position a =
Position HCoords Length a
-- | height of given 'Position' above the surface of the celestial body.
height :: (Model a) => Position a -> Length
height (Position _ h _) = h
-- | model of given 'Position' (e.g. WGS84).
model' :: (Model a) => Position a -> a
model' (Position _ _ m) = m
-- | class for data that provide coordinates.
class HasCoordinates a where
latitude :: a -> Angle -- ^ geodetic latitude
decimalLatitude :: a -> Double -- ^ geodetic latitude in decimal degrees
decimalLatitude = Angle.toDecimalDegrees . latitude
longitude :: a -> Angle -- ^ longitude
decimalLongitude :: a -> Double -- ^ longitude in decimal degrees
decimalLongitude = Angle.toDecimalDegrees . longitude
nvector :: a -> Math3d.V3 -- ^ /n/-vector; normal vector to the surface of a celestial body.
instance HasCoordinates (HorizontalPosition a) where
latitude (HorizontalPosition (HCoords lat _ _) _) = lat
longitude (HorizontalPosition (HCoords _ lon _) _) = lon
nvector (HorizontalPosition (HCoords _ _ nv) _) = nv
instance (Model a) => Show (HorizontalPosition a) where
show p = LL.showLatLong (latitude p, longitude p) ++ " (" ++ (show . model $ p) ++ ")"
-- model equality is ensured by @a@
instance (Model a) => Eq (HorizontalPosition a) where
p1 == p2 = latitude p1 == latitude p2 && longitude p1 == longitude p2
instance HasCoordinates (Position a) where
latitude (Position (HCoords lat _ _) _ _) = lat
longitude (Position (HCoords _ lon _) _ _) = lon
nvector (Position (HCoords _ _ nv) _ _) = nv
instance (Model a) => Show (Position a) where
show p =
LL.showLatLong (latitude p, longitude p) ++
" " ++ (show . height $ p) ++ " (" ++ (show . model' $ p) ++ ")"
-- model equality is ensured by @a@
instance (Model a) => Eq (Position a) where
p1 == p2 = latitude p1 == latitude p2 && longitude p1 == longitude p2 && height p1 == height p2
-- | 'HorizontalPosition' 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.
latLongPos :: (Model a) => Double -> Double -> a -> HorizontalPosition a
latLongPos lat lon = latLongPos' (Angle.decimalDegrees lat) (Angle.decimalDegrees lon)
-- | 'HorizontalPosition' from given geodetic latitude & longitude in the given model.
--
-- Latitude & longitude values are wrapped to their respective range.
latLongPos' :: (Model a) => Angle -> Angle -> a -> HorizontalPosition a
latLongPos' lat lon m = HorizontalPosition (HCoords wlat wlon nv) m
where
lon' = checkPole lat lon
nv = nvectorFromLatLong (lat, lon')
(wlat, wlon) = wrap lat lon' nv m
-- | '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' (Angle.decimalDegrees lat) (Angle.decimalDegrees lon)
-- | '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 = atHeight (latLongPos' lat lon m) h
-- | 'HorizontalPosition' from given geodetic latitude & longitude in __decimal degrees__ 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:
--
-- > Geodetic.latLongPos lat lon WGS84
wgs84Pos :: Double -> Double -> HorizontalPosition WGS84
wgs84Pos lat lon = latLongPos lat lon WGS84
-- | 'HorizontalPosition' from given geodetic latitude & longitude and height in the WGS84 datum.
--
-- Latitude & longitude values are wrapped to their respective range.
--
-- This is equivalent to:
--
-- > Geodetic.latLongPos' lat lon WGS84
wgs84Pos' :: Angle -> Angle -> HorizontalPosition WGS84
wgs84Pos' lat lon = latLongPos' lat lon WGS84
-- | 'HorizontalPosition' from given latitude & longitude in __decimal degrees__ 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:
--
-- > Geodetic.latLongPos lat lon S84
s84Pos :: Double -> Double -> HorizontalPosition S84
s84Pos lat lon = latLongPos lat lon S84
-- | 'Position' from given latitude & longitude in the spherical datum derived from WGS84.
--
-- Latitude & longitude values are wrapped to their respective range.
--
-- This is equivalent to:
--
-- > Geodetic.latLongPos' lat lon h S84
s84Pos' :: Angle -> Angle -> HorizontalPosition S84
s84Pos' lat lon = latLongPos' lat lon S84
-- | 'Position' from given /n/-vector (x, y, z coordinates) in the given model.
--
-- (x, y, z) will be converted first to latitude & longitude to ensure a consistent resolution with the rest of the API.
--
-- This is equivalent to:
--
-- > Geodetic.nvectorPos' (Math3d.vec3 x y z)
nvectorPos :: (Model a) => Double -> Double -> Double -> a -> HorizontalPosition a
nvectorPos x y z = nvectorPos' (Math3d.vec3 x y z)
-- | 'HorizontalPosition' from given /n/-vector (x, y, z coordinates) in the given model.
--
-- (x, y, z) will be converted first to latitude & longitude to ensure a consistent resolution with the rest of the API.
nvectorPos' :: (Model a) => Math3d.V3 -> a -> HorizontalPosition a
nvectorPos' v m = HorizontalPosition (HCoords lat wlon nv) m
where
(lat, lon) = nvectorToLatLong v
lon' = checkPole lat lon
nv = nvectorFromLatLong (lat, lon')
wlon = convertLon lon' m
-- | 'Position' from given /n/-vector (x, y, z coordinates) and height in the given model.
--
-- (x, y, z) will be converted first to latitude & longitude to ensure a consistent resolution with the rest of the API.
-- This is equivalent to:
--
-- > Geodetic.nvectorHeightPos' (Math3d.vec3 x y z) h
nvectorHeightPos :: (Model a) => Double -> Double -> Double -> Length -> a -> Position a
nvectorHeightPos x y z = nvectorHeightPos' (Math3d.vec3 x y z)
-- | 'Position' from given /n/-vector (x, y, z coordinates) and height in the given model.
--
-- (x, y, z) will be converted first to latitude & longitude to ensure a consistent resolution with the rest of the API.
nvectorHeightPos' :: (Model a) => Math3d.V3 -> Length -> a -> Position a
nvectorHeightPos' v h m = atHeight (nvectorPos' v m) h
-- | 'Position' from 'HorizontalPosition' & height.
atHeight :: (Model a) => HorizontalPosition a -> Length -> Position a
atHeight (HorizontalPosition c m) h = Position c h m
-- | 'HorizontalPosition' from 'Position'.
atSurface :: (Model a) => Position a -> HorizontalPosition a
atSurface (Position c _ m) = HorizontalPosition c m
-- | Reads an 'HorizontalPosition, from the given string using 'horizontalPosition', for example:
--
-- >>> Geodetic.readHorizontalPosition "55°36'21''N 013°00'02''E" WGS84
-- Just 55°36'21.000"N,13°0'2.000"E (WGS84)
readHorizontalPosition :: (Model a) => String -> a -> Maybe (HorizontalPosition a)
readHorizontalPosition s m =
case map fst $ filter (null . snd) $ readP_to_S (horizontalPosition m) s of
[] -> Nothing
p:_ -> Just p
-- | Reads a 'Position' from the given string using 'position', for example:
--
-- >>> Geodetic.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)
readPosition :: (Model a) => String -> a -> Maybe (Position a)
readPosition s m =
case map fst $ filter (null . snd) $ readP_to_S (position m) s of
[] -> Nothing
p:_ -> Just p
-- | Parses and returns a 'HorizontalPosition'.
--
-- 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
horizontalPosition :: (Model a) => a -> ReadP (HorizontalPosition a)
horizontalPosition m = do
(lat, lon) <- LL.latLongDms m
return (latLongPos' lat lon m)
-- | Parses and returns a 'Position': the beginning of the string is parsed by 'horizontalPosition' and additionally the
-- string may end by a valid 'Length'.
position :: (Model a) => a -> ReadP (Position a)
position m = do
hp <- horizontalPosition m
skipSpaces
h <- option Length.zero Length.length
return (atHeight hp h)
-- | @nvectorToLatLong nv@ returns (latitude, longitude) pair equivalent to the given /n/-vector @nv@.
-- Latitude is always in [-90°, 90°] and longitude in [-180°, 180°].
nvectorToLatLong :: Math3d.V3 -> (Angle, Angle)
nvectorToLatLong v = (lat, lon)
where
x = Math3d.v3x v
y = Math3d.v3y v
z = Math3d.v3z v
lat = Angle.atan2 z (sqrt (x * x + y * y))
lon = Angle.atan2 y x
-- | @nvectorFromLatLong ll@ returns /n/-vector equivalent to the given (latitude, longitude) pair @ll@.
nvectorFromLatLong :: (Angle, Angle) -> Math3d.V3
nvectorFromLatLong (lat, lon) = Math3d.vec3 x y z
where
cl = Angle.cos lat
x = cl * Angle.cos lon
y = cl * Angle.sin lon
z = Angle.sin lat
-- | @antipode p@ computes the antipodal position of @p@: the position which is diametrically opposite to @p@.
antipode :: (Model a) => HorizontalPosition a -> HorizontalPosition a
antipode p = nvectorPos' (anti . nvector $ p) (model p)
-- | @antipode p@ computes the antipodal position of @p@: the position which is diametrically opposite to @p@ at the
-- same height.
antipode' :: (Model a) => Position a -> Position a
antipode' p = nvectorHeightPos' (anti . nvector $ p) (height p) (model' p)
anti :: Math3d.V3 -> Math3d.V3
anti v = Math3d.scale v (-1.0)
-- | Horizontal position of the North Pole in the given model.
northPole :: (Model a) => a -> HorizontalPosition a
northPole = latLongPos 90 0
-- | Horizontal position of the South Pole in the given model.
southPole :: (Model a) => a -> HorizontalPosition a
southPole = latLongPos (-90) 0
wrap :: (Model a) => Angle -> Angle -> Math3d.V3 -> a -> (Angle, Angle)
wrap lat lon nv m =
if LL.isValidLatLong lat lon m
then (lat, lon)
else llWrapped nv m
llWrapped :: (Model a) => Math3d.V3 -> a -> (Angle, Angle)
llWrapped nv m = (lat, lon')
where
(lat, lon) = nvectorToLatLong nv
lon' = convertLon lon m
convertLon :: (Model a) => Angle -> a -> Angle
convertLon lon m =
case (longitudeRange m) of
L180 -> lon
L360 ->
if LL.isValidLong lon m
then lon
else Angle.add lon (Angle.decimalDegrees 360)
checkPole :: Angle -> Angle -> Angle
checkPole lat lon
| lat == Angle.decimalDegrees 90 || lat == Angle.decimalDegrees (-90) = Angle.zero
| otherwise = lon