[mapbox-vector-tile] 01/03: Imported Upstream version 0.5.0+ds

Bas Couwenberg sebastic at debian.org
Thu Oct 27 19:34:32 UTC 2016


This is an automated email from the git hooks/post-receive script.

sebastic pushed a commit to branch master
in repository mapbox-vector-tile.

commit 82654c2f34ce465dc9efa833500236653b45bd46
Author: Bas Couwenberg <sebastic at xs4all.nl>
Date:   Thu Oct 27 20:55:47 2016 +0200

    Imported Upstream version 0.5.0+ds
---
 MANIFEST.in                                     |   1 +
 PKG-INFO                                        | 318 ++++++++++++
 README.md                                       | 308 ++++++++++++
 mapbox_vector_tile/Mapbox/__init__.py           |   0
 mapbox_vector_tile/Mapbox/vector_tile_pb2.py    | 298 +++++++++++
 mapbox_vector_tile/Mapbox/vector_tile_pb2_p3.py | 324 ++++++++++++
 mapbox_vector_tile/__init__.py                  |  23 +
 mapbox_vector_tile/compat.py                    |  15 +
 mapbox_vector_tile/decoder.py                   | 183 +++++++
 mapbox_vector_tile/encoder.py                   | 545 ++++++++++++++++++++
 setup.cfg                                       |   8 +
 setup.py                                        |  33 ++
 tests/__init__.py                               |  12 +
 tests/test_decoder.py                           |  78 +++
 tests/test_encoder.py                           | 639 ++++++++++++++++++++++++
 15 files changed, 2785 insertions(+)

diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644
index 0000000..bb3ec5f
--- /dev/null
+++ b/MANIFEST.in
@@ -0,0 +1 @@
+include README.md
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000..8a2d031
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,318 @@
+Metadata-Version: 1.0
+Name: mapbox-vector-tile
+Version: 0.5.0
+Summary: Mapbox Vector Tile
+Home-page: https://github.com/tilezen/mapbox-vector-tile
+Author: Harish Krishna
+Author-email: harish.krsn at gmail.com
+License: MIT
+Description: Mapbox Vector Tile
+        ==================
+        
+        [![Build Status](https://travis-ci.org/tilezen/mapbox-vector-tile.svg?branch=master)](https://travis-ci.org/tilezen/mapbox-vector-tile)
+        [![Coverage Status](https://coveralls.io/repos/github/tilezen/mapbox-vector-tile/badge.svg?branch=master)](https://coveralls.io/github/tilezen/mapbox-vector-tile?branch=master)
+        
+        Installation
+        ------------
+        
+        mapbox-vector-tile is compatible with Python 2.6, 2.7 and 3.5. It is listed on PyPi as `mapbox-vector-tile`. The recommended way to install is via `pip`:
+        
+        ```shell
+        pip install mapbox-vector-tile
+        ```
+        
+        Note that `mapbox-vector-tile` depends on [Shapely](https://pypi.python.org/pypi/Shapely), a Python library for computational geometry which requires a library called [GEOS](https://trac.osgeo.org/geos/). Please see [Shapely's instructions](https://pypi.python.org/pypi/Shapely#installing-shapely) for information on how to install its prerequisites.
+        
+        Encoding
+        --------
+        
+        Encode method expects an array of layers or atleast a single valid layer. A valid layer is a dictionary with the following keys
+        
+        * `name`: layer name
+        * `features`: an array of features. A feature is a dictionary with the following keys:
+        
+          * `geometry`: representation of the feature geometry in WKT, WKB, or a shapely geometry. Coordinates are relative to the tile, scaled in the range `[0, 4096)`. See below for example code to perform the necessary transformation. *Note* that `GeometryCollection` types are not supported, and will trigger a `ValueError`.
+          * `properties`: a dictionary with a few keys and their corresponding values.
+        
+        ```python
+        
+          >>> import mapbox_vector_tile
+        
+          # Using WKT
+          >>> mapbox_vector_tile.encode([
+              {
+                "name": "water",
+                "features": [
+                  {
+                    "geometry":"POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))",
+                    "properties":{
+                      "uid":123,
+                      "foo":"bar",
+                      "cat":"flew"
+                    }
+                  }
+                ]
+              },
+              {
+                "name": "air",
+                "features": [
+                  {
+                    "geometry":"LINESTRING(159 3877, -1570 3877)",
+                    "properties":{
+                      "uid":1234,
+                      "foo":"bar",
+                      "cat":"flew"
+                    }
+                  }
+                ]
+              }
+            ])
+        
+          '\x1aH\n\x05water\x12\x18\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x02 {"\x06\n\x04flew(\x80 x\x02\x1aD\n\x03air\x12\x15\x12\x06\x00\x00\x01\x01\x02\x02\x18\x02"\t\t\xbe\x02\xb6\x03\n\x81\x1b\x00\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x03 \xd2\t"\x06\n\x04flew(\x80 x\x02'
+        
+        
+          # Using WKB
+          >>> mapbox_vector_tile.encode([
+              {
+                "name": "water",
+                "features": [
+                  {
+                    "geometry":"\001\003\000\000\000\001\000\000\000\005\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000",
+                    "properties":{
+                      "uid":123,
+                      "foo":"bar",
+                      "cat":"flew"
+                    }
+                  }
+                ]
+              },
+              {
+                "name": "air",
+                "features": [
+                  {
+                    "geometry":"\001\003\000\000\000\001\000\000\000\005\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000",
+                    "properties":{
+                      "uid":1234,
+                      "foo":"bar",
+                      "cat":"flew"
+                    }
+                  }
+                ]
+              }
+              ])
+        
+          '\x1aJ\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x02 {"\x06\n\x04flew(\x80 x\x02\x1aY\n\x03air\x12\x1c\x08\x01\x12\x08\x00\x00\x01\x01\x02\x02\x03\x03\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x05balls\x1a\x03cat"\x05\n\x03bar"\x03 \xd2\t"\x05\n\x03foo"\x06\n\x04flew(\x80 x\x02'
+        ```
+        
+        ### Coordinate transformations for encoding
+        
+        The encoder expects geometries either:
+        
+        1. In tile-relative coordinates, where the lower left corner is origin and values grow up and to the right, and the tile is 4096 pixels square. For example, `POINT(0 0)` is the lower left corner of the tile and `POINT(4096, 4096)` is the upper right corner of the tile. In this case, the library does no projection, and coordinates are encoded as-is.
+        2. In another coordinate system, with the tile bounds given by the `quantize_bounds` parameter. In this case, the library will scale coordinates so that the `quantize_bounds` fit within the range (0, 4096) in both `x` and `y` directions. Aside than the affine transformation, the library does no other projection.
+        
+        It is possible to control whether the tile is in a "y down" coordinate system by setting the parameter `y_coord_down=True` on the call to `encode()`. The default is "y up".
+        
+        It is possible to control the tile extents (by default, 4096 as used in the examples above), by setting the `extents` parameter on the call to `encode()`. The default is 4096.
+        
+        If you have geometries in longitude and latitude (EPSG:4326), you can convert to tile-based coordinates by first projecting to Spherical Mercator (EPSG:3857) and then computing the pixel location within the tile. This example code uses Django's included GEOS library to do the transformation for `LineString` objects:
+        
+        ```python
+          SRID_SPHERICAL_MERCATOR = 3857
+        
+          def linestring_in_tile(tile_bounds, line):
+              # `mapbox-vector-tile` has a hardcoded tile extent of 4096 units.
+              MVT_EXTENT = 4096
+              from django.contrib.gis.geos import LineString
+        
+              # We need tile bounds in spherical mercator
+              assert tile_bounds.srid == SRID_SPHERICAL_MERCATOR
+        
+              # And we need the line to be in a known projection so we can re-project
+              assert line.srid is not None
+              line.transform(SRID_SPHERICAL_MERCATOR)
+        
+              (x0, y0, x_max, y_max) = tile_bounds.extent
+              x_span = x_max - x0
+              y_span = y_max - y0
+              def xy_pairs():
+                  for x_merc, y_merc in line:
+                      yield (
+                          int((x_merc - x0) * MVT_EXTENT / x_span),
+                          int((y_merc - y0) * MVT_EXTENT / y_span),
+        ```
+        
+        The tile bounds can be found with `mercantile`, so a complete usage example might look like this:
+        
+        ```python
+          from django.contrib.gis.geos import LineString, Polygon
+          import mercantile
+          import mapbox_vector_tile
+        
+          SRID_LNGLAT = 4326
+          SRID_SPHERICAL_MERCATOR = 3857
+        
+          tile_xyz = (2452, 3422, 18)
+          tile_bounds = Polygon.from_bbox(mercantile.bounds(*tile_xyz))
+          tile_bounds.srid = SRID_LNGLAT
+          tile_bounds.transform(SRID_SPHERICAL_MERCATOR)
+        
+          lnglat_line = LineString(((-122.1, 45.1), (-122.2, 45.2)), srid=SRID_LNGLAT)
+          tile_line = linestring_in_tile(tile_bounds, lnglat_line)
+          tile_pbf = mapbox_vector_tile.encode({
+            "name": "my-layer",
+            "features": [ {
+              "geometry": tile_line.wkt,
+              "properties": { "stuff": "things" },
+            } ]
+          })
+        ```
+        
+        Note that this example may not have anything visible within the tile when rendered. It's up to you to make sure you put the right data in the tile!
+        
+        Also note that the spec allows the extents to be modified, even though they are often set to 4096 by convention. `mapbox-vector-tile` assumes an extent of 4096.
+        
+        ### Quantization
+        
+        The encoder also has options to quantize the data for you via the `quantize_bounds` option. When encoding, pass in the bounds in the form (minx, miny, maxx, maxy) and the coordinates will be scaled appropriately during encoding.
+        
+        ```python
+        mapbox_vector_tile.encode([
+              {
+                "name": "water",
+                "features": [
+                  {
+                    "geometry":"POINT(15 15)",
+                    "properties":{
+                      "foo":"bar",
+                    }
+                  }
+                ]
+              }
+            ], quantize_bounds=(10.0, 10.0, 20.0, 20.0))
+        ```
+        
+        In this example, the coordinate that would get encoded would be (2048, 2048)
+        
+        Additionally, if the data is already in a cooridnate system with y values going down, the encoder supports an option, `y_coord_down`, that can be set to True. This will suppress flipping the y coordinate values during encoding.
+        
+        ### Custom extents
+        
+        The encoder also supports passing in custom extents. These will be passed through to the layer in the pbf, and honored during any quantization or y coordinate flipping.
+        
+        ```python
+        mapbox_vector_tile.encode([
+              {
+                "name": "water",
+                "features": [
+                  {
+                    "geometry":"POINT(15 15)",
+                    "properties":{
+                      "foo":"bar",
+                    }
+                  }
+                ]
+              }
+            ], quantize_bounds=(0.0, 0.0, 10.0, 10.0), extents=50)
+        ```
+        
+        ### Custom rounding functions
+        
+        In order to maintain consistency between Python 2 and 3, the `decimal` module is used to explictly define `ROUND_HALF_EVEN` as the rounding method. This can be slower than the built-in `round()` function. Encode takes an optional `round_fn` where you can specify the round function to be used.
+        
+         ```python
+        mapbox_vector_tile.encode([
+              {
+                "name": "water",
+                "features": [
+                  {
+                    "geometry":"POINT(15 15)",
+                    "properties":{
+                      "foo":"bar",
+                    }
+                  }
+                ]
+              }
+            ], quantize_bounds=(0.0, 0.0, 10.0, 10.0), round_fn=round)
+        ```
+        
+        Decoding
+        --------
+        
+        Decode method takes in a valid google.protobuf.message Tile and returns decoded string in the following format:
+        
+        ```python
+          {
+            layername: {
+                'extent': 'integer layer extent'
+                'version': 'integer'
+                'features': [{
+                  'geometry': 'list of points',
+                  'properties': 'dictionary of key/value pairs',
+                  'id': 'unique id for the given feature within the layer '
+                  }, ...
+                ]
+            },
+            layername2: {
+              # ...
+            }
+          }
+        ```
+        
+        ```python
+          >>> import mapbox_vector_tile
+        
+          >>> mapbox_vector_tile.decode('\x1aJ\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x02 {"\x06\n\x04flew(\x80 x\x02\x1aY\n\x03air\x12\x1c\x08\x01\x12\x08\x00\x00\x01\x01\x02\x02\x03\x03\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x05balls\x1a\x03cat"\x05\n\x03bar"\x03 \xd2\t"\x05\n\x03foo"\x06\n\x04flew(\x80 x\x02')
+        
+          {
+            'water': {
+              'extent': 4096,
+              'version': 2,
+              'features': [{
+                  'geometry': [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]],
+                  'properties': {
+                    'foo': 'bar',
+                    'uid': 123,
+                    'cat': 'flew'
+                  },
+                  'type': 3,
+                  'id': 1
+                }
+              ]
+            },
+            'air': {
+              'extent': 4096,
+              'version': 2,
+              'features': [{
+                  'geometry': [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]],
+                  'properties': {
+                    'foo': 'bar',
+                    'uid': 1234,
+                    'balls': 'foo',
+                    'cat': 'flew'
+                  },
+                  'type': 3,
+                  'id': 1
+                }
+              ]
+            }
+          }
+        ```
+        
+        Here's how you might decode a tile from a file.
+        
+        ```python
+          >>> import mapbox_vector_tile
+          >>> with open('tile.mvt', 'rb') as f:
+          >>>     data = f.read()
+          >>> decoded_data = mapbox_vector_tile.decode(data)
+          >>> with open('out.txt', 'w') as f:
+          >>>     f.write(repr(decoded_data))
+        ```
+        
+        Changelog
+        ---------
+        
+        Click [here](https://github.com/tilezen/mapbox-vector-tile/blob/master/CHANGELOG.md) to see what changed over time in various versions.
+        
+Platform: UNKNOWN
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d3c35f9
--- /dev/null
+++ b/README.md
@@ -0,0 +1,308 @@
+Mapbox Vector Tile
+==================
+
+[![Build Status](https://travis-ci.org/tilezen/mapbox-vector-tile.svg?branch=master)](https://travis-ci.org/tilezen/mapbox-vector-tile)
+[![Coverage Status](https://coveralls.io/repos/github/tilezen/mapbox-vector-tile/badge.svg?branch=master)](https://coveralls.io/github/tilezen/mapbox-vector-tile?branch=master)
+
+Installation
+------------
+
+mapbox-vector-tile is compatible with Python 2.6, 2.7 and 3.5. It is listed on PyPi as `mapbox-vector-tile`. The recommended way to install is via `pip`:
+
+```shell
+pip install mapbox-vector-tile
+```
+
+Note that `mapbox-vector-tile` depends on [Shapely](https://pypi.python.org/pypi/Shapely), a Python library for computational geometry which requires a library called [GEOS](https://trac.osgeo.org/geos/). Please see [Shapely's instructions](https://pypi.python.org/pypi/Shapely#installing-shapely) for information on how to install its prerequisites.
+
+Encoding
+--------
+
+Encode method expects an array of layers or atleast a single valid layer. A valid layer is a dictionary with the following keys
+
+* `name`: layer name
+* `features`: an array of features. A feature is a dictionary with the following keys:
+
+  * `geometry`: representation of the feature geometry in WKT, WKB, or a shapely geometry. Coordinates are relative to the tile, scaled in the range `[0, 4096)`. See below for example code to perform the necessary transformation. *Note* that `GeometryCollection` types are not supported, and will trigger a `ValueError`.
+  * `properties`: a dictionary with a few keys and their corresponding values.
+
+```python
+
+  >>> import mapbox_vector_tile
+
+  # Using WKT
+  >>> mapbox_vector_tile.encode([
+      {
+        "name": "water",
+        "features": [
+          {
+            "geometry":"POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))",
+            "properties":{
+              "uid":123,
+              "foo":"bar",
+              "cat":"flew"
+            }
+          }
+        ]
+      },
+      {
+        "name": "air",
+        "features": [
+          {
+            "geometry":"LINESTRING(159 3877, -1570 3877)",
+            "properties":{
+              "uid":1234,
+              "foo":"bar",
+              "cat":"flew"
+            }
+          }
+        ]
+      }
+    ])
+
+  '\x1aH\n\x05water\x12\x18\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x02 {"\x06\n\x04flew(\x80 x\x02\x1aD\n\x03air\x12\x15\x12\x06\x00\x00\x01\x01\x02\x02\x18\x02"\t\t\xbe\x02\xb6\x03\n\x81\x1b\x00\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x03 \xd2\t"\x06\n\x04flew(\x80 x\x02'
+
+
+  # Using WKB
+  >>> mapbox_vector_tile.encode([
+      {
+        "name": "water",
+        "features": [
+          {
+            "geometry":"\001\003\000\000\000\001\000\000\000\005\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000",
+            "properties":{
+              "uid":123,
+              "foo":"bar",
+              "cat":"flew"
+            }
+          }
+        ]
+      },
+      {
+        "name": "air",
+        "features": [
+          {
+            "geometry":"\001\003\000\000\000\001\000\000\000\005\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000",
+            "properties":{
+              "uid":1234,
+              "foo":"bar",
+              "cat":"flew"
+            }
+          }
+        ]
+      }
+      ])
+
+  '\x1aJ\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x02 {"\x06\n\x04flew(\x80 x\x02\x1aY\n\x03air\x12\x1c\x08\x01\x12\x08\x00\x00\x01\x01\x02\x02\x03\x03\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x05balls\x1a\x03cat"\x05\n\x03bar"\x03 \xd2\t"\x05\n\x03foo"\x06\n\x04flew(\x80 x\x02'
+```
+
+### Coordinate transformations for encoding
+
+The encoder expects geometries either:
+
+1. In tile-relative coordinates, where the lower left corner is origin and values grow up and to the right, and the tile is 4096 pixels square. For example, `POINT(0 0)` is the lower left corner of the tile and `POINT(4096, 4096)` is the upper right corner of the tile. In this case, the library does no projection, and coordinates are encoded as-is.
+2. In another coordinate system, with the tile bounds given by the `quantize_bounds` parameter. In this case, the library will scale coordinates so that the `quantize_bounds` fit within the range (0, 4096) in both `x` and `y` directions. Aside than the affine transformation, the library does no other projection.
+
+It is possible to control whether the tile is in a "y down" coordinate system by setting the parameter `y_coord_down=True` on the call to `encode()`. The default is "y up".
+
+It is possible to control the tile extents (by default, 4096 as used in the examples above), by setting the `extents` parameter on the call to `encode()`. The default is 4096.
+
+If you have geometries in longitude and latitude (EPSG:4326), you can convert to tile-based coordinates by first projecting to Spherical Mercator (EPSG:3857) and then computing the pixel location within the tile. This example code uses Django's included GEOS library to do the transformation for `LineString` objects:
+
+```python
+  SRID_SPHERICAL_MERCATOR = 3857
+
+  def linestring_in_tile(tile_bounds, line):
+      # `mapbox-vector-tile` has a hardcoded tile extent of 4096 units.
+      MVT_EXTENT = 4096
+      from django.contrib.gis.geos import LineString
+
+      # We need tile bounds in spherical mercator
+      assert tile_bounds.srid == SRID_SPHERICAL_MERCATOR
+
+      # And we need the line to be in a known projection so we can re-project
+      assert line.srid is not None
+      line.transform(SRID_SPHERICAL_MERCATOR)
+
+      (x0, y0, x_max, y_max) = tile_bounds.extent
+      x_span = x_max - x0
+      y_span = y_max - y0
+      def xy_pairs():
+          for x_merc, y_merc in line:
+              yield (
+                  int((x_merc - x0) * MVT_EXTENT / x_span),
+                  int((y_merc - y0) * MVT_EXTENT / y_span),
+```
+
+The tile bounds can be found with `mercantile`, so a complete usage example might look like this:
+
+```python
+  from django.contrib.gis.geos import LineString, Polygon
+  import mercantile
+  import mapbox_vector_tile
+
+  SRID_LNGLAT = 4326
+  SRID_SPHERICAL_MERCATOR = 3857
+
+  tile_xyz = (2452, 3422, 18)
+  tile_bounds = Polygon.from_bbox(mercantile.bounds(*tile_xyz))
+  tile_bounds.srid = SRID_LNGLAT
+  tile_bounds.transform(SRID_SPHERICAL_MERCATOR)
+
+  lnglat_line = LineString(((-122.1, 45.1), (-122.2, 45.2)), srid=SRID_LNGLAT)
+  tile_line = linestring_in_tile(tile_bounds, lnglat_line)
+  tile_pbf = mapbox_vector_tile.encode({
+    "name": "my-layer",
+    "features": [ {
+      "geometry": tile_line.wkt,
+      "properties": { "stuff": "things" },
+    } ]
+  })
+```
+
+Note that this example may not have anything visible within the tile when rendered. It's up to you to make sure you put the right data in the tile!
+
+Also note that the spec allows the extents to be modified, even though they are often set to 4096 by convention. `mapbox-vector-tile` assumes an extent of 4096.
+
+### Quantization
+
+The encoder also has options to quantize the data for you via the `quantize_bounds` option. When encoding, pass in the bounds in the form (minx, miny, maxx, maxy) and the coordinates will be scaled appropriately during encoding.
+
+```python
+mapbox_vector_tile.encode([
+      {
+        "name": "water",
+        "features": [
+          {
+            "geometry":"POINT(15 15)",
+            "properties":{
+              "foo":"bar",
+            }
+          }
+        ]
+      }
+    ], quantize_bounds=(10.0, 10.0, 20.0, 20.0))
+```
+
+In this example, the coordinate that would get encoded would be (2048, 2048)
+
+Additionally, if the data is already in a cooridnate system with y values going down, the encoder supports an option, `y_coord_down`, that can be set to True. This will suppress flipping the y coordinate values during encoding.
+
+### Custom extents
+
+The encoder also supports passing in custom extents. These will be passed through to the layer in the pbf, and honored during any quantization or y coordinate flipping.
+
+```python
+mapbox_vector_tile.encode([
+      {
+        "name": "water",
+        "features": [
+          {
+            "geometry":"POINT(15 15)",
+            "properties":{
+              "foo":"bar",
+            }
+          }
+        ]
+      }
+    ], quantize_bounds=(0.0, 0.0, 10.0, 10.0), extents=50)
+```
+
+### Custom rounding functions
+
+In order to maintain consistency between Python 2 and 3, the `decimal` module is used to explictly define `ROUND_HALF_EVEN` as the rounding method. This can be slower than the built-in `round()` function. Encode takes an optional `round_fn` where you can specify the round function to be used.
+
+ ```python
+mapbox_vector_tile.encode([
+      {
+        "name": "water",
+        "features": [
+          {
+            "geometry":"POINT(15 15)",
+            "properties":{
+              "foo":"bar",
+            }
+          }
+        ]
+      }
+    ], quantize_bounds=(0.0, 0.0, 10.0, 10.0), round_fn=round)
+```
+
+Decoding
+--------
+
+Decode method takes in a valid google.protobuf.message Tile and returns decoded string in the following format:
+
+```python
+  {
+    layername: {
+        'extent': 'integer layer extent'
+        'version': 'integer'
+        'features': [{
+          'geometry': 'list of points',
+          'properties': 'dictionary of key/value pairs',
+          'id': 'unique id for the given feature within the layer '
+          }, ...
+        ]
+    },
+    layername2: {
+      # ...
+    }
+  }
+```
+
+```python
+  >>> import mapbox_vector_tile
+
+  >>> mapbox_vector_tile.decode('\x1aJ\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x03cat"\x05\n\x03bar"\x02 {"\x06\n\x04flew(\x80 x\x02\x1aY\n\x03air\x12\x1c\x08\x01\x12\x08\x00\x00\x01\x01\x02\x02\x03\x03\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03uid\x1a\x05balls\x1a\x03cat"\x05\n\x03bar"\x03 \xd2\t"\x05\n\x03foo"\x06\n\x04flew(\x80 x\x02')
+
+  {
+    'water': {
+      'extent': 4096,
+      'version': 2,
+      'features': [{
+          'geometry': [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]],
+          'properties': {
+            'foo': 'bar',
+            'uid': 123,
+            'cat': 'flew'
+          },
+          'type': 3,
+          'id': 1
+        }
+      ]
+    },
+    'air': {
+      'extent': 4096,
+      'version': 2,
+      'features': [{
+          'geometry': [[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]],
+          'properties': {
+            'foo': 'bar',
+            'uid': 1234,
+            'balls': 'foo',
+            'cat': 'flew'
+          },
+          'type': 3,
+          'id': 1
+        }
+      ]
+    }
+  }
+```
+
+Here's how you might decode a tile from a file.
+
+```python
+  >>> import mapbox_vector_tile
+  >>> with open('tile.mvt', 'rb') as f:
+  >>>     data = f.read()
+  >>> decoded_data = mapbox_vector_tile.decode(data)
+  >>> with open('out.txt', 'w') as f:
+  >>>     f.write(repr(decoded_data))
+```
+
+Changelog
+---------
+
+Click [here](https://github.com/tilezen/mapbox-vector-tile/blob/master/CHANGELOG.md) to see what changed over time in various versions.
diff --git a/mapbox_vector_tile/Mapbox/__init__.py b/mapbox_vector_tile/Mapbox/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/mapbox_vector_tile/Mapbox/vector_tile_pb2.py b/mapbox_vector_tile/Mapbox/vector_tile_pb2.py
new file mode 100644
index 0000000..6c1bee2
--- /dev/null
+++ b/mapbox_vector_tile/Mapbox/vector_tile_pb2.py
@@ -0,0 +1,298 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: vector_tile.proto
+
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='vector_tile.proto',
+  package='mapnik.vector',
+  serialized_pb='\n\x11vector_tile.proto\x12\rmapnik.vector\"\xc5\x04\n\x04tile\x12)\n\x06layers\x18\x03 \x03(\x0b\x32\x19.mapnik.vector.tile.layer\x1a\xa1\x01\n\x05value\x12\x14\n\x0cstring_value\x18\x01 \x01(\t\x12\x13\n\x0b\x66loat_value\x18\x02 \x01(\x02\x12\x14\n\x0c\x64ouble_value\x18\x03 \x01(\x01\x12\x11\n\tint_value\x18\x04 \x01(\x03\x12\x12\n\nuint_value\x18\x05 \x01(\x04\x12\x12\n\nsint_value\x18\x06 \x01(\x12\x12\x12\n\nbool_value\x18\x07 \x01(\x08*\x08\x08\x08\x10\x80\x80\x8 [...]
+
+
+
+_TILE_GEOMTYPE = _descriptor.EnumDescriptor(
+  name='GeomType',
+  full_name='mapnik.vector.tile.GeomType',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='Unknown', index=0, number=0,
+      options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='Point', index=1, number=1,
+      options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='LineString', index=2, number=2,
+      options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='Polygon', index=3, number=3,
+      options=None,
+      type=None),
+  ],
+  containing_type=None,
+  options=None,
+  serialized_start=548,
+  serialized_end=611,
+)
+
+
+_TILE_VALUE = _descriptor.Descriptor(
+  name='value',
+  full_name='mapnik.vector.tile.value',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='string_value', full_name='mapnik.vector.tile.value.string_value', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='float_value', full_name='mapnik.vector.tile.value.float_value', index=1,
+      number=2, type=2, cpp_type=6, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='double_value', full_name='mapnik.vector.tile.value.double_value', index=2,
+      number=3, type=1, cpp_type=5, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='int_value', full_name='mapnik.vector.tile.value.int_value', index=3,
+      number=4, type=3, cpp_type=2, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='uint_value', full_name='mapnik.vector.tile.value.uint_value', index=4,
+      number=5, type=4, cpp_type=4, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='sint_value', full_name='mapnik.vector.tile.value.sint_value', index=5,
+      number=6, type=18, cpp_type=2, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='bool_value', full_name='mapnik.vector.tile.value.bool_value', index=6,
+      number=7, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=True,
+  extension_ranges=[(8, 536870912), ],
+  serialized_start=89,
+  serialized_end=250,
+)
+
+_TILE_FEATURE = _descriptor.Descriptor(
+  name='feature',
+  full_name='mapnik.vector.tile.feature',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='id', full_name='mapnik.vector.tile.feature.id', index=0,
+      number=1, type=4, cpp_type=4, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='tags', full_name='mapnik.vector.tile.feature.tags', index=1,
+      number=2, type=13, cpp_type=3, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), '\020\001')),
+    _descriptor.FieldDescriptor(
+      name='type', full_name='mapnik.vector.tile.feature.type', index=2,
+      number=3, type=14, cpp_type=8, label=1,
+      has_default_value=True, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='geometry', full_name='mapnik.vector.tile.feature.geometry', index=3,
+      number=4, type=13, cpp_type=3, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), '\020\001')),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  extension_ranges=[],
+  serialized_start=252,
+  serialized_end=366,
+)
+
+_TILE_LAYER = _descriptor.Descriptor(
+  name='layer',
+  full_name='mapnik.vector.tile.layer',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='version', full_name='mapnik.vector.tile.layer.version', index=0,
+      number=15, type=13, cpp_type=3, label=2,
+      has_default_value=True, default_value=1,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='name', full_name='mapnik.vector.tile.layer.name', index=1,
+      number=1, type=9, cpp_type=9, label=2,
+      has_default_value=False, default_value=unicode("", "utf-8"),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='features', full_name='mapnik.vector.tile.layer.features', index=2,
+      number=2, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='keys', full_name='mapnik.vector.tile.layer.keys', index=3,
+      number=3, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='values', full_name='mapnik.vector.tile.layer.values', index=4,
+      number=4, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='extent', full_name='mapnik.vector.tile.layer.extent', index=5,
+      number=5, type=13, cpp_type=3, label=1,
+      has_default_value=True, default_value=4096,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=True,
+  extension_ranges=[(16, 536870912), ],
+  serialized_start=369,
+  serialized_end=546,
+)
+
+_TILE = _descriptor.Descriptor(
+  name='tile',
+  full_name='mapnik.vector.tile',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='layers', full_name='mapnik.vector.tile.layers', index=0,
+      number=3, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_TILE_VALUE, _TILE_FEATURE, _TILE_LAYER, ],
+  enum_types=[
+    _TILE_GEOMTYPE,
+  ],
+  options=None,
+  is_extendable=True,
+  extension_ranges=[(16, 8192), ],
+  serialized_start=37,
+  serialized_end=618,
+)
+
+_TILE_VALUE.containing_type = _TILE;
+_TILE_FEATURE.fields_by_name['type'].enum_type = _TILE_GEOMTYPE
+_TILE_FEATURE.containing_type = _TILE;
+_TILE_LAYER.fields_by_name['features'].message_type = _TILE_FEATURE
+_TILE_LAYER.fields_by_name['values'].message_type = _TILE_VALUE
+_TILE_LAYER.containing_type = _TILE;
+_TILE.fields_by_name['layers'].message_type = _TILE_LAYER
+_TILE_GEOMTYPE.containing_type = _TILE;
+DESCRIPTOR.message_types_by_name['tile'] = _TILE
+
+class tile(_message.Message):
+  __metaclass__ = _reflection.GeneratedProtocolMessageType
+
+  class value(_message.Message):
+    __metaclass__ = _reflection.GeneratedProtocolMessageType
+    DESCRIPTOR = _TILE_VALUE
+
+    # @@protoc_insertion_point(class_scope:mapnik.vector.tile.value)
+
+  class feature(_message.Message):
+    __metaclass__ = _reflection.GeneratedProtocolMessageType
+    DESCRIPTOR = _TILE_FEATURE
+
+    # @@protoc_insertion_point(class_scope:mapnik.vector.tile.feature)
+
+  class layer(_message.Message):
+    __metaclass__ = _reflection.GeneratedProtocolMessageType
+    DESCRIPTOR = _TILE_LAYER
+
+    # @@protoc_insertion_point(class_scope:mapnik.vector.tile.layer)
+  DESCRIPTOR = _TILE
+
+  # @@protoc_insertion_point(class_scope:mapnik.vector.tile)
+
+
+DESCRIPTOR.has_options = True
+DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), 'H\003')
+_TILE_FEATURE.fields_by_name['tags'].has_options = True
+_TILE_FEATURE.fields_by_name['tags']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), '\020\001')
+_TILE_FEATURE.fields_by_name['geometry'].has_options = True
+_TILE_FEATURE.fields_by_name['geometry']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), '\020\001')
+# @@protoc_insertion_point(module_scope)
diff --git a/mapbox_vector_tile/Mapbox/vector_tile_pb2_p3.py b/mapbox_vector_tile/Mapbox/vector_tile_pb2_p3.py
new file mode 100644
index 0000000..71ce6a3
--- /dev/null
+++ b/mapbox_vector_tile/Mapbox/vector_tile_pb2_p3.py
@@ -0,0 +1,324 @@
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: vector_tile.proto
+
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf import descriptor_pb2
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='vector_tile.proto',
+  package='mapnik.vector',
+  syntax='proto2',
+  serialized_pb=b'\n\x11vector_tile.proto\x12\rmapnik.vector\"\xc5\x04\n\x04tile\x12)\n\x06layers\x18\x03 \x03(\x0b\x32\x19.mapnik.vector.tile.layer\x1a\xa1\x01\n\x05value\x12\x14\n\x0cstring_value\x18\x01 \x01(\t\x12\x13\n\x0b\x66loat_value\x18\x02 \x01(\x02\x12\x14\n\x0c\x64ouble_value\x18\x03 \x01(\x01\x12\x11\n\tint_value\x18\x04 \x01(\x03\x12\x12\n\nuint_value\x18\x05 \x01(\x04\x12\x12\n\nsint_value\x18\x06 \x01(\x12\x12\x12\n\nbool_value\x18\x07 \x01(\x08*\x08\x08\x08\x10\x80\x80\x [...]
+)
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+
+
+_TILE_GEOMTYPE = _descriptor.EnumDescriptor(
+  name='GeomType',
+  full_name='mapnik.vector.tile.GeomType',
+  filename=None,
+  file=DESCRIPTOR,
+  values=[
+    _descriptor.EnumValueDescriptor(
+      name='Unknown', index=0, number=0,
+      options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='Point', index=1, number=1,
+      options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='LineString', index=2, number=2,
+      options=None,
+      type=None),
+    _descriptor.EnumValueDescriptor(
+      name='Polygon', index=3, number=3,
+      options=None,
+      type=None),
+  ],
+  containing_type=None,
+  options=None,
+  serialized_start=548,
+  serialized_end=611,
+)
+_sym_db.RegisterEnumDescriptor(_TILE_GEOMTYPE)
+
+
+_TILE_VALUE = _descriptor.Descriptor(
+  name='value',
+  full_name='mapnik.vector.tile.value',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='string_value', full_name='mapnik.vector.tile.value.string_value', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='float_value', full_name='mapnik.vector.tile.value.float_value', index=1,
+      number=2, type=2, cpp_type=6, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='double_value', full_name='mapnik.vector.tile.value.double_value', index=2,
+      number=3, type=1, cpp_type=5, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='int_value', full_name='mapnik.vector.tile.value.int_value', index=3,
+      number=4, type=3, cpp_type=2, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='uint_value', full_name='mapnik.vector.tile.value.uint_value', index=4,
+      number=5, type=4, cpp_type=4, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='sint_value', full_name='mapnik.vector.tile.value.sint_value', index=5,
+      number=6, type=18, cpp_type=2, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='bool_value', full_name='mapnik.vector.tile.value.bool_value', index=6,
+      number=7, type=8, cpp_type=7, label=1,
+      has_default_value=False, default_value=False,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=True,
+  syntax='proto2',
+  extension_ranges=[(8, 536870912), ],
+  oneofs=[
+  ],
+  serialized_start=89,
+  serialized_end=250,
+)
+
+_TILE_FEATURE = _descriptor.Descriptor(
+  name='feature',
+  full_name='mapnik.vector.tile.feature',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='id', full_name='mapnik.vector.tile.feature.id', index=0,
+      number=1, type=4, cpp_type=4, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='tags', full_name='mapnik.vector.tile.feature.tags', index=1,
+      number=2, type=13, cpp_type=3, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), b'\020\001')),
+    _descriptor.FieldDescriptor(
+      name='type', full_name='mapnik.vector.tile.feature.type', index=2,
+      number=3, type=14, cpp_type=8, label=1,
+      has_default_value=True, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='geometry', full_name='mapnik.vector.tile.feature.geometry', index=3,
+      number=4, type=13, cpp_type=3, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=_descriptor._ParseOptions(descriptor_pb2.FieldOptions(), b'\020\001')),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=False,
+  syntax='proto2',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=252,
+  serialized_end=366,
+)
+
+_TILE_LAYER = _descriptor.Descriptor(
+  name='layer',
+  full_name='mapnik.vector.tile.layer',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='version', full_name='mapnik.vector.tile.layer.version', index=0,
+      number=15, type=13, cpp_type=3, label=2,
+      has_default_value=True, default_value=1,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='name', full_name='mapnik.vector.tile.layer.name', index=1,
+      number=1, type=9, cpp_type=9, label=2,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='features', full_name='mapnik.vector.tile.layer.features', index=2,
+      number=2, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='keys', full_name='mapnik.vector.tile.layer.keys', index=3,
+      number=3, type=9, cpp_type=9, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='values', full_name='mapnik.vector.tile.layer.values', index=4,
+      number=4, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+    _descriptor.FieldDescriptor(
+      name='extent', full_name='mapnik.vector.tile.layer.extent', index=5,
+      number=5, type=13, cpp_type=3, label=1,
+      has_default_value=True, default_value=4096,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  options=None,
+  is_extendable=True,
+  syntax='proto2',
+  extension_ranges=[(16, 536870912), ],
+  oneofs=[
+  ],
+  serialized_start=369,
+  serialized_end=546,
+)
+
+_TILE = _descriptor.Descriptor(
+  name='tile',
+  full_name='mapnik.vector.tile',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='layers', full_name='mapnik.vector.tile.layers', index=0,
+      number=3, type=11, cpp_type=10, label=3,
+      has_default_value=False, default_value=[],
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      options=None),
+  ],
+  extensions=[
+  ],
+  nested_types=[_TILE_VALUE, _TILE_FEATURE, _TILE_LAYER, ],
+  enum_types=[
+    _TILE_GEOMTYPE,
+  ],
+  options=None,
+  is_extendable=True,
+  syntax='proto2',
+  extension_ranges=[(16, 8192), ],
+  oneofs=[
+  ],
+  serialized_start=37,
+  serialized_end=618,
+)
+
+_TILE_VALUE.containing_type = _TILE
+_TILE_FEATURE.fields_by_name['type'].enum_type = _TILE_GEOMTYPE
+_TILE_FEATURE.containing_type = _TILE
+_TILE_LAYER.fields_by_name['features'].message_type = _TILE_FEATURE
+_TILE_LAYER.fields_by_name['values'].message_type = _TILE_VALUE
+_TILE_LAYER.containing_type = _TILE
+_TILE.fields_by_name['layers'].message_type = _TILE_LAYER
+_TILE_GEOMTYPE.containing_type = _TILE
+DESCRIPTOR.message_types_by_name['tile'] = _TILE
+
+tile = _reflection.GeneratedProtocolMessageType('tile', (_message.Message,), dict(
+
+  value = _reflection.GeneratedProtocolMessageType('value', (_message.Message,), dict(
+    DESCRIPTOR = _TILE_VALUE,
+    __module__ = 'vector_tile_pb2_p3'
+    # @@protoc_insertion_point(class_scope:mapnik.vector.tile.value)
+    ))
+  ,
+
+  feature = _reflection.GeneratedProtocolMessageType('feature', (_message.Message,), dict(
+    DESCRIPTOR = _TILE_FEATURE,
+    __module__ = 'vector_tile_pb2_p3'
+    # @@protoc_insertion_point(class_scope:mapnik.vector.tile.feature)
+    ))
+  ,
+
+  layer = _reflection.GeneratedProtocolMessageType('layer', (_message.Message,), dict(
+    DESCRIPTOR = _TILE_LAYER,
+    __module__ = 'vector_tile_pb2_p3'
+    # @@protoc_insertion_point(class_scope:mapnik.vector.tile.layer)
+    ))
+  ,
+  DESCRIPTOR = _TILE,
+  __module__ = 'vector_tile_pb2_p3'
+  # @@protoc_insertion_point(class_scope:mapnik.vector.tile)
+  ))
+_sym_db.RegisterMessage(tile)
+_sym_db.RegisterMessage(tile.value)
+_sym_db.RegisterMessage(tile.feature)
+_sym_db.RegisterMessage(tile.layer)
+
+
+DESCRIPTOR.has_options = True
+DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), b'H\003')
+_TILE_FEATURE.fields_by_name['tags'].has_options = True
+_TILE_FEATURE.fields_by_name['tags']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), b'\020\001')
+_TILE_FEATURE.fields_by_name['geometry'].has_options = True
+_TILE_FEATURE.fields_by_name['geometry']._options = _descriptor._ParseOptions(descriptor_pb2.FieldOptions(), b'\020\001')
+# @@protoc_insertion_point(module_scope)
diff --git a/mapbox_vector_tile/__init__.py b/mapbox_vector_tile/__init__.py
new file mode 100644
index 0000000..02502fd
--- /dev/null
+++ b/mapbox_vector_tile/__init__.py
@@ -0,0 +1,23 @@
+from . import encoder
+from . import decoder
+
+
+def decode(tile, y_coord_down=False):
+    vector_tile = decoder.TileData()
+    message = vector_tile.getMessage(tile, y_coord_down)
+    return message
+
+
+def encode(layers, quantize_bounds=None, y_coord_down=False, extents=4096,
+           on_invalid_geometry=None, round_fn=None):
+    vector_tile = encoder.VectorTile(extents, on_invalid_geometry,
+                                     round_fn=round_fn)
+    if (isinstance(layers, list)):
+        for layer in layers:
+            vector_tile.addFeatures(layer['features'], layer['name'],
+                                    quantize_bounds, y_coord_down)
+    else:
+        vector_tile.addFeatures(layers['features'], layers['name'],
+                                quantize_bounds, y_coord_down)
+
+    return vector_tile.tile.SerializeToString()
diff --git a/mapbox_vector_tile/compat.py b/mapbox_vector_tile/compat.py
new file mode 100644
index 0000000..7abb584
--- /dev/null
+++ b/mapbox_vector_tile/compat.py
@@ -0,0 +1,15 @@
+import sys
+from builtins import map
+
+PY3 = sys.version_info[0] == 3
+
+if PY3:
+    from .Mapbox import vector_tile_pb2_p3
+    vector_tile = vector_tile_pb2_p3
+else:
+    from .Mapbox import vector_tile_pb2
+    vector_tile = vector_tile_pb2
+
+
+def apply_map(fn, x):
+    return list(map(fn, x))
diff --git a/mapbox_vector_tile/decoder.py b/mapbox_vector_tile/decoder.py
new file mode 100644
index 0000000..222e254
--- /dev/null
+++ b/mapbox_vector_tile/decoder.py
@@ -0,0 +1,183 @@
+from past.builtins import xrange
+from .compat import vector_tile
+
+cmd_bits = 3
+
+CMD_MOVE_TO = 1
+CMD_LINE_TO = 2
+CMD_SEG_END = 7
+
+UNKNOWN = 0
+POINT = 1
+LINESTRING = 2
+POLYGON = 3
+
+
+class TileData:
+    """
+    """
+
+    def __init__(self):
+        self.tile = vector_tile.tile()
+
+    def getMessage(self, pbf_data, y_coord_down=False):
+        self.tile.ParseFromString(pbf_data)
+
+        tile = {}
+        for layer in self.tile.layers:
+            keys = layer.keys
+            vals = layer.values
+
+            features = []
+            for feature in layer.features:
+                tags = feature.tags
+                props = {}
+                assert len(tags) % 2 == 0, 'Unexpected number of tags'
+                for key_idx, val_idx in zip(tags[::2], tags[1::2]):
+                    key = keys[key_idx]
+                    val = vals[val_idx]
+                    value = self.parse_value(val)
+                    props[key] = value
+
+                geometry = self.parse_geometry(feature.geometry, feature.type,
+                                               layer.extent, y_coord_down)
+                new_feature = {
+                    "geometry": geometry,
+                    "properties": props,
+                    "id": feature.id,
+                    "type": feature.type
+                }
+                features.append(new_feature)
+
+            tile[layer.name] = {
+                "extent": layer.extent,
+                "version": layer.version,
+                "features": features,
+            }
+        return tile
+
+    def zero_pad(self, val):
+        return '0' + val if val[0] == 'b' else val
+
+    def parse_value(self, val):
+        for candidate in ('bool_value',
+                          'double_value',
+                          'float_value',
+                          'int_value',
+                          'sint_value',
+                          'string_value',
+                          'uint_value'):
+            if val.HasField(candidate):
+                return getattr(val, candidate)
+        raise ValueError('%s is an unknown value')
+
+    def zig_zag_decode(self, n):
+        return (n >> 1) ^ (-(n & 1))
+
+    def parse_geometry(self, geom, ftype, extent, y_coord_down):
+        # [9 0 8192 26 0 10 2 0 0 2 15]
+        i = 0
+        coords = []
+        dx = 0
+        dy = 0
+        parts = []  # for multi linestrings and polygons
+
+        while i != len(geom):
+            item = bin(geom[i])
+            ilen = len(item)
+            cmd = int(self.zero_pad(item[(ilen - cmd_bits):ilen]), 2)
+            cmd_len = int(self.zero_pad(item[:ilen - cmd_bits]), 2)
+
+            i = i + 1
+
+            def _ensure_polygon_closed(coords):
+                if coords and coords[0] != coords[-1]:
+                    coords.append(coords[0])
+
+            if cmd == CMD_SEG_END:
+                if ftype == POLYGON:
+                    _ensure_polygon_closed(coords)
+                parts.append(coords)
+                coords = []
+
+            elif cmd == CMD_MOVE_TO or cmd == CMD_LINE_TO:
+
+                if coords and cmd == CMD_MOVE_TO:
+                    if ftype in (LINESTRING, POLYGON):
+                        # multi line string or polygon
+                        # our encoder includes CMD_SEG_END to denote
+                        # the end of a polygon ring, but this path
+                        # would also handle the case where we receive
+                        # a move without a previous close on polygons
+
+                        # for polygons, we want to ensure that it is
+                        # closed
+                        if ftype == POLYGON:
+                            _ensure_polygon_closed(coords)
+                        parts.append(coords)
+                        coords = []
+
+                for point in xrange(0, cmd_len):
+                    x = geom[i]
+                    i = i + 1
+
+                    y = geom[i]
+                    i = i + 1
+
+                    # zipzag decode
+                    x = self.zig_zag_decode(x)
+                    y = self.zig_zag_decode(y)
+
+                    x = x + dx
+                    y = y + dy
+
+                    dx = x
+                    dy = y
+
+                    if not y_coord_down:
+                        y = extent - y
+
+                    coords.append([x, y])
+
+        if ftype == POINT:
+            return coords
+        elif ftype == LINESTRING:
+            if parts:
+                if coords:
+                    parts.append(coords)
+                return parts[0] if len(parts) == 1 else parts
+            else:
+                return coords
+        elif ftype == POLYGON:
+            if coords:
+                parts.append(coords)
+
+            def _area_sign(ring):
+                a = sum(ring[i][0]*ring[i+1][1] - ring[i+1][0]*ring[i][1] for i in range(0, len(ring)-1))  # noqa
+                return -1 if a < 0 else 1 if a > 0 else 0
+
+            polygon = []
+            polygons = []
+            winding = 0
+
+            for ring in parts:
+                a = _area_sign(ring)
+                if a == 0:
+                    continue
+                if winding == 0:
+                    winding = a
+
+                if winding == a:
+                    if polygon:
+                        polygons.append(polygon)
+                    polygon = [ring]
+                else:
+                    polygon.append(ring)
+
+            if polygon:
+                polygons.append(polygon)
+
+            return polygons[0] if len(polygons) == 1 else polygons
+
+        else:
+            raise ValueError('Unknown geometry type: %s' % ftype)
diff --git a/mapbox_vector_tile/encoder.py b/mapbox_vector_tile/encoder.py
new file mode 100644
index 0000000..22c187c
--- /dev/null
+++ b/mapbox_vector_tile/encoder.py
@@ -0,0 +1,545 @@
+from math import fabs
+from numbers import Number
+from past.builtins import long
+from past.builtins import unicode
+from past.builtins import xrange
+from shapely.geometry.base import BaseGeometry
+from shapely.geometry.multipolygon import MultiPolygon
+from shapely.geometry.polygon import orient
+from shapely.geometry.polygon import Polygon
+from shapely.geometry.polygon import LinearRing
+from shapely.ops import transform
+from shapely.wkb import loads as load_wkb
+from shapely.wkt import loads as load_wkt
+import decimal
+from .compat import PY3, vector_tile, apply_map
+
+
+# tiles are padded by this number of pixels for the current zoom level
+padding = 0
+
+cmd_bits = 3
+tolerance = 0
+
+CMD_MOVE_TO = 1
+CMD_LINE_TO = 2
+CMD_SEG_END = 7
+
+
+def on_invalid_geometry_raise(shape):
+    raise ValueError('Invalid geometry: %s' % shape.wkt)
+
+
+def on_invalid_geometry_ignore(shape):
+    return None
+
+
+def reverse_ring(shape):
+    assert shape.type == 'LinearRing'
+    return LinearRing(list(shape.coords)[::-1])
+
+
+def reverse_polygon(shape):
+    assert shape.type == 'Polygon'
+
+    exterior = reverse_ring(shape.exterior)
+    interiors = [reverse_ring(r) for r in shape.interiors]
+
+    return Polygon(exterior, interiors)
+
+
+def make_valid_polygon_flip(shape):
+    assert shape.type == 'Polygon'
+    # to handle cases where the area of the polygon is zero, we need to
+    # manually reverse the coords in the polygon, then buffer(0) it to make it
+    # valid in reverse, then reverse them again to get back to the original,
+    # intended orientation.
+
+    flipped = reverse_polygon(shape)
+    fixed = flipped.buffer(0)
+
+    if fixed.is_empty:
+        return None
+    else:
+        return reverse_polygon(fixed)
+
+
+def area_bounds(shape):
+    if shape.is_empty:
+        return 0
+
+    minx, miny, maxx, maxy = shape.bounds
+    return (maxx - minx) * (maxy - miny)
+
+
+def make_valid_polygon(shape):
+    prev_area = area_bounds(shape)
+    new_shape = shape.buffer(0)
+    assert new_shape.is_valid, \
+        'buffer(0) did not make geometry valid. old shape: %s' % shape.wkt
+    new_area = area_bounds(new_shape)
+
+    if new_area < 0.9 * prev_area:
+        alt_shape = make_valid_polygon_flip(shape)
+        if alt_shape:
+            new_shape = new_shape.union(alt_shape)
+
+    return new_shape
+
+
+def make_valid_multipolygon(shape):
+    new_g = []
+
+    for g in shape.geoms:
+        if g.is_empty:
+            continue
+
+        valid_g = on_invalid_geometry_make_valid(g)
+
+        if valid_g.type == 'MultiPolygon':
+            new_g.extend(valid_g.geoms)
+        else:
+            new_g.append(valid_g)
+
+    return MultiPolygon(new_g)
+
+
+def on_invalid_geometry_make_valid(shape):
+    if shape.is_empty:
+        return shape
+
+    elif shape.type == 'MultiPolygon':
+        shape = make_valid_multipolygon(shape)
+
+    elif shape.type == 'Polygon':
+        shape = make_valid_polygon(shape)
+
+    return shape
+
+
+class VectorTile:
+    """
+    """
+
+    def __init__(self, extents, on_invalid_geometry=None,
+                 max_geometry_validate_tries=5, round_fn=None):
+        self.tile = vector_tile.tile()
+        self.extents = extents
+        self.on_invalid_geometry = on_invalid_geometry
+        self.max_geometry_validate_tries = max_geometry_validate_tries
+        self.round_fn = round_fn
+
+    def _round(self, val):
+        # Prefer provided round function.
+        if self.round_fn:
+            return self.round_fn(val)
+
+        # round() has different behavior in python 2/3
+        # For consistency between 2 and 3 we use quantize, however
+        # it is slower than the built in round function.
+        d = decimal.Decimal(val)
+        rounded = d.quantize(1, rounding=decimal.ROUND_HALF_EVEN)
+        return float(rounded)
+
+    def addFeatures(self, features, layer_name='',
+                    quantize_bounds=None, y_coord_down=False):
+
+        self.layer = self.tile.layers.add()
+        self.layer.name = layer_name
+        self.layer.version = 1
+        self.layer.extent = self.extents
+
+        self.key_idx = 0
+        self.val_idx = 0
+        self.seen_keys_idx = {}
+        self.seen_values_idx = {}
+
+        for feature in features:
+
+            # skip missing or empty geometries
+            geometry_spec = feature.get('geometry')
+            if geometry_spec is None:
+                continue
+            shape = self._load_geometry(geometry_spec)
+
+            if shape is None:
+                raise NotImplementedError(
+                    'Can\'t do geometries that are not wkt, wkb, or shapely '
+                    'geometries')
+
+            if shape.is_empty:
+                continue
+
+            if quantize_bounds:
+                shape = self.quantize(shape, quantize_bounds)
+
+            shape = self.enforce_winding_order(shape, y_coord_down)
+
+            if shape is not None and not shape.is_empty:
+                self.addFeature(feature, shape, y_coord_down)
+
+    def enforce_winding_order(self, shape, y_coord_down, n_try=1):
+        if shape.type == 'MultiPolygon':
+            # If we are a multipolygon, we need to ensure that the
+            # winding orders of the consituent polygons are
+            # correct. In particular, the winding order of the
+            # interior rings need to be the opposite of the
+            # exterior ones, and all interior rings need to follow
+            # the exterior one. This is how the end of one polygon
+            # and the beginning of another are signaled.
+            shape = self.enforce_multipolygon_winding_order(
+                shape, y_coord_down, n_try)
+
+        elif shape.type == 'Polygon':
+            # Ensure that polygons are also oriented with the
+            # appropriate winding order. Their exterior rings must
+            # have a clockwise order, which is translated into a
+            # clockwise order in MVT's tile-local coordinates with
+            # the Y axis in "screen" (i.e: +ve down) configuration.
+            # Note that while the Y axis flips, we also invert the
+            # Y coordinate to get the tile-local value, which means
+            # the clockwise orientation is unchanged.
+            shape = self.enforce_polygon_winding_order(
+                shape, y_coord_down, n_try)
+
+        # other shapes just get passed through
+        return shape
+
+    def quantize(self, shape, bounds):
+        minx, miny, maxx, maxy = bounds
+
+        def fn(x, y, z=None):
+            xfac = self.extents / (maxx - minx)
+            yfac = self.extents / (maxy - miny)
+            x = xfac * (x - minx)
+            y = yfac * (y - miny)
+            return self._round(x), self._round(y)
+
+        return transform(fn, shape)
+
+    def handle_shape_validity(self, shape, y_coord_down, n_try):
+        if shape.is_valid:
+            return shape
+
+        if n_try >= self.max_geometry_validate_tries:
+            # ensure that we don't recurse indefinitely with an
+            # invalid geometry handler that doesn't validate
+            # geometries
+            return None
+
+        if self.on_invalid_geometry:
+            shape = self.on_invalid_geometry(shape)
+            if shape is not None and not shape.is_empty:
+                # this means that we have a handler that might have
+                # altered the geometry. We'll run through the process
+                # again, but keep track of which attempt we are on to
+                # terminate the recursion.
+                shape = self.enforce_winding_order(
+                    shape, y_coord_down, n_try + 1)
+
+        return shape
+
+    def enforce_multipolygon_winding_order(self, shape, y_coord_down, n_try):
+        assert shape.type == 'MultiPolygon'
+
+        parts = []
+        for part in shape.geoms:
+            part = self.enforce_polygon_winding_order(
+                part, y_coord_down, n_try)
+            if part is not None and not part.is_empty:
+                parts.append(part)
+
+        if not parts:
+            return None
+
+        if len(parts) == 1:
+            oriented_shape = parts[0]
+        else:
+            oriented_shape = MultiPolygon(parts)
+
+        oriented_shape = self.handle_shape_validity(
+            oriented_shape, y_coord_down, n_try)
+        return oriented_shape
+
+    def enforce_polygon_winding_order(self, shape, y_coord_down, n_try):
+        assert shape.type == 'Polygon'
+
+        def fn(point):
+            x, y = point
+            return self._round(x), self._round(y)
+
+        exterior = apply_map(fn, shape.exterior.coords)
+        rings = None
+
+        if len(shape.interiors) > 0:
+            rings = [apply_map(fn, ring.coords) for ring in shape.interiors]
+
+        sign = 1.0 if y_coord_down else -1.0
+        oriented_shape = orient(Polygon(exterior, rings), sign=sign)
+        oriented_shape = self.handle_shape_validity(
+            oriented_shape, y_coord_down, n_try)
+        return oriented_shape
+
+    def _load_geometry(self, geometry_spec):
+        if isinstance(geometry_spec, BaseGeometry):
+            return geometry_spec
+
+        try:
+            return load_wkb(geometry_spec)
+        except:
+            try:
+                return load_wkt(geometry_spec)
+            except:
+                return None
+
+    def addFeature(self, feature, shape, y_coord_down):
+        f = self.layer.features.add()
+
+        fid = feature.get('id')
+        if fid is not None:
+            if isinstance(fid, Number) and fid >= 0:
+                f.id = fid
+
+        # properties
+        properties = feature.get('properties')
+        if properties is not None:
+            self._handle_attr(self.layer, f, properties)
+
+        f.type = self._get_feature_type(shape)
+        self._geo_encode(f, shape, y_coord_down)
+
+    def _get_feature_type(self, shape):
+        if shape.type == 'Point' or shape.type == 'MultiPoint':
+            return self.tile.Point
+        elif shape.type == 'LineString' or shape.type == 'MultiLineString':
+            return self.tile.LineString
+        elif shape.type == 'Polygon' or shape.type == 'MultiPolygon':
+            return self.tile.Polygon
+        elif shape.type == 'GeometryCollection':
+            raise ValueError('Encoding geometry collections not supported')
+        else:
+            raise ValueError('Cannot encode unknown geometry type: %s' %
+                             shape.type)
+
+    def _encode_cmd_length(self, cmd, length):
+        return (length << cmd_bits) | (cmd & ((1 << cmd_bits) - 1))
+
+    def _chunker(self, seq, size):
+        return [seq[pos:pos + size] for pos in xrange(0, len(seq), size)]
+
+    def _can_handle_key(self, k):
+        return isinstance(k, (str, unicode))
+
+    def _can_handle_val(self, v):
+        if isinstance(v, (str, unicode)):
+            return True
+        elif isinstance(v, bool):
+            return True
+        elif isinstance(v, (int, long)):
+            return True
+        elif isinstance(v, float):
+            return True
+
+        return False
+
+    def _can_handle_attr(self, k, v):
+        return self._can_handle_key(k) and \
+            self._can_handle_val(v)
+
+    def _handle_attr(self, layer, feature, props):
+        for k, v in props.items():
+            if self._can_handle_attr(k, v):
+                if not PY3 and isinstance(k, str):
+                    k = k.decode('utf-8')
+
+                if k not in self.seen_keys_idx:
+                    layer.keys.append(k)
+                    self.seen_keys_idx[k] = self.key_idx
+                    self.key_idx += 1
+
+                feature.tags.append(self.seen_keys_idx[k])
+
+                if v not in self.seen_values_idx:
+                    self.seen_values_idx[v] = self.val_idx
+                    self.val_idx += 1
+
+                    val = layer.values.add()
+                    if isinstance(v, bool):
+                        val.bool_value = v
+                    elif isinstance(v, str):
+                        if PY3:
+                            val.string_value = v
+                        else:
+                            val.string_value = unicode(v, 'utf-8')
+                    elif isinstance(v, unicode):
+                        val.string_value = v
+                    elif isinstance(v, (int, long)):
+                        val.int_value = v
+                    elif isinstance(v, float):
+                        val.double_value = v
+
+                feature.tags.append(self.seen_values_idx[v])
+
+    def _handle_skipped_last(self, f, skipped_index, cur_x, cur_y, x_, y_):
+        last_x = f.geometry[skipped_index - 2]
+        last_y = f.geometry[skipped_index - 1]
+        last_dx = ((last_x >> 1) ^ (-(last_x & 1)))
+        last_dy = ((last_y >> 1) ^ (-(last_y & 1)))
+        dx = cur_x - x_ + last_dx
+        dy = cur_y - y_ + last_dy
+        x_ = cur_x
+        y_ = cur_y
+        f.geometry.__setitem__(skipped_index - 2, ((dx << 1) ^ (dx >> 31)))
+        f.geometry.__setitem__(skipped_index - 1, ((dy << 1) ^ (dy >> 31)))
+
+    def _parseGeometry(self, shape):
+        coordinates = []
+        lineType = "line"
+        polygonType = "polygon"
+
+        def _get_arc_obj(arc, type):
+            cmd = CMD_MOVE_TO
+            length = len(arc.coords)
+            for i, (x, y) in enumerate(arc.coords):
+                if i == 0:
+                    cmd = CMD_MOVE_TO
+                elif i == length - 1 and type == polygonType:
+                    cmd = CMD_SEG_END
+                else:
+                    cmd = CMD_LINE_TO
+                coordinates.append((x, y, cmd))
+
+        if shape.type == 'GeometryCollection':
+            # do nothing
+            coordinates = []
+
+        elif shape.type == 'Point':
+            coordinates.append((shape.x, shape.y, CMD_MOVE_TO))
+
+        elif shape.type == 'LineString':
+            _get_arc_obj(shape, lineType)
+
+        elif shape.type == 'Polygon':
+            rings = [shape.exterior] + list(shape.interiors)
+            for ring in rings:
+                _get_arc_obj(ring, polygonType)
+
+        elif shape.type == 'MultiPoint':
+            coordinates += [(point.x, point.y, CMD_MOVE_TO)
+                            for point in shape.geoms]
+
+        elif shape.type == 'MultiLineString':
+            for arc in shape.geoms:
+                _get_arc_obj(arc, lineType)
+
+        elif shape.type == 'MultiPolygon':
+            for polygon in shape.geoms:
+                rings = [polygon.exterior] + list(polygon.interiors)
+                for ring in rings:
+                    _get_arc_obj(ring, polygonType)
+
+        else:
+            raise NotImplementedError("Can't do %s geometries" % shape.type)
+
+        return coordinates
+
+    def _geo_encode(self, f, shape, y_coord_down):
+        x_, y_ = 0, 0
+
+        cmd = -1
+        cmd_idx = -1
+        vtx_cmd = -1
+        prev_cmd = -1
+
+        skipped_index = -1
+        skipped_last = False
+        cur_x = 0
+        cur_y = 0
+
+        it = 0
+        length = 0
+
+        coordinates = self._parseGeometry(shape)
+
+        while it < len(coordinates):
+            x, y, vtx_cmd = coordinates[it]
+
+            if vtx_cmd != cmd:
+                if cmd_idx >= 0:
+                    f.geometry[cmd_idx] = self._encode_cmd_length(cmd, length)
+
+                cmd = vtx_cmd
+                length = 0
+                cmd_idx = len(f.geometry)
+                f.geometry.append(0)  # placeholder added in first pass
+
+            if (vtx_cmd == CMD_MOVE_TO or vtx_cmd == CMD_LINE_TO):
+                if cmd == CMD_MOVE_TO and skipped_last and skipped_index > 1:
+                    self._handle_skipped_last(
+                        f, skipped_index, cur_x, cur_y, x_, y_)
+
+                # ensure that floating point values don't get truncated
+                if isinstance(x, float):
+                    x = self._round(x)
+                if isinstance(y, float):
+                    y = self._round(y)
+
+                x = int(x)
+                y = int(y)
+
+                if not y_coord_down:
+                    y = self.extents - y
+
+                # Compute delta to the previous coordinate.
+                cur_x = int(x)
+                cur_y = int(y)
+
+                dx = cur_x - x_
+                dy = cur_y - y_
+
+                sharp_turn_ahead = False
+
+                if (it + 2 <= len(coordinates)):
+                    next_x, next_y, next_cmd = coordinates[it + 1]
+                    if next_cmd == CMD_LINE_TO:
+                        next_dx = fabs(cur_x - int(next_x))
+                        next_dy = fabs(cur_y - int(next_y))
+                        if ((next_dx == 0 and next_dy >= tolerance) or
+                                (next_dy == 0 and next_dx >= tolerance)):
+                            sharp_turn_ahead = True
+
+                # Keep all move_to commands, but omit other movements
+                # that are not >= the tolerance threshold and should
+                # be considered no-ops.
+                # NOTE: length == 0 indicates the command has changed and will
+                # preserve any non duplicate move_to or line_to
+                if (length == 0 or sharp_turn_ahead or
+                        fabs(dx) >= tolerance or fabs(dy) >= tolerance):
+                    # Manual zigzag encoding.
+                    f.geometry.append((dx << 1) ^ (dx >> 31))
+                    f.geometry.append((dy << 1) ^ (dy >> 31))
+                    x_ = cur_x
+                    y_ = cur_y
+                    skipped_last = False
+                    length = length + 1
+                else:
+                    skipped_last = True
+                    skipped_index = len(f.geometry)
+            elif vtx_cmd == CMD_SEG_END:
+                if prev_cmd != CMD_SEG_END:
+                    length = length + 1
+            else:
+                raise Exception("Unknown command type: '%s'" % vtx_cmd)
+
+            it = it + 1
+            prev_cmd = cmd
+
+        # at least one vertex + cmd/length
+        if skipped_last and skipped_index > 1:
+            # if we skipped previous vertex we just update it to the
+            # last one here.
+            self._handle_skipped_last(f, skipped_index, cur_x, cur_y, x_, y_)
+
+        # Update the last length/command value.
+        if cmd_idx >= 0:
+            f.geometry[cmd_idx] = self._encode_cmd_length(cmd, length)
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..6f08d0e
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,8 @@
+[bdist_wheel]
+universal = 1
+
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..1b7033b
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,33 @@
+import io
+from setuptools import setup, find_packages
+
+
+with io.open('README.md') as readme_file:
+    long_description = readme_file.read()
+
+
+def test_suite():
+    try:
+        import unittest2 as unittest
+    except:
+        import unittest
+
+    suite = unittest.TestLoader().discover("tests")
+    return suite
+
+setup(name='mapbox-vector-tile',
+      version='0.5.0',
+      description=u"Mapbox Vector Tile",
+      long_description=long_description,
+      classifiers=[],
+      keywords='',
+      author=u"Harish Krishna",
+      author_email='harish.krsn at gmail.com',
+      url='https://github.com/tilezen/mapbox-vector-tile',
+      license='MIT',
+      packages=find_packages(),
+      include_package_data=True,
+      zip_safe=False,
+      test_suite="setup.test_suite",
+      install_requires=["setuptools", "protobuf", "shapely", "future"]
+      )
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..4efcb7b
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1,12 @@
+import doctest
+import glob
+import os
+
+optionflags = (doctest.REPORT_ONLY_FIRST_FAILURE |
+               doctest.NORMALIZE_WHITESPACE |
+               doctest.ELLIPSIS)
+
+_basedir = os.path.dirname(__file__)
+paths = glob.glob("%s/*.txt" % _basedir)
+test_suite = doctest.DocFileSuite(*paths, **dict(module_relative=False,
+                                                 optionflags=optionflags))
diff --git a/tests/test_decoder.py b/tests/test_decoder.py
new file mode 100644
index 0000000..3521bae
--- /dev/null
+++ b/tests/test_decoder.py
@@ -0,0 +1,78 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for vector_tile/decoder.py
+"""
+
+import unittest
+
+import mapbox_vector_tile
+from mapbox_vector_tile.compat import PY3
+
+
+class BaseTestCase(unittest.TestCase):
+
+    def test_decoder(self):
+        if PY3:
+            vector_tile = b'\x1aI\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03baz\x1a\x03uid"\x05\n\x03bar"\x05\n\x03foo"\x02 {(\x80 x\x02'  # noqa
+        else:
+            vector_tile = '\x1aI\n\x05water\x12\x1a\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x03"\x0c\t\x00\x80@\x1a\x00\x01\x02\x00\x00\x02\x0f\x1a\x03foo\x1a\x03baz\x1a\x03uid"\x05\n\x03bar"\x05\n\x03foo"\x02 {(\x80 x\x02'  # noqa
+        self.assertEqual(mapbox_vector_tile.decode(vector_tile), {
+            'water': {
+                'version': 2,
+                'extent': 4096,
+                'features': [{
+                    'geometry': [[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]],
+                    'properties': {
+                        'foo': 'bar',
+                        'baz': 'foo',
+                        'uid': 123
+                    },
+                    'id': 1,
+                    'type': 3
+                }],
+            },
+        })
+
+    def test_decode_polygon_no_cmd_seg_end(self):
+        # the binary here was generated without including the
+        # CMD_SEG_END after the polygon parts
+        # this tests that the decoder can detect that a new
+        # CMD_MOVE_TO implicitly closes the previous polygon
+        if PY3:
+            vector_tile = b'\x1a+\n\x05water\x12\x1d\x18\x03"\x19\t\x00\x80@"\x08\x00\x00\x07\x07\x00\x00\x08\t\x02\x01"\x00\x03\x04\x00\x00\x04\x03\x00(\x80 x\x02'  # noqa
+        else:
+            vector_tile = '\x1a+\n\x05water\x12\x1d\x18\x03"\x19\t\x00\x80@"\x08\x00\x00\x07\x07\x00\x00\x08\t\x02\x01"\x00\x03\x04\x00\x00\x04\x03\x00(\x80 x\x02'  # noqa
+        self.assertEqual(mapbox_vector_tile.decode(vector_tile), {
+            'water': {
+                'version': 2,
+                'extent': 4096,
+                'features': [{
+                    'geometry': [
+                        [[0, 0], [4, 0], [4, 4], [0, 4], [0, 0]],
+                        [[1, 1], [1, 3], [3, 3], [3, 1], [1, 1]],
+                    ],
+                    'properties': {},
+                    'id': 0,
+                    'type': 3
+                }],
+            },
+        })
+
+    def test_nondefault_extent(self):
+        if PY3:
+            vector_tile = b'\x1aK\n\x05water\x12\x1c\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x02"\x0e\t\x80}\xd0\x12\x12\xbf>\xd86\xbf>\xd86\x1a\x03foo\x1a\x03baz\x1a\x03uid"\x05\n\x03bar"\x05\n\x03foo"\x02 {(\x80 at x\x02'  # noqa
+        else:
+            vector_tile = '\x1aK\n\x05water\x12\x1c\x08\x01\x12\x06\x00\x00\x01\x01\x02\x02\x18\x02"\x0e\t\x80}\xd0\x12\x12\xbf>\xd86\xbf>\xd86\x1a\x03foo\x1a\x03baz\x1a\x03uid"\x05\n\x03bar"\x05\n\x03foo"\x02 {(\x80 at x\x02'  # noqa
+
+        self.assertEqual(mapbox_vector_tile.decode(vector_tile), {
+            'water': {
+                'version': 2,
+                'extent': 8192,
+                'features': [{
+                    'geometry': [[8000, 7000], [4000, 3500], [0, 0]],
+                    'id': 1,
+                    'properties': {'baz': 'foo', 'foo': 'bar', 'uid': 123},
+                    'type': 2
+                }],
+            }
+        })
diff --git a/tests/test_encoder.py b/tests/test_encoder.py
new file mode 100644
index 0000000..e1b64f4
--- /dev/null
+++ b/tests/test_encoder.py
@@ -0,0 +1,639 @@
+# -*- coding: utf-8 -*-
+"""
+Tests for vector_tile/encoder.py
+"""
+import unittest
+
+import mapbox_vector_tile
+from mapbox_vector_tile import encode, decode
+from mapbox_vector_tile.compat import PY3
+from past.builtins import long, unicode
+
+from shapely import wkt
+
+
+class BaseTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.layer_name = "water"
+        self.feature_properties = {
+            "uid": 123,
+            "foo": "bar",
+            "baz": "foo"
+        }
+        self.feature_geometry = 'POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))'
+
+    def assertRoundTrip(self, input_geometry, expected_geometry, name=None,
+                        properties=None, id=None, expected_len=1,
+                        expected_properties=None):
+        if input_geometry is None:
+            input_geometry = self.feature_geometry
+        if name is None:
+            name = self.layer_name
+        if properties is None:
+            properties = self.feature_properties
+        if expected_properties is None:
+            expected_properties = properties
+        source = [{
+            "name": name,
+            "features": [{
+                "geometry": input_geometry,
+                "properties": properties
+            }]
+        }]
+        if id:
+            source[0]['features'][0]['id'] = id
+        encoded = encode(source)
+        decoded = decode(encoded)
+        self.assertIn(name, decoded)
+        layer = decoded[name]
+        features = layer['features']
+        self.assertEqual(expected_len, len(features))
+        self.assertEqual(features[0]['properties'], expected_properties)
+        self.assertEqual(features[0]['geometry'], expected_geometry)
+        if id:
+            self.assertEqual(features[0]['id'], id)
+
+
+class TestDifferentGeomFormats(BaseTestCase):
+
+    def test_encoder(self):
+        self.assertRoundTrip(
+            input_geometry='POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))',
+            expected_geometry=[[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]])
+
+    def test_encoder_quantize_before_orient(self):
+        self.assertRoundTrip(
+            input_geometry='POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 3 2, 2 2, 1 1))',  # noqa
+            expected_geometry=[[[0, 0], [0, 4], [4, 4], [4, 0], [0, 0]],
+                               [[1, 1], [3, 2], [2, 2], [1, 1]]])
+
+    def test_encoder_winding_order_polygon(self):
+        # example from the spec
+        # https://github.com/mapbox/vector-tile-spec/tree/master/2.1#4355-example-polygon
+        # the order given in the example is clockwise in a y-up coordinate
+        # system, but the coordinate system given for the example is y-down!
+        # therefore the y coordinate in this example is flipped negative.
+        self.assertRoundTrip(
+            input_geometry='POLYGON ((3 -6, 8 -12, 20 -34, 3 -6))',
+            expected_geometry=[[[3, -6], [8, -12], [20, -34], [3, -6]]])
+
+    def test_encoder_winding_order_polygon_reverse(self):
+        # tests that encode _corrects_ the winding order
+        # example is the same as above - note the flipped coordinate system.
+        self.assertRoundTrip(
+            input_geometry='POLYGON ((3 -6, 20 -34, 8 -12, 3 -6))',
+            expected_geometry=[[[3, -6], [8, -12], [20, -34], [3, -6]]])
+
+    def test_encoder_winding_order_multipolygon(self):
+        # example from the spec
+        # https://github.com/mapbox/vector-tile-spec/tree/master/2.1#4356-example-multi-polygon
+        # the order given in the example is clockwise in a y-up coordinate
+        # system, but the coordinate system given for the example is y-down!
+        self.assertRoundTrip(
+            input_geometry=('MULTIPOLYGON (' +
+                            '((0 0, 10 0, 10 -10, 0 -10, 0 0)),' +
+                            '((11 -11, 20 -11, 20 -20, 11 -20, 11 -11),' +
+                            ' (13 -13, 13 -17, 17 -17, 17 -13, 13 -13)))'),
+            expected_geometry=[
+                [[[0, 0], [10, 0], [10, -10], [0, -10], [0, 0]]],
+                [[[11, -11], [20, -11], [20, -20], [11, -20], [11, -11]],
+                 [[13, -13], [13, -17], [17, -17], [17, -13], [13, -13]]]])
+
+    def test_encoder_ensure_winding_after_quantization(self):
+        self.assertRoundTrip(
+            input_geometry='POLYGON ((0 0, 4 0, 4 4, 0 4, 0 0), (1 1, 3 2.4, 2 1.6, 1 1))',  # noqa
+            # should be single polygon with hole
+            expected_geometry=[[[0, 0], [0, 4], [4, 4], [4, 0], [0, 0]],
+                               [[1, 1], [3, 2], [2, 2], [1, 1]]])
+        # but becomes multi-polygon
+        # expected_geometry=[[[[0, 0], [0, 4], [4, 4], [4, 0], [0, 0]]],
+        #                   [[[1, 1], [2, 2], [3, 2], [1, 1]]]])
+
+    def test_with_wkt(self):
+        self.assertRoundTrip(
+            input_geometry="LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)",  # noqa
+            expected_geometry=[[-71, 42], [-71, 42], [-71, 42]])
+
+    def test_with_wkb(self):
+        self.assertRoundTrip(
+            input_geometry=b"\001\003\000\000\000\001\000\000\000\005\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\360?\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000",  # noqa
+            expected_geometry=[[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]])
+
+    def test_with_shapely(self):
+        geometry = "LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)"  # noqa
+        geometry = wkt.loads(geometry)
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[[-71, 42], [-71, 42], [-71, 42]])
+
+    def test_with_invalid_geometry(self):
+        expected_result = ('Can\'t do geometries that are not wkt, wkb, or '
+                           'shapely geometries')
+        with self.assertRaises(NotImplementedError) as ex:
+            mapbox_vector_tile.encode([{
+                "name": self.layer_name,
+                "features": [{
+                    "geometry": "xyz",
+                    "properties": self.feature_properties
+                }]
+            }])
+        self.assertEqual(str(ex.exception), expected_result)
+
+    def test_encode_unicode_property(self):
+        if PY3:
+            func = str
+        else:
+            func = unicode
+        geometry = "LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)"  # noqa
+        properties = {
+            "foo": func(self.feature_properties["foo"]),
+            "baz": func(self.feature_properties["baz"]),
+        }
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[[-71, 42], [-71, 42], [-71, 42]],
+            properties=properties)
+
+    def test_encode_unicode_property_key(self):
+        geometry = "LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)"  # noqa
+        properties = {
+            u'☺': u'☺'
+        }
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[[-71, 42], [-71, 42], [-71, 42]],
+            properties=properties)
+
+    def test_encode_float_little_endian(self):
+        geometry = "LINESTRING(-71.160281 42.258729,-71.160837 42.259113,-71.161144 42.25932)"  # noqa
+        properties = {
+            'floatval': 3.14159
+        }
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[[-71, 42], [-71, 42], [-71, 42]],
+            properties=properties)
+
+    def test_encode_feature_with_id(self):
+        geometry = 'POINT(1 1)'
+        self.assertRoundTrip(input_geometry=geometry,
+                             expected_geometry=[[1, 1]], id=42)
+
+    def test_encode_polygon_reverse_winding_order(self):
+        geometry = 'POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))'
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]])
+
+    def test_encode_multilinestring(self):
+        geometry = 'MULTILINESTRING ((10 10, 20 20, 10 40), (40 40, 30 30, 40 20, 30 10))'  # noqa
+        self.assertRoundTrip(input_geometry=geometry,
+                             expected_geometry=[
+                                 [[10, 10], [20, 20], [10, 40]],
+                                 [[40, 40], [30, 30], [40, 20], [30, 10]],
+                             ])
+
+    def test_encode_multipolygon_normal_winding_order(self):
+        geometry = 'MULTIPOLYGON (((40 40, 20 45, 45 30, 40 40)), ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20)))'  # noqa
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[
+                [[[40, 40], [45, 30], [20, 45], [40, 40]]],
+                [[[20, 35], [45, 20], [30, 5], [10, 10], [10, 30], [20, 35]],
+                 [[30, 20], [20, 25], [20, 15], [30, 20]]],
+            ],
+            expected_len=1)
+
+    def test_encode_multipolygon_normal_winding_order_zero_area(self):
+        geometry = 'MULTIPOLYGON (((40 40, 40 20, 40 45, 40 40)), ((20 35, 10 30, 10 10, 30 5, 45 20, 20 35), (30 20, 20 15, 20 25, 30 20)))'  # noqa
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[
+                [[20, 35], [45, 20], [30, 5], [10, 10], [10, 30], [20, 35]],
+                [[30, 20], [20, 25], [20, 15], [30, 20]],
+            ],
+            expected_len=1)
+
+    def test_encode_multipolygon_reverse_winding_order(self):
+        geometry = 'MULTIPOLYGON (((10 10, 10 0, 0 0, 0 10, 10 10), (8 8, 2 8, 2 0, 8 0, 8 8)))'  # noqa
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[
+                [[10, 10], [10, 0], [0, 0], [0, 10], [10, 10]],
+                [[8, 8], [2, 8], [2, 0], [8, 0], [8, 8]],
+            ],
+            expected_len=1)
+
+    def test_encode_property_bool(self):
+        geometry = 'POINT(0 0)'
+        properties = {
+            'test_bool_true': True,
+            'test_bool_false': False
+        }
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[[0, 0]],
+            properties=properties)
+
+    def test_encode_property_long(self):
+        geometry = 'POINT(0 0)'
+        properties = {
+            'test_int': int(1),
+            'test_long': long(1)
+        }
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[[0, 0]],
+            properties=properties)
+
+    def test_encode_property_null(self):
+        geometry = 'POINT(0 0)'
+        properties = {
+            'test_none': None,
+            'test_empty': ""
+        }
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[[0, 0]],
+            properties=properties,
+            expected_properties={'test_empty': ''})
+
+    def test_encode_property_list(self):
+        geometry = 'POINT(0 0)'
+        properties = {
+            'test_list': [1, 2, 3],
+            'test_empty': ""
+        }
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=[[0, 0]],
+            properties=properties,
+            expected_properties={'test_empty': ''})
+
+    def test_encode_multiple_values_test(self):
+        geometry = 'POINT(0 0)'
+        properties1 = dict(foo='bar', baz='bar')
+        properties2 = dict(quux='morx', baz='bar')
+        name = 'foo'
+        feature1 = dict(geometry=geometry, properties=properties1)
+        feature2 = dict(geometry=geometry, properties=properties2)
+        source = [{
+            "name": name,
+            "features": [feature1, feature2]
+        }]
+        encoded = encode(source)
+        decoded = decode(encoded)
+        self.assertIn(name, decoded)
+        layer = decoded[name]
+        features = layer['features']
+        self.assertEqual(2, len(features))
+        self.assertEqual(features[0]['properties'], properties1)
+        self.assertEqual(features[1]['properties'], properties2)
+
+    def test_encode_rounding_floats(self):
+        geometry = 'LINESTRING(1.1 1.1, 41.5 41.8)'
+        exp_geoemtry = [[1, 1], [42, 42]]
+        self.assertRoundTrip(
+            input_geometry=geometry,
+            expected_geometry=exp_geoemtry,
+        )
+
+
+class QuantizeTest(unittest.TestCase):
+
+    def test_quantize(self):
+        from mapbox_vector_tile import decode
+        from mapbox_vector_tile import encode
+        props = dict(foo='bar')
+        shape = 'POINT(15 15)'
+        feature = dict(geometry=shape, properties=props)
+        features = [feature]
+        source = dict(name='layername', features=features)
+        bounds = 10.0, 10.0, 20.0, 20.0
+        pbf = encode(source, quantize_bounds=bounds)
+        result = decode(pbf)
+        act_feature = result['layername']['features'][0]
+        act_geom = act_feature['geometry']
+        exp_geom = [[2048, 2048]]
+        self.assertEqual(exp_geom, act_geom)
+
+    def test_y_coord_down(self):
+        from mapbox_vector_tile import decode
+        from mapbox_vector_tile import encode
+        props = dict(foo='bar')
+        shape = 'POINT(10 10)'
+        feature = dict(geometry=shape, properties=props)
+        features = [feature]
+        source = dict(name='layername', features=features)
+        pbf = encode(source, y_coord_down=True)
+        result = decode(pbf, y_coord_down=True)
+        act_feature = result['layername']['features'][0]
+        act_geom = act_feature['geometry']
+        exp_geom = [[10, 10]]
+        self.assertEqual(exp_geom, act_geom)
+
+    def test_quantize_and_y_coord_down(self):
+        from mapbox_vector_tile import decode
+        from mapbox_vector_tile import encode
+        props = dict(foo='bar')
+        shape = 'POINT(30 30)'
+        feature = dict(geometry=shape, properties=props)
+        features = [feature]
+        source = dict(name='layername', features=features)
+        bounds = 0.0, 0.0, 50.0, 50.0
+        pbf = encode(source, quantize_bounds=bounds, y_coord_down=True)
+
+        result_decode_no_flip = decode(pbf, y_coord_down=True)
+        act_feature = result_decode_no_flip['layername']['features'][0]
+        act_geom = act_feature['geometry']
+        exp_geom = [[2458, 2458]]
+        self.assertEqual(exp_geom, act_geom)
+
+        result_decode_flip = decode(pbf)
+        act_feature = result_decode_flip['layername']['features'][0]
+        act_geom = act_feature['geometry']
+        exp_geom = [[2458, 1638]]
+        self.assertEqual(exp_geom, act_geom)
+
+
+class ExtentTest(unittest.TestCase):
+
+    def test_custom_extent(self):
+        from mapbox_vector_tile import decode
+        from mapbox_vector_tile import encode
+        props = dict(foo='bar')
+        shape = 'POINT(10 10)'
+        feature = dict(geometry=shape, properties=props)
+        features = [feature]
+        source = dict(name='layername', features=features)
+        bounds = 0.0, 0.0, 10.0, 10.0
+        pbf = encode(source, quantize_bounds=bounds, extents=50)
+        result = decode(pbf)
+        act_feature = result['layername']['features'][0]
+        act_geom = act_feature['geometry']
+        exp_geom = [[50, 50]]
+        self.assertEqual(exp_geom, act_geom)
+
+
+class RoundTest(unittest.TestCase):
+
+    def test_custom_rounding_function(self):
+        from mapbox_vector_tile import decode
+        from mapbox_vector_tile import encode
+        props = dict(foo='bar')
+        shape = 'POINT(10 10)'
+        feature = dict(geometry=shape, properties=props)
+        features = [feature]
+        source = dict(name='layername', features=features)
+        bounds = 0.0, 0.0, 10.0, 10.0
+        # A really bad, custom "rounding" function
+        pbf = encode(source, quantize_bounds=bounds, round_fn=lambda x: 5)
+        result = decode(pbf)
+
+        act_feature = result['layername']['features'][0]
+        act_geom = act_feature['geometry']
+        exp_geom = [[5, 5]]
+        self.assertEqual(exp_geom, act_geom)
+
+
+class InvalidGeometryTest(unittest.TestCase):
+
+    def test_invalid_geometry_ignore(self):
+        from mapbox_vector_tile import encode
+        from mapbox_vector_tile.encoder import on_invalid_geometry_ignore
+        import shapely.wkt
+        geometry = 'POLYGON ((10 10, 20 10, 20 20, 15 15, 15 5, 10 10))'
+        shape = shapely.wkt.loads(geometry)
+        self.assertFalse(shape.is_valid)
+        feature = dict(geometry=shape, properties={})
+        source = dict(name='layername', features=[feature])
+        pbf = encode(source, on_invalid_geometry=on_invalid_geometry_ignore)
+        result = decode(pbf)
+        self.assertEqual(0, len(result['layername']['features']))
+
+    def test_invalid_geometry_raise(self):
+        from mapbox_vector_tile import encode
+        from mapbox_vector_tile.encoder import on_invalid_geometry_raise
+        import shapely.wkt
+        geometry = 'POLYGON ((10 10, 20 10, 20 20, 15 15, 15 5, 10 10))'
+        shape = shapely.wkt.loads(geometry)
+        self.assertFalse(shape.is_valid)
+        feature = dict(geometry=shape, properties={})
+        source = dict(name='layername', features=[feature])
+        with self.assertRaises(Exception):
+            encode(source, on_invalid_geometry=on_invalid_geometry_raise)
+
+    def test_invalid_geometry_make_valid(self):
+        from mapbox_vector_tile import encode
+        from mapbox_vector_tile.encoder import on_invalid_geometry_make_valid
+        import shapely.geometry
+        import shapely.wkt
+        geometry = 'POLYGON ((10 10, 20 10, 20 20, 15 15, 15 5, 10 10))'
+        shape = shapely.wkt.loads(geometry)
+        self.assertFalse(shape.is_valid)
+        feature = dict(geometry=shape, properties={})
+        source = dict(name='layername', features=[feature])
+        pbf = encode(source,
+                     on_invalid_geometry=on_invalid_geometry_make_valid)
+        result = decode(pbf)
+        self.assertEqual(1, len(result['layername']['features']))
+        valid_geometry = result['layername']['features'][0]['geometry']
+        shape = shapely.geometry.Polygon(valid_geometry[0])
+        self.assertTrue(shape.is_valid)
+
+    def test_bowtie_self_touching(self):
+        from mapbox_vector_tile import encode
+        from mapbox_vector_tile.encoder import on_invalid_geometry_make_valid
+        import shapely.geometry
+        import shapely.wkt
+        bowtie = ('POLYGON ((0 0, 0 2, 1 1, 2 2, 2 0, 1 1, 0 0))')
+        shape = shapely.wkt.loads(bowtie)
+        self.assertFalse(shape.is_valid)
+        feature = dict(geometry=shape, properties={})
+        source = dict(name='layername', features=[feature])
+        pbf = encode(source,
+                     on_invalid_geometry=on_invalid_geometry_make_valid)
+        result = decode(pbf)
+        self.assertEqual(1, len(result['layername']['features']))
+        valid_geometries = result['layername']['features'][0]['geometry']
+        self.assertEqual(2, len(valid_geometries))
+        shape1, shape2 = [shapely.geometry.Polygon(x[0])
+                          for x in valid_geometries]
+        self.assertTrue(shape1.is_valid)
+        self.assertTrue(shape2.is_valid)
+        self.assertGreater(shape1.area, 0)
+        self.assertGreater(shape2.area, 0)
+
+    def test_bowtie_self_crossing(self):
+        from mapbox_vector_tile import encode
+        from mapbox_vector_tile.encoder import on_invalid_geometry_make_valid
+        import shapely.geometry
+        import shapely.wkt
+        bowtie = ('POLYGON ((0 0, 2 2, 2 0, 0 2, 0 0))')
+        shape = shapely.wkt.loads(bowtie)
+        self.assertFalse(shape.is_valid)
+        feature = dict(geometry=shape, properties={})
+        source = dict(name='layername', features=[feature])
+        pbf = encode(source,
+                     on_invalid_geometry=on_invalid_geometry_make_valid)
+        result = decode(pbf)
+        self.assertEqual(1, len(result['layername']['features']))
+        valid_geometries = result['layername']['features'][0]['geometry']
+
+        total_area = 0
+        for g in valid_geometries:
+            self.assertEquals(1, len(g))
+            p = shapely.geometry.Polygon(g[0])
+            self.assertTrue(p.is_valid)
+            self.assertGreater(p.area, 0)
+            total_area += p.area
+        self.assertEquals(2, total_area)
+
+    def test_validate_generates_rounding_error(self):
+        from mapbox_vector_tile import encode
+        from mapbox_vector_tile.encoder import on_invalid_geometry_make_valid
+        import shapely.geometry
+        import shapely.wkt
+        bowtie = ('POLYGON((0 0, 1 1, 0 1, 1 0, 0 0))')
+        shape = shapely.wkt.loads(bowtie)
+        self.assertFalse(shape.is_valid)
+        feature = dict(geometry=shape, properties={})
+        source = dict(name='layername', features=[feature])
+        pbf = encode(source,
+                     on_invalid_geometry=on_invalid_geometry_make_valid)
+        result = decode(pbf)
+        features = result['layername']['features']
+        self.assertEqual(1, len(features))
+        shape = shapely.geometry.Polygon(features[0]['geometry'][0])
+        self.assertTrue(shape.is_valid)
+        self.assertGreater(shape.area, 0)
+
+    def test_geometry_collection_raises(self):
+        from mapbox_vector_tile import encode
+        import shapely.wkt
+        collection = shapely.wkt.loads('GEOMETRYCOLLECTION (GEOMETRYCOLLECTION (POINT (4095 3664), LINESTRING (2889 0, 2889 0)), POINT (4095 3664), LINESTRING (2889 0, 2912 158, 3757 1700, 3732 1999, 4095 3277))')  # noqa
+        with self.assertRaises(ValueError):
+            encode({'name': 'streets', 'features': [{'geometry': collection}]})
+
+    def test_quantize_makes_mutlipolygon_invalid(self):
+        from mapbox_vector_tile import encode
+        from mapbox_vector_tile.encoder import on_invalid_geometry_make_valid
+        import shapely.wkt
+        shape = shapely.wkt.loads('MULTIPOLYGON (((656510.8206577231 5674684.979891453, 656511.16 5674685.9, 656514.1758819892 5674684.979891453, 656510.8206577231 5674684.979891453)), ((657115.9120547654 5674684.979891453, 657118.85 5674690, 657118.0689111941 5674684.979891453, 657115.9120547654 5674684.979891453)))')  # noqa
+        quantize_bounds = (645740.0149532147, 5674684.979891453, 665307.8941942193, 5694252.8591324575)  # noqa
+        features = [dict(geometry=shape, properties={})]
+        pbf = encode({'name': 'foo', 'features': features},
+                     quantize_bounds=quantize_bounds,
+                     on_invalid_geometry=on_invalid_geometry_make_valid)
+        result = decode(pbf)
+        features = result['foo']['features']
+        self.assertEqual(1, len(features))
+
+
+class LowLevelEncodingTestCase(unittest.TestCase):
+    def test_example_multi_polygon(self):
+        from mapbox_vector_tile.encoder import VectorTile
+        # example from spec:
+        # https://github.com/mapbox/vector-tile-spec/tree/master/2.1#4356-example-multi-polygon
+        # note that examples are in **tile local coordinates** which are
+        # y-down.
+        input_geometry = 'MULTIPOLYGON (' + \
+                         '((0 0, 10 0, 10 10, 0 10, 0 0)),' + \
+                         '((11 11, 20 11, 20 20, 11 20, 11 11),' + \
+                         ' (13 13, 13 17, 17 17, 17 13, 13 13)))'
+        expected_commands = [
+            9,       # 1 x move to
+            0,   0,  # ........... +0,+0
+            26,      # 3 x line to
+            20,  0,  # ........... +10,+0
+            0,  20,  # ........... +0,+10
+            19,  0,  # ........... -10,+0
+            15,      # 1 x close path
+            9,       # 1 x move to
+            22,  2,  # ........... +11,+1
+            26,      # 3 x line to
+            18,  0,  # ........... +9,+0
+            0,  18,  # ........... +0,+9
+            17,  0,  # ........... -9,+0
+            15,      # 1 x close path
+            9,       # 1 x move to
+            4,  13,  # ........... +2,-7
+            26,      # 3 x line to
+            0,   8,  # ........... +0,+4
+            8,   0,  # ........... +4,+0
+            0,   7,  # ........... +0,-4
+            15       # 1 x close path
+        ]
+
+        tile = VectorTile(4096)
+        tile.addFeatures([dict(geometry=input_geometry)],
+                         'example_layer', quantize_bounds=None,
+                         y_coord_down=True)
+        self.assertEqual(1, len(tile.layer.features))
+        f = tile.layer.features[0]
+        self.assertEqual(expected_commands, list(f.geometry))
+
+    def test_example_multi_polygon_y_up(self):
+        from mapbox_vector_tile.encoder import VectorTile
+        # example from spec:
+        # https://github.com/mapbox/vector-tile-spec/tree/master/2.1#4356-example-multi-polygon
+        # in this example, we transform the coordinates to their equivalents
+        # in a y-up coordinate system.
+        input_geometry = 'MULTIPOLYGON (' + \
+                         '((0 20, 10 20, 10 10, 0 10, 0 20)),' + \
+                         '((11 9, 20 9, 20 0, 11 0, 11 9),' + \
+                         ' (13 7, 13 3, 17 3, 17 7, 13 7)))'
+        expected_commands = [
+            9,       # 1 x move to
+            0,   0,  # ........... +0,+0
+            26,      # 3 x line to
+            20,  0,  # ........... +10,+0
+            0,  20,  # ........... +0,+10
+            19,  0,  # ........... -10,+0
+            15,      # 1 x close path
+            9,       # 1 x move to
+            22,  2,  # ........... +11,+1
+            26,      # 3 x line to
+            18,  0,  # ........... +9,+0
+            0,  18,  # ........... +0,+9
+            17,  0,  # ........... -9,+0
+            15,      # 1 x close path
+            9,       # 1 x move to
+            4,  13,  # ........... +2,-7
+            26,      # 3 x line to
+            0,   8,  # ........... +0,+4
+            8,   0,  # ........... +4,+0
+            0,   7,  # ........... +0,-4
+            15       # 1 x close path
+        ]
+
+        tile = VectorTile(20)
+        tile.addFeatures([dict(geometry=input_geometry)],
+                         'example_layer', quantize_bounds=None,
+                         y_coord_down=False)
+        self.assertEqual(1, len(tile.layer.features))
+        f = tile.layer.features[0]
+        self.assertEqual(expected_commands, list(f.geometry))
+
+    def test_issue_57(self):
+        from mapbox_vector_tile.encoder import VectorTile
+        # example from issue:
+        # https://github.com/tilezen/mapbox-vector-tile/issues/57
+        input_geometry = 'POLYGON ((2 2, 5 4, 2 6, 2 2))'
+        expected_commands = [
+            9,       # 1 x move to
+            4,   4,  # ........... +2,+2
+            18,      # 2 x line to
+            6,   4,  # ........... +3,+2
+            5,   4,  # ........... -3,+2
+            15       # 1 x close path
+        ]
+
+        tile = VectorTile(4096)
+        tile.addFeatures([dict(geometry=input_geometry)],
+                         'example_layer', quantize_bounds=None,
+                         y_coord_down=True)
+        self.assertEqual(1, len(tile.layer.features))
+        f = tile.layer.features[0]
+        self.assertEqual(expected_commands, list(f.geometry))

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-grass/mapbox-vector-tile.git



More information about the Pkg-grass-devel mailing list