{-# LANGUAGE OverloadedStrings, ScopedTypeVariables, ViewPatterns, PatternSynonyms #-}
module Data.LatLong (
LatLong(LatLongZ, LatLong), lat, long,
earthRadius, geoDistance, geoSquare,
LatLongTile, latLongTileInterval,
latLongTileCover, latLongTileCoverSquare, tileSetElem, withinTileSet
) where
import Data.Morton
import Data.Aeson
import Data.Proxy
import qualified Data.Text as T
import Control.Monad
import Control.Lens (Lens')
import Data.Word
import Numeric
import Web.HttpApiData
import Database.Persist.Sql
newtype LatLong =
LatLongZ Morton
deriving (Eq, Ord)
two32f :: Double
two32f = 2 ^ (32 :: Int)
makeLatLong :: Double -> Double -> LatLong
makeLatLong theta phi = LatLongZ (MortonPair theta' phi') where
theta' = clampLat . floor $ (theta + 90) / 360 * two32f
phi' = wrapLong . floor $ (phi + 180) / 360 * two32f
clampLat (x::Int) = fromIntegral (max 0 (min 0x7fffffff x))
wrapLong (x::Int) = fromIntegral (x `mod` 0x100000000)
latLongCoords :: LatLong -> (Double, Double)
latLongCoords (LatLongZ (MortonPair theta' phi')) = (theta,phi) where
theta = (fromIntegral theta' * 360 / two32f) - 90
phi = (fromIntegral phi' * 360 / two32f) - 180
pattern LatLong :: Double -> Double -> LatLong
pattern LatLong theta phi <- (latLongCoords -> (theta,phi)) where
LatLong theta phi = makeLatLong theta phi
{-# COMPLETE LatLong #-}
instance ToJSON LatLong where
toJSON (LatLong theta phi) = object ["lat" .= theta, "long" .= phi]
toEncoding (LatLong theta phi) = pairs $ "lat" .= theta <> "long" .= phi
instance FromJSON LatLong where
parseJSON = withObject "LatLong" $ \o -> do
theta <- o .: "lat"
phi <- o .: "long"
guard $ theta > -90 && theta < 90
guard $ phi >= -180 && phi < 180
return $ LatLong theta phi
instance FromHttpApiData LatLong where
parseUrlPiece s = maybe (Left "malformed coordinate pair") Right $ do
[theta',phi'] <- return $ T.splitOn "," s
Right theta <- return $ parseUrlPiece theta'
Right phi <- return $ parseUrlPiece phi'
guard $ theta >= -90 && theta <= 90
guard $ phi >= -180 && phi <= 180
return $ LatLong theta phi
instance ToHttpApiData LatLong where
toUrlPiece (LatLong theta phi) = toUrlPiece theta <> "," <> toUrlPiece phi
instance Show LatLong where
show (LatLong theta phi) = join
[ showFFloat (Just 5) (abs theta) []
, if theta >= 0 then " N " else " S "
, showFFloat (Just 5) (abs phi) []
, if phi >= 0 then " E" else " W" ]
instance PersistField LatLong where
toPersistValue (LatLongZ (Morton x)) = toPersistValue x
fromPersistValue = fmap (LatLongZ . Morton) . fromPersistValue
instance PersistFieldSql LatLong where
sqlType _ = sqlType (Proxy :: Proxy Word64)
lat :: Lens' LatLong Double
lat f (LatLong theta phi) = fmap (\theta' -> LatLong theta' phi) (f theta)
long :: Lens' LatLong Double
long f (LatLong theta phi) = fmap (\phi' -> LatLong theta phi') (f phi)
earthRadius :: Double
earthRadius = 6371.2e3
rads :: Double->Double
rads x = x / 180 * pi
degs :: Double->Double
degs x = x * 180 / pi
sindeg :: Double->Double
sindeg = sin . rads
cosdeg :: Double->Double
cosdeg = cos . rads
geoDistance :: LatLong -> LatLong -> Double
geoDistance (LatLong theta1 phi1) (LatLong theta2 phi2) = earthRadius * sigma where
havdelta x y = sindeg ((x-y)/2) ^ (2::Int)
hav = havdelta theta1 theta2 + (cosdeg theta1 * cosdeg theta2 * havdelta phi1 phi2)
sigma = 2 * asin (sqrt hav)
geoSquare :: LatLong -> Double -> (LatLong, LatLong)
geoSquare (LatLong theta phi) r = let
dtheta = degs (r / earthRadius)
dphi = degs (r / earthRadius / cosdeg theta)
se = LatLong (theta - dtheta) (phi - dphi)
nw = LatLong (theta + dtheta) (phi + dphi)
in (se,nw)
newtype LatLongTile = LatLongTile MortonTile deriving (Eq, Read, Show)
latLongTileInterval :: LatLongTile -> Interval LatLong
latLongTileInterval (LatLongTile t) = fmap LatLongZ (mortonTileBounds t)
latLongTileCover :: LatLong -> LatLong -> [LatLongTile]
latLongTileCover se nw = let
LatLong s _ = se
LatLong n _ = nw
LatLongZ y = se
LatLongZ x = nw
in if s > n then [] else fmap LatLongTile (mortonTileCoverTorus x y)
latLongTileCoverSquare :: LatLong -> Double -> [LatLongTile]
latLongTileCoverSquare c r = uncurry latLongTileCover $ geoSquare c r
tileSetElem :: LatLong -> [LatLongTile] -> Bool
tileSetElem p ts = or [intervalElem p (latLongTileInterval t) | t <- ts]
withinTileSet :: (EntityField row LatLong) -> [LatLongTile] -> Filter row
withinTileSet field tiles = let
tfilter tile = let Interval a b = latLongTileInterval tile in FilterAnd [field >=. a, field <=. b]
in FilterOr $ fmap tfilter tiles