diff --git a/Pipfile b/Pipfile index f3a69398d69..d2d085e72f1 100644 --- a/Pipfile +++ b/Pipfile @@ -7,6 +7,7 @@ name = "pypi" geojson = ">=1.3.1" requests = ">=2.20.0" nose = ">=1.3.7" +shapely = ">=1.6.4" [dev-packages] diff --git a/overpass/api.py b/overpass/api.py index f6f5c4aefa7..a7790ad737d 100644 --- a/overpass/api.py +++ b/overpass/api.py @@ -8,6 +8,7 @@ import csv import geojson import logging +from shapely.geometry import Polygon, Point from io import StringIO from .errors import ( OverpassSyntaxError, @@ -191,21 +192,58 @@ def _as_geojson(self, elements): geometry = None for elem in elements: elem_type = elem.get("type") - if elem_type and elem_type == "node": + elem_tags = elem.get("tags") + elem_geom = elem.get("geometry", []) + if elem_type == "node": + # Create Point geometry geometry = geojson.Point((elem.get("lon"), elem.get("lat"))) - elif elem_type and elem_type == "way": - points = [] - geom = elem.get("geometry") - if geom: - for coords in elem.get("geometry"): - points.append((coords["lon"], coords["lat"])) - geometry = geojson.LineString(points) + elif elem_type == "way": + # Create LineString geometry + geometry = geojson.LineString([(coords["lon"], coords["lat"]) for coords in elem_geom]) + elif elem_type == "relation": + # Initialize polygon list + polygons = [] + # First obtain the outer polygons + for member in elem.get("members", []): + if member["role"] == "outer": + points = [(coords["lon"], coords["lat"]) for coords in member.get("geometry", [])] + # Check that the outer polygon is complete + if points and points[-1] == points[0]: + polygons.append([points]) + else: + raise UnknownOverpassError("Received corrupt data from Overpass (incomplete polygon).") + # Then get the inner polygons + for member in elem.get("members", []): + if member["role"] == "inner": + points = [(coords["lon"], coords["lat"]) for coords in member.get("geometry", [])] + # Check that the inner polygon is complete + if points and points[-1] == points[0]: + # We need to check to which outer polygon the inner polygon belongs + point = Point(points[0]) + check = False + for poly in polygons: + polygon = Polygon(poly[0]) + if polygon.contains(point): + poly.append(points) + check = True + break + if not check: + raise UnknownOverpassError("Received corrupt data from Overpass (inner polygon cannot " + "be matched to outer polygon).") + else: + raise UnknownOverpassError("Received corrupt data from Overpass (incomplete polygon).") + # Finally create MultiPolygon geometry + if polygons: + geometry = geojson.MultiPolygon(polygons) else: - continue - - feature = geojson.Feature( - id=elem["id"], geometry=geometry, properties=elem.get("tags") - ) - features.append(feature) + raise UnknownOverpassError("Received corrupt data from Overpass (invalid element).") + + if geometry: + feature = geojson.Feature( + id=elem["id"], + geometry=geometry, + properties=elem_tags + ) + features.append(feature) return geojson.FeatureCollection(features) diff --git a/requirements.txt b/requirements.txt index 2cb2b214320..43a6b41f359 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ geojson>=1.3.1 requests>=2.8.1 nose>=1.3.7 +shapely>=1.6.4 \ No newline at end of file diff --git a/setup.py b/setup.py index 20456efc0fc..754b14f2cb9 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,6 @@ "Topic :: Scientific/Engineering :: GIS", "Topic :: Utilities", ], - install_requires=["requests>=2.3.0", "geojson>=1.0.9"], + install_requires=["requests>=2.3.0", "geojson>=1.0.9", "shapely>=1.6.4"], extras_require={"test": ["pytest"]}, ) diff --git a/tests/example.json b/tests/example.json new file mode 100644 index 00000000000..54019de1451 --- /dev/null +++ b/tests/example.json @@ -0,0 +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": {}}]} \ No newline at end of file diff --git a/tests/example.response b/tests/example.response new file mode 100644 index 00000000000..f7973c2dcbe Binary files /dev/null and b/tests/example.response differ diff --git a/tests/test_api.py b/tests/test_api.py index 00a3622da0d..757ba396e3e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -4,6 +4,9 @@ # See LICENSE.txt for the full license text. import overpass +import geojson +import pickle +import os def test_initialize_api(): @@ -21,3 +24,32 @@ def test_geojson(): osm_geo = api.get("node(area:3602758138)[amenity=cafe]") assert len(osm_geo["features"]) > 1 + + +def test_geojson_extended(): + + class API(overpass.API): + def _get_from_overpass(self, query): + return pickle.load(open(os.path.join(os.path.dirname(__file__), "example.response"), "rb")) + + # The commented code should only be executed once when major changes to the Overpass API and/or to this wrapper are + # introduced. One than has to manually verify that the date in the example.response file from the Overpass API + # matches the data in the example.json file generated by this wrapper. + # + # The reason for this approach is the following: It is not safe to make calls to the actual API in this test as the + # API might momentarily be unavailable and the underlying data can also change at any moment. The commented code is + # needed to create the example.response and example.json files. The example.response file is subsequently used to + # fake the _get_from_overpass method during the tests and the example.json file is the reference that we are + # asserting against. + # + # api = overpass.API() + # osm_geo = api.get("rel(6518385);out body geom;way(10322303);out body geom;node(4927326183);", verbosity='body geom') + # pickle.dump(api._get_from_overpass("[out:json];rel(6518385);out body geom;way(10322303);out body geom;node(4927326183);out body geom;"), + # open(os.path.join(os.path.dirname(__file__), "example.response"), "wb"), + # protocol=2) + # geojson.dump(osm_geo, open(os.path.join(os.path.dirname(__file__), "example.json"), "w")) + + api = API() + osm_geo = api.get("rel(6518385);out body geom;way(10322303);out body geom;node(4927326183);", verbosity='body geom') + ref_geo = geojson.load(open(os.path.join(os.path.dirname(__file__), "example.json"), "r")) + assert osm_geo==ref_geo