{-# LANGUAGE FlexibleInstances #-} -- | -- Module: Data.Geo.Jord.Geodetics -- Copyright: (c) 2018 Cedric Liegeois -- License: BSD3 -- Maintainer: Cedric Liegeois <ofmooseandmen@yahoo.fr> -- Stability: experimental -- Portability: portable -- -- Geodetic calculations assuming a __spherical__ earth model. -- -- 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> -- module Data.Geo.Jord.Geodetics ( -- * The 'GreatCircle' type GreatCircle , IsGreatCircle(..) -- * Calculations , angularDistance , antipode , crossTrackDistance , crossTrackDistance84 , destination , destination84 , finalBearing , initialBearing , interpolate , intersections , insideSurface , mean , surfaceDistance , surfaceDistance84 ) where import Control.Monad.Fail import Data.Fixed import Data.Geo.Jord.Angle import Data.Geo.Jord.AngularPosition import Data.Geo.Jord.Earth (r84) import Data.Geo.Jord.LatLong import Data.Geo.Jord.Length import Data.Geo.Jord.NVector import Data.Geo.Jord.Quantity import Data.Geo.Jord.Transformation import Data.Geo.Jord.Vector3d import Data.List (subsequences) import Data.Maybe (fromMaybe) import Prelude hiding (fail) -- | A circle on the __surface__ of the Earth which lies in a plane passing through -- the Earth's centre. Every two distinct and non-antipodal points on the surface -- of the Earth define a Great Circle. -- -- It is internally represented as its normal vector - i.e. the normal vector -- to the plane containing the great circle. -- -- See 'greatCircle', 'greatCircleE', 'greatCircleF' or 'greatCircleBearing' constructors. -- data GreatCircle = GreatCircle { normal :: Vector3d , dscr :: String } deriving (Eq) instance Show GreatCircle where show = dscr -- | Class for data from which a 'GreatCircle' can be computed. class (Show a) => IsGreatCircle a where greatCircle :: a -> GreatCircle -- ^ 'GreatCircle' from @a@, if 'greateCircleE' returns a 'Left', this function 'error's. greatCircle a = fromMaybe (error (show a ++ " do not define a Great Circle")) (greatCircleF a) greatCircleE :: a -> Either String GreatCircle -- ^ 'GreatCircle' from @a@, A 'Left' indicates an error. greatCircleF :: (MonadFail m) => a -> m GreatCircle -- ^ 'GreatCircle' from @a@, if 'greateCircleE' returns a 'Left', this function 'fail's. greatCircleF a = case e of Left err -> fail err Right gc -> return gc where e = greatCircleE a -- | 'GreatCircle' passing by both given positions'. A 'Left' indicates that given positions are -- equal or antipodal. -- -- @ -- let p1 = decimalLatLongHeight 45.0 (-143.5) (metres 1500) -- let p2 = decimalLatLongHeight 46.0 14.5 (metres 3000) -- greatCircle (p1, p2) -- heights are ignored, great circle are always at earth surface. -- @ instance (NTransform a, Show a) => IsGreatCircle (a, a) where greatCircleE (p1, p2) | v1 == v2 = Left "Invalid Great Circle: positions are equal" | (realToFrac (vnorm (vadd v1 v2)) :: Nano) == 0 = Left "Invalid Great Circle: positions are antipodal" | otherwise = Right (GreatCircle (vcross v1 v2) ("passing by " ++ show (ll p1) ++ " & " ++ show (ll p2))) where v1 = vector3d p1 v2 = vector3d p2 -- | 'GreatCircle' passing by the given position and heading on given bearing. -- -- @ -- greatCircle (readLatLong "283321N0290700W", decimalDegrees 33.0) -- @ instance (NTransform a, Show a) => IsGreatCircle (a, Angle) where greatCircleE (p, b) = Right (GreatCircle (vsub n' e') ("passing by " ++ show (ll p) ++ " heading on " ++ show b)) where v = vector3d p e = vcross (vec northPole) v -- easting n = vcross v e -- northing e' = vscale e (cos' b / vnorm e) n' = vscale n (sin' b / vnorm n) -- | @angularDistance p1 p2 n@ computes the angle between the horizontal positions @p1@ and @p2@. -- If @n@ is 'Nothing', the angle is always in [0..180], otherwise it is in [-180, +180], -- signed + if @p1@ is clockwise looking along @n@, - in opposite direction. angularDistance :: (NTransform a) => a -> a -> Maybe a -> Angle angularDistance p1 p2 n = angularDistance' v1 v2 vn where v1 = vector3d p1 v2 = vector3d p2 vn = fmap vector3d n -- | @antipode p@ computes the antipodal horizontal position of @p@: -- the horizontal position on the surface of the Earth which is diametrically opposite to @p@. antipode :: (NTransform a) => a -> a antipode p = fromNVector (angular (vscale (vector3d nv) (-1.0)) h) where (AngularPosition nv h) = toNVector p -- | @crossTrackDistance p gc@ computes the signed distance from horizontal position @p@ to great circle @gc@. -- Returns a negative 'Length' if position if left of great circle, -- positive 'Length' if position if right of great circle; the orientation of the -- great circle is therefore important: -- -- @ -- let gc1 = greatCircle (decimalLatLong 51 0) (decimalLatLong 52 1) -- let gc2 = greatCircle (decimalLatLong 52 1) (decimalLatLong 51 0) -- crossTrackDistance p gc1 = (- crossTrackDistance p gc2) -- -- let p = decimalLatLong 53.2611 (-0.7972) -- let gc = greatCircleBearing (decimalLatLong 53.3206 (-1.7297)) (decimalDegrees 96.0) -- crossTrackDistance p gc r84 -- -305.663 metres -- @ crossTrackDistance :: (NTransform a) => a -> GreatCircle -> Length -> Length crossTrackDistance p gc = arcLength (sub (angularDistance' (normal gc) (vector3d p) Nothing) (decimalDegrees 90)) -- | 'crossTrackDistance' using the mean radius of the WGS84 reference ellipsoid. crossTrackDistance84 :: (NTransform a) => a -> GreatCircle -> Length crossTrackDistance84 p gc = crossTrackDistance p gc r84 -- | @destination p b d r@ computes the destination position from position @p@ having -- travelled the distance @d@ on the initial bearing (compass angle) @b@ (bearing will normally vary -- before destination is reached) and using the earth radius @r@. -- -- @ -- let p0 = ecefToNVector (ecefMetres 3812864.094 (-115142.863) 5121515.161) s84 -- let p1 = ecefMetres 3826406.4710518294 8900.536398998282 5112694.233184049 -- let p = destination p0 (decimalDegrees 96.0217) (metres 124800) r84 -- nvectorToEcef p s84 = p1 -- @ destination :: (NTransform a) => a -> Angle -> Length -> Length -> a destination p b d r | toMetres d == 0.0 = p | otherwise = fromNVector (angular vd h) where (AngularPosition nv h) = toNVector p v = vec nv ed = vunit (vcross (vec northPole) v) -- east direction vector at v nd = vcross v ed -- north direction vector at v ta = central d r -- central angle de = vadd (vscale nd (cos' b)) (vscale ed (sin' b)) -- vunit vector in the direction of the azimuth vd = vadd (vscale v (cos' ta)) (vscale de (sin' ta)) -- | 'destination' using the mean radius of the WGS84 reference ellipsoid. destination84 :: (NTransform a) => a -> Angle -> Length -> a destination84 p b d = destination p b d r84 -- | @finalBearing p1 p2@ computes the final bearing arriving at @p2@ from @p1@ in compass angle. -- -- Compass angles are clockwise angles from true north: 0 = north, 90 = east, 180 = south, 270 = west. -- -- The final bearing will differ from the 'initialBearing' by varying degrees according to distance and latitude. -- -- Returns 'Nothing' if both horizontal positions are equals. finalBearing :: (Eq a, NTransform a) => a -> a -> Maybe Angle finalBearing p1 p2 = fmap (\b -> normalise b (decimalDegrees 180)) (initialBearing p2 p1) -- | @initialBearing p1 p2@ computes the initial bearing from @p1@ to @p2@ in compass angle. -- -- Compass angles are clockwise angles from true north: 0 = north, 90 = east, 180 = south, 270 = west. -- -- Returns 'Nothing' if both horizontal positions are equals. initialBearing :: (Eq a, NTransform a) => a -> a -> Maybe Angle initialBearing p1 p2 | p1 == p2 = Nothing | otherwise = Just (normalise (angularDistance' gc1 gc2 (Just v1)) (decimalDegrees 360)) where v1 = vector3d p1 v2 = vector3d p2 gc1 = vcross v1 v2 -- great circle through p1 & p2 gc2 = vcross v1 (vec northPole) -- great circle through p1 & north pole -- | @interpolate p0 p1 f# computes the horizontal position at fraction @f@ between the @p0@ and @p1@. -- -- Special conditions: -- -- @ -- interpolate p0 p1 0.0 = p0 -- interpolate p0 p1 1.0 = p1 -- @ -- -- 'error's if @f < 0 || f > 1@ -- -- @ -- let p1 = latLongHeight (readLatLong "53°28'46''N 2°14'43''W") (metres 10000) -- let p2 = latLongHeight (readLatLong "55°36'21''N 13°02'09''E") (metres 20000) -- interpolate p1 p2 0.5 = decimalLatLongHeight 54.7835574 5.1949856 (metres 15000) -- @ interpolate :: (NTransform 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 (angular iv ih) where (AngularPosition nv0 h0) = toNVector p0 (AngularPosition nv1 h1) = toNVector p1 v0 = vec nv0 v1 = vec nv1 iv = vunit (vadd v0 (vscale (vsub v1 v0) f)) ih = lrph h0 h1 f -- | @insideSurface p ps@ determines whether the @p@ is inside the polygon defined by the list of positions @ps@. -- 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 @ps@ does not at least defines a triangle. -- -- @ -- let malmo = decimalLatLong 55.6050 13.0038 -- let ystad = decimalLatLong 55.4295 13.82 -- let lund = decimalLatLong 55.7047 13.1910 -- let helsingborg = decimalLatLong 56.0465 12.6945 -- let kristianstad = decimalLatLong 56.0294 14.1567 -- let polygon = [malmo, ystad, kristianstad, helsingborg, lund] -- let hoor = decimalLatLong 55.9295 13.5297 -- let hassleholm = decimalLatLong 56.1589 13.7668 -- insideSurface hoor polygon = True -- insideSurface hassleholm polygon = False -- @ insideSurface :: (Eq a, NTransform a) => a -> [a] -> Bool insideSurface p ps | null ps = False | head ps == last ps = insideSurface p (init ps) | length ps < 3 = False | otherwise = let aSum = foldl (\a v' -> add a (uncurry angularDistance' v' (Just v))) (decimalDegrees 0) (egdes (map (vsub v) vs)) in abs (toDecimalDegrees aSum) > 180.0 where v = vector3d p vs = fmap vector3d ps -- | Computes the intersections between the two given 'GreatCircle's. -- Two 'GreatCircle's intersect exactly twice unless there are equal (regardless of orientation), -- in which case 'Nothing' is returned. -- -- @ -- let gc1 = greatCircleBearing (decimalLatLong 51.885 0.235) (decimalDegrees 108.63) -- let gc2 = greatCircleBearing (decimalLatLong 49.008 2.549) (decimalDegrees 32.72) -- let (i1, i2) = fromJust (intersections gc1 gc2) -- i1 = decimalLatLong 50.9017226 4.4942782 -- i2 = antipode i1 -- @ intersections :: (NTransform a) => GreatCircle -> GreatCircle -> Maybe (a, a) intersections gc1 gc2 | (vnorm i :: Double) == 0.0 = Nothing | otherwise , let ni = fromNVector (angular (vunit i) zero) = Just (ni, antipode ni) where i = vcross (normal gc1) (normal gc2) -- | @mean ps@ computes the mean geographic horitzontal position of @ps@, if it is defined. -- -- The geographic mean is not defined for antipodals position (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 :: (NTransform a) => [a] -> Maybe a mean [] = Nothing mean [p] = Just p mean ps = if null antipodals then Just (fromNVector (angular (vunit (foldl vadd vzero vs)) zero)) else Nothing where vs = fmap vector3d ps ts = filter (\l -> length l == 2) (subsequences vs) antipodals = filter (\t -> (realToFrac (vnorm (vadd (head t) (last t)) :: Double) :: Nano) == 0) ts -- | @surfaceDistance p1 p2@ computes the surface distance (length of geodesic) between the positions @p1@ and @p2@. surfaceDistance :: (NTransform a) => a -> a -> Length -> Length surfaceDistance p1 p2 = arcLength (angularDistance p1 p2 Nothing) -- | 'surfaceDistance' using the mean radius of the WGS84 reference ellipsoid. surfaceDistance84 :: (NTransform a) => a -> a -> Length surfaceDistance84 p1 p2 = surfaceDistance p1 p2 r84 -- | Angle between the two given n-vectors. -- 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' :: Vector3d -> Vector3d -> Maybe Vector3d -> Angle angularDistance' v1 v2 n = atan2' sinO cosO where sign = maybe 1 (signum . vdot (vcross v1 v2)) n sinO = sign * vnorm (vcross v1 v2) cosO = vdot v1 v2 -- | [p1, p2, p3, p4] to [(p1, p2), (p2, p3), (p3, p4), (p4, p1)] egdes :: [Vector3d] -> [(Vector3d, Vector3d)] egdes ps = zip ps (tail ps ++ [head ps]) lrph :: Length -> Length -> Double -> Length lrph h0 h1 f = metres h where h0' = toMetres h0 h1' = toMetres h1 h = h0' + (h1' - h0') * f vector3d :: (NTransform a) => a -> Vector3d vector3d = vec . pos . toNVector angular :: Vector3d -> Length -> AngularPosition NVector angular v = nvectorHeight (nvector (vx v) (vy v) (vz v)) ll :: (NTransform a) => a -> LatLong ll = nvectorToLatLong . pos . toNVector