packages feed

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 #-}