packages feed

jord-2.0.0.0: test/Data/Geo/Jord/GreatCircleSpec.hs

module Data.Geo.Jord.GreatCircleSpec
    ( spec
    ) where

import Control.Exception.Base (evaluate)
import Control.Monad (join)
import Data.Maybe (fromJust)

import Test.Hspec

import qualified Data.Geo.Jord.Angle as Angle
import Data.Geo.Jord.Geodetic as Geodetic (HorizontalPosition)
import qualified Data.Geo.Jord.Geodetic as Geodetic
import qualified Data.Geo.Jord.GreatCircle as GreatCircle
import qualified Data.Geo.Jord.Length as Length
import qualified Data.Geo.Jord.Math3d as Math3d (cross)
import Data.Geo.Jord.Models (S84(..))
import Data.Geo.Jord.Places

spec :: Spec
spec = do
    describe "alongTrackDistance" $ do
        it "returns a positive length when position is ahead start of great arc" $ do
            let p = Geodetic.s84Pos 53.2611 (-0.7972)
            let g =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos 53.3206 (-1.7297))
                        (Geodetic.s84Pos 53.1887 0.1334)
            fmap (GreatCircle.alongTrackDistance p) g `shouldBe` Just (Length.kilometres 62.3315791)
        it "returns a negative length when position is ahead start of great arc" $ do
            let p = Geodetic.s84Pos 53.3206 (-1.7297)
            let g =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos 53.2611 (-0.7972))
                        (Geodetic.s84Pos 53.1887 0.1334)
            fmap (GreatCircle.alongTrackDistance p) g `shouldBe`
                Just (Length.kilometres (-62.329309979))
        it "returns 0 when position is start of great arc" $ do
            let p = Geodetic.s84Pos 53.2611 (-0.7972)
            let g = GreatCircle.minorArc p (Geodetic.s84Pos 53.1887 0.1334)
            fmap (GreatCircle.alongTrackDistance p) g `shouldBe` Just Length.zero
    describe "crossTrackDistance" $ do
        it "returns a negative length when position is left of great circle (bearing)" $ do
            let p = Geodetic.s84Pos 53.2611 (-0.7972)
            let gc =
                    GreatCircle.headingOn
                        (Geodetic.s84Pos 53.3206 (-1.7297))
                        (Angle.decimalDegrees 96.0)
            GreatCircle.crossTrackDistance p gc `shouldBe` Length.metres (-305.665267)
        it "returns a negative length when position is left of great circle" $ do
            let p = Geodetic.s84Pos 53.2611 (-0.7972)
            let gc =
                    GreatCircle.through
                        (Geodetic.s84Pos 53.3206 (-1.7297))
                        (Geodetic.s84Pos 53.1887 0.1334)
            fmap (GreatCircle.crossTrackDistance p) gc `shouldBe` Just (Length.metres (-307.549992))
        it "returns a positve length when position is right of great circle (bearing)" $ do
            let p = Geodetic.s84Pos 53.261111 (-1.797222)
            let gc =
                    GreatCircle.headingOn
                        (Geodetic.s84Pos 53.320556 (-1.729722))
                        (Angle.decimalDegrees 96.02166667)
            GreatCircle.crossTrackDistance p gc `shouldBe` Length.metres 7042.396068
        it "returns a positive length when position is left of great circle" $ do
            let p = Geodetic.antipode (Geodetic.s84Pos 53.2611 (-0.7972))
            let gc =
                    GreatCircle.through
                        (Geodetic.s84Pos 53.3206 (-1.7297))
                        (Geodetic.s84Pos 53.1887 0.1334)
            fmap (GreatCircle.crossTrackDistance p) gc `shouldBe` Just (Length.metres 307.549992)
        it "return zero when position is on the great circle" $ do
            let gc1 = Geodetic.s84Pos 53.3206 (-1.7297)
            let gc2 = Geodetic.s84Pos 53.1887 (0.1334)
            let gc = fromJust $ GreatCircle.through gc1 gc2
            let ps =
                    fmap
                        (\f -> GreatCircle.interpolated gc1 gc2 f)
                        (take 11 (iterate (\x -> x + 0.1 :: Double) 0.0))
            fmap (\p -> GreatCircle.crossTrackDistance p gc) ps `shouldBe`
                (replicate 11 Length.zero)
    describe "destination" $ do
        it "return the given position if distance is 0 meter" $ do
            let p0 = Geodetic.s84Pos 53.320556 (-1.729722)
            GreatCircle.destination p0 (Angle.decimalDegrees 96.0217) Length.zero `shouldBe` p0
        it "return the position along the great circle at distance and bearing" $ do
            let p0 = Geodetic.s84Pos 53.320556 (-1.729722)
            let p1 = Geodetic.s84Pos 53.18826954833333 0.13327449055555557
            GreatCircle.destination p0 (Angle.decimalDegrees 96.0217) (Length.metres 124800) `shouldBe`
                p1
    describe "distance" $ do
        it "returns 0 if both points are equal" $ do
            let p = Geodetic.s84Pos 50.066389 (-5.714722)
            GreatCircle.distance p p `shouldBe` Length.zero
        it "returns the distance between 2 points" $ do
            let p1 = Geodetic.s84Pos 50.066389 (-5.714722)
            let p2 = Geodetic.s84Pos 58.643889 (-3.07)
            GreatCircle.distance p1 p2 `shouldBe` Length.metres 968854.878007
        it "handles singularity at the pole" $
            GreatCircle.distance (Geodetic.northPole S84) (Geodetic.southPole S84) `shouldBe`
            Length.kilometres 20015.114352233
        it "handles the discontinuity at the Date Line" $ do
            let p1 = Geodetic.s84Pos 50.066389 (-179.999722)
            let p2 = Geodetic.s84Pos 50.066389 179.999722
            GreatCircle.distance p1 p2 `shouldBe` Length.metres 39.685092
    describe "greatCircle through position" $
        it "fails if both positions are equal" $
        GreatCircle.through (Geodetic.s84Pos 3 154) (Geodetic.s84Pos 3 154) `shouldBe` Nothing
    describe "finalBearing" $ do
        it "returns the Nothing if both positions are the same" $ do
            let p = Geodetic.s84Pos 50.066389 (-5.714722)
            GreatCircle.finalBearing p p `shouldBe` Nothing
            GreatCircle.finalBearing p (Geodetic.s84Pos 50.066389 (-5.714722)) `shouldBe` Nothing
        it "returns 0° if both positions have the same longitude (going north)" $ do
            let p1 = Geodetic.s84Pos 50.066389 (-5.714722)
            let p2 = Geodetic.s84Pos 58.643889 (-5.714722)
            GreatCircle.finalBearing p1 p2 `shouldBe` Just (Angle.zero)
        it "returns 180° if both positions have the same longitude (going south)" $ do
            let p1 = Geodetic.s84Pos 58.643889 (-5.714722)
            let p2 = Geodetic.s84Pos 50.066389 (-5.714722)
            GreatCircle.finalBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 180)
        it "returns 90° at the equator going east" $ do
            let p1 = Geodetic.s84Pos 0 0
            let p2 = Geodetic.s84Pos 0 1
            GreatCircle.finalBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 90)
        it "returns 270° at the equator going west" $ do
            let p1 = Geodetic.s84Pos 0 1
            let p2 = Geodetic.s84Pos 0 0
            GreatCircle.finalBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 270)
        it "returns the final bearing in compass angle" $ do
            let p1 = Geodetic.s84Pos 50.066389 (-5.714722)
            let p2 = Geodetic.s84Pos 58.643889 (-3.07)
            GreatCircle.finalBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 11.27520031611111)
        it "returns the final bearing in compass angle" $ do
            let p1 = Geodetic.s84Pos 58.643889 (-3.07)
            let p2 = Geodetic.s84Pos 50.066389 (-5.714722)
            GreatCircle.finalBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 189.1198173275)
        it "returns the final bearing in compass angle" $ do
            let p1 = Geodetic.s84Pos (-53.994722) (-25.9875)
            let p2 = Geodetic.s84Pos 54 154
            GreatCircle.finalBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 125.68508662305555)
    describe "initialBearing" $ do
        it "returns Nothing if both positions are the same" $ do
            let p = Geodetic.s84Pos 50.066389 (-179.999722)
            GreatCircle.initialBearing p p `shouldBe` Nothing
            GreatCircle.initialBearing p (Geodetic.s84Pos 50.066389 (-179.999722)) `shouldBe`
                Nothing
        it "returns 0° if both positions have the same longitude (going north)" $ do
            let p1 = Geodetic.s84Pos 50.066389 (-5.714722)
            let p2 = Geodetic.s84Pos 58.643889 (-5.714722)
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.zero)
        it "returns 180° if both positions have the same longitude (going south)" $ do
            let p1 = Geodetic.s84Pos 58.643889 (-5.714722)
            let p2 = Geodetic.s84Pos 50.066389 (-5.714722)
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 180)
        it "returns 90° at the equator going east" $ do
            let p1 = Geodetic.s84Pos 0 0
            let p2 = Geodetic.s84Pos 0 1
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 90)
        it "returns 270° at the equator going west" $ do
            let p1 = Geodetic.s84Pos 0 1
            let p2 = Geodetic.s84Pos 0 0
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 270)
        it "returns 0° at the prime meridian going north" $ do
            let p1 = Geodetic.s84Pos 50 0
            let p2 = Geodetic.s84Pos 58 0
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.zero)
        it "returns 180° at the prime meridian going south" $ do
            let p1 = Geodetic.s84Pos 58 0
            let p2 = Geodetic.s84Pos 50 0
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 180)
        it "returns 0° at the date line going north" $ do
            let p1 = Geodetic.s84Pos 50 180
            let p2 = Geodetic.s84Pos 58 180
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.zero)
        it "returns 180° at the date line going south" $ do
            let p1 = Geodetic.s84Pos 58 180
            let p2 = Geodetic.s84Pos 50 180
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 180)
        it "returns 0° going from the south pole to the north pole" $ do
            let p1 = Geodetic.southPole S84
            let p2 = Geodetic.northPole S84
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.zero)
        it "returns 0° going from the north pole to the south pole" $ do
            let p1 = Geodetic.northPole S84
            let p2 = Geodetic.southPole S84
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.zero)
        it "returns 180° going from the south pole to anywhere on the date line" $ do
            let p1 = Geodetic.southPole S84
            let p2 = Geodetic.s84Pos 50 180
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 180)
        it "returns the initial bearing in compass angle" $ do
            let p1 = Geodetic.s84Pos 50.066389 (-5.714722)
            let p2 = Geodetic.s84Pos 58.643889 (-3.07)
            GreatCircle.initialBearing p1 p2 `shouldBe` Just (Angle.decimalDegrees 9.1198173275)
        it "returns the initial bearing in compass angle" $ do
            let p1 = Geodetic.s84Pos 58.643889 (-3.07)
            let p2 = Geodetic.s84Pos 50.066389 (-5.714722)
            GreatCircle.initialBearing p1 p2 `shouldBe`
                Just (Angle.decimalDegrees 191.27520031611112)
    describe "interpolated" $ do
        let p1 = Geodetic.s84Pos 44 44
        let p2 = Geodetic.s84Pos 46 46
        it "fails if f < 0.0" $
            evaluate (GreatCircle.interpolated p1 p2 (-0.5)) `shouldThrow`
            errorCall "fraction must be in range [0..1], was -0.5"
        it "fails if f > 1.0" $
            evaluate (GreatCircle.interpolated p1 p2 1.1) `shouldThrow`
            errorCall "fraction must be in range [0..1], was 1.1"
        it "returns p0 if f == 0" $ GreatCircle.interpolated p1 p2 0.0 `shouldBe` p1
        it "returns p1 if f == 1" $ GreatCircle.interpolated p1 p2 1.0 `shouldBe` p2
        it "returns the interpolated position" $ do
            let p3 = Geodetic.s84Pos 53.479444 (-2.245278)
            let p4 = Geodetic.s84Pos 55.605833 13.035833
            GreatCircle.interpolated p3 p4 0.5 `shouldBe`
                Geodetic.s84Pos 54.78355703138889 5.194985318055555
    describe "enclosedBy" $ do
        let p1 = Geodetic.s84Pos 45 1
        let p2 = Geodetic.s84Pos 45 2
        let p3 = Geodetic.s84Pos 46 1
        let p4 = Geodetic.s84Pos 46 2
        let p5 = Geodetic.s84Pos 45.1 1.1
        it "return False if polygon is empty" $ GreatCircle.enclosedBy p1 [] `shouldBe` False
        it "return False if polygon does not define at least a triangle" $
            GreatCircle.enclosedBy p1 [p1, p2] `shouldBe` False
        it "returns True if position is inside polygon" $ do
            let polygon = [p1, p2, p4, p3]
            GreatCircle.enclosedBy p5 polygon `shouldBe` True
        it "returns False if position is inside polygon" $ do
            let polygon = [p1, p2, p4, p3]
            let p = Geodetic.antipode p5
            GreatCircle.enclosedBy p polygon `shouldBe` False
        it "returns False if position is a vertex of the polygon" $ do
            let convex = [p1, p2, p4, p3]
            fmap (\p -> GreatCircle.enclosedBy p convex) convex `shouldBe` replicate 4 False
            let concave = [malmo, ystad, kristianstad, helsingborg, lund]
            fmap (\p -> GreatCircle.enclosedBy p concave) concave `shouldBe` replicate 5 False
        it "handles closed polygons" $ do
            let polygon = [p1, p2, p4, p3, p1]
            GreatCircle.enclosedBy p5 polygon `shouldBe` True
        it "handles concave polygons" $ do
            let polygon = [malmo, ystad, kristianstad, helsingborg, lund]
            GreatCircle.enclosedBy hoor polygon `shouldBe` True
            GreatCircle.enclosedBy hassleholm polygon `shouldBe` False
        it "considers a point on an edge to be in one polygon only" $ do
            let i = GreatCircle.interpolated helsingborg lund 0.5
            let poly1 = [malmo, kristianstad, helsingborg, lund]
            let poly2 = [helsingborg, lund, copenhagen]
            GreatCircle.enclosedBy i poly1 `shouldBe` True
            GreatCircle.enclosedBy i poly2 `shouldBe` False
    describe "intersection" $ do
        it "returns nothing if both great arc are equals" $ do
            let a =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos 51.885 0.235)
                        (Geodetic.s84Pos 52.885 1.235)
            join (GreatCircle.intersection <$> a <*> a) `shouldBe` Nothing
        it "returns nothing if both great arc are equals (opposite orientation)" $ do
            let a1 =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos 51.885 0.235)
                        (Geodetic.s84Pos 52.885 1.235)
            let a2 =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos 52.885 1.235)
                        (Geodetic.s84Pos 51.885 0.235)
            join (GreatCircle.intersection <$> a1 <*> a2) `shouldBe` Nothing
        it "returns nothing if great circle intersection is outside either great arc" $ do
            let a1 = GreatCircle.minorArc (Geodetic.s84Pos 0 0) (Geodetic.s84Pos 0 10)
            let a2 = GreatCircle.minorArc (Geodetic.s84Pos (-5) 5) (Geodetic.s84Pos (-1) 5)
            join (GreatCircle.intersection <$> a1 <*> a2) `shouldBe` Nothing
        it "returns nothing if great circle intersection is outside both great arcs" $ do
            let a1 = GreatCircle.minorArc (Geodetic.s84Pos 0 (-10)) (Geodetic.s84Pos 0 (-1))
            let a2 = GreatCircle.minorArc (Geodetic.s84Pos (-5) 5) (Geodetic.s84Pos (-1) 5)
            join (GreatCircle.intersection <$> a1 <*> a2) `shouldBe` Nothing
        it "returns the point where the two great arcs intersect" $ do
            let a1 =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos 51.885 0.235)
                        (Geodetic.s84Pos 48.269 13.093)
            let a2 =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos 49.008 2.549)
                        (Geodetic.s84Pos 56.283 11.304)
            join (GreatCircle.intersection <$> a1 <*> a2) `shouldBe`
                Just (Geodetic.s84Pos 50.901738961111114 4.49418117)
        it "handles a minor arc across the equator" $ do
            let a1 = GreatCircle.minorArc (Geodetic.s84Pos 54 154) (Geodetic.s84Pos (-54) 154)
            let a2 = GreatCircle.minorArc (Geodetic.s84Pos 53 153) (Geodetic.s84Pos 53 155)
            join (GreatCircle.intersection <$> a1 <*> a2) `shouldBe`
                Just (Geodetic.s84Pos 53.00419442027778 154)
        it "returns the common start position between the 2 minor arcs" $ do
            let a1 =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos (-41.52) 141)
                        (Geodetic.s84Pos (-65.444811) 111.616598)
            let a2 =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos (-42.35) 141)
                        (Geodetic.s84Pos (-39.883333) 141)
            join (GreatCircle.intersection <$> a1 <*> a2) `shouldBe`
                Just (Geodetic.s84Pos (-41.52) 141.0)
        it "returns the common end position between the 2 minor arcs" $ do
            let a1 =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos (-65.444811) 111.616598)
                        (Geodetic.s84Pos (-41.52) 141)
            let a2 =
                    GreatCircle.minorArc
                        (Geodetic.s84Pos (-39.883333) 141)
                        (Geodetic.s84Pos (-41.52) 141)
            join (GreatCircle.intersection <$> a1 <*> a2) `shouldBe`
                Just (Geodetic.s84Pos (-41.52) 141.0)
        it "handles an intersection exactly on one of the minor arcs" $ do
            let a1 = GreatCircle.minorArc (Geodetic.s84Pos 0 (-10)) (Geodetic.s84Pos 0 10)
            let a2 = GreatCircle.minorArc (Geodetic.s84Pos (-10) 0) (Geodetic.s84Pos 10 0)
            join (GreatCircle.intersection <$> a1 <*> a2) `shouldBe` Just (Geodetic.s84Pos 0 0)
    describe "intersections" $ do
        it "returns nothing if both great circle are equals" $ do
            let gc =
                    GreatCircle.headingOn
                        (Geodetic.s84Pos 51.885 0.235)
                        (Angle.decimalDegrees 108.63)
            GreatCircle.intersections gc gc `shouldBe` Nothing
        it "returns nothing if both great circle are equals (opposite orientation)" $ do
            let gc1 =
                    GreatCircle.through
                        (Geodetic.s84Pos 51.885 0.235)
                        (Geodetic.s84Pos 52.885 1.235)
            let gc2 =
                    GreatCircle.through
                        (Geodetic.s84Pos 52.885 1.235)
                        (Geodetic.s84Pos 51.885 0.235)
            join (GreatCircle.intersections <$> gc1 <*> gc2) `shouldBe` Nothing
        it "returns the two positions where the two great circles intersects" $ do
            let gc1 =
                    GreatCircle.headingOn
                        (Geodetic.s84Pos 51.885 0.235)
                        (Angle.decimalDegrees 108.63)
            let gc2 =
                    GreatCircle.headingOn
                        (Geodetic.s84Pos 49.008 2.549)
                        (Angle.decimalDegrees 32.72)
            let (i1, i2) = fromJust (GreatCircle.intersections gc1 gc2)
            i1 `shouldBe` Geodetic.s84Pos 50.90172260888889 4.494278278888889
            i2 `shouldBe` Geodetic.antipode i1
    describe "mean" $ do
        it "returns Nothing if no position is given" $
            (GreatCircle.mean [] :: (Maybe (HorizontalPosition S84))) `shouldBe` Nothing
        it "returns the unique given position" $ do
            let p = Geodetic.s84Pos 50.066389 (-5.714722)
            GreatCircle.mean [p] `shouldBe` Just p
        it "returns the geographical mean" $ do
            let p1 = Geodetic.s84Pos 50.066389 (-5.714722)
            let p2 = Geodetic.s84Pos 58.643889 (-3.07)
            let e = Geodetic.s84Pos 54.3622869375 (-4.530672405)
            GreatCircle.mean [p1, p2] `shouldBe` Just e
        it "returns Nothing if list contains antipodal positions" $ do
            let points =
                    [ Geodetic.s84Pos 45 1
                    , Geodetic.s84Pos 45 2
                    , Geodetic.s84Pos 46 2
                    , Geodetic.s84Pos 46 1
                    , Geodetic.antipode (Geodetic.s84Pos 45 2)
                    ]
            GreatCircle.mean points `shouldBe` Nothing
    describe "projection" $ do
        it "returns Nothing if position is the normal to minor arc (1/2)" $ do
            let s = Geodetic.s84Pos 3 (-10)
            let e = (Geodetic.s84Pos 4 10)
            let ma = fromJust (GreatCircle.minorArc s e)
            let p =
                    Geodetic.nvectorPos'
                        (Math3d.cross (Geodetic.nvector s) (Geodetic.nvector e))
                        S84
            GreatCircle.projection p ma `shouldBe` Nothing
        it "returns Nothing if position is the normal to minor arc (2/2)" $ do
            let ma =
                    fromJust (GreatCircle.minorArc (Geodetic.s84Pos 0 (-10)) (Geodetic.s84Pos 0 10))
            let p = Geodetic.northPole S84
            GreatCircle.projection p ma `shouldBe` Nothing
        it "returns Nothing if position is the antipode of the normal to minor arc (1/2)" $ do
            let s = Geodetic.s84Pos 3 (-10)
            let e = (Geodetic.s84Pos 4 10)
            let ma = fromJust (GreatCircle.minorArc s e)
            let p =
                    Geodetic.antipode
                        (Geodetic.nvectorPos'
                             (Math3d.cross (Geodetic.nvector s) (Geodetic.nvector e))
                             S84)
            GreatCircle.projection p ma `shouldBe` Nothing
        it "returns Nothing if position is the antipode of the normal to minor arc (2/2)" $ do
            let ma =
                    fromJust (GreatCircle.minorArc (Geodetic.s84Pos 0 (-10)) (Geodetic.s84Pos 0 10))
            let p = Geodetic.southPole S84
            GreatCircle.projection p ma `shouldBe` Nothing
        it "returns Nothing if projection is outside minor arc" $ do
            let ma = fromJust (GreatCircle.minorArc (Geodetic.s84Pos 54 15) (Geodetic.s84Pos 54 20))
            let p = Geodetic.s84Pos 54 10
            GreatCircle.projection p ma `shouldBe` Nothing
        it "returns the projection if within the minor arc" $ do
            let s = Geodetic.s84Pos 53.3206 (-1.7297)
            let e = Geodetic.s84Pos 53.1887 0.1334
            let ma = fromJust (GreatCircle.minorArc s e)
            let p = Geodetic.s84Pos 53.2611 (-0.7972)
            let proj = GreatCircle.projection p ma
            proj `shouldBe` Just (Geodetic.s84Pos 53.25835330666666 (-0.7977433863888889))
            -- absolute cross track distance from p to great circle should be distance between projection and p
            let stx = (GreatCircle.crossTrackDistance p (fromJust (GreatCircle.through s e)))
            abs (Length.toMetres stx) `shouldBe`
                Length.toMetres (GreatCircle.distance (fromJust proj) p)
        it "handles p exactly being the start of the minor arc (1/2)" $ do
            let s = Geodetic.s84Pos 54 15
            let ma = fromJust (GreatCircle.minorArc s (Geodetic.s84Pos 54 20))
            GreatCircle.projection s ma `shouldBe` (Just s)
        it "handles p exactly being the start of the minor arc (2/2)" $ do
            let s = Geodetic.s84Pos 13.733333587646484 100.5
            let ma = fromJust (GreatCircle.minorArc s (Geodetic.s84Pos 12.0 100.58499908447266))
            GreatCircle.projection s ma `shouldBe` (Just s)
        it "handles p exactly being the end of the minor arc (1/2)" $ do
            let e = Geodetic.s84Pos 54 20
            let ma = fromJust (GreatCircle.minorArc (Geodetic.s84Pos 54 15) e)
            GreatCircle.projection e ma `shouldBe` (Just e)
        it "handles p exactly being the end of the minor arc (2/2)" $ do
            let e = Geodetic.s84Pos 12.0 100.58499908447266
            let ma = fromJust (GreatCircle.minorArc (Geodetic.s84Pos 13.733333587646484 100) e)
            GreatCircle.projection e ma `shouldBe` (Just e)
    describe "side" $ do
        it "retuns None if p1 is antipode of p2" $ do
            GreatCircle.side ystad helsingborg (Geodetic.antipode helsingborg) `shouldBe`
                GreatCircle.None
        it "returns None if (p1, p2) are equal" $ do
            GreatCircle.side ystad helsingborg helsingborg `shouldBe` GreatCircle.None
        it "returns None if p0 is on the great circle" $ do
            GreatCircle.side (Geodetic.s84Pos 0 0) (Geodetic.s84Pos 45 0) (Geodetic.northPole S84) `shouldBe`
                GreatCircle.None
            GreatCircle.side helsingborg helsingborg kristianstad `shouldBe` GreatCircle.None
            GreatCircle.side kristianstad helsingborg kristianstad `shouldBe` GreatCircle.None
        it "return LeftOf or RightOf" $ do
            GreatCircle.side ystad helsingborg kristianstad `shouldBe` GreatCircle.RightOf
            GreatCircle.side ystad kristianstad helsingborg `shouldBe` GreatCircle.LeftOf
            GreatCircle.side malmo lund helsingborg `shouldBe` GreatCircle.LeftOf
            GreatCircle.side malmo helsingborg lund `shouldBe` GreatCircle.RightOf
    describe "turn" $ do
        it "returns a negative angle when turning left" $ do
            GreatCircle.turn (Geodetic.s84Pos 0 0) (Geodetic.s84Pos 45 0) (Geodetic.s84Pos 60 (-10)) `shouldBe`
                Angle.decimalDegrees 18.192705871944444
        it "returns a positive angle when turning right" $ do
            GreatCircle.turn (Geodetic.s84Pos 0 0) (Geodetic.s84Pos 45 0) (Geodetic.s84Pos 60 10) `shouldBe`
                Angle.decimalDegrees (-18.192705871944444)
        it "returns 0 when a, b & c are aligned" $ do
            GreatCircle.turn (Geodetic.s84Pos 0 0) (Geodetic.s84Pos 45 0) (Geodetic.northPole S84) `shouldBe`
                Angle.zero
        it "returns 0 is any 2 of the 3 positions are equal" $ do
            let a = Geodetic.s84Pos 45 63
            let b = Geodetic.s84Pos (-54) (-89)
            GreatCircle.turn a a a `shouldBe` Angle.zero
            GreatCircle.turn a a b `shouldBe` Angle.zero
            GreatCircle.turn a b b `shouldBe` Angle.zero
            GreatCircle.turn b b b `shouldBe` Angle.zero
            GreatCircle.turn b b a `shouldBe` Angle.zero
            GreatCircle.turn b a a `shouldBe` Angle.zero