├── .bumpversion.cfg ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── __init__.py └── vt_to_geodataframe.py ├── setup.py ├── tests ├── __init__.py ├── sample_14_8185_5449.pbf └── test_conversions.py └── vt2geojson ├── __init__.py ├── cli.py ├── features.py └── tools.py /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.2.1 3 | commit = True 4 | tag = True 5 | tag_name = {new_version} 6 | 7 | [bumpversion:file:CHANGELOG.md] 8 | search = **unreleased** 9 | replace = **unreleased** 10 | 11 | **v{new_version}** 12 | 13 | [bumpversion:file:vt2geojson/__init__.py] 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don’t work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | 107 | # Spyder project settings 108 | .spyderproject 109 | .spyproject 110 | 111 | # Rope project settings 112 | .ropeproject 113 | 114 | # mkdocs documentation 115 | /site 116 | 117 | # mypy 118 | .mypy_cache/ 119 | .dmypy.json 120 | dmypy.json 121 | 122 | # Pyre type checker 123 | .pyre/ 124 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | install: 5 | - python setup.py develop 6 | script: 7 | - python setup.py test 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | **unreleased** 3 | 4 | **v0.2.1** 5 | 6 | - Add `MultiPoint` support. 7 | - Fix Mapbox uri example. 8 | - Add a license file. 9 | 10 | **v0.2.0** 11 | 12 | - Add a MultiPolygon handler. 13 | - Add bump2version config. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019-now The python-vt2geojson developers. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-vt2geojson [![Build Status](https://travis-ci.org/Amyantis/python-vt2geojson.svg?branch=master)](https://travis-ci.org/Amyantis/python-vt2geojson) 2 | Dump vector tiles to GeoJSON from remote URLs or local system files. 3 | 4 | Inspired from https://github.com/mapbox/vt2geojson. 5 | 6 | ## Installation 7 | ``` 8 | pip install vt2geojson 9 | ``` 10 | 11 | ## Usage 12 | Using the CLI: 13 | ``` 14 | vt2geojson --help 15 | ``` 16 | 17 | Making a GeoDataframe from a PBF vector tile file: 18 | ```python 19 | import geopandas as gpd 20 | import requests 21 | 22 | from vt2geojson.tools import vt_bytes_to_geojson 23 | 24 | MAPBOX_ACCESS_TOKEN = "*****" 25 | 26 | x = 150 27 | y = 194 28 | z = 9 29 | 30 | url = f"https://api.mapbox.com/v4/mapbox.mapbox-streets-v6/{z}/{x}/{y}.vector.pbf?access_token={MAPBOX_ACCESS_TOKEN}" 31 | r = requests.get(url) 32 | assert r.status_code == 200, r.content 33 | vt_content = r.content 34 | 35 | features = vt_bytes_to_geojson(vt_content, x, y, z) 36 | gdf = gpd.GeoDataFrame.from_features(features) 37 | ``` 38 | 39 | ## Todos: 40 | * Add more test using `vector-tile/test/fixtures/*.vector.pbf` data files. 41 | 42 | ## Notes 43 | This library has only been tested against **Python 3.6**. 44 | 45 | Feel free to [submit your issues](https://github.com/Amyantis/python-vt2geojson/issues). 46 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amyantis/python-vt2geojson/0ab4f10fcf5dc51ce3aa605506dd46f3601292ae/examples/__init__.py -------------------------------------------------------------------------------- /examples/vt_to_geodataframe.py: -------------------------------------------------------------------------------- 1 | import geopandas as gpd 2 | import requests 3 | 4 | from vt2geojson.tools import vt_bytes_to_geojson 5 | 6 | MAPBOX_ACCESS_TOKEN = "pk.eyJ1IjoiZXhhbXBsZXMiLCJhIjoiY2p0MG01MXRqMW45cjQzb2R6b2ptc3J4MSJ9.zA2W0IkI0c6KaAhJfk9bWg" 7 | 8 | x = 150 9 | y = 194 10 | z = 9 11 | 12 | url = f"https://api.mapbox.com/v4/mapbox.mapbox-streets-v6/{z}/{x}/{y}.vector.pbf?access_token={MAPBOX_ACCESS_TOKEN}" 13 | r = requests.get(url) 14 | assert r.status_code == 200, r.content 15 | vt_content = r.content 16 | 17 | features = vt_bytes_to_geojson(vt_content, x, y, z) 18 | gdf = gpd.GeoDataFrame.from_features(features) 19 | print(gdf.head()) 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | from vt2geojson import NAME, DESCRIPTION, AUTHOR, AUTHOR_EMAIL, URL, __version__ 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setup( 9 | name=NAME, 10 | version=__version__, 11 | description=DESCRIPTION, 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | author=AUTHOR, 15 | author_email=AUTHOR_EMAIL, 16 | url=URL, 17 | packages=find_packages(), 18 | entry_points={ 19 | "console_scripts": ['vt2geojson=vt2geojson.cli:main'] 20 | }, 21 | install_requires=[ 22 | "mapbox_vector_tile" 23 | ], 24 | test_suite="tests" 25 | ) 26 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amyantis/python-vt2geojson/0ab4f10fcf5dc51ce3aa605506dd46f3601292ae/tests/__init__.py -------------------------------------------------------------------------------- /tests/sample_14_8185_5449.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amyantis/python-vt2geojson/0ab4f10fcf5dc51ce3aa605506dd46f3601292ae/tests/sample_14_8185_5449.pbf -------------------------------------------------------------------------------- /tests/test_conversions.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import TestCase 3 | 4 | from vt2geojson.tools import vt_bytes_to_geojson 5 | 6 | DIRPATH = os.path.dirname(os.path.realpath(__file__)) 7 | SAMPLE_FILEPATH = os.path.join(DIRPATH, "sample_14_8185_5449.pbf") 8 | 9 | with open(SAMPLE_FILEPATH, "rb") as f: 10 | sample_content = f.read() 11 | 12 | 13 | class TestConversions(TestCase): 14 | def test_bytes_to_geojson(self): 15 | result = vt_bytes_to_geojson(sample_content, 8185, 5449, 14) 16 | assert result['features'] 17 | -------------------------------------------------------------------------------- /vt2geojson/__init__.py: -------------------------------------------------------------------------------- 1 | NAME = "vt2geojson" 2 | DESCRIPTION = "Dump vector tiles to GeoJSON from remote URLs or local system files." 3 | AUTHOR = "Theophile Dancoisne" 4 | AUTHOR_EMAIL = "dancoisne.theophile@gmail.com" 5 | URL = "https://github.com/Amyantis/python-vt2geojson" 6 | 7 | __version__ = "0.2.1" 8 | -------------------------------------------------------------------------------- /vt2geojson/cli.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import json 3 | from re import search 4 | from urllib.request import urlopen 5 | 6 | from vt2geojson import DESCRIPTION 7 | from vt2geojson.tools import vt_bytes_to_geojson, _is_url 8 | 9 | XYZ_REGEX = r"\/(\d+)\/(\d+)\/(\d+)" 10 | 11 | 12 | def main(): 13 | parser = argparse.ArgumentParser(description=DESCRIPTION) 14 | parser.add_argument("uri", help="URI (url or filepath)") 15 | parser.add_argument("-l", "--layer", help="include only the specified layer", type=str) 16 | parser.add_argument("-x", help="tile x coordinate (normally inferred from the URL)", type=int) 17 | parser.add_argument("-y", help="tile y coordinate (normally inferred from the URL)", type=int) 18 | parser.add_argument("-z", help="tile z coordinate (normally inferred from the URL)", type=int) 19 | args = parser.parse_args() 20 | 21 | if _is_url(args.uri): 22 | matches = search(XYZ_REGEX, args.uri) 23 | if matches is None: 24 | raise ValueError("Unknown url, no `/z/x/y` pattern.") 25 | z, x, y = map(int, matches.groups()) 26 | r = urlopen(args.uri) 27 | content = r.read() 28 | else: 29 | if args.x is None or args.y is None or args.z is None: 30 | raise ValueError("You provided a path to a file. Therefore you must specify x, y and z.") 31 | x = args.x 32 | y = args.y 33 | z = args.z 34 | with open(args.uri, "rb") as f: 35 | content = f.read() 36 | 37 | geojson_result = vt_bytes_to_geojson(content, x, y, z, args.layer) 38 | print(json.dumps(geojson_result)) 39 | 40 | 41 | if __name__ == "__main__": 42 | main() 43 | -------------------------------------------------------------------------------- /vt2geojson/features.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from math import pi, atan, exp 3 | 4 | 5 | class GeometryType(Enum): 6 | UNKNOWN = 'Unknown' 7 | POINT = 'Point' 8 | LINESTRING = 'LineString' 9 | POLYGON = 'Polygon' 10 | MULTILINESTRING = 'MultiLineString' 11 | MULTIPOLYGON = 'MultiPolygon' 12 | MULTIPOINT = 'MultiPoint' 13 | 14 | 15 | class Feature: 16 | def __init__(self, x, y, z, obj, extent=4096): 17 | self.x = x 18 | self.y = y 19 | self.z = z 20 | self.obj = obj 21 | self.extent = extent 22 | 23 | @property 24 | def tiles_coordinates(self): 25 | return self.obj['geometry']['coordinates'] 26 | 27 | @property 28 | def geometry_type(self): 29 | return GeometryType(self.obj['geometry']['type']) 30 | 31 | @property 32 | def properties(self): 33 | return self.obj['properties'] 34 | 35 | def toGeoJSON(self): 36 | size = self.extent * 2 ** self.z 37 | x0 = self.extent * self.x 38 | y0 = self.extent * self.y 39 | 40 | def project_one(p_x, p_y): 41 | y2 = 180 - (p_y + y0) * 360. / size 42 | long_res = (p_x + x0) * 360. / size - 180 43 | lat_res = 360. / pi * atan(exp(y2 * pi / 180)) - 90 44 | return [long_res, lat_res] 45 | 46 | def project(coords): 47 | if all(isinstance(x, int) or isinstance(x, float) for x in coords): 48 | assert len(coords) == 2 49 | return project_one(coords[0], coords[1]) 50 | return [project(l) for l in coords] 51 | 52 | coords = project(self.tiles_coordinates) 53 | geometry_type = self.geometry_type.value 54 | 55 | result = { 56 | "type": "Feature", 57 | "geometry": { 58 | "type": geometry_type, 59 | "coordinates": coords 60 | }, 61 | "properties": self.properties 62 | } 63 | return result 64 | 65 | 66 | class Layer: 67 | def __init__(self, x, y, z, name, obj): 68 | self.x = x 69 | self.y = y 70 | self.z = z 71 | self.name = name 72 | self.obj = obj 73 | 74 | @property 75 | def extent(self): 76 | return self.obj['extent'] 77 | 78 | def toGeoJSON(self): 79 | return { 80 | "type": "FeatureCollection", 81 | "features": [Feature(x=self.x, y=self.y, z=self.z, obj=f, extent=self.extent).toGeoJSON() 82 | for f in self.obj['features']] 83 | } 84 | -------------------------------------------------------------------------------- /vt2geojson/tools.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import urlparse 2 | 3 | from mapbox_vector_tile import decode 4 | 5 | from vt2geojson.features import Layer 6 | 7 | 8 | def _is_url(uri: str) -> bool: 9 | """ 10 | Checks if the uri is actually an url. 11 | :param uri: the string to check. 12 | :return: a boolean. 13 | """ 14 | return urlparse(uri).scheme != "" 15 | 16 | 17 | def vt_bytes_to_geojson(b_content: bytes, x: int, y: int, z: int, layer=None) -> dict: 18 | """ 19 | Make a GeoJSON from bytes in the vector tiles format. 20 | :param b_content: the bytes to convert. 21 | :param x: tile x coordinate. 22 | :param y: tile y coordinate. 23 | :param z: tile z coordinate. 24 | :param layer: include only the specified layer. 25 | :return: a features collection (GeoJSON). 26 | """ 27 | data = decode(b_content, y_coord_down=True) 28 | features_collections = [Layer(x=x, y=y, z=z, name=name, obj=layer_obj).toGeoJSON() 29 | for name, layer_obj in data.items() if layer is None or name == layer] 30 | return { 31 | "type": "FeatureCollection", 32 | "features": [f for fc in features_collections for f in fc["features"]] 33 | } 34 | --------------------------------------------------------------------------------