Skip to content

Commit 7988b3f

Browse files
mvexelMartijn van Exel
authored and
Martijn van Exel
committed
Merge pull request mvexel#115 from lukasmu/relation_and_multipolygon_support
Relation and multipolygon support
2 parents c28b3c0 + 574270e commit 7988b3f

File tree

8 files changed

+90
-20
lines changed

8 files changed

+90
-20
lines changed

Pipfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ name = "pypi"
77
geojson = ">=1.3.1"
88
requests = ">=2.20.0"
99
nose = ">=1.3.7"
10+
shapely = ">=1.6.4"
1011

1112
[dev-packages]
1213

overpass/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"""Thin wrapper around the OpenStreetMap Overpass API."""
99

1010
__title__ = "overpass"
11-
__version__ = "0.6.1"
11+
__version__ = "0.7"
1212
__license__ = "Apache 2.0"
1313

1414
from .api import API

overpass/api.py

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import csv
99
import geojson
1010
import logging
11+
from shapely.geometry import Polygon, Point
1112
from io import StringIO
1213
from .errors import (
1314
OverpassSyntaxError,
@@ -191,21 +192,58 @@ def _as_geojson(self, elements):
191192
geometry = None
192193
for elem in elements:
193194
elem_type = elem.get("type")
194-
if elem_type and elem_type == "node":
195+
elem_tags = elem.get("tags")
196+
elem_geom = elem.get("geometry", [])
197+
if elem_type == "node":
198+
# Create Point geometry
195199
geometry = geojson.Point((elem.get("lon"), elem.get("lat")))
196-
elif elem_type and elem_type == "way":
197-
points = []
198-
geom = elem.get("geometry")
199-
if geom:
200-
for coords in elem.get("geometry"):
201-
points.append((coords["lon"], coords["lat"]))
202-
geometry = geojson.LineString(points)
200+
elif elem_type == "way":
201+
# Create LineString geometry
202+
geometry = geojson.LineString([(coords["lon"], coords["lat"]) for coords in elem_geom])
203+
elif elem_type == "relation":
204+
# Initialize polygon list
205+
polygons = []
206+
# First obtain the outer polygons
207+
for member in elem.get("members", []):
208+
if member["role"] == "outer":
209+
points = [(coords["lon"], coords["lat"]) for coords in member.get("geometry", [])]
210+
# Check that the outer polygon is complete
211+
if points and points[-1] == points[0]:
212+
polygons.append([points])
213+
else:
214+
raise UnknownOverpassError("Received corrupt data from Overpass (incomplete polygon).")
215+
# Then get the inner polygons
216+
for member in elem.get("members", []):
217+
if member["role"] == "inner":
218+
points = [(coords["lon"], coords["lat"]) for coords in member.get("geometry", [])]
219+
# Check that the inner polygon is complete
220+
if points and points[-1] == points[0]:
221+
# We need to check to which outer polygon the inner polygon belongs
222+
point = Point(points[0])
223+
check = False
224+
for poly in polygons:
225+
polygon = Polygon(poly[0])
226+
if polygon.contains(point):
227+
poly.append(points)
228+
check = True
229+
break
230+
if not check:
231+
raise UnknownOverpassError("Received corrupt data from Overpass (inner polygon cannot "
232+
"be matched to outer polygon).")
233+
else:
234+
raise UnknownOverpassError("Received corrupt data from Overpass (incomplete polygon).")
235+
# Finally create MultiPolygon geometry
236+
if polygons:
237+
geometry = geojson.MultiPolygon(polygons)
203238
else:
204-
continue
205-
206-
feature = geojson.Feature(
207-
id=elem["id"], geometry=geometry, properties=elem.get("tags")
208-
)
209-
features.append(feature)
239+
raise UnknownOverpassError("Received corrupt data from Overpass (invalid element).")
240+
241+
if geometry:
242+
feature = geojson.Feature(
243+
id=elem["id"],
244+
geometry=geometry,
245+
properties=elem_tags
246+
)
247+
features.append(feature)
210248

211249
return geojson.FeatureCollection(features)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
geojson>=1.3.1
22
requests>=2.8.1
33
nose>=1.3.7
4+
shapely>=1.6.4

setup.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
setup(
44
name="overpass",
55
packages=["overpass"],
6-
version="0.6.1",
6+
version="0.7",
77
description="Python wrapper for the OpenStreetMap Overpass API",
88
long_description="See README.md",
99
author="Martijn van Exel",
@@ -13,15 +13,12 @@
1313
keywords=["openstreetmap", "overpass", "wrapper"],
1414
classifiers=[
1515
"License :: OSI Approved :: Apache Software License",
16-
"Programming Language :: Python :: 2.7",
17-
"Programming Language :: Python :: 3.3",
18-
"Programming Language :: Python :: 3.4",
1916
"Programming Language :: Python :: 3.5",
2017
"Programming Language :: Python :: 3.6",
2118
"Programming Language :: Python :: 3.7",
2219
"Topic :: Scientific/Engineering :: GIS",
2320
"Topic :: Utilities",
2421
],
25-
install_requires=["requests>=2.3.0", "geojson>=1.0.9"],
22+
install_requires=["requests>=2.3.0", "geojson>=1.0.9", "shapely>=1.6.4"],
2623
extras_require={"test": ["pytest"]},
2724
)

tests/example.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"type": "FeatureCollection", "features": [{"type": "Feature", "id": 6518385, "geometry": {"type": "MultiPolygon", "coordinates": [[[[-122.197877, 37.85778], [-122.194092, 37.857792], [-122.194969, 37.856665], [-122.19383, 37.856063], [-122.192671, 37.855601], [-122.191239, 37.854941], [-122.190287, 37.854202], [-122.18951, 37.853395], [-122.189155, 37.85316], [-122.188619, 37.852042], [-122.188587, 37.850433], [-122.188458, 37.8468], [-122.193004, 37.84685], [-122.193102, 37.844092], [-122.196167, 37.843758], [-122.196416, 37.843891], [-122.196527, 37.84398], [-122.196596, 37.844063], [-122.196625, 37.844159], [-122.196626, 37.844275], [-122.196591, 37.8446], [-122.196555, 37.844785], [-122.19655, 37.844859], [-122.196562, 37.845099], [-122.196595, 37.845336], [-122.196707, 37.845596], [-122.19685, 37.845815], [-122.197436, 37.846209], [-122.197763, 37.846534], [-122.1982, 37.846859], [-122.198509, 37.847245], [-122.198686, 37.847378], [-122.198832, 37.847411], [-122.199029, 37.847391], [-122.199673, 37.847378], [-122.200188, 37.847506], [-122.200603, 37.847587], [-122.200899, 37.847626], [-122.201293, 37.847766], [-122.201452, 37.847847], [-122.201547, 37.847906], [-122.201615, 37.847969], [-122.201662, 37.84805], [-122.201686, 37.848126], [-122.201714, 37.848254], [-122.201749, 37.848329], [-122.201797, 37.848392], [-122.201848, 37.848454], [-122.201904, 37.848511], [-122.201952, 37.848548], [-122.202784, 37.849109], [-122.202848, 37.849167], [-122.202922, 37.849257], [-122.203008, 37.849391], [-122.20307, 37.849528], [-122.203293, 37.849857], [-122.203312, 37.849699], [-122.203361, 37.849483], [-122.203425, 37.84928], [-122.203476, 37.84912], [-122.203566, 37.848937], [-122.203649, 37.848788], [-122.203775, 37.848634], [-122.203864, 37.848741], [-122.203814, 37.848821], [-122.203839, 37.848899], [-122.203951, 37.84896], [-122.204071, 37.848987], [-122.204475, 37.848981], [-122.204632, 37.849013], [-122.204699, 37.849004], [-122.204719, 37.848972], [-122.204722, 37.848938], [-122.204701, 37.848915], [-122.204612, 37.848879], [-122.204503, 37.848825], [-122.204432, 37.848755], [-122.204427, 37.848645], [-122.204533, 37.84831], [-122.20456, 37.848005], [-122.204584, 37.847916], [-122.204615, 37.847869], [-122.204655, 37.847849], [-122.204778, 37.847778], [-122.204807, 37.847701], [-122.204831, 37.847448], [-122.204949, 37.847111], [-122.205346, 37.846512], [-122.205394, 37.846387], [-122.20541, 37.846273], [-122.205352, 37.845942], [-122.205358, 37.845857], [-122.205495, 37.845739], [-122.205632, 37.84565], [-122.205692, 37.845581], [-122.205716, 37.845454], [-122.205755, 37.845186], [-122.205853, 37.844948], [-122.205926, 37.844826], [-122.206013, 37.844757], [-122.206318, 37.844628], [-122.206408, 37.844569], [-122.206432, 37.844509], [-122.206444, 37.844375], [-122.206447, 37.844294], [-122.206449, 37.844236], [-122.206428, 37.844158], [-122.206375, 37.844106], [-122.206287, 37.844071], [-122.206153, 37.844079], [-122.206148, 37.843571], [-122.206622, 37.843483], [-122.208596, 37.850482], [-122.208577, 37.850955], [-122.208566, 37.851015], [-122.208541, 37.851063], [-122.208502, 37.851109], [-122.208438, 37.851157], [-122.208486, 37.85169], [-122.209044, 37.852325], [-122.210718, 37.85423], [-122.210978, 37.854526], [-122.210778, 37.855026], [-122.210957, 37.855322], [-122.211079, 37.855523], [-122.211163, 37.856313], [-122.210923, 37.856645], [-122.211096, 37.856762], [-122.21085, 37.856934], [-122.211025, 37.857108], [-122.211307, 37.856894], [-122.212047, 37.857809], [-122.212137, 37.858256], [-122.211748, 37.85828], [-122.212917, 37.859008], [-122.213903, 37.858986], [-122.215701, 37.860658], [-122.216433, 37.860582], [-122.216481, 37.860648], [-122.216546, 37.860723], [-122.21661, 37.860783], [-122.216911, 37.86104], [-122.217074, 37.861197], [-122.217137, 37.861274], [-122.217197, 37.861362], [-122.217243, 37.861458], [-122.217274, 37.861549], [-122.217316, 37.861709], [-122.217372, 37.861838], [-122.217531, 37.861998], [-122.217921, 37.862287], [-122.21805, 37.862382], [-122.218422, 37.862781], [-122.218616, 37.86293], [-122.218267, 37.863282], [-122.217, 37.862143], [-122.21633, 37.862981], [-122.215336, 37.86253], [-122.214499, 37.862013], [-122.213738, 37.861522], [-122.212933, 37.8612], [-122.21206, 37.86088], [-122.2115, 37.860812], [-122.211345, 37.860794], [-122.210262, 37.860607], [-122.209757, 37.860235], [-122.209453, 37.859626], [-122.208785, 37.859769], [-122.20885, 37.859909], [-122.208941, 37.860063], [-122.207408, 37.859896], [-122.207189, 37.861261], [-122.209038, 37.861482], [-122.208806, 37.862683], [-122.206636, 37.862631], [-122.206689, 37.861446], [-122.198009, 37.861454], [-122.197877, 37.85778]], [[-122.195626, 37.850528], [-122.195622, 37.850664], [-122.194665, 37.85065], [-122.194668, 37.850514], [-122.195626, 37.850528]], [[-122.193631, 37.850507], [-122.193653, 37.849682], [-122.192015, 37.849655], [-122.191993, 37.85048], [-122.191979, 37.851031], [-122.192966, 37.851048], [-122.192981, 37.850497], [-122.193631, 37.850507]]], [[[-122.214506, 37.865654], [-122.215787, 37.863741], [-122.213115, 37.862469], [-122.211749, 37.862006], [-122.211099, 37.861874], [-122.211011, 37.861948], [-122.210893, 37.862033], [-122.210753, 37.862151], [-122.210621, 37.862247], [-122.210469, 37.86238], [-122.210317, 37.862539], [-122.210193, 37.862681], [-122.21007, 37.862832], [-122.209987, 37.86298], [-122.209865, 37.863175], [-122.209788, 37.863354], [-122.209725, 37.863555], [-122.209671, 37.863764], [-122.209635, 37.863967], [-122.209617, 37.864195], [-122.209609, 37.864417], [-122.20983, 37.86445], [-122.210077, 37.864798], [-122.212723, 37.864933], [-122.212632, 37.865082], [-122.214506, 37.865654]]], [[[-122.183933, 37.846772], [-122.179485, 37.846769], [-122.179469, 37.850483], [-122.174886, 37.850508], [-122.174897, 37.846908], [-122.174357, 37.846912], [-122.174214, 37.845764], [-122.174206, 37.845438], [-122.174174, 37.844848], [-122.174222, 37.844019], [-122.174158, 37.843805], [-122.173633, 37.843335], [-122.173283, 37.842525], [-122.173042, 37.842191], [-122.172325, 37.841867], [-122.171624, 37.841611], [-122.170393, 37.841024], [-122.170366, 37.839856], [-122.173416, 37.839848], [-122.175136, 37.839844], [-122.176957, 37.837722], [-122.178038, 37.838142], [-122.178221, 37.837887], [-122.179037, 37.838177], [-122.179495, 37.838386], [-122.179526, 37.839474], [-122.18098, 37.839502], [-122.180953, 37.839917], [-122.182271, 37.839908], [-122.182634, 37.839717], [-122.182912, 37.83957], [-122.183949, 37.839595], [-122.183929, 37.840341], [-122.183935, 37.841137], [-122.183933, 37.846772]]]]}, "properties": {"boundary": "national_park", "contact:website": "http://www.ebparks.org/parks/sibley", "leisure": "park", "name": "Sibley Volcanic Regional Preserve", "operator": "East Bay Regional Park District", "owner": "East Bay Regional Park District", "source": "https://www.ebparks.org/images/Assets/files/parks/sibley/Sibley-map_2250w-04-23-18.gif", "type": "multipolygon", "website": "https://www.ebparks.org/parks/sibley/", "wikidata": "Q7349780", "wikipedia": "en:Robert Sibley Volcanic Regional Preserve"}}, {"type": "Feature", "id": 10322303, "geometry": {"type": "LineString", "coordinates": [[-122.318477, 37.869901], [-122.318412, 37.869652], [-122.318357, 37.869442], [-122.318313, 37.869271], [-122.318271, 37.86911], [-122.318218, 37.868906], [-122.318134, 37.868831], [-122.317998, 37.868763], [-122.317754, 37.86875], [-122.317622, 37.868773], [-122.317266, 37.86893], [-122.317185, 37.869015], [-122.317255, 37.869279], [-122.317297, 37.869439], [-122.317345, 37.869618], [-122.317421, 37.869906], [-122.317464, 37.87007]]}, "properties": {"addr:city": "Berkeley", "foot": "yes", "highway": "service"}}, {"type": "Feature", "id": 4927326183, "geometry": {"type": "Point", "coordinates": [-122.318412, 37.869652]}, "properties": {}}]}

tests/example.response

18 KB
Binary file not shown.

tests/test_api.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
# See LICENSE.txt for the full license text.
55

66
import overpass
7+
import geojson
8+
import pickle
9+
import os
710

811

912
def test_initialize_api():
@@ -21,3 +24,32 @@ def test_geojson():
2124

2225
osm_geo = api.get("node(area:3602758138)[amenity=cafe]")
2326
assert len(osm_geo["features"]) > 1
27+
28+
29+
def test_geojson_extended():
30+
31+
class API(overpass.API):
32+
def _get_from_overpass(self, query):
33+
return pickle.load(open(os.path.join(os.path.dirname(__file__), "example.response"), "rb"))
34+
35+
# The commented code should only be executed once when major changes to the Overpass API and/or to this wrapper are
36+
# introduced. One than has to manually verify that the date in the example.response file from the Overpass API
37+
# matches the data in the example.json file generated by this wrapper.
38+
#
39+
# The reason for this approach is the following: It is not safe to make calls to the actual API in this test as the
40+
# API might momentarily be unavailable and the underlying data can also change at any moment. The commented code is
41+
# needed to create the example.response and example.json files. The example.response file is subsequently used to
42+
# fake the _get_from_overpass method during the tests and the example.json file is the reference that we are
43+
# asserting against.
44+
#
45+
# api = overpass.API()
46+
# osm_geo = api.get("rel(6518385);out body geom;way(10322303);out body geom;node(4927326183);", verbosity='body geom')
47+
# pickle.dump(api._get_from_overpass("[out:json];rel(6518385);out body geom;way(10322303);out body geom;node(4927326183);out body geom;"),
48+
# open(os.path.join(os.path.dirname(__file__), "example.response"), "wb"),
49+
# protocol=2)
50+
# geojson.dump(osm_geo, open(os.path.join(os.path.dirname(__file__), "example.json"), "w"))
51+
52+
api = API()
53+
osm_geo = api.get("rel(6518385);out body geom;way(10322303);out body geom;node(4927326183);", verbosity='body geom')
54+
ref_geo = geojson.load(open(os.path.join(os.path.dirname(__file__), "example.json"), "r"))
55+
assert osm_geo==ref_geo

0 commit comments

Comments
 (0)