packages feed

geodetics-1.0.0: src/Geodetics/Grid.hs

{-# LANGUAGE FunctionalDependencies #-}

module Geodetics.Grid (
   -- * Grid types
   GridClass (..),
   GridPoint (..),
   GridOffset (..),
   -- * Grid operations
   polarOffset,
   offsetScale,
   offsetNegate,
   applyOffset,
   offsetDistance,
   offsetDistanceSq,
   offsetBearing,
   gridOffset,
   -- * Unsafe conversion
   unsafeGridCoerce,
   -- * Utility functions for grid references
   fromGridDigits,
   toGridDigits
) where

import Data.Char
import Geodetics.Altitude
import Geodetics.Ellipsoids
import Geodetics.Geodetic

-- | A Grid is a two-dimensional projection of the ellipsoid onto a plane. Any given type of grid can
-- usually be instantiated with parameters such as a tangential point or line, and these parameters
-- will include the terrestrial reference frame ("Ellipsoid" in this library) used as a foundation.
-- Hence conversion from a geodetic to a grid point requires the \"basis\" for the grid in question,
-- and grid points carry that basis with them because without it there is no defined relationship
-- between the grid points and terrestrial positions.
class GridClass r e | r->e where
   fromGrid :: GridPoint r -> Geodetic e
   toGrid :: r -> Geodetic e -> GridPoint r
   gridEllipsoid :: r -> e


-- | A point on the specified grid.
data GridPoint r = GridPoint {
   eastings, northings, altGP :: Double,
   gridBasis :: r
} deriving (Show)


instance Eq (GridPoint r) where
   p1 == p2  =
      eastings p1 == eastings p2 &&
      northings p1 == northings p2 &&
      altGP p1 == altGP p2

instance HasAltitude (GridPoint g) where
   altitude = altGP
   setAltitude h gp = gp{altGP = h}


-- | A vector relative to a point on a grid. All distances are in meters.
-- Operations that use offsets will only give
-- meaningful results if all the points come from the same grid.
--
-- The monoid instance is the sum of offsets.
data GridOffset = GridOffset {
   deltaEast, deltaNorth, deltaAltitude :: Double
} deriving (Eq, Show)

instance Semigroup GridOffset where
  g1 <> g2 = GridOffset (deltaEast g1 + deltaEast g2)
                        (deltaNorth g1 + deltaNorth g2)
                        (deltaAltitude g1 + deltaAltitude g2)

instance Monoid GridOffset where
   mempty = GridOffset 0 0 0
   mappend = (<>)

-- | An offset defined by a distance (m) and a bearing (radians) to the right of North.
--
-- There is no elevation parameter because we are using a plane to approximate an ellipsoid,
-- so elevation would not provide a useful result.  If you want to work with elevations
-- then "rayPath" will give meaningful results.
polarOffset :: Double -> Double -> GridOffset
polarOffset r d = GridOffset (r * sin d) (r * cos d) 0


-- | Scale an offset by a scalar.
offsetScale :: Double -> GridOffset -> GridOffset
offsetScale s off = GridOffset (deltaEast off * s)
                               (deltaNorth off * s)
                               (deltaAltitude off * s)

-- | Invert an offset.
offsetNegate :: GridOffset -> GridOffset
offsetNegate off = GridOffset (negate $ deltaEast off)
                              (negate $ deltaNorth off)
                              (negate $ deltaAltitude off)


-- Add an offset on to a point to get another point.
applyOffset :: GridOffset -> GridPoint g -> GridPoint g
applyOffset off p = GridPoint (eastings p + deltaEast off)
                           (northings p + deltaNorth off)
                           (altitude p + deltaAltitude off)
                           (gridBasis p)


-- | The distance represented by an offset.
offsetDistance :: GridOffset -> Double
offsetDistance = sqrt . offsetDistanceSq


-- | The square of the distance represented by an offset.
offsetDistanceSq :: GridOffset -> Double
offsetDistanceSq off =
   deltaEast off ^ _2 + deltaNorth off ^ _2 + deltaAltitude off ^ _2


-- | The direction represented by an offset, as bearing to the right of North.
offsetBearing :: GridOffset -> Double
offsetBearing off = atan2 (deltaEast off) (deltaNorth off)


-- | The offset required to move from p1 to p2.
gridOffset :: GridPoint g -> GridPoint g -> GridOffset
gridOffset p1 p2 = GridOffset (eastings p2 - eastings p1)
                              (northings p2 - northings p1)
                              (altitude p2 - altitude p1)


-- | Coerce a grid point of one type into a grid point of a different type,
-- but with the same easting, northing and altitude. This is unsafe because it
-- will produce a different position unless the two grids are actually equal.
--
-- It should be used only to convert between distinguished grids (e.g. "UkNationalGrid") and
-- their equivalent numerical definitions.
unsafeGridCoerce :: b -> GridPoint a -> GridPoint b
unsafeGridCoerce base p = GridPoint (eastings p) (northings p) (altitude p) base



-- | Convert a list of digits to a distance. The first argument is the size of the
-- grid square within which these digits specify a position. The first digit is
-- in units of one tenth of the grid square, the second one hundredth, and so on.
-- The first result is the lower limit of the result, and the second is the size
-- of the specified offset.
-- So for instance @fromGridDigits (100 * kilometer) "237"@ will return
--
-- > Just (23700 meters, 100 meters)
--
-- If there are any non-digits in the string then the function returns @Nothing@.
fromGridDigits :: Double -> String -> Maybe (Double, Double)
fromGridDigits sq ds = if all isDigit ds then Just (d, p) else Nothing
   where
      n :: Integer
      n = fromIntegral $ length ds
      d = sum $ zipWith (*)
         (map (fromIntegral . digitToInt) ds)
         (drop 1 $ iterate (/ 10) sq)
      p = sq / fromIntegral ((10 :: Int) ^ n)

-- | Convert a distance into a digit string suitable for printing as part
-- of a grid reference. The result is the nearest position to the specified
-- number of digits, expressed as an integer count of squares and a string of digits.
-- If any arguments are invalid then @Nothing@ is returned.
toGridDigits ::
   Double           -- ^ Size of enclosing grid square. Must be at least 1000m.
   -> Int           -- ^ Number of digits to return. Must be positive.
   -> Double        -- ^ Offset to convert into grid (m).
   -> Maybe (Integer, String)
toGridDigits sq n d =
   if sq < 1000 || n < 0 || d < 0
   then Nothing
   else
      Just (sqs, pad)
   where
      p :: Integer
      p = 10 ^ n
      unit :: Double
      unit = sq / fromIntegral p
      u = round (d / unit)
      (sqs, d1) = u `divMod` p
      s = show d1
      pad = if n == 0 then "" else replicate (n - length s) '0' ++ s