From 4a61d9f3c4b9254db63f6daad81275763474f1b1 Mon Sep 17 00:00:00 2001 From: Williams Date: Fri, 26 May 2017 18:43:10 -0400 Subject: [PATCH 1/7] added support for relations --- overpass/api.py | 79 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 8 deletions(-) diff --git a/overpass/api.py b/overpass/api.py index 3c40eff87b7..38fc98cdf2d 100644 --- a/overpass/api.py +++ b/overpass/api.py @@ -139,18 +139,81 @@ def _asGeoJSON(self, elements): elem_type = elem["type"] if elem_type == "node": geometry = geojson.Point((elem["lon"], elem["lat"])) + feature = geojson.Feature( + id=elem["id"], + geometry=geometry, + properties=elem.get("tags")) + features.append(feature) + elif elem_type == "way": points = [] for coords in elem["geometry"]: points.append((coords["lon"], coords["lat"])) geometry = geojson.LineString(points) - else: - continue - - feature = geojson.Feature( - id=elem["id"], - geometry=geometry, - properties=elem.get("tags")) - features.append(feature) + feature = geojson.Feature( + id=elem["id"], + geometry=geometry, + properties=elem.get("tags")) + features.append(feature) + + elif elem_type == "relation": + # initialize result lists + polygons = [] + poly = [] + points = [] + # conditions + prev = "inner" + not_first = False + for mem in elem["members"]: + # address outer values + if mem['role'] == 'outer': + if prev == "inner": + # start new outer polygon + points = [] + + if points == [] and not_first: + # append the previous poly to the polygon list + polygons.append(poly) + poly = [] + + for coords in mem["geometry"]: + points.append([coords["lon"], coords["lat"]]) + + if points[-1] == points[0]: + # finish the outer polygon if it has met the start + is_complete = True + poly.append(points) + points = [] + # update condition + prev = "outer" + + # address inner points + if mem['role'] == "inner": + for coords in mem["geometry"]: + points.append([coords["lon"], coords["lat"]]) + + # check if the inner is complete + if points[-1] == points[0]: + poly.append(points) + points = [] + # update conditoin + prev = "inner" + + not_first = True + # + polygons.append(poly) + + if polygons != [[]]: + # create MultiPolygon feature + poly_props = elem.get("tags") + poly_props.update({'id':elem['id']}) + multipoly = {"type": "Feature", + "properties" : poly_props, + "geometry": { + "type": "MultiPolygon", + "coordinates": polygons}} + + # add to features + features.append(multipoly) return geojson.FeatureCollection(features) From 51c4ae9ecd42e1bc377b802651ad84c2db058964 Mon Sep 17 00:00:00 2001 From: Williams Date: Tue, 12 Sep 2017 11:19:41 -0400 Subject: [PATCH 2/7] added support for multipolygons --- overpass/api.py | 93 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 19 deletions(-) diff --git a/overpass/api.py b/overpass/api.py index 38fc98cdf2d..097250bedca 100644 --- a/overpass/api.py +++ b/overpass/api.py @@ -2,9 +2,10 @@ import json import geojson import logging +import code from .errors import (OverpassSyntaxError, TimeoutError, MultipleRequestsError, - ServerLoadError, UnknownOverpassError, ServerRuntimeError) + ServerLoadError, UnknownOverpassError, ServerRuntimeError) class API(object): """A simple Python wrapper for the OpenStreetMap Overpass API""" @@ -135,6 +136,7 @@ def _GetFromOverpass(self, query): def _asGeoJSON(self, elements): features = [] + no_match_count = 0 for elem in elements: elem_type = elem["type"] if elem_type == "node": @@ -164,7 +166,55 @@ def _asGeoJSON(self, elements): # conditions prev = "inner" not_first = False - for mem in elem["members"]: + + for pos in range(len(elem["members"])): + mem = elem['members'][pos] + + # check whether the coordinates of the next member need to be reversed + # also sometimes the next member may not actually connect to the previous member, so if necessary, find a matching member + if points != []: + dist_start = (points[-1][0] - mem["geometry"][0]["lon"])**2 + (points[-1][1] - mem["geometry"][0]["lat"])**2 + dist_end = (points[-1][0] - mem["geometry"][-1]["lon"])**2 + (points[-1][1] - mem["geometry"][-1]["lat"])**2 + if dist_start == 0: + pass # don't need to do anything + elif dist_end == 0: + # flip the next member - it is entered in the wrong direction + mem["geometry"] = list(reversed(mem["geometry"])) + else: + # try flipping the previous member + dist_flipped_start = (points[0][0] - mem["geometry"][0]["lon"])**2 + (points[0][1] - mem["geometry"][0]["lat"])**2 + dist_flipped_end = (points[0][0] - mem["geometry"][-1]["lon"])**2 + (points[0][1] - mem["geometry"][-1]["lat"])**2 + if dist_flipped_start == 0: + # just flip the start + points = list(reversed(points)) + elif dist_flipped_end == 0: + # both need to be flipped + points = list(reversed(points)) + mem["geometry"] = list(reversed(mem["geometry"])) + else: + # no matches -- look for a new match + point_found = False + for i in range(pos + 1, len(elem['members'])): + if not point_found: + new_pt = elem['members'][i] + dist_start = (new_pt['geometry'][0]['lon'] - points[-1][0])**2 + (new_pt['geometry'][0]['lat'] - points[-1][1])**2 + dist_end = (new_pt['geometry'][-1]['lon'] - points[-1][0])**2 + (new_pt['geometry'][-1]['lat'] - points[-1][1])**2 + + if dist_start == 0 or dist_end == 0: + point_found = True + # swap the order of the members -- we have found the one we want + elem['members'][pos], elem['members'][i] = elem['members'][i], elem['members'][pos] + # save this new point as mem + mem = elem['members'][pos] + + if dist_end == 0: + mem['geometry'] = list(reversed(mem['geometry'])) + + if not point_found: + no_match_count += 1 + # don't work with this park + continue + # address outer values if mem['role'] == 'outer': if prev == "inner": @@ -175,13 +225,15 @@ def _asGeoJSON(self, elements): # append the previous poly to the polygon list polygons.append(poly) poly = [] - + for coords in mem["geometry"]: - points.append([coords["lon"], coords["lat"]]) + try: + points.append([coords["lon"], coords["lat"]]) + except: + code.interact(local=locals()) if points[-1] == points[0]: # finish the outer polygon if it has met the start - is_complete = True poly.append(points) points = [] # update condition @@ -200,20 +252,23 @@ def _asGeoJSON(self, elements): prev = "inner" not_first = True - # + + # add in the final poly polygons.append(poly) if polygons != [[]]: - # create MultiPolygon feature - poly_props = elem.get("tags") - poly_props.update({'id':elem['id']}) - multipoly = {"type": "Feature", - "properties" : poly_props, - "geometry": { - "type": "MultiPolygon", - "coordinates": polygons}} - - # add to features - features.append(multipoly) - - return geojson.FeatureCollection(features) + # create MultiPolygon feature - separate multipolygon for each outer + for outer_poly in polygons: + poly_props = elem.get("tags") + poly_props.update({'id':elem['id']}) + multipoly = {"type": "Feature", + "properties" : poly_props, + "geometry": { + "type": "MultiPolygon", + "coordinates": [outer_poly]}} + + # add to features + features.append(multipoly) + + print('number of unmatched parks: {}'.format(no_match_count)) + return geojson.FeatureCollection(features) \ No newline at end of file From f0c4560a2086b03f28025513ecde5f20733b8636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=BCller?= Date: Tue, 10 Sep 2019 19:21:25 +0200 Subject: [PATCH 3/7] Small code improvements --- overpass/api.py | 52 +++++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/overpass/api.py b/overpass/api.py index ac396ca7232..110e27c517c 100644 --- a/overpass/api.py +++ b/overpass/api.py @@ -190,9 +190,9 @@ def _as_geojson(self, elements): features = [] geometry = None - no_match_count = 0 for elem in elements: elem_type = elem.get("type") + elem_tags = elem.get("tags") if elem_type and elem_type == "node": geometry = geojson.Point((elem.get("lon"), elem.get("lat"))) elif elem_type and elem_type == "way": @@ -218,8 +218,10 @@ def _as_geojson(self, elements): # also sometimes the next member may not actually connect to the previous member, so if necessary, # find a matching member if points: - dist_start = (points[-1][0] - mem["geometry"][0]["lon"])**2 + (points[-1][1] - mem["geometry"][0]["lat"])**2 - dist_end = (points[-1][0] - mem["geometry"][-1]["lon"])**2 + (points[-1][1] - mem["geometry"][-1]["lat"])**2 + dist_start = (points[-1][0] - mem["geometry"][0]["lon"]) ** 2 + ( + points[-1][1] - mem["geometry"][0]["lat"]) ** 2 + dist_end = (points[-1][0] - mem["geometry"][-1]["lon"]) ** 2 + ( + points[-1][1] - mem["geometry"][-1]["lat"]) ** 2 if dist_start == 0: pass # don't need to do anything elif dist_end == 0: @@ -227,8 +229,10 @@ def _as_geojson(self, elements): mem["geometry"] = list(reversed(mem["geometry"])) else: # try flipping the previous member - dist_flipped_start = (points[0][0] - mem["geometry"][0]["lon"])**2 + (points[0][1] - mem["geometry"][0]["lat"])**2 - dist_flipped_end = (points[0][0] - mem["geometry"][-1]["lon"])**2 + (points[0][1] - mem["geometry"][-1]["lat"])**2 + dist_flipped_start = (points[0][0] - mem["geometry"][0]["lon"]) ** 2 + ( + points[0][1] - mem["geometry"][0]["lat"]) ** 2 + dist_flipped_end = (points[0][0] - mem["geometry"][-1]["lon"]) ** 2 + ( + points[0][1] - mem["geometry"][-1]["lat"]) ** 2 if dist_flipped_start == 0: # just flip the start points = list(reversed(points)) @@ -242,24 +246,22 @@ def _as_geojson(self, elements): for i in range(pos + 1, len(elem['members'])): if not point_found: new_pt = elem['members'][i] - dist_start = (new_pt['geometry'][0]['lon'] - points[-1][0])**2 + (new_pt['geometry'][0]['lat'] - points[-1][1])**2 - dist_end = (new_pt['geometry'][-1]['lon'] - points[-1][0])**2 + (new_pt['geometry'][-1]['lat'] - points[-1][1])**2 + dist_start = (new_pt['geometry'][0]['lon'] - points[-1][0]) ** 2 + ( + new_pt['geometry'][0]['lat'] - points[-1][1]) ** 2 + dist_end = (new_pt['geometry'][-1]['lon'] - points[-1][0]) ** 2 + ( + new_pt['geometry'][-1]['lat'] - points[-1][1]) ** 2 if dist_start == 0 or dist_end == 0: point_found = True # swap the order of the members -- we have found the one we want - elem['members'][pos], elem['members'][i] = elem['members'][i], elem['members'][pos] + elem['members'][pos], elem['members'][i] = elem['members'][i], \ + elem['members'][pos] # save this new point as mem mem = elem['members'][pos] if dist_end == 0: mem['geometry'] = list(reversed(mem['geometry'])) - if not point_found: - no_match_count += 1 - # don't work with this park - continue - # address outer values if mem['role'] == 'outer': if prev == "inner": @@ -293,7 +295,7 @@ def _as_geojson(self, elements): if points[-1] == points[0]: poly.append(points) points = [] - # update conditoin + # update condition prev = "inner" not_first = True @@ -302,30 +304,16 @@ def _as_geojson(self, elements): polygons.append(poly) if polygons != [[]]: - # create MultiPolygon feature - separate multipolygon for each outer - for outer_poly in polygons: - poly_props = elem.get("tags") - poly_props.update({'id': elem['id']}) - multipoly = { - "type": "Feature", - "properties": poly_props, - "geometry": { - "type": "MultiPolygon", - "coordinates": [outer_poly] - } - } - # add to features - features.append(multipoly) + geometry = geojson.MultiPolygon(polygons) else: continue - if elem_type and (elem_type == "node" or elem_type == "way"): + if geometry: feature = geojson.Feature( id=elem["id"], geometry=geometry, - properties=elem.get("tags") + properties=elem_tags ) features.append(feature) - - # print('number of unmatched parks: {}'.format(no_match_count)) + return geojson.FeatureCollection(features) From 5fabd38b65ceeb06e2aa9b8310c8639e1aff9077 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=BCller?= Date: Tue, 10 Sep 2019 22:25:43 +0200 Subject: [PATCH 4/7] Bugfixed and cleaned up _as_geojson method --- Pipfile | 1 + overpass/api.py | 141 +++++++++++------------------------------------ requirements.txt | 1 + setup.py | 2 +- 4 files changed, 34 insertions(+), 111 deletions(-) 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 110e27c517c..e53a4b33daa 100644 --- a/overpass/api.py +++ b/overpass/api.py @@ -8,7 +8,7 @@ import csv import geojson import logging -import code +from shapely.geometry import Polygon, Point from io import StringIO from .errors import ( OverpassSyntaxError, @@ -193,117 +193,38 @@ def _as_geojson(self, elements): for elem in elements: elem_type = elem.get("type") elem_tags = elem.get("tags") - if elem_type and elem_type == "node": + 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 and elem_type == "relation": - # initialize result lists + 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 = [] - poly = [] - points = [] - # conditions - prev = "inner" - not_first = False - - for pos in range(len(elem["members"])): - mem = elem['members'][pos] - - # check whether the coordinates of the next member need to be reversed - # also sometimes the next member may not actually connect to the previous member, so if necessary, - # find a matching member - if points: - dist_start = (points[-1][0] - mem["geometry"][0]["lon"]) ** 2 + ( - points[-1][1] - mem["geometry"][0]["lat"]) ** 2 - dist_end = (points[-1][0] - mem["geometry"][-1]["lon"]) ** 2 + ( - points[-1][1] - mem["geometry"][-1]["lat"]) ** 2 - if dist_start == 0: - pass # don't need to do anything - elif dist_end == 0: - # flip the next member - it is entered in the wrong direction - mem["geometry"] = list(reversed(mem["geometry"])) - else: - # try flipping the previous member - dist_flipped_start = (points[0][0] - mem["geometry"][0]["lon"]) ** 2 + ( - points[0][1] - mem["geometry"][0]["lat"]) ** 2 - dist_flipped_end = (points[0][0] - mem["geometry"][-1]["lon"]) ** 2 + ( - points[0][1] - mem["geometry"][-1]["lat"]) ** 2 - if dist_flipped_start == 0: - # just flip the start - points = list(reversed(points)) - elif dist_flipped_end == 0: - # both need to be flipped - points = list(reversed(points)) - mem["geometry"] = list(reversed(mem["geometry"])) - else: - # no matches -- look for a new match - point_found = False - for i in range(pos + 1, len(elem['members'])): - if not point_found: - new_pt = elem['members'][i] - dist_start = (new_pt['geometry'][0]['lon'] - points[-1][0]) ** 2 + ( - new_pt['geometry'][0]['lat'] - points[-1][1]) ** 2 - dist_end = (new_pt['geometry'][-1]['lon'] - points[-1][0]) ** 2 + ( - new_pt['geometry'][-1]['lat'] - points[-1][1]) ** 2 - - if dist_start == 0 or dist_end == 0: - point_found = True - # swap the order of the members -- we have found the one we want - elem['members'][pos], elem['members'][i] = elem['members'][i], \ - elem['members'][pos] - # save this new point as mem - mem = elem['members'][pos] - - if dist_end == 0: - mem['geometry'] = list(reversed(mem['geometry'])) - - # address outer values - if mem['role'] == 'outer': - if prev == "inner": - # start new outer polygon - points = [] - - if points == [] and not_first: - # append the previous poly to the polygon list - polygons.append(poly) - poly = [] - - for coords in mem["geometry"]: - try: - points.append([coords["lon"], coords["lat"]]) - except: - code.interact(local=locals()) - - if points[-1] == points[0]: - # finish the outer polygon if it has met the start - poly.append(points) - points = [] - # update condition - prev = "outer" - - # address inner points - if mem['role'] == "inner": - for coords in mem["geometry"]: - points.append([coords["lon"], coords["lat"]]) - - # check if the inner is complete - if points[-1] == points[0]: - poly.append(points) - points = [] - # update condition - prev = "inner" - - not_first = True - - # add in the final poly - polygons.append(poly) - - if 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]) + # 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]) + for poly in polygons: + polygon = Polygon(poly[0]) + if polygon.contains(point): + poly.append(points) + break + # Finally create MultiPolygon geometry + if polygons: geometry = geojson.MultiPolygon(polygons) else: continue 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"]}, ) From f6ba57a9c09a124be6dc9e072c6fc484244e92c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=BCller?= Date: Tue, 10 Sep 2019 23:40:39 +0200 Subject: [PATCH 5/7] Added extended geojson test based on example data --- tests/example.json | 1 + tests/example.response | Bin 0 -> 18475 bytes tests/test_api.py | 22 ++++++++++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 tests/example.json create mode 100644 tests/example.response 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 0000000000000000000000000000000000000000..f7973c2dcbe2a7506a5f82c2a8531953ed243d62 GIT binary patch literal 18475 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 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")) + + # 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 From e0b2fcf23818c201aead8809f4adf89564ca656f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=BCller?= Date: Fri, 13 Sep 2019 12:33:21 +0200 Subject: [PATCH 6/7] Implemented feedback --- overpass/api.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/overpass/api.py b/overpass/api.py index e53a4b33daa..a7790ad737d 100644 --- a/overpass/api.py +++ b/overpass/api.py @@ -206,28 +206,37 @@ def _as_geojson(self, elements): # 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', [])] + 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', [])] + 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 + raise UnknownOverpassError("Received corrupt data from Overpass (invalid element).") if geometry: feature = geojson.Feature( From 574270ee9b10a8842ac040b868e345a16bc73337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20M=C3=BCller?= Date: Mon, 21 Oct 2019 13:28:16 +0200 Subject: [PATCH 7/7] Added explanatory comment to test --- tests/test_api.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_api.py b/tests/test_api.py index 4e0cb4c89b9..757ba396e3e 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -32,6 +32,16 @@ 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;"),