diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f218bb..42c7a1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ - accept shapely Polygon and MultiPolygon for `bpolys` input parameter - if a request fails a bash script containing the respective `curl` command is logged (if possible). This allows for easier debugging and sharing of failed requests. +### Changed + + - breaking: geodataframes now contain a `@other_tags` colum containing all OSM tags. This behaviour can be adapted using the `explode_tags` parameter that allows to specify tags that should be in a separate column or to disable the feature completely. The latter will result in a potentially wide but sparse data frame. + ### Removed - support for python < 3.10 diff --git a/ohsome/response.py b/ohsome/response.py index ad38d01..94d582c 100644 --- a/ohsome/response.py +++ b/ohsome/response.py @@ -4,6 +4,7 @@ """Class for ohsome API response""" import json +from typing import Optional import geopandas as gpd import pandas as pd @@ -22,17 +23,22 @@ def __init__(self, response=None, url=None, params=None): self.parameters = params self.data = response.json() - def as_dataframe(self, multi_index=True): + def as_dataframe( + self, multi_index: Optional[bool] = True, explode_tags: Optional[tuple] = () + ): """ Converts the ohsome response to a pandas.DataFrame or a geopandas.GeoDataFrame if the response contains geometries :param multi_index: If true returns the dataframe with a multi index + :param explode_tags: By default, tags of extracted features are stored in a single dict-column. You can specify + a tuple of tags that should be popped from this column. To disable it completely, pass None. Yet, be aware that + you may get a large but sparse data frame. :return: pandas.DataFrame or geopandas.GeoDataFrame """ if "features" not in self.data.keys(): return self._as_dataframe(multi_index) else: - return self._as_geodataframe(multi_index) + return self._as_geodataframe(multi_index, explode_tags) def _as_dataframe(self, multi_index=True): """ @@ -67,7 +73,9 @@ def _as_dataframe(self, multi_index=True): return result_df.sort_index() - def _as_geodataframe(self, multi_index=True): + def _as_geodataframe( + self, multi_index: Optional[bool] = True, explode_tags: Optional[tuple] = () + ): """ Converts the ohsome response to a geopandas.GeoDataFrame :param multi_index: If true returns the dataframe with a multi index @@ -78,7 +86,25 @@ def _as_geodataframe(self, multi_index=True): return gpd.GeoDataFrame(crs="epsg:4326", columns=["@osmId", "geometry"]) try: + if explode_tags is not None: + for feature in self.data["features"]: + properties = feature["properties"] + tags = {} + new_properties = {} + for k in properties.keys(): + if ( + (k.startswith("@")) + or (k == "timestamp") + or (k in explode_tags) + ): + new_properties[k] = properties.get(k) + else: + tags[k] = properties.get(k) + new_properties["@other_tags"] = tags + feature["properties"] = new_properties + features = gpd.GeoDataFrame().from_features(self.data, crs="epsg:4326") + except TypeError: raise TypeError( "This result type cannot be converted to a GeoPandas GeoDataFrame object." diff --git a/ohsome/test/cassettes/test_response/test_extra_tags_argument.yaml b/ohsome/test/cassettes/test_response/test_extra_tags_argument.yaml new file mode 100644 index 0000000..58f3ab3 --- /dev/null +++ b/ohsome/test/cassettes/test_response/test_extra_tags_argument.yaml @@ -0,0 +1,98 @@ +interactions: +- request: + body: bboxes=8.7137%2C49.4096%2C8.717%2C49.4119&time=2016-01-01&filter=name%3DKrautturm+and+type%3Away&properties=tags%2Cmetadata + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '123' + Content-Type: + - application/x-www-form-urlencoded + user-agent: + - ohsome-py/0.2.0 + method: POST + uri: https://api.ohsome.org/v1/elements/geometry + response: + body: + string: "{\n \"attribution\" : {\n \"url\" : \"https://ohsome.org/copyrights\",\n + \ \"text\" : \"\xA9 OpenStreetMap contributors\"\n },\n \"apiVersion\" + : \"1.10.1\",\n \"type\" : \"FeatureCollection\",\n \"features\" : [{\n + \ \"type\" : \"Feature\",\n \"geometry\" : {\n \"type\" : \"Polygon\",\n + \ \"coordinates\" : [\n [\n [\n 8.7160632,\n + \ 49.4102899\n ],\n [\n 8.7160749,\n + \ 49.4103121\n ],\n [\n 8.7160827,\n + \ 49.4103235\n ],\n [\n 8.7160963,\n + \ 49.4103374\n ],\n [\n 8.716121,\n + \ 49.4103549\n ],\n [\n 8.7161392,\n + \ 49.4103691\n ],\n [\n 8.7161626,\n + \ 49.4103819\n ],\n [\n 8.716186,\n + \ 49.4103894\n ],\n [\n 8.716225,\n + \ 49.4103952\n ],\n [\n 8.7162679,\n + \ 49.4103926\n ],\n [\n 8.7162893,\n + \ 49.4103882\n ],\n [\n 8.7163176,\n + \ 49.410378\n ],\n [\n 8.7163507,\n + \ 49.4103615\n ],\n [\n 8.7163829,\n + \ 49.4103317\n ],\n [\n 8.7163975,\n + \ 49.4103057\n ],\n [\n 8.7164024,\n + \ 49.4102791\n ],\n [\n 8.7164004,\n + \ 49.4102506\n ],\n [\n 8.7163868,\n + \ 49.4102277\n ],\n [\n 8.7162815,\n + \ 49.4102258\n ],\n [\n 8.7162659,\n + \ 49.4102189\n ],\n [\n 8.7162503,\n + \ 49.4102144\n ],\n [\n 8.7162289,\n + \ 49.4102106\n ],\n [\n 8.7162113,\n + \ 49.4102093\n ],\n [\n 8.7161987,\n + \ 49.4101992\n ],\n [\n 8.7161665,\n + \ 49.4101992\n ],\n [\n 8.7161519,\n + \ 49.4101954\n ],\n [\n 8.7161343,\n + \ 49.4101846\n ],\n [\n 8.7161119,\n + \ 49.4101967\n ],\n [\n 8.7160934,\n + \ 49.4102119\n ],\n [\n 8.7160807,\n + \ 49.4102277\n ],\n [\n 8.7160719,\n + \ 49.4102449\n ],\n [\n 8.7160671,\n + \ 49.4102594\n ],\n [\n 8.7160641,\n + \ 49.4102753\n ],\n [\n 8.7160632,\n + \ 49.4102899\n ]\n ]\n ]\n },\n \"properties\" + : {\n \"@changesetId\" : 30687511,\n \"@lastEdit\" : \"2015-05-01T10:33:22Z\",\n + \ \"@osmId\" : \"way/24885641\",\n \"@osmType\" : \"way\",\n \"@snapshotTimestamp\" + : \"2016-01-01T00:00:00Z\",\n \"@version\" : 4,\n \"building\" : + \"yes\",\n \"name\" : \"Krautturm\"\n }\n }]\n}\n" + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - Origin,Accept,X-Requested-With,Content-Type,Access-Control-Request-Method,Access-Control-Request-Headers,Authorization + Access-Control-Allow-Methods: + - POST, GET + Access-Control-Allow-Origin: + - '*' + Access-Control-Max-Age: + - '3600' + Connection: + - Keep-Alive + Content-Encoding: + - gzip + Content-Type: + - application/geo+json;charset=utf-8 + Content-disposition: + - attachment;filename=ohsome.geojson + Date: + - Fri, 17 Nov 2023 14:22:26 GMT + Keep-Alive: + - timeout=5, max=100 + Server: + - Apache + Strict-Transport-Security: + - max-age=63072000; includeSubdomains; + Transfer-Encoding: + - chunked + vary: + - accept-encoding + status: + code: 200 + message: '' +version: 1 diff --git a/ohsome/test/test_response.py b/ohsome/test/test_response.py index 3d12952..f5d187f 100644 --- a/ohsome/test/test_response.py +++ b/ohsome/test/test_response.py @@ -265,6 +265,28 @@ def test_elements_geometry(base_client): assert len(result) == 1 +@pytest.mark.vcr +def test_extra_tags_argument(base_client): + """ + Tests whether the result of elements.geometry is converted to a geopandas.GeoDataFrame + :return: + """ + bboxes = "8.7137,49.4096,8.717,49.4119" + time = "2016-01-01" + flter = "name=Krautturm and type:way" + + response = base_client.elements.geometry.post( + bboxes=bboxes, time=time, filter=flter, properties="tags,metadata" + ) + result = response.as_dataframe() + + assert "@other_tags" in result.columns + assert "@version" in result.columns + + assert result["@other_tags"].to_list() == [{"building": "yes", "name": "Krautturm"}] + assert result["@version"].to_list() == [4] + + @pytest.mark.vcr def test_elementsFullHistory_geometry(base_client): """