geodetics-2.0.0: src/Geodetics/PolarStereographic.hs
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
module Geodetics.PolarStereographic (
Pole (..),
PolarStereographic (trueOrigin, falseOrigin, polarEllipsoid, gridScale),
mkGridPolarStereographic,
UpsGrid,
upsNorth,
upsSouth,
fromUpsGridReference,
parseUpsGridReference,
toUpsGridReference
) where
import Control.Monad
import Data.Char
import Geodetics.Ellipsoids
import Geodetics.Geodetic
import Geodetics.Grid
import Text.Parsec
import Text.Parsec.Error
import Text.Read (readMaybe)
import Text.Printf
-- | Polar stereographic grids are defined for true origins at the north and south poles.
data Pole = NorthPole | SouthPole deriving (Show, Ord, Eq, Enum, Bounded)
{- | Polar Stereographic Grids
Formulae are taken from
/The Universal Grids: Univerersal Transverse Mercator (UTM) and Universal Polar Stereographic (UPS)/
DMA Technical Manual 8358.2, Defense Mapping Agency, Fairfax, VA. https://apps.dtic.mil/sti/tr/pdf/ADA266497.pdf
When working with polar grids all directions are relative to the grid rather than the actual pole.
So in the Arctic \"North\" on the Universal Polar Stereographic grid means towards the Bering Sea
rather than towards the North Pole.
-}
data PolarStereographic e = PolarStereographic {
trueOrigin :: Pole,
falseOrigin :: GridOffset,
-- ^ The negation of the grid position of the true origin. Used to avoid negative coordinates over the area
-- of interest. The altitude gives a vertical offset from the ellipsoid.
polarEllipsoid :: e,
-- ^ The ellipsoid for the projection. Arguments passed to `toGrid` *must* use this ellipsoid.
-- The type system cannot verify this for `LocalEllipsoid`.
gridScale :: Double,
-- ^ The scaling factor applied at the pole. This balances the distortion between the center
-- and edges of the projection.
-- Remaining elements are memoised parameters computed from the ellipsoid.
gridA, gridB, gridC, gridD, gridC0 :: !Double
} deriving (Show)
instance (Eq e) => Eq (PolarStereographic e) where
g1 == g2 =
trueOrigin g1 == trueOrigin g2 &&
falseOrigin g1 == falseOrigin g2 &&
polarEllipsoid g1 == polarEllipsoid g2 &&
gridScale g1 == gridScale g2
instance (Ellipsoid e) => GridClass (PolarStereographic e) e where
fromGrid p = Geodetic lat long (altGP p) (polarEllipsoid gb)
where
gridZero = GridPoint 0 0 0 gb
gb = gridBasis p
p' = gridZero `gridOffset` (falseOrigin gb `applyOffset` p)
radius = offsetDistance p'
isoColat = 2 * atan (radius / (gridScale gb * gridC0 gb))
isoLat = pi/2 - isoColat
lat1 = isoLat +
gridA gb * sin (2*isoLat) +
gridB gb * sin (4*isoLat) +
gridC gb * sin (6*isoLat) +
gridD gb * sin (8*isoLat)
lat = case trueOrigin gb of
NorthPole -> lat1
SouthPole -> negate lat1
long = case trueOrigin gb of
NorthPole -> offsetBearing p' { deltaNorth = negate $ deltaNorth p'}
SouthPole -> offsetBearing p'
toGrid r geo = offsetNegate (falseOrigin r) `applyOffset` GridPoint east north 0 r
where
absLat = abs $ latitude geo
e = sqrt (eccentricity2 $ polarEllipsoid r)
eSinLat = e * sin absLat
tz2 = ((1 + eSinLat)/(1-eSinLat))**(e/2) * tan (pi/4 - absLat / 2)
radius = gridScale r * gridC0 r * tz2
north = case trueOrigin r of
NorthPole -> negate $ radius * cos (longitude geo)
SouthPole -> radius * cos (longitude geo)
east = radius * sin (longitude geo)
gridEllipsoid = polarEllipsoid
mkGridPolarStereographic :: (Ellipsoid e) =>
Pole -- ^ True origin at north or south pole.
-> e -- ^ The ellipsoid used for the projection.
-> GridOffset -- ^ Vector from true origin to the false origin.
-> Double -- ^ Scale factor.
-> PolarStereographic e
mkGridPolarStereographic pole ellip offset scale =
PolarStereographic {
trueOrigin = pole,
falseOrigin = offset,
polarEllipsoid = ellip,
gridScale = scale,
gridA = e2/2 + (5/24)*e4 + e6/12 + (13/360)*e8,
gridB = (7/48)*e4 + (29/240)*e6 + (811/11520)*e8,
gridC = (7/120)*e6 + (81/1120)*e8,
gridD = (4279/161280)*e8,
gridC0 = (2 * majorRadius ellip / sqrt (1 - e2)) * ((1-e1)/(1+e1))**(e1/2)
}
where
e1 = sqrt $ eccentricity2 ellip
e2 = eccentricity2 ellip
e4 = e2^_2
e6 = e2^_3
e8 = e2^_4
-- | The Universal Polar Stereographic (UPS) grids for north and south poles.
type UpsGrid = PolarStereographic WGS84
-- | UPS grid for the North Pole.
upsNorth :: UpsGrid
upsNorth = mkGridPolarStereographic
NorthPole
WGS84
(GridOffset { deltaEast = -(2000 * kilometer), deltaNorth = -(2000 * kilometer), deltaAltitude = 0 })
0.994 -- Scale factor
-- | UPS grid for the South Pole.
upsSouth :: UpsGrid
upsSouth = mkGridPolarStereographic
SouthPole
WGS84
(GridOffset { deltaEast = -(2000 * kilometer), deltaNorth = -(2000 * kilometer), deltaAltitude = 0 })
0.994 -- Scale factor
-- | Convert a grid reference into a UPS grid location.
--
-- There doesn't appear to be any conventional representation for polar grid references,
-- so this is an attempt to cover as many bases as possible. It takes an Easting followed
-- by a Northing with spaces in between. Both can have optional units of m or km,
-- and be optionally followed by an \"N\" or \"E\" as appropriate.
--
-- The choice of pole is provided in an extra argument rather than within the string because humans
-- will normally assume this from the context and so not provide it.
--
-- If the string cannot be parsed then one or more error messages are returned.
fromUpsGridReference :: Pole -> String -> Either [String] (GridPoint UpsGrid)
fromUpsGridReference pole str = case parse (parseUpsGridReference pole) "" str of
Left err -> Left $ lines $ showErrorMessages
"or" "unknown parse error" "expecting" "unexpected" "end of input"
(errorMessages err)
Right r -> Right r
parseUpsGridReference :: Stream s m Char => Pole -> ParsecT s u m (GridPoint UpsGrid)
parseUpsGridReference pole = do
spaces1
(eastings1, eastUnit) <- readDistance
spaces
optional (oneOf "Ee" <?> "E")
spaces1
(northings1, northUnit) <- readDistance
unless (eastUnit == northUnit) $ fail "Northings and Eastings units don't match."
spaces1
optional (oneOf "Nn" <?> "N")
spaces1
eof
return $ GridPoint eastings1 northings1 0 $
case pole of
NorthPole -> upsNorth
SouthPole -> upsSouth
where
readDistance = do -- Returns (distance in meters, unit from input)
digits <- many1 (digit <|> char '.' <?> "number")
spaces1
when (length digits > 20) $ fail "Too many digits."
(multiplier, unit) <- do
unit <- optionMaybe (string1' "m" <|> string1' "km" <?> "units (m or km)")
case unit of
Just "km" -> pure (1000, Just GridKilometers)
Just _ -> pure (1, Just GridMeters)
Nothing -> pure (1, Nothing)
case readMaybe digits of
Just d -> return (d * multiplier, unit)
Nothing -> fail $ "Cannot read number: " <> digits
string1' target = try $ do -- Case-insensitive version of string'
cs <- count (length target) anyToken
if map toLower target == map toLower cs then return cs else unexpected cs
spaces1 = void $ many (char ' ' <?> "space") -- Other white space not permitted.
toUpsGridReference ::
Maybe GridUnit -- ^ Include explicit units in the output. @Nothing@ means meters without units.
-> Bool -- ^ Include \"E\" and \"N\" in the output.
-> Int -- ^ Digits of resolution. 0 = 1m resolution, 1 = 10m, 2 = 100m etc. (-2) = 1cm.
-> GridPoint UpsGrid
-> String
toUpsGridReference unit letters res gp =
dist (eastings gp) <> (if letters then "E " else " ") <>
dist (northings gp) <> (if letters then "N" else "")
where
res1 :: Double
res1 = 10 ** fromIntegral res -- Resolution in meters.
floorRes :: Double -> Double
floorRes d = res1 * fromIntegral (floor (d/res1) :: Integer)
dist d = case unit of
Nothing -> printf "%.*f" (-res) $ floorRes d
Just GridMeters -> printf "%.*fm" (-res) $ floorRes d
Just GridKilometers -> printf "%.*fkm" (3-res) $ floorRes d / 1000