linear-geo-0.1.0.0: src/Linear/Geo/PlaneAngle.hs
{-|
Module : Linear.Geo.PlaneAngle
Copyright : Travis Whitaker 2023
License : MIT
Maintainer : pi.boy.travis@gmail.com
Stability : Provisional
Portability : Portable (Windows, POSIX)
Types for dealing with different representations of angles in the plane.
-}
{-# LANGUAGE BangPatterns
, DeriveAnyClass
, DeriveDataTypeable
, DeriveFunctor
, DeriveGeneric
, DerivingStrategies
, GeneralizedNewtypeDeriving
#-}
module Linear.Geo.PlaneAngle (
PlaneAngle(..)
, Radians(..)
, Degrees(..)
, DMS(..)
, dmsToDegrees
, degreesToDMS
, DM(..)
, dmToDegrees
, degreesToDM
) where
import Control.Applicative
import Control.DeepSeq (NFData)
import Control.Monad.Fix
import Control.Monad.Zip
import Data.Coerce
import Data.Data (Data)
import Data.Distributive
import Data.Fixed (divMod', mod')
import GHC.Generics (Generic)
-- | Plane angles.
class PlaneAngle ang where
-- | Put the angle into the canonical range 0 to 2*pi.
normalizeAngle :: (Floating a, Real a) => ang a -> ang a
-- | Convert the angle to radians.
toRadians :: (Floating a, Real a) => ang a -> Radians a
-- | Convert the angle from radians.
fromRadians :: (Floating a, Real a) => Radians a -> ang a
-- | A quantity representing a plane angle that satisfies the equation
-- @S = r * a@ where @r@ is the radius of a circle, @a@ is the measure of some
-- angle subtending the circle, and @S@ is the length of the subtended arc.
newtype Radians a = Radians a
deriving stock ( Eq
, Ord
, Show
, Generic
, Data
, Bounded
, Functor
)
deriving newtype ( Num
, Fractional
, Floating
, Real
, RealFrac
, RealFloat
, NFData
)
instance Applicative Radians where
pure = coerce
Radians f <*> Radians x = Radians (f x)
instance Monad Radians where
return = pure
Radians x >>= f = f x
instance MonadZip Radians where
mzipWith = liftA2
instance MonadFix Radians where
mfix f = Radians (let Radians x = f x in x)
instance Foldable Radians where
foldMap f (Radians x) = f x
instance Traversable Radians where
traverse f (Radians x) = Radians <$> f x
instance Distributive Radians where
distribute f = Radians (fmap coerce f)
instance PlaneAngle Radians where
normalizeAngle = coerce . (`mod'` (2 * pi))
toRadians = id
fromRadians = id
{-# INLINEABLE normalizeAngle #-}
{-# INLINEABLE toRadians #-}
{-# INLINEABLE fromRadians #-}
-- | One degree is @pi / 180@ radians.
newtype Degrees a = Degrees a
deriving stock ( Eq
, Ord
, Show
, Generic
, Data
, Bounded
, Functor
)
deriving newtype ( Num
, Fractional
, Floating
, Real
, RealFrac
, RealFloat
, NFData
)
instance Applicative Degrees where
pure = coerce
Degrees f <*> Degrees x = Degrees (f x)
instance Monad Degrees where
return = pure
Degrees x >>= f = f x
instance MonadZip Degrees where
mzipWith = liftA2
instance MonadFix Degrees where
mfix f = Degrees (let Degrees x = f x in x)
instance Foldable Degrees where
foldMap f (Degrees x) = f x
instance Traversable Degrees where
traverse f (Degrees x) = Degrees <$> f x
instance Distributive Degrees where
distribute f = Degrees (fmap coerce f)
instance PlaneAngle Degrees where
normalizeAngle = coerce . (`mod'` 360)
toRadians (Degrees d) = Radians (pi * (d / 180))
fromRadians (Radians r) = Degrees ((r / pi) * 180)
{-# INLINEABLE normalizeAngle #-}
{-# INLINEABLE toRadians #-}
{-# INLINEABLE fromRadians #-}
-- | An angle represented as degrees, minutes, and seconds of arc.
data DMS a = DMS {
dmsDeg :: !a
, dmsMin :: !a
, dmsSec :: !a
} deriving stock ( Eq
, Ord
, Show
, Generic
, Data
, Bounded
, Functor
)
deriving anyclass (NFData)
instance Applicative DMS where
pure x = DMS x x x
(DMS df mf sf) <*> (DMS d m s) = DMS (df d) (mf m) (sf s)
instance Monad DMS where
return = pure
(DMS d m s) >>= f = let DMS d' _ _ = f d
DMS _ m' _ = f m
DMS _ _ s' = f s
in DMS d' m' s'
instance MonadZip DMS where
mzipWith = liftA2
instance MonadFix DMS where
mfix f = DMS (let DMS d _ _ = f d in d)
(let DMS _ m _ = f m in m)
(let DMS _ _ s = f s in s)
instance Foldable DMS where
foldMap f (DMS d m s) = f d <> f m <> f s
instance Traversable DMS where
traverse f (DMS d m s) = DMS <$> f d <*> f m <*> f s
instance Distributive DMS where
distribute f = DMS (fmap (\(DMS d _ _) -> d) f)
(fmap (\(DMS _ m _) -> m) f)
(fmap (\(DMS _ _ s) -> s) f)
-- | Convert DMS to Degrees. This does not normalize the angle.
dmsToDegrees :: Fractional a => DMS a -> Degrees a
dmsToDegrees (DMS d m s) = Degrees (d + (m * (1 / 60)) + (s * (1 / 3600)))
{-# INLINEABLE dmsToDegrees #-}
-- | Convert degrees to DMS. This does not normalize the angle.
degreesToDMS :: (Real a, Fractional a) => Degrees a -> DMS a
degreesToDMS (Degrees d) =
let (dint, dleft) = divMod' d 1
(mint, mleft) = divMod' dleft (1 / 60)
sleft = mleft / (1 / 3600)
in DMS (fromIntegral dint) (fromIntegral mint) sleft
{-# INLINEABLE degreesToDMS #-}
instance PlaneAngle DMS where
normalizeAngle = degreesToDMS . normalizeAngle . dmsToDegrees
toRadians = toRadians . dmsToDegrees
fromRadians = degreesToDMS . fromRadians
{-# INLINEABLE normalizeAngle #-}
{-# INLINEABLE toRadians #-}
{-# INLINEABLE fromRadians #-}
-- | An angle represented as degrees and minutes of arc.
data DM a = DM {
dmDeg :: !a
, dmMin :: !a
} deriving stock ( Eq
, Ord
, Show
, Generic
, Data
, Bounded
, Functor
)
deriving anyclass (NFData)
instance Applicative DM where
pure x = DM x x
(DM df mf) <*> (DM d m) = DM (df d) (mf m)
instance Monad DM where
return = pure
(DM d m) >>= f = let DM d' _ = f d
DM _ m' = f m
in DM d' m'
instance MonadZip DM where
mzipWith = liftA2
instance MonadFix DM where
mfix f = DM (let DM d _ = f d in d)
(let DM _ m = f m in m)
instance Foldable DM where
foldMap f (DM d m) = f d <> f m
instance Traversable DM where
traverse f (DM d m) = DM <$> f d <*> f m
instance Distributive DM where
distribute f = DM (fmap (\(DM d _) -> d) f)
(fmap (\(DM _ m) -> m) f)
-- | Convert DM to degrees. This does not normalize the angle.
dmToDegrees :: Fractional a => DM a -> Degrees a
dmToDegrees (DM d m) = Degrees (d + (m * (1 / 60)))
{-# INLINEABLE dmToDegrees #-}
-- | Convert degrees to DM. This does not normalize the angle.
degreesToDM :: (Fractional a, Real a) => Degrees a -> DM a
degreesToDM (Degrees d) =
let (dint, m) = divMod' d 1
in DM (fromIntegral dint) (m / (1 / 60))
{-# INLINEABLE degreesToDM #-}
instance PlaneAngle DM where
normalizeAngle = degreesToDM . normalizeAngle . dmToDegrees
toRadians = toRadians . dmToDegrees
fromRadians = degreesToDM . fromRadians
{-# INLINEABLE normalizeAngle #-}
{-# INLINEABLE toRadians #-}
{-# INLINEABLE fromRadians #-}