-- | -- Module: Data.Geo.Jord.Angle -- Copyright: (c) 2018 Cedric Liegeois -- License: BSD3 -- Maintainer: Cedric Liegeois -- Stability: experimental -- Portability: portable -- -- Types and functions for working with angles representing latitudes, longitude and bearings. -- module Data.Geo.Jord.Angle ( -- * The 'Angle' type Angle -- * Smart constructors , decimalDegrees , dms , dmsE , dmsF -- * Calculations , arcLength , central , isNegative , isWithin , negate' , normalise -- * Trigonometric functions , asin' , atan2' , cos' , sin' -- * Accessors , getDegrees , getMinutes , getSeconds , getMilliseconds , toDecimalDegrees -- * Read , angle , readAngle , readAngleE , readAngleF ) where import Control.Applicative import Control.Monad.Fail import Data.Fixed import Data.Geo.Jord.Length import Data.Geo.Jord.Parse import Data.Geo.Jord.Quantity import Data.Maybe import Prelude hiding (fail, length) import Text.ParserCombinators.ReadP import Text.Printf import Text.Read hiding (get, look, pfail) -- | An angle with a resolution of a milliseconds of a degree. -- When used as a latitude/longitude this roughly translate to a precision -- of 30 millimetres at the equator. newtype Angle = Angle { milliseconds :: Int } deriving (Eq) -- | See 'readAngle'. instance Read Angle where readsPrec _ = readP_to_S angle -- | Angle is shown degrees, minutes, seconds and milliseconds - e.g. 154°25'43.5". instance Show Angle where show a = s ++ show d ++ "°" ++ show (getMinutes a) ++ "'" ++ show (getSeconds a) ++ "." ++ printf "%03d" (getMilliseconds a) ++ "\"" where d = getDegrees a s = if d == 0 && milliseconds a < 0 then "-" else "" -- | Add/Subtract 'Angle'. instance Quantity Angle where add (Angle millis1) (Angle millis2) = Angle (millis1 + millis2) sub (Angle millis1) (Angle millis2) = Angle (millis1 - millis2) zero = Angle 0 -- | 'Angle' from given decimal degrees. Any 'Double' is accepted: it must be -- validated by the call site when used to represent a latitude or longitude. decimalDegrees :: Double -> Angle decimalDegrees dec = Angle (round (dec * 3600000.0)) -- | 'Angle' from the given given degrees, minutes, seconds and milliseconds. -- 'error's if given minutes, seconds and/or milliseconds are invalid. -- Degrees are not validated and can be any 'Int': they must be validated by the call site -- when used to represent a latitude or longitude. dms :: Int -> Int -> Int -> Int -> Angle dms degs mins secs millis = fromMaybe (error ("Invalid minutes=" ++ show mins ++ " or seconds=" ++ show secs ++ " or milliseconds=" ++ show millis)) (dmsF degs mins secs millis) -- | 'Angle' from the given given degrees, minutes, seconds and milliseconds. -- A 'Left' indicates that given minutes, seconds and/or milliseconds are invalid. -- Degrees are not validated and can be any 'Int': they must be validated by the call site -- when used to represent a latitude or longitude. dmsE :: Int -> Int -> Int -> Int -> Either String Angle dmsE degs mins secs millis | mins < 0 || mins > 59 = Left ("Invalid minutes: " ++ show mins) | secs < 0 || secs >= 60 = Left ("Invalid seconds: " ++ show secs) | millis < 0 || millis >= 1000 = Left ("Invalid milliseconds: " ++ show millis) | otherwise = Right (decimalDegrees ms) where ms = signed (fromIntegral (abs degs) + (fromIntegral mins / 60.0 :: Double) + (fromIntegral secs / 3600.0 :: Double) + (fromIntegral millis / 3600000.0 :: Double)) (signum degs) -- | 'Angle' from the given given degrees, minutes, seconds and milliseconds. -- 'fail's if given minutes, seconds and/or milliseconds are invalid. -- Degrees are not validated and can be any 'Int': they must be validated by the call site -- when used to represent a latitude or longitude. dmsF :: (MonadFail m) => Int -> Int -> Int -> Int -> m Angle dmsF degs mins secs millis = case e of Left err -> fail err Right a -> return a where e = dmsE degs mins secs millis -- | @arcLength a r@ computes the 'Length' of the arc that subtends the angle @a@ for radius @r@. arcLength :: Angle -> Length -> Length arcLength a r = metres (toMetres r * toRadians a) -- | @central l r@ computes the central 'Angle' from the arc length @l@ and radius @r@. central :: Length -> Length -> Angle central s r = fromRadians (toMetres s / toMetres r) -- | Returns the given 'Angle' negated. negate' :: Angle -> Angle negate' (Angle millis) = Angle (-millis) -- | @normalise a n@ normalises @a@ to [0, @n@]. normalise :: Angle -> Angle -> Angle normalise a n = decimalDegrees dec where dec = mod' (toDecimalDegrees a + toDecimalDegrees n) 360.0 -- | Is given 'Angle' < 0? isNegative :: Angle -> Bool isNegative (Angle millis) = millis < 0 -- | Is given 'Angle' within range [@low@..@high@] inclusive? isWithin :: Angle -> Angle -> Angle -> Bool isWithin (Angle millis) (Angle low) (Angle high) = millis >= low && millis <= high -- | @atan2' y x@ computes the 'Angle' (from the positive x-axis) of the vector from the origin to the point (x,y). atan2' :: Double -> Double -> Angle atan2' y x = fromRadians (atan2 y x) -- | @asin' a@ computes arc sine of @a@. asin' :: Double -> Angle asin' a = fromRadians (asin a) -- | @cos' a@ returns the cosinus of @a@. cos' :: Angle -> Double cos' a = cos (toRadians a) -- | @sin' a@ returns the sinus of @a@. sin' :: Angle -> Double sin' a = sin (toRadians a) -- | radians to degrees. fromRadians :: Double -> Angle fromRadians r = decimalDegrees (r / pi * 180.0) -- | degrees to radians. toRadians :: Angle -> Double toRadians a = toDecimalDegrees a * pi / 180.0 -- | Converts the given 'Angle' to decimal degrees. toDecimalDegrees :: Angle -> Double toDecimalDegrees (Angle millis) = fromIntegral millis / 3600000.0 -- | @getDegrees a@ returns the degree component of @a@. getDegrees :: Angle -> Int getDegrees a = signed (field a 3600000.0 360.0) (signum (milliseconds a)) -- | @getMinutes a@ returns the minute component of @a@. getMinutes :: Angle -> Int getMinutes a = field a 60000.0 60.0 -- | @getSeconds a@ returns the second component of @a@. getSeconds :: Angle -> Int getSeconds a = field a 1000.0 60.0 -- | @getMilliseconds a@ returns the milliseconds component of @a@. getMilliseconds :: Angle -> Int getMilliseconds (Angle millis) = mod (abs millis) 1000 field :: Angle -> Double -> Double -> Int field (Angle millis) divisor modulo = truncate (mod' (fromIntegral (abs millis) / divisor) modulo) :: Int signed :: (Num a, Num b, Ord b) => a -> b -> a signed n s | s < 0 = -n | otherwise = n -- | Parses and returns an 'Angle'. angle :: ReadP Angle angle = degsMinsSecs <|> decimal -- | Obtains a 'Angle' from the given string formatted as either: -- -- * d°m′s.ms″ - e.g. 55°36'21.3", where minutes, seconds and milliseconds are optional. -- -- * decimal° - e.g. 55.6050° or -133° -- -- Symbols: -- -- * degree: ° or d -- -- * minute: ', ′ or m -- -- * second: ", ″, '' or s -- -- This simply calls @read s :: Angle@ so 'error' should be handled at the call site. -- readAngle :: String -> Angle readAngle s = read s :: Angle -- | Same as 'readAngle' but returns an 'Either'. readAngleE :: String -> Either String Angle readAngleE s = case readMaybe s of Nothing -> Left ("couldn't read angle " ++ s) Just a -> Right a -- | Same as 'readAngle' but returns a 'MonadFail'. readAngleF :: (MonadFail m) => String -> m Angle readAngleF s = let p = readAngleE s in case p of Left e -> fail e Right l -> return l -- | Parses DMS.MS and returns an 'Angle'. degsMinsSecs :: ReadP Angle degsMinsSecs = do d' <- fmap fromIntegral integer degSymbol (m', s', ms') <- option (0, 0, 0) (minsSecs <|> minsOnly) dmsF d' m' s' ms' -- | Parses minutes, seconds with optionally milliseconds. minsSecs :: ReadP (Int, Int, Int) minsSecs = do m' <- natural minSymbol s' <- natural ms' <- option 0 (char '.' >> natural) secSymbol return (m', s', ms') -- | Parses minutes. minsOnly :: ReadP (Int, Int, Int) minsOnly = do m' <- natural minSymbol return (m', 0, 0) -- | Parses decimal degrees. decimal :: ReadP Angle decimal = do d <- double degSymbol return (decimalDegrees d) -- | skips degree symbol. degSymbol :: ReadP () degSymbol = do _ <- char '°' <|> char 'd' return () -- | skips minute symbol. minSymbol :: ReadP () minSymbol = do _ <- char '\'' <|> char '′' <|> char 'm' return () -- | skips second symbol. secSymbol :: ReadP () secSymbol = do _ <- string "\"" <|> string "''" <|> string "″" <|> string "s" return ()