{-# LANGUAGE MultiParamTypeClasses #-}
module Geodetics.UK (
   OSGB36 (..),
   UkNationalGrid (..),
   ukGrid,
   fromUkGridReference,
   toUkGridReference
) where
import Control.Applicative
import Control.Monad
import Data.Array
import Data.Char
import Data.Monoid
import Geodetics.Geodetic
import Geodetics.Grid
import Geodetics.Ellipsoids
import Geodetics.TransverseMercator
import Numeric.Units.Dimensional.Prelude
import qualified Prelude as P
data OSGB36 = OSGB36 deriving (Eq, Show)
instance Ellipsoid OSGB36 where
   majorRadius _ = 6377563.396 *~ meter
   flatR _ = 299.3249646 *~ one
   helmert _ = Helmert {
      cX = 446.448 *~ meter, cY = (-125.157) *~ meter, cZ = 542.06 *~ meter,
      helmertScale = (-20.4894) *~ one,
      rX = 0.1502 *~ arcsecond, rY = 0.247 *~ arcsecond, rZ = 0.8421 *~ arcsecond }
data UkNationalGrid = UkNationalGrid deriving (Eq, Show)
instance GridClass UkNationalGrid OSGB36 where
   toGrid _ = unsafeGridCoerce UkNationalGrid . toGrid ukGrid
   fromGrid = fromGrid . unsafeGridCoerce ukGrid
   gridEllipsoid _ = OSGB36
ukTrueOrigin :: Geodetic OSGB36
ukTrueOrigin = Geodetic {
   latitude = 49 *~ degree,
   longitude = (-2) *~ degree,
   geoAlt = 0 *~ meter,
   ellipsoid = OSGB36
}
ukFalseOrigin :: GridOffset 
ukFalseOrigin = GridOffset ((-400) *~ kilo meter) (100 *~ kilo meter) (0 *~ meter)
ukGrid :: GridTM OSGB36
ukGrid = mkGridTM ukTrueOrigin ukFalseOrigin 
   ((10 *~ one) ** (0.9998268 *~ one - _1))
ukGridSquare :: Length Double
ukGridSquare = 100 *~ kilo meter
fromUkGridReference :: String -> Maybe (GridPoint UkNationalGrid, GridOffset)
fromUkGridReference str = if length str < 2 then Nothing else do
      let 
         c1:c2:ds = str
         n = length ds
      guard $ even n
      let (dsE, dsN) = splitAt (n `div` 2) ds
      (east, sq) <- fromGridDigits ukGridSquare dsE
      (north, _) <- fromGridDigits ukGridSquare dsN
      base <- fromUkGridLetters c1 c2
      let half = sq / (2 *~ one)
      return (applyOffset (GridOffset east north (0 *~ meter)) base,
              GridOffset half half (0 *~ meter))
      
fromUkGridLetters :: Char -> Char -> Maybe (GridPoint UkNationalGrid)
fromUkGridLetters c1 c2 = applyOffset <$> (mappend <$> g1 <*> g2) <*> letterOrigin
   where
      letterOrigin = Just $ GridPoint ((-1000) *~ kilo meter) ((-500) *~ kilo meter) m0 UkNationalGrid
      gridIndex c = 
         if inRange ('A', 'H') c then Just $ ord c P.- ord 'A'  
         else if inRange ('J', 'Z') c then Just $ ord c P.- ord 'B'
         else Nothing
      gridSquare c = do 
         g <- gridIndex c
         let (y,x) = g `divMod` 5 
         return (fromIntegral x *~ one, _4 - fromIntegral y *~ one)
      g1 = do
         (x,y) <- gridSquare c1
         return $ GridOffset (x * (500 *~ kilo meter)) (y * (500 *~ kilo meter)) m0
      g2 = do
         (x,y) <- gridSquare c2
         return $ GridOffset (x * (100 *~ kilo meter)) (y * (100 *~ kilo meter)) m0
      m0 = 0 *~ meter
toUkGridReference :: Int -> GridPoint UkNationalGrid -> Maybe String
toUkGridReference n p
   | n < 0         = error "toUkGridReference: precision argument must not be negative."
   | otherwise     = do
      (gx, strEast) <- toGridDigits ukGridSquare n $ eastings p + 1000 *~ kilo meter
      (gy, strNorth) <- toGridDigits ukGridSquare n $ northings p + 500 *~ kilo meter
      let (gx1, gx2) = (fromIntegral gx) `divMod` 5
          (gy1, gy2) = (fromIntegral gy) `divMod` 5
      guard (gx1 < 5 && gy1 < 5)
      let c1 = gridSquare gx1 gy1
          c2 = gridSquare gx2 gy2
      return $ c1 : c2 : strEast ++ strNorth
   where
      gridSquare x y = letters ! (4 P.- y, x)
      letters :: Array (Int, Int) Char
      letters = listArray ((0,0),(4,4)) $ ['A'..'H'] ++ ['J'..'Z']