-- | -- 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