packages feed

postgis-trivial-0.0.1.0: src/Database/Postgis/Trivial/Internal.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}

-----------------------------------------------------------------------------
-- |
-- Module      :  Database.Postgis.Trivial.Internal
--
-- Low level operations on the Postgis extention of the PostgreSQL Database
--
-----------------------------------------------------------------------------

{- Some techniques in this module were taken from
haskell-postgis <https://hackage.haskell.org/package/haskell-postgis>

Since that package comes with MIT license, we provide it here.

> Copyright (c) 2014 Peter
>
> Permission is hereby granted, free of charge, to any person obtaining
> a copy of this software and associated documentation files (the
> "Software"), to deal in the Software without restriction, including
> without limitation the rights to use, copy, modify, merge, publish,
> distribute, sublicense, and/or sell copies of the Software, and to
> permit persons to whom the Software is furnished to do so, subject to
> the following conditions:
>
> The above copyright notice and this permission notice shall be included
> in all copies or substantial portions of the Software.
>
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-}


module Database.Postgis.Trivial.Internal where

import GHC.Base hiding ( foldr )
import GHC.Num ( Num(..) )
import GHC.Show ( Show(show) )
import GHC.Real ( fromIntegral, Integral )
import System.Endian ( getSystemEndianness, Endianness(..) )
import Foreign ( Int64, Bits((.&.), (.|.)) )
import Control.Applicative ( (<$>) )
import Control.Monad (void )
import Control.Monad.Reader ( ReaderT(runReaderT), asks, MonadTrans(lift) )
import Control.Exception ( throw )
import Data.Foldable ( Foldable(..) )
import Data.ByteString.Lex.Integral ( packHexadecimal, readHexadecimal )
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BC
import Data.Binary ( Word8, Word32, Word64, Get, Put, byteSwap32, byteSwap64,
    Binary(get, put) )
import Data.Binary.Get ( getLazyByteString, lookAhead )
import Data.Binary.Put ( putLazyByteString )
import Data.Maybe ( isJust )
import Data.Binary.IEEE754 ( wordToDouble, doubleToWord )

import Database.Postgis.Trivial.PGISConst
import Database.Postgis.Trivial.Types


type HeaderGetter = Getter Header

newtype ByteOrder = ByteOrder Endianness deriving Show

-- | Header record
data Header = Header {
    _byteOrder  :: ByteOrder
  , _geoType    :: Word32
  , _srid       :: SRID
} deriving Show

instance Binary ByteOrder where
    get = fromBin <$> getLazyByteString 2
    put = putLazyByteString . toBin

instance Binary Header where
    get = getHeader
    put (Header bo gt s) =
        put bo >> (putInt . fromIntegral) gt  >> putMaybe s putInt

-- | Serializable class
class Serializable a where
    toBin :: a -> BL.ByteString
    fromBin :: BL.ByteString -> a

instance Serializable ByteOrder where
    toBin (ByteOrder BigEndian) = "00" :: BL.ByteString
    toBin (ByteOrder LittleEndian) = "01" :: BL.ByteString
    fromBin bo = case _fromBin bo :: Word8 of
        0 -> ByteOrder BigEndian
        1 -> ByteOrder LittleEndian
        _ -> throw $ GeometryError $ "Incorrect ByteOrder " ++ show bo

instance Serializable Word32 where
    toBin = _toBin 8
    fromBin = _fromBin

instance Serializable Word64 where
    toBin = _toBin 16
    fromBin = _fromBin

_toBin :: Integral a => Int -> a -> BL.ByteString
_toBin l w = case bsRes of
    Just s -> BL.fromChunks [pad l s]
    Nothing -> throw $ GeometryError "toBin: cannot convert word"
    where
        bsRes = packHexadecimal w
        pad l' bs' = BS.append (BC.replicate (l' - BS.length bs') '0') bs'

_fromBin :: Integral a => BL.ByteString -> a
_fromBin bs = case hexRes of
    Just (v, _) -> v
    Nothing -> throw $ GeometryError "fromBin: cannot parse hexadecimal"
    where
        hexRes = readHexadecimal $ BS.concat . BL.toChunks $ bs

{-# INLINE byteSwapFn #-}
byteSwapFn :: ByteOrder -> (a -> a) -> a -> a
byteSwapFn bo f = case bo of
    ByteOrder BigEndian     -> id
    ByteOrder LittleEndian  -> f

makeHeader :: SRID -> Word32 -> (Bool, Bool) -> Header
makeHeader srid geoType (hasM, hasZ) =
    Header (ByteOrder getSystemEndianness) gt srid where
        wOr acc (p, h) = if p then h .|. acc else acc
        gt = foldl wOr geoType
            [(hasM, wkbM), (hasZ, wkbZ), (isJust srid, wkbSRID)]

{-# INLINE lookupType #-}
lookupType :: Header -> Word32
lookupType h = _geoType h .&. ewkbTypeOffset

-- | Putters
putDouble :: Putter Double
putDouble = putLazyByteString . toBin .
    byteSwapFn (ByteOrder getSystemEndianness) byteSwap64 . doubleToWord

putInt :: Putter Int
putInt = putLazyByteString . toBin .
    byteSwapFn (ByteOrder getSystemEndianness) byteSwap32 . fromIntegral

{-# INLINE putMaybe #-}
putMaybe :: Maybe a -> Putter a -> Put
putMaybe mi = case mi of
    Just i -> ($ i)
    Nothing -> \_ -> return ()

{-# INLINE putChainLen #-}
putChainLen :: Putter Int
putChainLen = putInt

putHeader :: SRID -> Word32 -> (Bool, Bool) -> Put
putHeader s geoType (hasMBool, hasZBool) =
    put $ makeHeader s geoType (hasMBool, hasZBool)

putPoint :: Putter (Double, Double, Maybe Double, Maybe Double)
putPoint (x, y, Nothing, Nothing)   = putDouble x >> putDouble y
putPoint (x, y, Just z, Nothing)    = putDouble x >> putDouble y >> putDouble z
putPoint (x, y, Nothing, Just m)    = putDouble x >> putDouble y >> putDouble m
putPoint (x, y, Just z, Just m)     = putDouble x >> putDouble y >> putDouble z
                                        >> putDouble m

{-# INLINE putPointND #-}
putPointND :: PointND a => Putter a
putPointND a = putPoint $ components a

-- | BinGetters
getHeader :: Get Header
getHeader = do
    bo <- get
    t <- fromIntegral <$>  _getInt bo
    s <- if t .&. wkbSRID > 0
        then Just . fromIntegral <$> _getInt bo
        else return Nothing
    return $ Header bo t s

{-# INLINE skipHeader #-}
skipHeader :: HeaderGetter ()
skipHeader = void (lift getHeader)

{-# INLINE getHeaderPre #-}
getHeaderPre :: Get Header
getHeaderPre = lookAhead get

getPoint :: HeaderGetter (Double, Double, Maybe Double, Maybe Double)
getPoint = do
    gt <- asks _geoType
    let hasM = (gt .&. wkbM) > 0
        hasZ = (gt .&. wkbZ) > 0
    x <- getDouble
    y <- getDouble
    zMb <- if hasZ then Just <$> getDouble else return Nothing
    mMb <- if hasM then Just <$> getDouble else return Nothing
    return (x, y, zMb, mMb)

{-# INLINE getPointND #-}
getPointND :: PointND a => HeaderGetter a
getPointND = do
    (x, y, zMb, mMb) <- getPoint
    return $ fromComponents (x, y, zMb, mMb)

{-# INLINE getNumber #-}
getNumber :: (Serializable a) => (a -> a) -> Int64 -> ByteOrder -> Get a
getNumber f l bo  = do
    bs <- getLazyByteString l
    case bo of
        ByteOrder BigEndian     -> return $ fromBin bs
        ByteOrder LittleEndian  -> return . f . fromBin $ bs

_getInt :: ByteOrder -> Get Int
_getInt = fmap fromIntegral . getNumber byteSwap32 8

getInt :: HeaderGetter Int
getInt = asks _byteOrder >>= lift . _getInt

_getDouble :: ByteOrder -> Get Double
_getDouble = fmap wordToDouble . getNumber byteSwap64 16

getDouble :: HeaderGetter Double
getDouble = asks _byteOrder >>= lift . _getDouble

{-# INLINE getChainLen #-}
getChainLen :: HeaderGetter Int
getChainLen = getInt

-- | Geometry returning function
makeResult :: Header -> HeaderGetter a -> Get (a, SRID)
makeResult h getter = (,_srid h) <$> runReaderT getter h