{-# LANGUAGE RecordWildCards #-} module Database.Shapefile.Shp where import Database.Shapefile.ShapeTypes import Database.Shapefile.Misc import Data.Binary.Get import Data.Binary.Put import Data.Binary.IEEE754 import Data.Word import Data.Maybe import qualified Data.ByteString.Lazy as BS data ShpFileHeader = ShpFileHeader { -- |File length in 16-bit words. Unsigned, I assume - spec doesn't say. shpFileLength :: Word32 , shpFileShapeType :: ESRIShapeType , shpFileBBox :: BBox (Double, Double) , shpFileZBnd :: Maybe (Double, Double) , shpFileMBnd :: Maybe (Double, Double) } deriving (Eq, Show) -- |Shape file length in bytes shpFileLengthBytes :: ShpFileHeader -> Integer shpFileLengthBytes = (2*) . toInteger . shpFileLength putShpFileHeader :: ShpFileHeader -> Put putShpFileHeader ShpFileHeader {..} = do {- 0: File Code -} putWord32be 9994 {- 4: Unused -} putWord32be 0 {- 8: Unused -} putWord32be 0 {- 12: Unused -} putWord32be 0 {- 16: Unused -} putWord32be 0 {- 20: Unused -} putWord32be 0 {- 24: File Length -} putWord32be shpFileLength {- 28: Version -} putWord32le 1000 {- 32: Shape Type -} putShapeType32le shpFileShapeType {- 36: Bounding Box -} putBBox (putPair putFloat64le) shpFileBBox {- 68: Z Bounds -} putPair putFloat64le (fromMaybe (0,0) shpFileZBnd) {- 84: M Bounds -} putPair putFloat64le (fromMaybe (0,0) shpFileMBnd) {- 100 bytes total -} getShpFileHeader :: Get ShpFileHeader getShpFileHeader = do {- 0: File Code -} getWord32be `expecting` 9994 {- 4: Unused -} getWord32be `expecting` 0 {- 8: Unused -} getWord32be `expecting` 0 {- 12: Unused -} getWord32be `expecting` 0 {- 16: Unused -} getWord32be `expecting` 0 {- 20: Unused -} getWord32be `expecting` 0 {- 24: File Length -} shpFileLength <- getWord32be {- 28: Version -} getWord32le `expecting` 1000 {- 32: Shape Type -} shpFileShapeType <- getShapeType32le {- 36: Bounding Box -} shpFileBBox <- getBBox (getPair getFloat64le) {- 68: Z Bounds -} shpFileZBnd <- getPair getFloat64le {- 84: M Bounds -} shpFileMBnd <- getPair getFloat64le {- 100 bytes total -} return $ ShpFileHeader { shpFileLength = shpFileLength , shpFileShapeType = shpFileShapeType , shpFileBBox = shpFileBBox , shpFileZBnd = if hasZ shpFileShapeType || nonZero shpFileZBnd then Just shpFileZBnd else Nothing , shpFileMBnd = if hasM shpFileShapeType || nonZero shpFileMBnd then Just shpFileMBnd else Nothing } where nonZero (0,0) = False nonZero _ = True data ShpRecHeader = ShpRecHeader { -- |Index of the record. First index is 1. shpRecNum :: Word32 , -- |Size of the record in 16-bit words, excluding this header. shpRecSize :: Word32 } deriving (Eq, Show) -- |Size of the record in bytes, excluding the record header shpRecSizeBytes :: ShpRecHeader -> Integer shpRecSizeBytes = (*2) . toInteger . shpRecSize putShpRecHeader :: ShpRecHeader -> Put putShpRecHeader ShpRecHeader {..} = do {- 0 : Record Number -} putWord32be shpRecNum {- 4 : Content Length -} putWord32be shpRecSize getShpRecHeader :: Get ShpRecHeader getShpRecHeader = do {- 0 : Record Number -} shpRecNum <- getWord32be {- 4 : Content Length -} shpRecSize <- getWord32be return ShpRecHeader { shpRecNum = shpRecNum , shpRecSize = shpRecSize } data ShpRec = ShpRec { shpRecHdr :: ShpRecHeader , shpRecData :: BS.ByteString } deriving (Eq, Show) -- |Total size of the shape record in bytes, including the header shpRecTotalSizeBytes :: ShpRec -> Integer shpRecTotalSizeBytes = (8 +) . shpRecSizeBytes . shpRecHdr -- |A shape record type isn't "part of" the header, but every shape format starts with -- a word indicating the shape type. This function extracts it. shpRecShapeType :: ShpRec -> ESRIShapeType shpRecShapeType ShpRec{..} | BS.length shpRecData < 4 = NullShape | otherwise = runGet getShapeType32le shpRecData -- |Pack several raw shape records into 'ShpRec's, setting proper record numbers -- and sizes. mkShpRecs :: [BS.ByteString] -> [ShpRec] mkShpRecs recData = zipWith mkShpRec [1..] recData -- |Pack the data for a shape into a 'ShpRec' with the specified record number mkShpRec :: Word32 -> BS.ByteString -> ShpRec mkShpRec n recData = ShpRec (ShpRecHeader n (bsLength recData)) recData where bsLength bs = case fromIntegral len `divMod` 2 of (words, 0) -> words (words, _) -> error ("uneven length shape record (" ++ show len ++ " bytes)") where len = BS.length bs putShpRec :: ShpRec -> Put putShpRec ShpRec {..} = do {- 0 : Record Header -} putShpRecHeader shpRecHdr {- 8 : Record content -} putLazyByteString shpRecData {- (8 + BS.length shpRecData) bytes total -} getShpRec :: Get ShpRec getShpRec = do {- 0 : Record Header -} shpRecHdr@ShpRecHeader {shpRecSize = len} <- getShpRecHeader {- 8 : Record content -} shpRecData <- getLazyByteString (2 * fromIntegral len) {- (8 + len) bytes total -} return ShpRec { shpRecHdr = shpRecHdr , shpRecData = shpRecData } putShpFile :: ShpFileHeader -> [ShpRec] -> Put putShpFile shpHdr shpRecs = do putShpFileHeader shpHdr mapM_ putShpRec shpRecs getShpFile :: Get (ShpFileHeader, [ShpRec]) getShpFile = do hdr <- getShpFileHeader rest <- getLazyByteString (fromInteger (shpFileLengthBytes hdr) - 100) let n = shpFileLengthBytes hdr - 100 return (hdr, slurp n rest) where slurp 0 rest = [] slurp n rest = flip runGet rest $ do rec <- getShpRec rest <- getRemainingLazyByteString let n' = n - shpRecTotalSizeBytes rec return (rec : slurp n' rest)