jord-0.2.0.0: src/Data/Geo/Jord/Position.hs
-- |
-- Module: Data.Geo.Jord.GreatCircle
-- Copyright: (c) 2018 Cedric Liegeois
-- License: BSD3
-- Maintainer: Cedric Liegeois <ofmooseandmen@yahoo.fr>
-- Stability: experimental
-- Portability: portable
--
-- Types and functions for working with positions.
--
-- All functions are implemented using the vector-based approached described in
-- <http://www.navlab.net/Publications/A_Nonsingular_Horizontal_Position_Representation.pdf Gade, K. (2010). A Non-singular Horizontal Position Representation>
--
-- This module assumes a spherical earth.
--
module Data.Geo.Jord.Position
(
-- * The 'Position' type
Position(..)
-- * Geodetic calculations
, angularDistance
, antipode
, destination
, destination'
, distance
, distance'
, finalBearing
, initialBearing
, interpolate
, isInside
, mean
-- * Misc.
, meanEarthRadius
, northPole
, southPole
) where
import Data.Geo.Jord.Angle
import Data.Geo.Jord.LatLong
import Data.Geo.Jord.Length
import Data.Geo.Jord.NVector
import Data.Geo.Jord.Quantity
import Data.List (subsequences)
import Prelude hiding (fail)
-- | The 'Position' class defines 2 functions to convert a position to and from a 'NVector'.
-- All functions in this module first convert 'Position' to 'NVector' and any resulting 'NVector' back
-- to a 'Position'. This allows the call site to pass either 'NVector' or 'LatLong' and to get back
-- the same class instance.
class Position a where
-- | Converts a 'NVector' into 'Position' instance.
fromNVector :: NVector -> a
-- | Converts the 'Position' instance into a 'NVector'.
toNVector :: a -> NVector
-- | 'LatLong' to/from 'NVector'.
instance Position LatLong where
fromNVector v = latLong lat lon
where
lat = atan2' (z v) (sqrt (x v * x v + y v * y v))
lon = atan2' (y v) (x v)
toNVector g = nvector x' y' z'
where
lat = latitude g
lon = longitude g
cl = cos' lat
x' = cl * cos' lon
y' = cl * sin' lon
z' = sin' lat
-- | Identity.
instance Position NVector where
fromNVector v = v
toNVector v = v
-- | Angle between the two given 'NVector's.
-- If @n@ is 'Nothing', the angle is always in [0..180], otherwise it is in [-180, +180],
-- signed + if @v1@ is clockwise looking along @n@, - in opposite direction.
angularDistance :: NVector -> NVector -> Maybe NVector -> Angle
angularDistance v1 v2 n = atan2' sinO cosO
where
sign = maybe 1 (signum . dot (cross v1 v2)) n
sinO = sign * norm (cross v1 v2)
cosO = dot v1 v2
-- | Returns the antipodal 'Position' of the given 'Position' - i.e. the position on the surface
-- of the Earth which is diametrically opposite to the given position.
antipode :: (Position a) => a -> a
antipode p = fromNVector (scale (toNVector p) (-1.0))
-- | 'destination'' assuming a radius of 'meanEarthRadius'.
destination :: (Position a) => a -> Angle -> Length -> a
destination p b d = destination' p b d meanEarthRadius
-- | Computes the destination 'Position' from the given 'Position' having travelled the given distance on the
-- given initial bearing (bearing will normally vary before destination is reached) and using the given earth radius.
--
-- This is known as the direct geodetic problem.
destination' :: (Position a) => a -> Angle -> Length -> Length -> a
destination' p b d r
| isZero d = p
| otherwise = fromNVector (add (scale v (cos' ta)) (scale de (sin' ta)))
where
v = toNVector p
ed = unit (cross northPole v) -- east direction vector at v
nd = cross v ed -- north direction vector at v
ta = central d r -- central angle
de = add (scale nd (cos' b)) (scale ed (sin' b)) -- unit vector in the direction of the azimuth
-- | 'distance'' assuming a radius of 'meanEarthRadius'.
distance :: (Position a) => a -> a -> Length
distance p1 p2 = distance' p1 p2 meanEarthRadius
-- | Computes the surface distance (length of geodesic) in 'Meters' assuming a
-- spherical Earth between the two given 'Position's and using the given earth radius.
distance' :: (Position a) => a -> a -> Length -> Length
distance' p1 p2 = arcLength (angularDistance v1 v2 Nothing)
where
v1 = toNVector p1
v2 = toNVector p2
-- | Computes the final bearing arriving at given destination @p2@ 'Position' from given 'Position' @p1@.
-- the final bearing will differ from the 'initialBearing' by varying degrees according to distance and latitude.
-- Returns 180 if both position are equals.
finalBearing :: (Position a) => a -> a -> Angle
finalBearing p1 p2 = normalise (initialBearing p2 p1) (decimalDegrees 180)
-- | Computes the initial bearing from given @p1@ 'Position' to given @p2@ 'Position', in compass degrees.
-- Returns 0 if both position are equals.
initialBearing :: (Position a) => a -> a -> Angle
initialBearing p1 p2 = normalise (angularDistance gc1 gc2 (Just v1)) (decimalDegrees 360)
where
v1 = toNVector p1
v2 = toNVector p2
gc1 = cross v1 v2 -- great circle through p1 & p2
gc2 = cross v1 northPole -- great circle through p1 & north pole
-- | Computes the 'Position' at given fraction @f@ between the two given 'Position's @p0@ and @p1@.
--
-- Special conditions:
--
-- @
-- interpolate p0 p1 0.0 => p0
-- interpolate p0 p1 1.0 => p1
-- @
--
-- 'error's if @f < 0 || f > 1.0@
--
interpolate :: (Position a) => a -> a -> Double -> a
interpolate p0 p1 f
| f < 0 || f > 1 = error ("fraction must be in range [0..1], was " ++ show f)
| f == 0 = p0
| f == 1 = p1
| otherwise = fromNVector (unit (add v0 (scale (sub v1 v0) f)))
where
v0 = toNVector p0
v1 = toNVector p1
-- | Determines whether the given 'Position' is inside the polygon defined by the given list of 'Position's.
-- The polygon is closed if needed (i.e. if @head ps /= last ps@).
--
-- Uses the angle summation test: on a sphere, due to spherical excess, enclosed point angles
-- will sum to less than 360°, and exterior point angles will be small but non-zero.
--
-- Always returns 'False' if positions does not at least defines a triangle.
--
isInside :: (Eq a, Position a) => a -> [a] -> Bool
isInside p ps
| null ps = False
| head ps == last ps = isInside p (init ps)
| length ps < 3 = False
| otherwise =
let aSum = foldl (\a v' -> add a (uncurry angularDistance v' (Just v))) (decimalDegrees 0) es
in abs (toDecimalDegrees aSum) > 180.0
where
v = toNVector p
es = egdes (map (sub v . toNVector) ps)
-- | [p1, p2, p3, p4] to [(p1, p2), (p2, p3), (p3, p4), (p4, p1)]
egdes :: [NVector] -> [(NVector, NVector)]
egdes ps = zip ps ps'
where
ps' = tail ps ++ [head ps]
-- | Computes the geographic mean 'Position' of the given 'Position's if it is defined.
--
-- The geographic mean is not defined for the antipodals positions (since they
-- cancel each other).
--
-- Special conditions:
--
-- @
-- mean [] == Nothing
-- mean [p] == Just p
-- mean [p1, p2, p3] == Just circumcentre
-- mean [p1, .., antipode p1] == Nothing
-- @
--
mean :: (Position a) => [a] -> Maybe a
mean [] = Nothing
mean [p] = Just p
mean ps =
if null antipodals
then Just (fromNVector (unit (foldl add zero vs)))
else Nothing
where
vs = map toNVector ps
ts = filter (\l -> length l == 2) (subsequences vs)
antipodals =
filter
(\t -> (fromNVector (antipode (head t)) :: LatLong) == (fromNVector (last t) :: LatLong))
ts
-- | Mean Earth radius: 6,371,008.8 metres.
meanEarthRadius :: Length
meanEarthRadius = metres 6371008.8
-- | 'Position' of the North Pole.
northPole :: (Position a) => a
northPole = fromNVector (nvector 0.0 0.0 1.0)
-- | 'Position' of the South Pole.
southPole :: (Position a) => a
southPole = fromNVector (nvector 0.0 0.0 (-1.0))