QuackOSM 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quackosm/__init__.py +17 -0
- quackosm/_constants.py +7 -0
- quackosm/_geo_arrow_io.py +382 -0
- quackosm/_osm_tags_filters.py +130 -0
- quackosm/_osm_way_polygon_features.py +127 -0
- quackosm/_typing.py +28 -0
- quackosm/conftest.py +21 -0
- quackosm/functions.py +373 -0
- quackosm/pbf_file_reader.py +1601 -0
- quackosm-0.1.0.dist-info/METADATA +36 -0
- quackosm-0.1.0.dist-info/RECORD +13 -0
- quackosm-0.1.0.dist-info/WHEEL +4 -0
- quackosm-0.1.0.dist-info/licenses/LICENSE +201 -0
quackosm/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QuackOSM.
|
|
3
|
+
|
|
4
|
+
QuackOSM is a Python library used for reading pbf (ProtoBuffer) files with OpenStreetMap data using
|
|
5
|
+
DuckDB spatial extension without GDAL.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from quackosm.functions import convert_pbf_to_gpq, get_features_gdf
|
|
9
|
+
from quackosm.pbf_file_reader import PbfFileReader
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"PbfFileReader",
|
|
15
|
+
"convert_pbf_to_gpq",
|
|
16
|
+
"get_features_gdf",
|
|
17
|
+
]
|
quackosm/_constants.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
# pragma: no cover
|
|
3
|
+
"""
|
|
4
|
+
GeoArrow IO helper module.
|
|
5
|
+
|
|
6
|
+
Will be removed after new version of https://github.com/geoarrow/geoarrow-python/ is released.
|
|
7
|
+
Those two functions: `read_geoparquet_table` and `write_geoparquet_table` are required in
|
|
8
|
+
QuackOSM library and this file is composed of files from current unreleased version of the library.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
import geoarrow.pyarrow as _ga
|
|
13
|
+
import pyarrow.parquet as _pq
|
|
14
|
+
import pyarrow.types as _types
|
|
15
|
+
import pyarrow_hotfix as _ # noqa: F401
|
|
16
|
+
from geoarrow.pyarrow._compute import ensure_storage
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def read_geoparquet_table(*args, **kwargs):
|
|
20
|
+
"""Read GeoParquet using PyArrow."""
|
|
21
|
+
tab = _pq.read_table(*args, **kwargs)
|
|
22
|
+
tab_metadata = tab.schema.metadata or {}
|
|
23
|
+
if b"geo" in tab_metadata:
|
|
24
|
+
geo_meta = json.loads(tab_metadata[b"geo"])
|
|
25
|
+
else:
|
|
26
|
+
geo_meta = {}
|
|
27
|
+
|
|
28
|
+
# Remove "geo" schema metadata key since few transformations following
|
|
29
|
+
# the read operation check that schema metadata is valid (e.g., column
|
|
30
|
+
# subset or rename)
|
|
31
|
+
non_geo_meta = {k: v for k, v in tab_metadata.items() if k != b"geo"}
|
|
32
|
+
tab = tab.replace_schema_metadata(non_geo_meta)
|
|
33
|
+
|
|
34
|
+
# Assign extension types to columns
|
|
35
|
+
if "columns" in geo_meta:
|
|
36
|
+
columns = geo_meta["columns"]
|
|
37
|
+
else:
|
|
38
|
+
columns = _geoparquet_guess_geometry_columns(tab.schema)
|
|
39
|
+
|
|
40
|
+
return _geoparquet_table_to_geoarrow(tab, columns)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def write_geoparquet_table(
|
|
44
|
+
table,
|
|
45
|
+
*args,
|
|
46
|
+
primary_geometry_column=None,
|
|
47
|
+
geometry_columns=None,
|
|
48
|
+
write_bbox=False,
|
|
49
|
+
write_geometry_types=None,
|
|
50
|
+
check_wkb=True,
|
|
51
|
+
**kwargs,
|
|
52
|
+
):
|
|
53
|
+
"""Write GeoParquet using PyArrow."""
|
|
54
|
+
geo_meta = _geoparquet_metadata_from_schema(
|
|
55
|
+
table.schema,
|
|
56
|
+
primary_geometry_column=primary_geometry_column,
|
|
57
|
+
geometry_columns=geometry_columns,
|
|
58
|
+
add_geometry_types=write_geometry_types,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Note: this will also update geo_meta with geometry_types and bbox if requested
|
|
62
|
+
for i, name in enumerate(table.schema.names):
|
|
63
|
+
if name in geo_meta["columns"]:
|
|
64
|
+
table = table.set_column(
|
|
65
|
+
i,
|
|
66
|
+
name,
|
|
67
|
+
_geoparquet_encode_chunked_array(
|
|
68
|
+
table[i],
|
|
69
|
+
geo_meta["columns"][name],
|
|
70
|
+
add_geometry_types=write_geometry_types,
|
|
71
|
+
add_bbox=write_bbox,
|
|
72
|
+
check_wkb=check_wkb,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
metadata = table.schema.metadata or {}
|
|
77
|
+
metadata["geo"] = json.dumps(geo_meta)
|
|
78
|
+
table = table.replace_schema_metadata(metadata)
|
|
79
|
+
return _pq.write_table(table, *args, **kwargs)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _geoparquet_guess_geometry_columns(schema):
|
|
83
|
+
# Only attempt guessing the "geometry" or "geography" column
|
|
84
|
+
columns = {}
|
|
85
|
+
|
|
86
|
+
for name in ("geometry", "geography"):
|
|
87
|
+
if name not in schema.names:
|
|
88
|
+
continue
|
|
89
|
+
|
|
90
|
+
spec = {}
|
|
91
|
+
type = schema.field(name).type
|
|
92
|
+
|
|
93
|
+
if _types.is_binary(type) or _types.is_large_binary(type):
|
|
94
|
+
spec["encoding"] = "WKB"
|
|
95
|
+
elif _types.is_string(type) or _types.is_large_string(type):
|
|
96
|
+
# WKT is not actually a geoparquet encoding but the guidance on
|
|
97
|
+
# putting geospatial things in parquet without metadata says you
|
|
98
|
+
# can do it and this is the internal sentinel for that case.
|
|
99
|
+
spec["encoding"] = "WKT"
|
|
100
|
+
|
|
101
|
+
# A column named "geography" has spherical edges according to the
|
|
102
|
+
# compatible Parquet guidance.
|
|
103
|
+
if name == "geography":
|
|
104
|
+
spec["edges"] = "spherical"
|
|
105
|
+
|
|
106
|
+
columns[name] = spec
|
|
107
|
+
|
|
108
|
+
return columns
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _geoparquet_chunked_array_to_geoarrow(item, spec):
|
|
112
|
+
# If item was written as a GeoArrow extension type to the Parquet file,
|
|
113
|
+
# ignore any information in the column spec
|
|
114
|
+
if isinstance(item.type, _ga.GeometryExtensionType):
|
|
115
|
+
return item
|
|
116
|
+
|
|
117
|
+
if "encoding" not in spec:
|
|
118
|
+
raise ValueError("Invalid GeoParquet column specification: missing 'encoding'")
|
|
119
|
+
|
|
120
|
+
encoding = spec["encoding"]
|
|
121
|
+
if encoding in ("WKB", "WKT"):
|
|
122
|
+
item = _ga.array(item)
|
|
123
|
+
else:
|
|
124
|
+
raise ValueError(f"Invalid GeoParquet encoding value: '{encoding}'")
|
|
125
|
+
|
|
126
|
+
if "crs" not in spec:
|
|
127
|
+
crs = json.dumps(_CRS_LONLAT).encode("UTF-8")
|
|
128
|
+
else:
|
|
129
|
+
crs = json.dumps(spec["crs"]).encode("UTF-8")
|
|
130
|
+
|
|
131
|
+
if "edges" not in spec or spec["edges"] == "planar":
|
|
132
|
+
edge_type = None
|
|
133
|
+
elif spec["edges"] == "spherical":
|
|
134
|
+
edge_type = _ga.EdgeType.SPHERICAL
|
|
135
|
+
else:
|
|
136
|
+
raise ValueError("Invalid GeoParquet column edges value")
|
|
137
|
+
|
|
138
|
+
if crs is not None:
|
|
139
|
+
item = _ga.with_crs(item, crs)
|
|
140
|
+
|
|
141
|
+
if edge_type is not None:
|
|
142
|
+
item = _ga.with_edge_type(item, edge_type)
|
|
143
|
+
|
|
144
|
+
return item
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _geoparquet_table_to_geoarrow(tab, columns):
|
|
148
|
+
tab_names = set(tab.schema.names)
|
|
149
|
+
for col_name, spec in columns.items():
|
|
150
|
+
# col_name might not exist if only a subset of columns were read from file
|
|
151
|
+
if col_name not in tab_names:
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
col_i = tab.schema.get_field_index(col_name)
|
|
155
|
+
new_geometry = _geoparquet_chunked_array_to_geoarrow(tab[col_i], spec)
|
|
156
|
+
tab = tab.set_column(col_i, col_name, new_geometry)
|
|
157
|
+
|
|
158
|
+
return tab
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _geoparquet_guess_primary_geometry_column(schema, primary_geometry_column=None):
|
|
162
|
+
if primary_geometry_column is not None:
|
|
163
|
+
return primary_geometry_column
|
|
164
|
+
|
|
165
|
+
# If there's a "geometry" or "geography" column, pick that one
|
|
166
|
+
if "geometry" in schema.names:
|
|
167
|
+
return "geometry"
|
|
168
|
+
elif "geography" in schema.names:
|
|
169
|
+
return "geography"
|
|
170
|
+
|
|
171
|
+
# Otherwise, pick the first thing we know is actually geometry
|
|
172
|
+
for name, type in zip(schema.names, schema.types):
|
|
173
|
+
if isinstance(type, _ga.GeometryExtensionType):
|
|
174
|
+
return name
|
|
175
|
+
|
|
176
|
+
raise ValueError("write_geoparquet_table() requires source with at least one geometry column")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _geoparquet_column_spec_from_type(type, add_geometry_types=None):
|
|
180
|
+
# We always encode to WKB since it's the only supported value
|
|
181
|
+
spec = {"encoding": "WKB", "geometry_types": []}
|
|
182
|
+
|
|
183
|
+
# Pass along extra information from GeoArrow extension type metadata
|
|
184
|
+
if isinstance(type, _ga.GeometryExtensionType):
|
|
185
|
+
if type.crs_type == _ga.CrsType.PROJJSON:
|
|
186
|
+
spec["crs"] = json.loads(type.crs)
|
|
187
|
+
elif type.crs_type == _ga.CrsType.NONE:
|
|
188
|
+
spec["crs"] = None
|
|
189
|
+
else:
|
|
190
|
+
import pyproj
|
|
191
|
+
|
|
192
|
+
spec["crs"] = pyproj.CRS(type.crs).to_json_dict()
|
|
193
|
+
|
|
194
|
+
if type.edge_type == _ga.EdgeType.SPHERICAL:
|
|
195
|
+
spec["edges"] = "spherical"
|
|
196
|
+
|
|
197
|
+
# GeoArrow-encoded types can confidently declare a single geometry type
|
|
198
|
+
maybe_known_geometry_type = type.geometry_type
|
|
199
|
+
maybe_known_dimensions = type.dimensions
|
|
200
|
+
if (
|
|
201
|
+
add_geometry_types is not False
|
|
202
|
+
and maybe_known_geometry_type != _ga.GeometryType.GEOMETRY
|
|
203
|
+
and maybe_known_dimensions != _ga.Dimensions.UNKNOWN
|
|
204
|
+
):
|
|
205
|
+
geometry_type = _GEOPARQUET_GEOMETRY_TYPE_LABELS[maybe_known_geometry_type]
|
|
206
|
+
dimensions = _GEOPARQUET_DIMENSION_LABELS[maybe_known_dimensions]
|
|
207
|
+
spec["geometry_types"] = [f"{geometry_type}{dimensions}"]
|
|
208
|
+
|
|
209
|
+
return spec
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _geoparquet_columns_from_schema(
|
|
213
|
+
schema, geometry_columns=None, primary_geometry_column=None, add_geometry_types=None
|
|
214
|
+
):
|
|
215
|
+
schema_names = schema.names
|
|
216
|
+
schema_types = schema.types
|
|
217
|
+
|
|
218
|
+
if geometry_columns is None:
|
|
219
|
+
geometry_columns = set()
|
|
220
|
+
if primary_geometry_column is not None:
|
|
221
|
+
geometry_columns.add(primary_geometry_column)
|
|
222
|
+
|
|
223
|
+
for name, type in zip(schema_names, schema_types):
|
|
224
|
+
if isinstance(type, _ga.GeometryExtensionType):
|
|
225
|
+
geometry_columns.add(name)
|
|
226
|
+
else:
|
|
227
|
+
geometry_columns = set(geometry_columns)
|
|
228
|
+
|
|
229
|
+
specs = {}
|
|
230
|
+
for name, type in zip(schema_names, schema_types):
|
|
231
|
+
if name in geometry_columns:
|
|
232
|
+
specs[name] = _geoparquet_column_spec_from_type(
|
|
233
|
+
type, add_geometry_types=add_geometry_types
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
return specs
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _geoparquet_metadata_from_schema(
|
|
240
|
+
schema, geometry_columns=None, primary_geometry_column=None, add_geometry_types=None
|
|
241
|
+
):
|
|
242
|
+
primary_geometry_column = _geoparquet_guess_primary_geometry_column(
|
|
243
|
+
schema, primary_geometry_column
|
|
244
|
+
)
|
|
245
|
+
columns = _geoparquet_columns_from_schema(
|
|
246
|
+
schema, geometry_columns, add_geometry_types=add_geometry_types
|
|
247
|
+
)
|
|
248
|
+
return {
|
|
249
|
+
"version": "1.0.0",
|
|
250
|
+
"primary_column": primary_geometry_column,
|
|
251
|
+
"columns": columns,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _geoparquet_update_spec_geometry_types(item, spec):
|
|
256
|
+
geometry_type_labels = []
|
|
257
|
+
for element in _ga.unique_geometry_types(item).to_pylist():
|
|
258
|
+
geometry_type = _GEOPARQUET_GEOMETRY_TYPE_LABELS[element["geometry_type"]]
|
|
259
|
+
dimensions = _GEOPARQUET_DIMENSION_LABELS[element["dimensions"]]
|
|
260
|
+
geometry_type_labels.append(f"{geometry_type}{dimensions}")
|
|
261
|
+
|
|
262
|
+
spec["geometry_types"] = geometry_type_labels
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _geoparquet_update_spec_bbox(item, spec):
|
|
266
|
+
box = _ga.box_agg(item).as_py()
|
|
267
|
+
spec["bbox"] = [box["xmin"], box["ymin"], box["xmax"], box["ymax"]]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _geoparquet_encode_chunked_array(
|
|
271
|
+
item, spec, add_geometry_types=None, add_bbox=False, check_wkb=True
|
|
272
|
+
):
|
|
273
|
+
# ...because we're currently only ever encoding using WKB
|
|
274
|
+
if spec["encoding"] == "WKB":
|
|
275
|
+
item_out = _ga.as_wkb(item)
|
|
276
|
+
else:
|
|
277
|
+
encoding = spec["encoding"]
|
|
278
|
+
raise ValueError(f"Expected column encoding 'WKB' but got '{encoding}'")
|
|
279
|
+
|
|
280
|
+
# For everything except a well-known text-encoded column, we want to do
|
|
281
|
+
# calculations on the pre-WKB-encoded value.
|
|
282
|
+
if spec["encoding"] == "WKT":
|
|
283
|
+
item_calc = item_out
|
|
284
|
+
else:
|
|
285
|
+
item_calc = item
|
|
286
|
+
|
|
287
|
+
# geometry_types that are fixed at the data type level have already been
|
|
288
|
+
# added to the spec in an earlier step. The unique_geometry_types()
|
|
289
|
+
# function is sufficiently optimized such that this potential
|
|
290
|
+
# re-computation is not expensive.
|
|
291
|
+
if add_geometry_types is True:
|
|
292
|
+
_geoparquet_update_spec_geometry_types(item_calc, spec)
|
|
293
|
+
|
|
294
|
+
if add_bbox:
|
|
295
|
+
_geoparquet_update_spec_bbox(item_calc, spec)
|
|
296
|
+
|
|
297
|
+
return ensure_storage(item_out)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
_GEOPARQUET_GEOMETRY_TYPE_LABELS = [
|
|
301
|
+
"Geometry",
|
|
302
|
+
"Point",
|
|
303
|
+
"LineString",
|
|
304
|
+
"Polygon",
|
|
305
|
+
"MultiPoint",
|
|
306
|
+
"MultiLineString",
|
|
307
|
+
"MultiPolygon",
|
|
308
|
+
]
|
|
309
|
+
|
|
310
|
+
_GEOPARQUET_DIMENSION_LABELS = [None, "", " Z", " M", " ZM"]
|
|
311
|
+
|
|
312
|
+
_CRS_LONLAT = {
|
|
313
|
+
"$schema": "https://proj.org/schemas/v0.7/projjson.schema.json",
|
|
314
|
+
"type": "GeographicCRS",
|
|
315
|
+
"name": "WGS 84 (CRS84)",
|
|
316
|
+
"datum_ensemble": {
|
|
317
|
+
"name": "World Geodetic System 1984 ensemble",
|
|
318
|
+
"members": [
|
|
319
|
+
{
|
|
320
|
+
"name": "World Geodetic System 1984 (Transit)",
|
|
321
|
+
"id": {"authority": "EPSG", "code": 1166},
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
"name": "World Geodetic System 1984 (G730)",
|
|
325
|
+
"id": {"authority": "EPSG", "code": 1152},
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"name": "World Geodetic System 1984 (G873)",
|
|
329
|
+
"id": {"authority": "EPSG", "code": 1153},
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
"name": "World Geodetic System 1984 (G1150)",
|
|
333
|
+
"id": {"authority": "EPSG", "code": 1154},
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
"name": "World Geodetic System 1984 (G1674)",
|
|
337
|
+
"id": {"authority": "EPSG", "code": 1155},
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
"name": "World Geodetic System 1984 (G1762)",
|
|
341
|
+
"id": {"authority": "EPSG", "code": 1156},
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
"name": "World Geodetic System 1984 (G2139)",
|
|
345
|
+
"id": {"authority": "EPSG", "code": 1309},
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
"ellipsoid": {
|
|
349
|
+
"name": "WGS 84",
|
|
350
|
+
"semi_major_axis": 6378137,
|
|
351
|
+
"inverse_flattening": 298.257223563,
|
|
352
|
+
},
|
|
353
|
+
"accuracy": "2.0",
|
|
354
|
+
"id": {"authority": "EPSG", "code": 6326},
|
|
355
|
+
},
|
|
356
|
+
"coordinate_system": {
|
|
357
|
+
"subtype": "ellipsoidal",
|
|
358
|
+
"axis": [
|
|
359
|
+
{
|
|
360
|
+
"name": "Geodetic longitude",
|
|
361
|
+
"abbreviation": "Lon",
|
|
362
|
+
"direction": "east",
|
|
363
|
+
"unit": "degree",
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
"name": "Geodetic latitude",
|
|
367
|
+
"abbreviation": "Lat",
|
|
368
|
+
"direction": "north",
|
|
369
|
+
"unit": "degree",
|
|
370
|
+
},
|
|
371
|
+
],
|
|
372
|
+
},
|
|
373
|
+
"scope": "Not known.",
|
|
374
|
+
"area": "World.",
|
|
375
|
+
"bbox": {
|
|
376
|
+
"south_latitude": -90,
|
|
377
|
+
"west_longitude": -180,
|
|
378
|
+
"north_latitude": 90,
|
|
379
|
+
"east_longitude": 180,
|
|
380
|
+
},
|
|
381
|
+
"id": {"authority": "OGC", "code": "CRS84"},
|
|
382
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Module contains a dedicated type alias for OSM tags filter."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Union, cast, overload
|
|
5
|
+
|
|
6
|
+
from quackosm._typing import is_expected_type
|
|
7
|
+
|
|
8
|
+
OsmTagsFilter = dict[str, Union[list[str], str, bool]]
|
|
9
|
+
|
|
10
|
+
GroupedOsmTagsFilter = dict[str, OsmTagsFilter]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@overload
|
|
14
|
+
def merge_osm_tags_filter(osm_tags_filter: OsmTagsFilter) -> OsmTagsFilter: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@overload
|
|
18
|
+
def merge_osm_tags_filter(osm_tags_filter: GroupedOsmTagsFilter) -> OsmTagsFilter: ...
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@overload
|
|
22
|
+
def merge_osm_tags_filter(osm_tags_filter: Iterable[OsmTagsFilter]) -> OsmTagsFilter: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@overload
|
|
26
|
+
def merge_osm_tags_filter(osm_tags_filter: Iterable[GroupedOsmTagsFilter]) -> OsmTagsFilter: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def merge_osm_tags_filter(
|
|
30
|
+
osm_tags_filter: Union[
|
|
31
|
+
OsmTagsFilter, GroupedOsmTagsFilter, Iterable[OsmTagsFilter], Iterable[GroupedOsmTagsFilter]
|
|
32
|
+
]
|
|
33
|
+
) -> OsmTagsFilter:
|
|
34
|
+
"""
|
|
35
|
+
Merge OSM tags filter into `OsmTagsFilter` type.
|
|
36
|
+
|
|
37
|
+
Optionally merges `GroupedOsmTagsFilter` into `OsmTagsFilter` to allow loaders to load all
|
|
38
|
+
defined groups during single operation.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
osm_tags_filter: OSM tags filter definition.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
AttributeError: When provided tags don't match both
|
|
45
|
+
`OsmTagsFilter` or `GroupedOsmTagsFilter`.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
OsmTagsFilter: Merged filters.
|
|
49
|
+
"""
|
|
50
|
+
if is_expected_type(osm_tags_filter, OsmTagsFilter):
|
|
51
|
+
return cast(OsmTagsFilter, osm_tags_filter)
|
|
52
|
+
elif is_expected_type(osm_tags_filter, GroupedOsmTagsFilter):
|
|
53
|
+
return _merge_grouped_osm_tags_filter(cast(GroupedOsmTagsFilter, osm_tags_filter))
|
|
54
|
+
elif is_expected_type(osm_tags_filter, Iterable):
|
|
55
|
+
return _merge_multiple_osm_tags_filters(
|
|
56
|
+
[
|
|
57
|
+
merge_osm_tags_filter(
|
|
58
|
+
cast(Union[OsmTagsFilter, GroupedOsmTagsFilter], sub_osm_tags_filter)
|
|
59
|
+
)
|
|
60
|
+
for sub_osm_tags_filter in osm_tags_filter
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
raise AttributeError(
|
|
65
|
+
"Provided tags don't match required type definitions"
|
|
66
|
+
" (OsmTagsFilter or GroupedOsmTagsFilter)."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _merge_grouped_osm_tags_filter(grouped_filter: GroupedOsmTagsFilter) -> OsmTagsFilter:
|
|
71
|
+
"""
|
|
72
|
+
Merge grouped osm tags filter into a base one.
|
|
73
|
+
|
|
74
|
+
Function merges all filter categories into a single one for an OSM loader to use.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
grouped_filter (GroupedOsmTagsFilter): Grouped filter to be merged into a single one.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
osm_tags_type: Merged filter.
|
|
81
|
+
"""
|
|
82
|
+
if not is_expected_type(grouped_filter, GroupedOsmTagsFilter):
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"Provided filter doesn't match required `GroupedOsmTagsFilter` definition."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
return _merge_multiple_osm_tags_filters(grouped_filter.values())
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _merge_multiple_osm_tags_filters(osm_tags_filters: Iterable[OsmTagsFilter]) -> OsmTagsFilter:
|
|
91
|
+
"""
|
|
92
|
+
Merge multiple osm tags filters into a single one.
|
|
93
|
+
|
|
94
|
+
Function merges all OsmTagsFilters into a single one for an OSM loader to use.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
osm_tags_filters (Iterable[OsmTagsFilter]): List of filters to be merged into a single one.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
osm_tags_type: Merged filter.
|
|
101
|
+
"""
|
|
102
|
+
if not is_expected_type(osm_tags_filters, Iterable[OsmTagsFilter]):
|
|
103
|
+
raise ValueError(
|
|
104
|
+
"Provided filter doesn't match required `Iterable[OsmTagsFilter]` definition."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
result: OsmTagsFilter = {}
|
|
108
|
+
for osm_tags_filter in osm_tags_filters:
|
|
109
|
+
for osm_tag_key, osm_tag_value in osm_tags_filter.items():
|
|
110
|
+
if osm_tag_key not in result:
|
|
111
|
+
result[osm_tag_key] = []
|
|
112
|
+
|
|
113
|
+
# If filter is already a positive boolean, skip
|
|
114
|
+
if isinstance(result[osm_tag_key], bool) and result[osm_tag_key]:
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
current_values_list = cast(list[str], result[osm_tag_key])
|
|
118
|
+
|
|
119
|
+
# Check bool
|
|
120
|
+
if isinstance(osm_tag_value, bool) and osm_tag_value:
|
|
121
|
+
result[osm_tag_key] = True
|
|
122
|
+
# Check string
|
|
123
|
+
elif isinstance(osm_tag_value, str) and osm_tag_value not in current_values_list:
|
|
124
|
+
current_values_list.append(osm_tag_value)
|
|
125
|
+
# Check list
|
|
126
|
+
elif isinstance(osm_tag_value, list):
|
|
127
|
+
new_values = [value for value in osm_tag_value if value not in current_values_list]
|
|
128
|
+
current_values_list.extend(new_values)
|
|
129
|
+
|
|
130
|
+
return result
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from typing import Any, NamedTuple, cast
|
|
3
|
+
|
|
4
|
+
from quackosm._typing import is_expected_type
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class OsmWayPolygonConfig(NamedTuple):
|
|
8
|
+
"""OSM Way polygon features config object."""
|
|
9
|
+
|
|
10
|
+
all: Iterable[str]
|
|
11
|
+
allowlist: dict[str, Iterable[str]]
|
|
12
|
+
denylist: dict[str, Iterable[str]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def parse_dict_to_config_object(raw_config: dict[str, Any]) -> OsmWayPolygonConfig:
|
|
16
|
+
all_tags = raw_config.get("all", [])
|
|
17
|
+
allowlist_tags = raw_config.get("allowlist", {})
|
|
18
|
+
denylist_tags = raw_config.get("denylist", {})
|
|
19
|
+
if not is_expected_type(all_tags, Iterable[str]):
|
|
20
|
+
raise ValueError(f"Wrong type of key: all ({type(all_tags)})")
|
|
21
|
+
|
|
22
|
+
if not is_expected_type(allowlist_tags, dict[str, Iterable[str]]):
|
|
23
|
+
raise ValueError(f"Wrong type of key: all ({type(allowlist_tags)})")
|
|
24
|
+
|
|
25
|
+
if not is_expected_type(denylist_tags, dict[str, Iterable[str]]):
|
|
26
|
+
raise ValueError(f"Wrong type of key: denylist ({type(denylist_tags)})")
|
|
27
|
+
|
|
28
|
+
return OsmWayPolygonConfig(
|
|
29
|
+
all=cast(Iterable[str], all_tags),
|
|
30
|
+
allowlist=cast(dict[str, Iterable[str]], allowlist_tags),
|
|
31
|
+
denylist=cast(dict[str, Iterable[str]], denylist_tags),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Config based on two sources + manual OSM wiki check
|
|
36
|
+
# 1. https://github.com/tyrasd/osm-polygon-features/blob/master/polygon-features.json
|
|
37
|
+
# 2. https://github.com/ideditor/id-area-keys/blob/main/areaKeys.json
|
|
38
|
+
OSM_WAY_POLYGON_CONFIG_RAW = {
|
|
39
|
+
"all": [
|
|
40
|
+
"allotments",
|
|
41
|
+
"area:highway",
|
|
42
|
+
"boundary",
|
|
43
|
+
"bridge:support",
|
|
44
|
+
"building:part",
|
|
45
|
+
"building",
|
|
46
|
+
"cemetery",
|
|
47
|
+
"club",
|
|
48
|
+
"craft",
|
|
49
|
+
"demolished:building",
|
|
50
|
+
"disused:amenity",
|
|
51
|
+
"disused:leisure",
|
|
52
|
+
"disused:shop",
|
|
53
|
+
"healthcare",
|
|
54
|
+
"historic",
|
|
55
|
+
"industrial",
|
|
56
|
+
"internet_access",
|
|
57
|
+
"junction",
|
|
58
|
+
"landuse",
|
|
59
|
+
"leisure",
|
|
60
|
+
"office",
|
|
61
|
+
"place",
|
|
62
|
+
"police",
|
|
63
|
+
"polling_station",
|
|
64
|
+
"public_transport",
|
|
65
|
+
"residential",
|
|
66
|
+
"ruins",
|
|
67
|
+
"seamark:type",
|
|
68
|
+
"shop",
|
|
69
|
+
"sport",
|
|
70
|
+
"telecom",
|
|
71
|
+
"tourism",
|
|
72
|
+
],
|
|
73
|
+
"allowlist": {
|
|
74
|
+
"advertising": ["sculpture", "sign"],
|
|
75
|
+
"aerialway": ["station"],
|
|
76
|
+
"barrier": ["city_wall", "hedge", "wall", "toll_booth"],
|
|
77
|
+
"highway": ["services", "rest_area", "platform"],
|
|
78
|
+
"railway": ["station", "turntable", "roundhouse", "platform"],
|
|
79
|
+
"waterway": ["riverbank", "dock", "boatyard", "dam", "fuel"],
|
|
80
|
+
},
|
|
81
|
+
"denylist": {
|
|
82
|
+
"aeroway": ["jet_bridge", "parking_position", "taxiway", "no"],
|
|
83
|
+
"amenity": ["bench", "weighbridge"],
|
|
84
|
+
"attraction": ["river_rafting", "train", "water_slide", "boat_ride"],
|
|
85
|
+
"emergency": ["designated", "destination", "no", "official", "private", "yes"],
|
|
86
|
+
"geological": ["volcanic_caldera_rim", "fault"],
|
|
87
|
+
"golf": ["cartpath", "hole", "path"],
|
|
88
|
+
"indoor": ["corridor", "wall"],
|
|
89
|
+
"man_made": [
|
|
90
|
+
"yes",
|
|
91
|
+
"breakwater",
|
|
92
|
+
"carpet_hanger",
|
|
93
|
+
"crane",
|
|
94
|
+
"cutline",
|
|
95
|
+
"dyke",
|
|
96
|
+
"embankment",
|
|
97
|
+
"goods_conveyor",
|
|
98
|
+
"groyne",
|
|
99
|
+
"pier",
|
|
100
|
+
"pipeline",
|
|
101
|
+
"torii",
|
|
102
|
+
"video_wall",
|
|
103
|
+
],
|
|
104
|
+
"military": ["trench"],
|
|
105
|
+
"natural": [
|
|
106
|
+
"bay",
|
|
107
|
+
"cliff",
|
|
108
|
+
"coastline",
|
|
109
|
+
"ridge",
|
|
110
|
+
"strait",
|
|
111
|
+
"tree_row",
|
|
112
|
+
"valley",
|
|
113
|
+
"no",
|
|
114
|
+
"arete",
|
|
115
|
+
],
|
|
116
|
+
"piste:type": ["downhill", "hike", "ice_skate", "nordic", "skitour", "sled", "sleigh"],
|
|
117
|
+
"playground": [
|
|
118
|
+
"balancebeam",
|
|
119
|
+
"rope_traverse",
|
|
120
|
+
"stepping_stone",
|
|
121
|
+
"stepping_post",
|
|
122
|
+
"rope_swing",
|
|
123
|
+
"climbing_slope",
|
|
124
|
+
],
|
|
125
|
+
"power": ["cable", "line", "minor_line", "insulator", "busbar", "bay", "portal"],
|
|
126
|
+
},
|
|
127
|
+
}
|
quackosm/_typing.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Utility function for typing purposes."""
|
|
2
|
+
|
|
3
|
+
from contextlib import suppress
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from typeguard import TypeCheckError, check_type
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def is_expected_type(value: object, expected_type: Any) -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Check if an object is a given type.
|
|
12
|
+
|
|
13
|
+
Uses `typeguard` library to check objects using `typing` definitions.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
value (object): Value to be checked against `expected_type`.
|
|
17
|
+
expected_type (Any): A class or generic type instance.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
bool: Flag whether the object is an instance of the required type.
|
|
21
|
+
"""
|
|
22
|
+
result = False
|
|
23
|
+
|
|
24
|
+
with suppress(TypeCheckError):
|
|
25
|
+
check_type(value, expected_type)
|
|
26
|
+
result = True
|
|
27
|
+
|
|
28
|
+
return result
|