Skip to content

Vector Tiles - #1563

Merged
lossyrob merged 84 commits into
locationtech:masterfrom
fosskers:feature/vector-tiles
Sep 23, 2016
Merged

lossyrob merged 84 commits into
locationtech:masterfrom
fosskers:feature/vector-tiles

Conversation

@fosskers

@fosskers fosskers commented Jun 28, 2016 •

Copy link
Copy Markdown
Contributor

TODO

  • Move to scalapb
  • Higher level types with Geotrellis Geometry types
  • Decoding
    • Decode bytes into protobuf-level objects
    • Command conversion
    • Protobuf backend for high-level traits
    • Geometry decoding
    • Lazy Geometry Streams
    • Lazy Feature construction
  • Encoding
    • Geometry encoding (toCommands instances)
    • VectorTile encoding
  • API Improvements
    • Handle Geotrellis LayoutDefinitions on decode
    • Handle Geotrellis LayoutDefinitions on encode
    • Fix tests that broke with API change
  • Commit new test .mvt files
  • Port tests
  • Be fast
  • Benchmark decoding and encoding
  • Add/remove extra project files (build scripts, etc)
  • Fix Exception output (InvalidCommand, etc)

Motivation

Invented by Mapbox, they are a combination of the ideas of finite-sized tiles and vector geometries. Mapbox maintains the official implementation spec for VectorTile codecs.

VectorTiles are advantageous over raster tiles in that:

  1. They are typically smaller to store
  2. They can be easily transformed (rotated, etc.) in real time
  3. They allow for continuous (as opposed to step-wise) zoom in Slippy Maps.

Raw VectorTile data is stored in the protobuf format. Any codec implementing
the spec must decode and encode data according to this .proto schema.

Post-PR Next Steps

  • Layer IO via Spark
  • Parsing Feature-level metadata into useful case classes
  • Rework LayoutDefinition to not assume Rasters. (inner TileLayout does)
  • Massage Geotrellis to accomodate VectorTileRDD
  • Inform Mapbox about the codecs

kevinzau and others added 19 commits March 17, 2016 15:18
… future, added better filter funcitonality, added more test data and test data generating examples
case LineTo(ds) => unparseCmd(2, ds.length) +: params(ds) // (+:) is bad!
case ClosePath => Array(unparseCmd(7, 1))
})
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lossyrob flatMapping here is the clean thing, but very likely not the fast thing.

fosskers added 3 commits June 29, 2016 07:44
- This is to make for easier addition of backends
- These traits don't assume a backend, and should first be extended
  by classes like `ProtobufTile`, etc.
@fosskers

Copy link
Copy Markdown
Contributor Author

@lossyrob Something I need to get back into on Monday: We'll be storing RDDs of high-level VectorTiles, right? Would such a case class need all its data fields available at the parameter level? A la the a b c in: case class Foo(a: A, b: B, c: C). If we're doing funky lazy parsing stuff within the class body, I can see there being awkwardness when dealing with RDDs / Avro serialized versions of these data types.

@fosskers

fosskers commented Jun 30, 2016 •

Copy link
Copy Markdown
Contributor Author

Unless it's okay to Avro serialize:

 case class ProtobufLayer(
   name: String,
   extent: Int,
   rawFeatures: Seq[vt.Tile.Feature] // from the source `vt.Tile.Layer`
 ) extends Layer { ... } // `Layer` is a trait from the parent package

and have these within RDDs. Then any geometry manipulation on them will be lazy and internal as usual, as if they had come straight from protobuf bytes.

@fosskers

Copy link
Copy Markdown
Contributor Author

It turns out the vt.Tile.* classes (the autogen'd protobuf classes) are all Serializable.

- Using lazy Streams here allows us to avoid strictly holding the Stream head,
  meaning no Features of a geomtype we don't care about will be parsed,
  unless we ask.

- This is advantangeous for queries as well. If you are looking for a Feature
  match on some metadata point, only Features will be parsed until you find
  what you're looking for.

- Potential disadvantage being intermittent instances of the opposite
  Single/Multi you're looking for will fully parse as well. The alternative
  is the ad-hoc reimplementation of laziness with internal mutable data
  structures.

  Consider the following scenarios, where P and MP are Polygon and
  MultiPolygon respectively:

  [P MP P MP P MP]  -- A list of alternating raw Polygon features.

  (1) The user wants to find a particular Polygon, which unknown to them
      is the second one in the list. They have to parse the first P,
      do *something* to the first MP, parse the second P and match on it,
      then stop.

      With Streams, the original list now looks like: [ MP P MP ]
      With custom laziness, it looks like: [ MP MP P MP ]

      The custom laziness wins for speed here, since we were able to
      cancel the parsing of the first MP early, and the Streams
      approach fully parsed the first MP.

  (2) The user wants to perform another operation, this time across all
      Ps. Both approaches must thus map over the entire list.

      With Streams, the original list is now empty: []
      With custom laziness, it looks like: [ MP MP MP ]

      It's harder to tell who wins here, since while the Streams
      had to waste time fully parsing each MP, the custom laziness
      had to reparse old MPs it had already looked through.

  (3) The user wants to perform another operation on all Ps. The streams
      can go ahead since everything has been parsed. The custom approach
      must reparse all the MPs to check for Ps, since it wouldn't know
      there weren't any left.

  (4) The user wants to perform an operation on MPs this time. The streams
      can go ahead since the MPs are already parsed. The custom approach
      has to reparse the MPs for the fourth time.

  My takeaway: the custom approach is better for one-off operations on a
  particular geometry type. The stream approach quickly overtakes the other
  if you plan multiple operations over the same geometries.
@fosskers

Copy link
Copy Markdown
Contributor Author

@lossyrob I did some more thinking on Streams vs custom laziness:

Consider the following scenarios, where P and MP are Polygon and MultiPolygon respectively:

[P MP P MP P MP] -- A list of alternating raw Polygon features.

(1) The user wants to find a particular Polygon, which unknown to them
is the second one in the list. They have to parse the first P,
do something to the first MP, parse the second P and match on it,
then stop.

  With Streams, the original list now looks like: [ MP P MP ]
  With custom laziness, it looks like: [ MP MP P MP ]

  The custom laziness wins for speed here, since we were able to
  cancel the parsing of the first MP early, and the Streams
  approach fully parsed the first MP.

(2) The user wants to perform another operation, this time across all
Ps. Both approaches must thus map over the entire list.

  With Streams, the original list is now empty: []
  With custom laziness, it looks like: [ MP MP MP ]

  It's harder to tell who wins here, since while the Streams
  had to waste time fully parsing each MP, the custom laziness
  had to reparse old MPs it had already looked through.

(3) The user wants to perform another operation on all Ps. The streams
can go ahead since everything has been parsed. The custom approach
must reparse all the MPs to check for Ps, since it wouldn't know
there weren't any left.

(4) The user wants to perform an operation on MPs this time. The streams
can go ahead since the MPs are already parsed. The custom approach
has to reparse the MPs for the fourth time.

My takeaway: the custom approach is better for one-off operations on a
particular geometry type. The stream approach quickly overtakes the other
if you plan multiple operations over the same geometries.

@fosskers

Copy link
Copy Markdown
Contributor Author

A good shower made me realize your three-stage idea wins out, since we'd never have to reparse MPs until Step 4 above. @echeipesh warnings of over-engineering echo in my ear, mind you.

def fromPBTile(
tile: vt.Tile,
key: SpatialKey,
layout: LayoutDefinition

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually just want to pass in the Extent. SpatialKey/LayoutDefinition are Sparky things, and vector tiles shouldn't depend on sparky things (but be used by sparky things in the layer versions of them)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already made this change in the next PR.

- And so the only thing require we during IO is that Extent. This keeps
  the API simple.
- Renamed to avoid confusion with GeoTrellis `Extent`.
- This shaves off a bit more time from the vanilla decoding process
@fosskers

fosskers commented Sep 6, 2016

Copy link
Copy Markdown
Contributor Author

@lossyrob this is good to go.

@fosskers fosskers mentioned this pull request Sep 6, 2016
7 tasks done
@fosskers

fosskers commented Sep 7, 2016

Copy link
Copy Markdown
Contributor Author

@lossyrob getMeta fix made, and README added.

@lossyrob

Copy link
Copy Markdown
Member

@fosskers just needs an update and we're good to go.

@fosskers

Copy link
Copy Markdown
Contributor Author

@lossyrob updated to master.

@lossyrob

Copy link
Copy Markdown
Member

💯

@lossyrob
lossyrob merged commit ae84839 into locationtech:master Sep 23, 2016
@lossyrob lossyrob added this to the 1.0 milestone Oct 18, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants