Skip to content

Commit dcf5ee2

Browse files
committed
First version of vt2geojson.
0 parents  commit dcf5ee2

File tree

7 files changed

+309
-0
lines changed

7 files changed

+309
-0
lines changed

.gitignore

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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/

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Dump vector tiles to GeoJSON from remote URLs or local system files.
2+
3+
Inspired from https://github.com/mapbox/vt2geojson.
4+
5+
## Installation
6+
```
7+
pip install python-vt2geojson
8+
```
9+
10+
## Usage
11+
```
12+
vt2geojson --help
13+
```

setup.py

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from setuptools import setup
2+
3+
from vt2geojson import NAME, DESCRIPTION, AUTHOR, AUTHOR_EMAIL, URL, __version__
4+
5+
setup(
6+
name=NAME,
7+
version=__version__,
8+
description=DESCRIPTION,
9+
author=AUTHOR,
10+
author_email=AUTHOR_EMAIL,
11+
url=URL,
12+
entry_points={
13+
"console_scripts": ['vt2geojson=vt2geojson.cli:main']
14+
},
15+
install_requires=[
16+
"mapbox_vector_tile"
17+
]
18+
)

vt2geojson/__init__.py

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
VERSION = (0, 1, 0)
2+
NAME = "python-vt2geojson"
3+
DESCRIPTION = "Dump vector tiles to GeoJSON from remote URLs or local system files."
4+
AUTHOR = "Theophile Dancoisne"
5+
AUTHOR_EMAIL = "[email protected]"
6+
URL = ""
7+
8+
9+
def get_version():
10+
return '.'.join(map(str, VERSION))
11+
12+
13+
__version__ = get_version()

vt2geojson/cli.py

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import argparse
2+
from pprint import pprint
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+
pprint(geojson_result)
39+
40+
41+
if __name__ == "__main__":
42+
main()

vt2geojson/features.py

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
12+
13+
class Feature:
14+
def __init__(self, x, y, z, obj, extent=4096):
15+
self.x = x
16+
self.y = y
17+
self.z = z
18+
self.obj = obj
19+
self.extent = extent
20+
21+
@property
22+
def tiles_coordinates(self):
23+
return self.obj['geometry']['coordinates']
24+
25+
@property
26+
def geometry_type(self):
27+
return GeometryType(self.obj['geometry']['type'])
28+
29+
@property
30+
def properties(self):
31+
return self.obj['properties']
32+
33+
def toGeoJSON(self):
34+
size = self.extent * 2 ** self.z
35+
x0 = self.extent * self.x
36+
y0 = self.extent * self.y
37+
38+
def project_one(p_x, p_y):
39+
y2 = 180 - (p_y + y0) * 360. / size
40+
long_res = (p_x + x0) * 360. / size - 180
41+
lat_res = 360. / pi * atan(exp(y2 * pi / 180)) - 90
42+
return [long_res, lat_res]
43+
44+
def project(coords):
45+
if all(isinstance(x, int) or isinstance(x, float) for x in coords):
46+
assert len(coords) == 2
47+
return project_one(coords[0], coords[1])
48+
return [project(l) for l in coords]
49+
50+
coords = project(self.tiles_coordinates)
51+
geometry_type = self.geometry_type.value
52+
53+
result = {
54+
"type": "Feature",
55+
"geometry": {
56+
"type": geometry_type,
57+
"coordinates": coords
58+
},
59+
"properties": self.properties
60+
}
61+
return result
62+
63+
64+
class Layer:
65+
def __init__(self, x, y, z, name, obj):
66+
self.x = x
67+
self.y = y
68+
self.z = z
69+
self.name = name
70+
self.obj = obj
71+
72+
@property
73+
def extent(self):
74+
return self.obj['extent']
75+
76+
def toGeoJSON(self):
77+
return {
78+
"type": "FeatureCollection",
79+
"features": [Feature(x=self.x, y=self.y, z=self.z, obj=f, extent=self.extent).toGeoJSON()
80+
for f in self.obj['features']]
81+
}

vt2geojson/tools.py

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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):
9+
return urlparse(uri).scheme != ""
10+
11+
12+
def vt_bytes_to_geojson(b_content, x, y, z, layer=None):
13+
data = decode(b_content)
14+
features_collections = [Layer(x=x, y=y, z=z, name=name, obj=layer_obj).toGeoJSON()
15+
for name, layer_obj in data.items() if layer is None or name == layer]
16+
return {
17+
"type": "FeatureCollection",
18+
"features": [fc["features"] for fc in features_collections]
19+
}

0 commit comments

Comments
 (0)