-- | Module containing several useful projections for generating maps.
module GIS.Math.Projections where

import Control.Lens
import Control.Lens.Tuple
import GIS.Types
import GIS.Math.Utils
import GIS.Graphics.Types

-- | For use as a reference point in certain projections.
washingtonDC :: Point
washingtonDC = over _2 radians $ over _1 radians (38.9072, -77.0369)

-- | For use as a reference point with the Craig retroazimuthal projection
mecca :: Point
mecca = over _2 radians $ over _1 radians (21.3891, 39.8579)

-- | Littow retroazimuthal + conformal projection
littow :: Projection
littow (long, lat) = ((sin(long - referenceLong)/(cos lat)), (cos (long - referenceLong) * (tan lat)))
    where referenceLong = radians 0

-- | Craig retroazimuthal projection
-- (works on a subset of the world)
craig :: Point -> Projection
craig referencePoint (long, lat) = (long - referenceLong, y)
    where (referenceLong, referenceLat) = referencePoint
          y = if long - referenceLong == 0 then expr else (long - referenceLong)/(sin(long - referenceLong)) * expr
          expr = ((sin lat) * (cos (long - referenceLong)) - (tan referenceLat) * (cos lat)) 

-- | Winkel Tripel projection (standard for the National Geographic Society since 
winkel3 :: Projection
winkel3 (long, lat) = ((lambda * (cos phi1) + (2 * cos lat * sin (lambda/2)/(sinc alpha)))/2, (lat + (sin lat)/(sinc alpha))/2)
    where lambda = long - lambda0
          phi1 = acos $ 2 / pi
          alpha = acos $ cos lat * (cos (lambda/2))
          lambda0 = radians (-77.0369)

-- | Mercator projection.
mercator :: Projection
mercator (long, lat) = (long - meridian, asinh(tan(lat)))
    where meridian = radians (-98.5795)

-- | Bonne projection with standard parallel at 45 N and central meridian
-- centered at Washington DC
bonne :: Projection
bonne (long, lat) = (rho * (sin e), (cot phi1 - rho * (cos e)))
    where rho = (cot phi1) + phi1 - lat
          e = (long - meridian) * (cos lat) / rho
          phi1 = radians 45 -- standard parallel @ 45 N 
          meridian = radians (-77.0369) -- central meridian @ dc
          cot = (1/) . tan

-- | Albers projection for a given reference point. To make it usable you can
-- use
-- > ablers washingtonDC
albers :: Point -> Projection
albers referencePoint (long, lat) = (rho * (sin theta), rho' - rho * (cos theta))
    where n = (sin phi1 + sin phi2)/2
          theta = n * (long - referenceLong)
          c = (cos phi1)^2 + 2 * n * (sin phi1)
          rho = sqrt (c - 2 * n * (sin lat)) / n
          rho' = sqrt (c - 2 * n * (sin referenceLat)) / n
          phi1 = (radians 20) -- standard parallels @ 20, 50 degrees
          phi2 = (radians 50)
          (referenceLong, referenceLat) = referencePoint 

-- | Helper to project given a `Polygon`.
project :: Projection -> Polygon -> Polygon
project f = fmap (f . toRadians)

-- | Helper to apply a projection given a `Map`.
projectMap :: Projection -> Map -> Map
projectMap p = over (labelledDistricts) (fmap (over _1  (fmap (project p))))