[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
+ ==================
+
+ [](https://travis-ci.org/tilezen/mapbox-vector-tile)
+ [](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
+==================
+
+[](https://travis-ci.org/tilezen/mapbox-vector-tile)
+[](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