streaming-osm 1.0.0 → 1.0.0.1
raw patch · 12 files changed
+494/−525 lines, 12 filesdep +resourcetdep +streaming-attoparsecdep −criteriondep −streaming-utilsdep ~attoparsecdep ~basedep ~streamingPVP ok
version bump matches the API change (PVP)
Dependencies added: resourcet, streaming-attoparsec
Dependencies removed: criterion, streaming-utils
Dependency ranges changed: attoparsec, base, streaming, streaming-bytestring, tasty, tasty-hunit, text, vector, zlib
API changes (from Hackage documentation)
Files
- ChangeLog.md +5/−1
- bench/Bench.hs +0/−24
- lib/Streaming/Osm.hs +83/−0
- lib/Streaming/Osm/Internal/Parser.hs +173/−0
- lib/Streaming/Osm/Internal/Util.hs +94/−0
- lib/Streaming/Osm/Types.hs +79/−0
- src/Streaming/Osm.hs +0/−82
- src/Streaming/Osm/Internal/Parser.hs +0/−173
- src/Streaming/Osm/Internal/Util.hs +0/−93
- src/Streaming/Osm/Types.hs +0/−78
- streaming-osm.cabal +47/−66
- test/Test.hs +13/−8
ChangeLog.md view
@@ -1,3 +1,7 @@-### 1.0.0 ###+## 1.0.0.1++- Massaging bounds and future-proofing.++## 1.0.0 - Initial release.
− bench/Bench.hs
@@ -1,24 +0,0 @@-module Main where--import Criterion.Main-import Streaming-import Streaming.Osm-import qualified Streaming.Prelude as S-------main :: IO ()-main = defaultMain- [ bgroup "Counting Nodes"- [ bench "Uku Seq" $ nfIO (runResourceT . S.length_ . nodes . blocks $ blobs "test/uku.osm.pbf")- -- , bench "Uku Par" $ nfIO (runResourceT . blocks' (S.length_ . nodes) $ blobs "test/uku.osm.pbf")- -- , bench "Van Seq" $ nfIO (runResourceT . S.length_ . nodes . blocks $ blobs "test/vancouver.osm.pbf")- -- , bench "Van Par" $ nfIO (runResourceT . blocks' (S.length_ . nodes) $ blobs "test/vancouver.osm.pbf")- ]- , bgroup "Counting Ways"- [ bench "Uku" $ nfIO (runResourceT . S.length_ . ways . blocks $ blobs "test/uku.osm.pbf")- ]- , bgroup "Counting Relations"- [ bench "Uku" $ nfIO (runResourceT . S.length_ . relations . blocks $ blobs "test/uku.osm.pbf")- ]- ]
+ lib/Streaming/Osm.hs view
@@ -0,0 +1,83 @@+-- |+-- Module : Streaming.Osm+-- Copyright : (c) Azavea, 2017+-- License : BSD3+-- Maintainer: Colin Woodbury <colin@fosskers.ca>+--+-- This library provides the ability to read and process <http://www.openstreetmap.org/ OpenStreetMap>+-- data via the <https://hackage.haskell.org/package/streaming streaming> ecosystem. Since /streaming/+-- allows for very little RAM overhead despite file size, we can process very large OSM PBF files+-- just by providing a file path:+--+-- @+-- import Streaming+-- import Streaming.Osm+-- import qualified Streaming.Prelude as S+--+-- -- | Count all nodes.+-- count :: IO ()+-- count = do+-- len <- runResourceT . S.length_ . nodes . blocks $ blobs "yourfile.osm.pbf"+-- print len+-- @++module Streaming.Osm+ (+ -- * Streams+ blobs+ , blocks+ , nodes+ , ways+ , relations+ -- * Util+ , RIO+ ) where++import Codec.Compression.Zlib (decompress)+import Control.Monad.Trans.Resource (ResourceT)+import qualified Data.Attoparsec.ByteString as A+import qualified Data.Attoparsec.ByteString.Streaming as A+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Streaming as Q+import Streaming+import Streaming.Osm.Internal.Parser+import Streaming.Osm.Types+import qualified Streaming.Prelude as S++---++-- | A friendly alias. All `Stream`s in this module are constrained to this+-- type for optimal performance. The opposite, say:+--+-- @+-- nodes :: Monad m => Stream (Of Block) m () -> Stream (Of Node) m ()+-- @+-- will actually run significantly slower.+--+-- After evaluating your `Stream` to some final `RIO`, you can further+-- escape back to `IO` via `runResourceT`.+type RIO = ResourceT IO++-- | Given a `FilePath` to read OSM PBF data from, stream all parsed `Blob`s+-- out of it. A `Blob` is a potentially compressed @ByteString@ that further+-- parse into actual OSM Elements.+blobs :: FilePath -> Stream (Of Blob) RIO ()+blobs = void . A.parsed (header *> blob) . Q.readFile++-- | Every `Block` of ~8000 Elements.+blocks :: Stream (Of Blob) RIO () -> Stream (Of Block) RIO ()+blocks = S.concat . S.map f+ where f (Blob (Left bs)) = A.parseOnly block bs+ f (Blob (Right (_, bs))) = A.parseOnly block . BL.toStrict . decompress $ BL.fromStrict bs++-- | All OSM `Node`s.+nodes :: Stream (Of Block) RIO () -> Stream (Of Node) RIO ()+nodes = S.concat . S.map _nodes++-- | All OSM `Way`s.+ways :: Stream (Of Block) RIO () -> Stream (Of Way) RIO ()+ways = S.concat . S.map _ways++-- | All OSM `Relation`s.+relations :: Stream (Of Block) RIO () -> Stream (Of Relation) RIO ()+relations = S.concat . S.map _relations
+ lib/Streaming/Osm/Internal/Parser.hs view
@@ -0,0 +1,173 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Module : Streaming.Osm.Internal.Parser+-- Copyright : (c) Azavea, 2017+-- License : BSD3+-- Maintainer: Colin Woodbury <colin@fosskers.ca>++module Streaming.Osm.Internal.Parser where++import Control.Applicative (optional, (<|>))+import Control.Monad (void)+import qualified Data.Attoparsec.ByteString as A+import qualified Data.Attoparsec.Internal.Types as T+import Data.Bits+import qualified Data.ByteString as B+import Data.List (zipWith4, zipWith7)+import qualified Data.Map.Strict as M+import qualified Data.Vector as V+import Streaming.Osm.Internal.Util+import Streaming.Osm.Types++---++-- | Parse a `BlobHeader`.+header :: A.Parser ()+header = do+ void $ A.take 4+ void $ A.word8 0x0a+ void $ A.anyWord8+ void $ A.string "OSMHeader" <|> A.string "OSMData"+ void $ optional (A.word8 0x12 *> varint >>= advance)+ void (A.word8 0x18 *> varint)++-- | Borrowed from Attoparsec.+advance :: Int -> A.Parser ()+advance n = T.Parser $ \t pos more _lose suc -> suc t (pos + T.Pos n) more ()+{-# INLINE advance #-}++-- | Called a @Blob@ in the OSM literature.+blob :: A.Parser Blob+blob = Blob <$> A.eitherP dcmp comp+ where dcmp = A.word8 0x0a *> varint >>= A.take+ comp = (,) <$> (A.word8 0x10 *> varint) <*> (A.word8 0x1a *> varint >>= A.take)++-- | Called a @PrimitiveBlock@ in the OSM literature.+block :: A.Parser Block+block = do+ st <- A.word8 0x0a *> varint *> stringTable+ ns <- (A.word8 0x12 *> varint *> A.many1' (node st)) <|> pure []+ dn <- (A.word8 0x12 *> varint *> dense st) <|> pure []+ ws <- (A.word8 0x12 *> varint *> A.many1' (way st)) <|> pure []+ rs <- (A.word8 0x12 *> varint *> A.many1' (relation st)) <|> pure []+ void $ optional (A.word8 0x88 *> A.word8 0x01 *> varint) -- granularity+ void $ optional (A.word8 0x90 *> A.word8 0x01 *> varint) -- date_granularity+ void $ optional (A.word8 0x98 *> A.word8 0x01 *> varint) -- lat_offset+ void $ optional (A.word8 0xa0 *> A.word8 0x01 *> varint) -- lon_offset+ pure $ Block (ns ++ dn) ws rs++-- | The String Table will never be empty, since all Elements have+-- non-geographic metadata (username, etc.) which contain Strings. The result+-- must be a `V.Vector`, since we need random access to its contents.+stringTable :: A.Parser (V.Vector B.ByteString)+stringTable = V.fromList <$> A.many1' (A.word8 0x0a *> varint >>= A.take)++-- | Parse a `Node`. Uses `V.unsafeIndex` to quickly retrieve its tag+-- Strings, assuming that the Node's key/value pairs will always index a legal+-- value in the given String Table.+node :: V.Vector B.ByteString -> A.Parser Node+node st = do+ void $ A.word8 0x0a *> varint+ i <- unzig <$> (A.word8 0x08 *> varint) -- id+ ks <- packed <$> (A.word8 0x12 *> varint >>= A.take) <|> pure [] -- keys+ vs <- packed <$> (A.word8 0x1a *> varint >>= A.take) <|> pure [] -- vals+ oi <- optional (A.word8 0x22 *> varint *> info i st) -- info+ lat <- unzig <$> (A.word8 0x40 *> varint) -- lat+ lon <- unzig <$> (A.word8 0x48 *> varint) -- lon+ let ts = M.fromList $ zip (map (V.unsafeIndex st) ks) (map (V.unsafeIndex st) vs)+ pure $ Node (offset lat) (offset lon) oi ts++-- | Parse a @DenseNodes@ in a similar way to `node`.+dense :: V.Vector B.ByteString -> A.Parser [Node]+dense st = do+ void $ A.word8 0x12 *> varint+ ids <- ints <$> (A.word8 0x0a *> varint >>= A.take)+ ifs <- (A.word8 0x2a *> varint *> denseInfo ids st) <|> pure (repeat Nothing)+ lts <- ints <$> (A.word8 0x42 *> varint >>= A.take)+ lns <- ints <$> (A.word8 0x4a *> varint >>= A.take)+ kvs <- (packed <$> (A.word8 0x52 *> varint >>= A.take)) <|> pure []+ pure $ zipWith4 f lts lns ifs (denseTags st kvs)+ where f lat lon inf ts = Node (offset lat) (offset lon) inf ts++-- | Interpret a list of flattened key-value pairs as Tag metadata `Map`s.+denseTags :: V.Vector B.ByteString -> [Int] -> [M.Map B.ByteString B.ByteString]+denseTags _ [] = repeat M.empty+denseTags st kvs = map (M.fromList . map (both (V.unsafeIndex st)) . pairs) $ breakOn0 kvs++-- | Parse a `Way`.+way :: V.Vector B.ByteString -> A.Parser Way+way st = do+ void $ A.word8 0x1a *> varint+ i <- A.word8 0x08 *> varint -- id+ ks <- packed <$> (A.word8 0x12 *> varint >>= A.take) <|> pure [] -- keys+ vs <- packed <$> (A.word8 0x1a *> varint >>= A.take) <|> pure [] -- vals+ oi <- optional (A.word8 0x22 *> varint *> info i st) -- info+ ns <- ints <$> (A.word8 0x42 *> varint >>= A.take)+ let ts = M.fromList $ zip (map (V.unsafeIndex st) ks) (map (V.unsafeIndex st) vs)+ pure $ Way ns oi ts++-- | Parse a `Relation`.+relation :: V.Vector B.ByteString -> A.Parser Relation+relation st = do+ void $ A.word8 0x22 *> varint+ i <- A.word8 0x08 *> varint+ ks <- packed <$> (A.word8 0x12 *> varint >>= A.take) <|> pure [] -- keys+ vs <- packed <$> (A.word8 0x1a *> varint >>= A.take) <|> pure [] -- vals+ oi <- optional (A.word8 0x22 *> varint *> info i st) -- info+ rs <- packed <$> (A.word8 0x42 *> varint >>= A.take) <|> pure [] -- roles_sid+ ms <- map unzig . packed <$> (A.word8 0x4a *> varint >>= A.take) <|> pure [] -- memids+ ts <- map memtype . packed <$> (A.word8 0x52 *> varint >>= A.take) <|> pure [] -- types+ let tags = M.fromList $ zip (map (V.unsafeIndex st) ks) (map (V.unsafeIndex st) vs)+ mems = zipWith3 Member ms ts $ map (V.unsafeIndex st) rs+ pure $ Relation mems oi tags++-- | Parse an `Info`.+info :: Int -> V.Vector B.ByteString -> A.Parser Info+info i st = do+ vn <- (A.word8 0x08 *> varint) <|> pure (-1) -- version+ ts <- optional (A.word8 0x10 *> varint) -- timestamp+ cs <- optional (A.word8 0x18 *> varint) -- changeset+ ui <- optional (A.word8 0x20 *> varint) -- uid+ us <- optional (V.unsafeIndex st <$> (A.word8 0x28 *> varint)) -- user_sid+ vi <- (>>= booly) <$> optional (A.word8 0x30 *> varint) -- visible+ pure $ Info (fromIntegral i) vn (toffset <$> ts) cs ui us vi++-- | Parse a @DenseInfo@ message.+denseInfo :: [Int] -> V.Vector B.ByteString -> A.Parser [Maybe Info]+denseInfo nis st = do+ ver <- packed <$> (A.word8 0x0a *> varint >>= A.take)+ tms <- map Just . ints <$> (A.word8 0x12 *> varint >>= A.take)+ chs <- map Just . ints <$> (A.word8 0x1a *> varint >>= A.take)+ uid <- map Just . ints <$> (A.word8 0x22 *> varint >>= A.take)+ uss <- map (st V.!?) . ints <$> (A.word8 0x2a *> varint >>= A.take)+ vis <- (map booly . packed <$> (A.word8 0x32 *> varint >>= A.take)) <|> pure (repeat $ Just True)+ pure $ zipWith7 f nis ver tms chs uid uss vis+ where f ni vs tm ch ui us vi = Just $ Info ni vs (toffset <$> tm) ch ui us vi++-- | Parse some Varint, which may be made up of multiple bytes.+varint :: A.Parser Int+varint = foldBytes <$> A.takeWhile (`testBit` 7) <*> A.anyWord8+{-# INLINE varint #-}++-- | Reparse a `B.ByteString` as a list of some Varints.+packed :: B.ByteString -> [Int]+packed bs = either (const []) id $ A.parseOnly (A.many1' varint) bs++-- | Decode some packed, Z-encoded, delta-encoded Ints.+ints :: B.ByteString -> [Int]+ints = undelta . map unzig . packed++-- | Restore truncated LatLng values to their true `Double` representation.+offset :: Int -> Double+offset coord = 0.000000001 * fromIntegral (100 * coord)++-- | Restore truncated timestamps to the number of millis since the 1970 epoch.+toffset :: Int -> Int+toffset time = 1000 * time++-- | Try to parse a `Bool` from a bit.+booly :: Int -> Maybe Bool+booly 0 = Just False+booly 1 = Just True+booly _ = Nothing
+ lib/Streaming/Osm/Internal/Util.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE BinaryLiterals #-}++-- |+-- Module : Streaming.Osm.Internal.Util+-- Copyright : (c) Azavea, 2017+-- License : BSD3+-- Maintainer: Colin Woodbury <colin@fosskers.ca>++module Streaming.Osm.Internal.Util+ ( foldBytes+ , breakOn0+ , pairs+ , both+ , unzig, undelta+ ) where++import Data.Bits+import qualified Data.ByteString as BS+import Data.List (scanl')+import Data.Word++---++-- to16 :: Word8 -> Word16+-- to16 = fromIntegral++-- to8 :: Word16 -> Word8+-- to8 = fromIntegral++-- unkey :: Word8 -> Word8 -> Either (Word8, Word8) Word8+-- unkey f w = case (to8 $ shiftR smash 7, to8 $ smash .&. 0b01111111) of+-- (0, r) -> Right r+-- (l, r) -> Left (setBit r 7, l)+-- where smash = shift (to16 f) 3 .|. to16 w++-- | Discover a field's number and /Wire Type/. The wire type is expected to+-- be a value from 0 to 5. The field number itself can probably be any varint,+-- although in practice these are in `Word8` range.+--+-- The results are left as numbers, since pattern matching on those should+-- be faster.+-- key :: (Num t, Bits t) => t -> (t, t)+-- key w = (shiftR w 3, w .&. 0b00000111)++-- | For the case when two bytes denote the field number and /Wire Type/. We+-- know that for OSM data, the highest field number is 34. Encoding 34 with any+-- wire type takes 2 bytes, so we know we'll never need to check for more.+-- key2 :: Word8 -> Word8 -> (Word16, Word16)+-- key2 w1 w0 = key $ shift (to16 w0) 7 .|. to16 (clearBit w1 7)++-- `BS.foldr'` is tail-recursive, unlike List's foldr, so it should be just as fast as foldl.+-- | Fold a `BS.ByteString` into an `Int` which was parsed with wire-type 2+-- (Length-delimited). These follow the pattern @tagByte byteCount bytes@,+-- where we've parsed @byteCount@ and done an attoparsec @take@ on the @bytes@.+-- These bytes could be a packed repeated field of varints, meaning they could+-- all have different byte lengths.+--+-- This function uses the rules described+-- <https://developers.google.com/protocol-buffers/docs/encoding here> for+-- determining when to accumulate the current byte, or to consider it the first+-- byte of the next value. In short, the rule is:+--+-- 1. If the MSB of a byte is 1, then expect at least the next byte to belong to this value.+-- 2. If the MSB of a byte is 0, we're at the end of the current accumulating value (or+-- at the first byte of a single byte value).+foldBytes :: BS.ByteString -> Word8 -> Int+foldBytes bs b = BS.foldr' (\w acc -> shift acc 7 .|. clearBit (fromIntegral w) 7) (fromIntegral b) bs++-- | `words` for `Int`, where 0 is the whitespace. Implementation adapted+-- from `words`.+breakOn0 :: [Int] -> [[Int]]+breakOn0 [] = []+breakOn0 ns = xs : breakOn0 ys+ where (xs, 0 : ys) = span (/= 0) ns++-- | A sort of "self-zip", forming pairs from every two elements in a list.+-- Assumes that the list is of even length.+pairs :: [a] -> [(a,a)]+pairs [] = []+pairs (x:y:zs) = (x,y) : pairs zs+pairs _ = error "List was of uneven length."++-- | Apply a function to both elements of a tuple.+both :: (a -> b) -> (a, a) -> (b, b)+both f (a,b) = (f a, f b)++-- | Decode a Z-encoded `Int`.+unzig :: Int -> Int+unzig n = shift n (-1) `xor` negate (n .&. 1)++-- | Restore a list of numbers that have been Delta Encoded.+undelta :: [Int] -> [Int]+undelta [] = []+undelta (x : xs) = scanl' (+) x xs
+ lib/Streaming/Osm/Types.hs view
@@ -0,0 +1,79 @@+-- |+-- Module : Streaming.Osm.Types+-- Copyright : (c) Azavea, 2017+-- License : BSD3+-- Maintainer: Colin Woodbury <colin@fosskers.ca>++module Streaming.Osm.Types+ ( -- * OpenStreetMap /Elements/+ Node(..)+ , Way(..)+ , Relation(..)+ , Info(..)+ , Member(..)+ , MemType(..), memtype+ -- * Helper Types+ , Blob(..)+ , Block(..)+ ) where++import qualified Data.ByteString as B+import qualified Data.Map as M++---++-- | An OpenStreetMap /Node/ element. Represents a single point in 2D space.+data Node = Node { _lat :: Double+ , _lng :: Double+ , _ninfo :: Maybe Info+ , _ntags :: M.Map B.ByteString B.ByteString+ } deriving (Eq, Show)++-- | An OpenStreetMap /Way/ element. Made up of `Node`s, and represents+-- some line on the earth, or a Polygon if its first and last Nodes are the same.+data Way = Way { _nodeRefs :: [Int]+ , _winfo :: Maybe Info+ , _wtags :: M.Map B.ByteString B.ByteString+ } deriving (Eq, Show)++-- | An OpenStreetMap /Relation/ element. These are logical groups of Nodes, Ways,+-- and other Relations. They can represent large multipolygons, or more abstract+-- non-polygonal objects like bus route networks.+data Relation = Relation { _members :: [Member]+ , _rinfo :: Maybe Info+ , _rtags :: M.Map B.ByteString B.ByteString+ } deriving (Eq, Show)++-- | Equivalent to the /member/ tag, found in OSM `Relation`s.+data Member = Member { _mref :: Int, _mtype :: MemType, _mrole :: B.ByteString } deriving (Eq, Show)++-- | Is a `Member` entry referencing a Node, a Way, or a Relation?+data MemType = N | W | R deriving (Eq, Show)++-- | A bridge between the Int-based enum value for `MemType` is protobuf and+-- a more useful Haskell type.+memtype :: Int -> MemType+memtype 0 = N+memtype 1 = W+memtype 2 = R+memtype _ = error "Unknown `MemType`"++-- | Non-geographic `Element` metadata. The OSM database is a wild place, so+-- many of these fields may be missing for older Elements.+data Info = Info { _id :: Int+ , _version :: Int+ , _timestamp :: Maybe Int+ , _changeset :: Maybe Int+ , _uid :: Maybe Int+ , _username :: Maybe B.ByteString+ , _visible :: Maybe Bool+ } deriving (Eq, Show)++-- | Bytes parsed out of an @OSMData@ section. Either non-compressed (Left) or+-- compressed (Right) with its post-decompression size.+newtype Blob = Blob { bytes :: Either B.ByteString (Int, B.ByteString) } deriving (Eq, Show)++-- | A group of ~8000 OSM Elements.+data Block = Block { _nodes :: [Node]+ , _ways :: [Way]+ , _relations :: [Relation] } deriving (Eq, Show)
− src/Streaming/Osm.hs
@@ -1,82 +0,0 @@--- |--- Module : Streaming.Osm--- Copyright : (c) Azavea, 2017--- License : BSD3--- Maintainer: Colin Woodbury <colingw@gmail.com>------ This library provides the ability to read and process <http://www.openstreetmap.org/ OpenStreetMap>--- data via the <https://hackage.haskell.org/package/streaming streaming> ecosystem. Since /streaming/--- allows for very little RAM overhead despite file size, we can process very large OSM PBF files--- just by providing a file path:------ @--- import Streaming--- import Streaming.Osm--- import qualified Streaming.Prelude as S------ -- | Count all nodes.--- count :: IO ()--- count = do--- len <- runResourceT . S.length_ . nodes . blocks $ blobs "yourfile.osm.pbf"--- print len--- @--module Streaming.Osm- (- -- * Streams- blobs- , blocks- , nodes- , ways- , relations- -- * Util- , RIO- ) where--import Codec.Compression.Zlib (decompress)-import qualified Data.Attoparsec.ByteString as A-import qualified Data.Attoparsec.ByteString.Streaming as A-import qualified Data.ByteString.Lazy as BL-import qualified Data.ByteString.Streaming as Q-import Streaming-import Streaming.Osm.Internal.Parser-import Streaming.Osm.Types-import qualified Streaming.Prelude as S--------- | A friendly alias. All `Stream`s in this module are constrained to this--- type for optimal performance. The opposite, say:------ @--- nodes :: Monad m => Stream (Of Block) m () -> Stream (Of Node) m ()--- @--- will actually run significantly slower.------ After evaluating your `Stream` to some final `RIO`, you can further--- escape back to `IO` via `runResourceT`.-type RIO = ResourceT IO---- | Given a `FilePath` to read OSM PBF data from, stream all parsed `Blob`s--- out of it. A `Blob` is a potentially compressed @ByteString@ that further--- parse into actual OSM Elements.-blobs :: FilePath -> Stream (Of Blob) RIO ()-blobs = void . A.parsed (header *> blob) . Q.readFile---- | Every `Block` of ~8000 Elements.-blocks :: Stream (Of Blob) RIO () -> Stream (Of Block) RIO ()-blocks = S.concat . S.map f- where f (Blob (Left bs)) = A.parseOnly block bs- f (Blob (Right (_, bs))) = A.parseOnly block . BL.toStrict . decompress $ BL.fromStrict bs---- | All OSM `Node`s.-nodes :: Stream (Of Block) RIO () -> Stream (Of Node) RIO ()-nodes = S.concat . S.map _nodes---- | All OSM `Way`s.-ways :: Stream (Of Block) RIO () -> Stream (Of Way) RIO ()-ways = S.concat . S.map _ways---- | All OSM `Relation`s.-relations :: Stream (Of Block) RIO () -> Stream (Of Relation) RIO ()-relations = S.concat . S.map _relations
− src/Streaming/Osm/Internal/Parser.hs
@@ -1,173 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}---- |--- Module : Streaming.Osm.Internal.Parser--- Copyright : (c) Azavea, 2017--- License : BSD3--- Maintainer: Colin Woodbury <colingw@gmail.com>--module Streaming.Osm.Internal.Parser where--import Control.Applicative ((<|>), optional)-import Control.Monad (void)-import qualified Data.Attoparsec.ByteString as A-import qualified Data.Attoparsec.Internal.Types as T-import Data.Bits-import qualified Data.ByteString as B-import Data.List (zipWith4, zipWith7)-import qualified Data.Map.Strict as M-import qualified Data.Vector as V-import Streaming.Osm.Types-import Streaming.Osm.Internal.Util--------- | Parse a `BlobHeader`.-header :: A.Parser ()-header = do- A.take 4- A.word8 0x0a- A.anyWord8- A.string "OSMHeader" <|> A.string "OSMData"- optional (A.word8 0x12 *> varint >>= advance)- void (A.word8 0x18 *> varint)---- | Borrowed from Attoparsec.-advance :: Int -> A.Parser ()-advance n = T.Parser $ \t pos more _lose suc -> suc t (pos + T.Pos n) more ()-{-# INLINE advance #-}---- | Called a @Blob@ in the OSM literature.-blob :: A.Parser Blob-blob = Blob <$> A.eitherP dcmp comp- where dcmp = A.word8 0x0a *> varint >>= A.take- comp = (,) <$> (A.word8 0x10 *> varint) <*> (A.word8 0x1a *> varint >>= A.take)---- | Called a @PrimitiveBlock@ in the OSM literature.-block :: A.Parser Block-block = do- st <- A.word8 0x0a *> varint *> stringTable- ns <- (A.word8 0x12 *> varint *> A.many1' (node st)) <|> pure []- dn <- (A.word8 0x12 *> varint *> dense st) <|> pure []- ws <- (A.word8 0x12 *> varint *> A.many1' (way st)) <|> pure []- rs <- (A.word8 0x12 *> varint *> A.many1' (relation st)) <|> pure []- optional (A.word8 0x88 *> A.word8 0x01 *> varint) -- granularity- optional (A.word8 0x90 *> A.word8 0x01 *> varint) -- date_granularity- optional (A.word8 0x98 *> A.word8 0x01 *> varint) -- lat_offset- optional (A.word8 0xa0 *> A.word8 0x01 *> varint) -- lon_offset- pure $ Block (ns ++ dn) ws rs---- | The String Table will never be empty, since all Elements have--- non-geographic metadata (username, etc.) which contain Strings. The result--- must be a `V.Vector`, since we need random access to its contents.-stringTable :: A.Parser (V.Vector B.ByteString)-stringTable = V.fromList <$> A.many1' (A.word8 0x0a *> varint >>= A.take)---- | Parse a `Node`. Uses `V.unsafeIndex` to quickly retrieve its tag--- Strings, assuming that the Node's key/value pairs will always index a legal--- value in the given String Table.-node :: V.Vector B.ByteString -> A.Parser Node-node st = do- A.word8 0x0a *> varint- i <- unzig <$> (A.word8 0x08 *> varint) -- id- ks <- packed <$> (A.word8 0x12 *> varint >>= A.take) <|> pure [] -- keys- vs <- packed <$> (A.word8 0x1a *> varint >>= A.take) <|> pure [] -- vals- oi <- optional (A.word8 0x22 *> varint *> info i st) -- info- lat <- unzig <$> (A.word8 0x40 *> varint) -- lat- lon <- unzig <$> (A.word8 0x48 *> varint) -- lon- let ts = M.fromList $ zip (map (V.unsafeIndex st) ks) (map (V.unsafeIndex st) vs)- pure $ Node (offset lat) (offset lon) oi ts---- | Parse a @DenseNodes@ in a similar way to `node`.-dense :: V.Vector B.ByteString -> A.Parser [Node]-dense st = do- A.word8 0x12 *> varint- ids <- ints <$> (A.word8 0x0a *> varint >>= A.take)- ifs <- (A.word8 0x2a *> varint *> denseInfo ids st) <|> pure (repeat Nothing)- lts <- ints <$> (A.word8 0x42 *> varint >>= A.take)- lns <- ints <$> (A.word8 0x4a *> varint >>= A.take)- kvs <- (packed <$> (A.word8 0x52 *> varint >>= A.take)) <|> pure []- pure $ zipWith4 f lts lns ifs (denseTags st kvs)- where f lat lon inf ts = Node (offset lat) (offset lon) inf ts---- | Interpret a list of flattened key-value pairs as Tag metadata `Map`s.-denseTags :: V.Vector B.ByteString -> [Int] -> [M.Map B.ByteString B.ByteString]-denseTags _ [] = repeat M.empty-denseTags st kvs = map (M.fromList . map (both (V.unsafeIndex st)) . pairs) $ breakOn0 kvs---- | Parse a `Way`.-way :: V.Vector B.ByteString -> A.Parser Way-way st = do- A.word8 0x1a *> varint- i <- A.word8 0x08 *> varint -- id- ks <- packed <$> (A.word8 0x12 *> varint >>= A.take) <|> pure [] -- keys- vs <- packed <$> (A.word8 0x1a *> varint >>= A.take) <|> pure [] -- vals- oi <- optional (A.word8 0x22 *> varint *> info i st) -- info- ns <- ints <$> (A.word8 0x42 *> varint >>= A.take)- let ts = M.fromList $ zip (map (V.unsafeIndex st) ks) (map (V.unsafeIndex st) vs)- pure $ Way ns oi ts---- | Parse a `Relation`.-relation :: V.Vector B.ByteString -> A.Parser Relation-relation st = do- A.word8 0x22 *> varint- i <- A.word8 0x08 *> varint- ks <- packed <$> (A.word8 0x12 *> varint >>= A.take) <|> pure [] -- keys- vs <- packed <$> (A.word8 0x1a *> varint >>= A.take) <|> pure [] -- vals- oi <- optional (A.word8 0x22 *> varint *> info i st) -- info- rs <- packed <$> (A.word8 0x42 *> varint >>= A.take) <|> pure [] -- roles_sid- ms <- map unzig . packed <$> (A.word8 0x4a *> varint >>= A.take) <|> pure [] -- memids- ts <- map memtype . packed <$> (A.word8 0x52 *> varint >>= A.take) <|> pure [] -- types- let tags = M.fromList $ zip (map (V.unsafeIndex st) ks) (map (V.unsafeIndex st) vs)- mems = zipWith3 Member ms ts $ map (V.unsafeIndex st) rs- pure $ Relation mems oi tags---- | Parse an `Info`.-info :: Int -> V.Vector B.ByteString -> A.Parser Info-info i st = do- vn <- (A.word8 0x08 *> varint) <|> pure (-1) -- version- ts <- optional (A.word8 0x10 *> varint) -- timestamp- cs <- optional (A.word8 0x18 *> varint) -- changeset- ui <- optional (A.word8 0x20 *> varint) -- uid- us <- optional (V.unsafeIndex st <$> (A.word8 0x28 *> varint)) -- user_sid- vi <- (>>= booly) <$> optional (A.word8 0x30 *> varint) -- visible- pure $ Info (fromIntegral i) vn (toffset <$> ts) cs ui us vi---- | Parse a @DenseInfo@ message.-denseInfo :: [Int] -> V.Vector B.ByteString -> A.Parser [Maybe Info]-denseInfo nis st = do- ver <- packed <$> (A.word8 0x0a *> varint >>= A.take)- tms <- map Just . ints <$> (A.word8 0x12 *> varint >>= A.take)- chs <- map Just . ints <$> (A.word8 0x1a *> varint >>= A.take)- uid <- map Just . ints <$> (A.word8 0x22 *> varint >>= A.take)- uss <- map (st V.!?) . ints <$> (A.word8 0x2a *> varint >>= A.take)- vis <- (map booly . packed <$> (A.word8 0x32 *> varint >>= A.take)) <|> pure (repeat $ Just True)- pure $ zipWith7 f nis ver tms chs uid uss vis- where f ni vs tm ch ui us vi = Just $ Info ni vs (toffset <$> tm) ch ui us vi---- | Parse some Varint, which may be made up of multiple bytes.-varint :: A.Parser Int-varint = foldBytes <$> A.takeWhile (`testBit` 7) <*> A.anyWord8-{-# INLINE varint #-}---- | Reparse a `B.ByteString` as a list of some Varints.-packed :: B.ByteString -> [Int]-packed bs = either (const []) id $ A.parseOnly (A.many1' varint) bs---- | Decode some packed, Z-encoded, delta-encoded Ints.-ints :: B.ByteString -> [Int]-ints = undelta . map unzig . packed---- | Restore truncated LatLng values to their true `Double` representation.-offset :: Int -> Double-offset coord = 0.000000001 * fromIntegral (100 * coord)---- | Restore truncated timestamps to the number of millis since the 1970 epoch.-toffset :: Int -> Int-toffset time = 1000 * time---- | Try to parse a `Bool` from a bit.-booly :: Int -> Maybe Bool-booly 0 = Just False-booly 1 = Just True-booly _ = Nothing
− src/Streaming/Osm/Internal/Util.hs
@@ -1,93 +0,0 @@-{-# LANGUAGE BinaryLiterals #-}---- |--- Module : Streaming.Osm.Internal.Util--- Copyright : (c) Azavea, 2017--- License : BSD3--- Maintainer: Colin Woodbury <colingw@gmail.com>--module Streaming.Osm.Internal.Util- ( foldBytes- , breakOn0- , pairs- , both- , unzig, undelta- ) where--import Data.Bits-import qualified Data.ByteString as BS-import Data.List (scanl')-import Data.Word--------- to16 :: Word8 -> Word16--- to16 = fromIntegral---- to8 :: Word16 -> Word8--- to8 = fromIntegral---- unkey :: Word8 -> Word8 -> Either (Word8, Word8) Word8--- unkey f w = case (to8 $ shiftR smash 7, to8 $ smash .&. 0b01111111) of--- (0, r) -> Right r--- (l, r) -> Left (setBit r 7, l)--- where smash = shift (to16 f) 3 .|. to16 w---- | Discover a field's number and /Wire Type/. The wire type is expected to--- be a value from 0 to 5. The field number itself can probably be any varint,--- although in practice these are in `Word8` range.------ The results are left as numbers, since pattern matching on those should--- be faster.--- key :: (Num t, Bits t) => t -> (t, t)--- key w = (shiftR w 3, w .&. 0b00000111)---- | For the case when two bytes denote the field number and /Wire Type/. We--- know that for OSM data, the highest field number is 34. Encoding 34 with any--- wire type takes 2 bytes, so we know we'll never need to check for more.--- key2 :: Word8 -> Word8 -> (Word16, Word16)--- key2 w1 w0 = key $ shift (to16 w0) 7 .|. to16 (clearBit w1 7)---- `BS.foldr'` is tail-recursive, unlike List's foldr, so it should be just as fast as foldl.--- | Fold a `BS.ByteString` into an `Int` which was parsed with wire-type 2--- (Length-delimited). These follow the pattern @tagByte byteCount bytes@,--- where we've parsed @byteCount@ and done an attoparsec @take@ on the @bytes@.--- These bytes could be a packed repeated field of varints, meaning they could--- all have different byte lengths.------ This function uses the rules described--- <https://developers.google.com/protocol-buffers/docs/encoding here> for--- determining when to accumulate the current byte, or to consider it the first--- byte of the next value. In short, the rule is:------ 1. If the MSB of a byte is 1, then expect at least the next byte to belong to this value.--- 2. If the MSB of a byte is 0, we're at the end of the current accumulating value (or--- at the first byte of a single byte value).-foldBytes :: BS.ByteString -> Word8 -> Int-foldBytes bs b = BS.foldr' (\w acc -> shift acc 7 .|. clearBit (fromIntegral w) 7) (fromIntegral b) bs---- | `words` for `Int`, where 0 is the whitespace. Implementation adapted--- from `words`.-breakOn0 :: [Int] -> [[Int]]-breakOn0 [] = []-breakOn0 ns = xs : breakOn0 ys- where (xs, 0 : ys) = span (/= 0) ns---- | A sort of "self-zip", forming pairs from every two elements in a list.--- Assumes that the list is of even length.-pairs :: [a] -> [(a,a)]-pairs [] = []-pairs (x:y:zs) = (x,y) : pairs zs---- | Apply a function to both elements of a tuple.-both :: (a -> b) -> (a, a) -> (b, b)-both f (a,b) = (f a, f b)---- | Decode a Z-encoded `Int`.-unzig :: Int -> Int-unzig n = shift n (-1) `xor` negate (n .&. 1)---- | Restore a list of numbers that have been Delta Encoded.-undelta :: [Int] -> [Int]-undelta [] = []-undelta (x : xs) = scanl' (+) x xs
− src/Streaming/Osm/Types.hs
@@ -1,78 +0,0 @@--- |--- Module : Streaming.Osm.Types--- Copyright : (c) Azavea, 2017--- License : BSD3--- Maintainer: Colin Woodbury <colingw@gmail.com>--module Streaming.Osm.Types- ( -- * OpenStreetMap /Elements/- Node(..)- , Way(..)- , Relation(..)- , Info(..)- , Member(..)- , MemType(..), memtype- -- * Helper Types- , Blob(..)- , Block(..)- ) where--import qualified Data.ByteString as B-import qualified Data.Map as M--------- | An OpenStreetMap /Node/ element. Represents a single point in 2D space.-data Node = Node { _lat :: Double- , _lng :: Double- , _ninfo :: Maybe Info- , _ntags :: M.Map B.ByteString B.ByteString- } deriving (Eq, Show)---- | An OpenStreetMap /Way/ element. Made up of `Node`s, and represents--- some line on the earth, or a Polygon if its first and last Nodes are the same.-data Way = Way { _nodeRefs :: [Int]- , _winfo :: Maybe Info- , _wtags :: M.Map B.ByteString B.ByteString- } deriving (Eq, Show)---- | An OpenStreetMap /Relation/ element. These are logical groups of Nodes, Ways,--- and other Relations. They can represent large multipolygons, or more abstract--- non-polygonal objects like bus route networks.-data Relation = Relation { _members :: [Member]- , _rinfo :: Maybe Info- , _rtags :: M.Map B.ByteString B.ByteString- } deriving (Eq, Show)---- | Equivalent to the /member/ tag, found in OSM `Relation`s.-data Member = Member { _mref :: Int, _mtype :: MemType, _mrole :: B.ByteString } deriving (Eq, Show)---- | Is a `Member` entry referencing a Node, a Way, or a Relation?-data MemType = N | W | R deriving (Eq, Show)---- | A bridge between the Int-based enum value for `MemType` is protobuf and--- a more useful Haskell type.-memtype :: Int -> MemType-memtype 0 = N-memtype 1 = W-memtype 2 = R---- | Non-geographic `Element` metadata. The OSM database is a wild place, so--- many of these fields may be missing for older Elements.-data Info = Info { _id :: Int- , _version :: Int- , _timestamp :: Maybe Int- , _changeset :: Maybe Int- , _uid :: Maybe Int- , _username :: Maybe B.ByteString- , _visible :: Maybe Bool- } deriving (Eq, Show)---- | Bytes parsed out of an @OSMData@ section. Either non-compressed (Left) or--- compressed (Right) with its post-decompression size.-newtype Blob = Blob { bytes :: Either B.ByteString (Int, B.ByteString) } deriving (Eq, Show)---- | A group of ~8000 OSM Elements.-data Block = Block { _nodes :: [Node]- , _ways :: [Way]- , _relations :: [Relation] } deriving (Eq, Show)
streaming-osm.cabal view
@@ -1,84 +1,65 @@--- This file has been generated from package.yaml by hpack version 0.17.0.------ see: https://github.com/sol/hpack+cabal-version: 2.2 -name: streaming-osm-version: 1.0.0-synopsis: A hand-written streaming byte parser for OpenStreetMap Protobuf data.-description: A hand-written streaming byte parser for OpenStreetMap Protobuf data.-license: BSD3-license-file: LICENSE-author: Colin Woodbury-maintainer: colingw@gmail.com-copyright: Copyright (c) 2017 Azavea-category: Streaming-build-type: Simple-cabal-version: >= 1.10+name: streaming-osm+version: 1.0.0.1+synopsis: A hand-written streaming byte parser for OpenStreetMap Protobuf data.+description: A hand-written streaming byte parser for OpenStreetMap Protobuf data.+license: BSD-3-Clause+license-file: LICENSE+author: Colin Woodbury+maintainer: colin@fosskers.ca+copyright: Copyright (c) 2017 - 2019 Azavea+category: Streaming+build-type: Simple extra-source-files: ChangeLog.md- test/ajishima.osm.pbf- test/diomede.osm.pbf- test/north-van.osm.pbf- test/nozakijima.osm.pbf- test/shrine.osm.pbf- test/tashirojima.osm.pbf- test/uku.osm.pbf+ test/*.osm.pbf +common commons+ default-language: Haskell2010+ ghc-options: -Wall+ build-depends:+ base >= 4.8 && < 5+ , attoparsec ^>= 0.13+ , bytestring+ , resourcet ^>= 1.2+ , streaming ^>= 0.2+ , vector ^>= 0.12+ , zlib ^>= 0.6+ library+ import: commons+ hs-source-dirs: lib exposed-modules: Streaming.Osm Streaming.Osm.Internal.Parser Streaming.Osm.Internal.Util Streaming.Osm.Types build-depends:- base >=4.8 && <4.11- , attoparsec >=0.13 && <0.14- , bytestring- , streaming >=0.1 && <0.2- , vector >= 0.11 && < 0.13- , zlib >=0.6 && <0.7- , containers- , streaming-bytestring >=0.1 && <0.2- , streaming-utils >= 0.1 && < 0.2- , text+ containers+ , streaming-attoparsec ^>= 1.0+ , streaming-bytestring ^>= 0.1+ , text ^>= 1.2 , transformers- hs-source-dirs:- src- default-language: Haskell2010- ghc-options: -fwarn-unused-imports -fwarn-unused-binds -fwarn-name-shadowing -fwarn-unused-matches -O2 test-suite streaming-osm-test+ import: commons type: exitcode-stdio-1.0- build-depends:- base >=4.8 && <4.11- , attoparsec >=0.13 && <0.14- , bytestring- , streaming >=0.1 && <0.2- , vector >= 0.11 && < 0.13- , zlib >=0.6 && <0.7- , streaming-osm- , tasty >=0.11 && <0.12- , tasty-hunit >=0.9 && <0.10- hs-source-dirs:- test+ hs-source-dirs: test main-is: Test.hs- default-language: Haskell2010- ghc-options: -fwarn-unused-imports -fwarn-unused-binds -fwarn-name-shadowing -fwarn-unused-matches -O2 -threaded--benchmark streaming-osm-bench- type: exitcode-stdio-1.0- main-is: Bench.hs- hs-source-dirs:- bench- ghc-options: -fwarn-unused-imports -fwarn-unused-binds -fwarn-name-shadowing -fwarn-unused-matches -O2 -threaded+ ghc-options: -threaded -with-rtsopts=-N build-depends:- base >=4.8 && <4.11- , attoparsec >=0.13 && <0.14- , bytestring- , streaming >=0.1 && <0.2- , vector >= 0.11 && < 0.13- , zlib >=0.6 && <0.7- , criterion >= 1.1 && < 1.3- , streaming-osm- default-language: Haskell2010+ streaming-osm+ , tasty >= 1.1 && < 1.3+ , tasty-hunit ^>= 0.10++-- benchmark streaming-osm-bench+-- import: commons+-- type: exitcode-stdio-1.0+-- main-is: Bench.hs+-- hs-source-dirs: bench+-- ghc-options: -threaded -O2+-- build-depends:+-- criterion >=1.1+-- , streaming-osm
test/Test.hs view
@@ -1,11 +1,11 @@ module Main where import Codec.Compression.Zlib (decompress)+import Control.Monad.Trans.Resource (runResourceT) import Data.Attoparsec.ByteString as A import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BL import qualified Data.Vector as V-import Streaming import Streaming.Osm import Streaming.Osm.Internal.Parser import Streaming.Osm.Internal.Util@@ -87,7 +87,7 @@ assertRight :: Either t1 t -> Assertion assertRight (Left _) = assertFailure "Crap"-assertRight _ = pure ()+assertRight _ = pure () -- | Bytes which represent a `StringTable`. st :: BS.ByteString@@ -139,8 +139,13 @@ , 0x2a, 0x06 , 0x02, 0x00, 0x00, 0x00, 0x00, 0x00 ] +ids :: BS.ByteString ids = BS.pack [0x9c, 0xb3, 0xaf, 0xde, 0x0a, 0x04, 0x10, 0x16, 0x02, 0x10]++lts :: BS.ByteString lts = BS.pack [0x98, 0x88, 0x8a, 0xbc, 0x02, 0x78, 0xac, 0x02, 0xf8, 0x05, 0x28, 0x90, 0x03]++lns :: BS.ByteString lns = BS.pack [0xf4, 0xd4, 0xc6, 0xcf, 0x09, 0xa7, 0x0a, 0xe4, 0x0f, 0xf3, 0x0d, 0xe8, 0x0c, 0x8b, 0x06 ] -- | Bytes which represent a `Way` from @shrine.osm@.@@ -164,28 +169,28 @@ stringTableT :: Assertion stringTableT = case A.parseOnly stringTable st of- Left _ -> assertFailure "Couldn't parse StringTable"- Right t -> assert . not $ V.null t+ Left _ -> assertFailure "Couldn't parse StringTable"+ Right t -> assertBool "Damn" . not $ V.null t denseNodesT :: Assertion denseNodesT = case A.parseOnly (dense V.empty) dn of Left err -> assertFailure err- Right _ -> pure ()+ Right _ -> pure () denseInfoT :: Assertion denseInfoT = case A.parseOnly stringTable st >>= \t -> A.parseOnly (denseInfo [1..] t) dinf of Left err -> assertFailure err- Right _ -> pure ()+ Right _ -> pure () varintT :: BS.ByteString -> Assertion varintT bs = case A.parseOnly (A.many1 (unzig <$> varint)) bs of Left err -> assertFailure err- Right t -> length t @?= 6+ Right t -> length t @?= 6 wayT :: Assertion wayT = case A.parseOnly stringTable st >>= \t -> A.parseOnly (way t) wey of Left err -> assertFailure err- Right _ -> pure ()+ Right _ -> pure () shrineT :: Assertion shrineT = fileT "test/shrine.osm.pbf" >>= blockT (5, 1, 0)