Skip to content

Commit b3aa93b

Browse files
authored
feat: add bbox filter for cli (#170)
* feat: add bbox filter and clean cli descriptions * chore: add changelog entry
1 parent beb7e67 commit b3aa93b

File tree

4 files changed

+87
-57
lines changed

4 files changed

+87
-57
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Option to pass a bounding box as a geometry filter in CLI [#169](https://github.com/kraina-ai/quackosm/issues/169)
13+
14+
### Changed
15+
16+
- Modified CLI descriptions and hid unnecessary default values [#169](https://github.com/kraina-ai/quackosm/issues/169)
17+
1018
## [0.11.1] - 2024-10-09
1119

1220
### Added

quackosm/cli.py

Lines changed: 66 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,26 @@ def _empty_path_callback(ctx: typer.Context, value: Path) -> Optional[Path]:
4747
return _path_callback(ctx, value)
4848

4949

50+
class BboxGeometryParser(click.ParamType): # type: ignore
51+
"""Parser for geometry in WKT form."""
52+
53+
name = "BBOX"
54+
55+
def convert(self, value, param=None, ctx=None): # type: ignore
56+
"""Convert parameter value."""
57+
try:
58+
from shapely import box
59+
60+
bbox_values = [float(x.strip()) for x in value.split(",")]
61+
return box(*bbox_values)
62+
except ValueError: # ValueError raised when passing non-numbers to float()
63+
raise typer.BadParameter(
64+
"Cannot parse provided bounding box."
65+
" Valid value must contain 4 floating point numbers"
66+
" separated by commas."
67+
) from None
68+
69+
5070
class WktGeometryParser(click.ParamType): # type: ignore
5171
"""Parser for geometry in WKT form."""
5272

@@ -278,6 +298,7 @@ def main(
278298
help="PBF file to convert into GeoParquet. Can be an URL.",
279299
metavar="PBF file path",
280300
callback=_empty_path_callback,
301+
show_default=False,
281302
),
282303
] = None,
283304
osm_tags_filter: Annotated[
@@ -293,6 +314,7 @@ def main(
293314
" [bold bright_cyan]osm-tags-filter-file[/bold bright_cyan]."
294315
),
295316
click_type=OsmTagsFilterJsonParser(),
317+
show_default=False,
296318
),
297319
] = None,
298320
osm_tags_filter_file: Annotated[
@@ -308,6 +330,7 @@ def main(
308330
" [bold bright_cyan]osm-tags-filter[/bold bright_cyan]."
309331
),
310332
click_type=OsmTagsFilterFileParser(),
333+
show_default=False,
311334
),
312335
] = None,
313336
keep_all_tags: Annotated[
@@ -326,22 +349,32 @@ def main(
326349
show_default=False,
327350
),
328351
] = False,
352+
geom_filter_bbox: Annotated[
353+
Optional[str],
354+
typer.Option(
355+
help=(
356+
"Geometry to use as a filter in the"
357+
" [bold dark_orange]bounding box[/bold dark_orange] format - 4 floating point"
358+
" numbers separated by commas."
359+
" Cannot be used together with other"
360+
" [bold bright_cyan]geom-filter-...[/bold bright_cyan] parameters."
361+
),
362+
click_type=BboxGeometryParser(),
363+
show_default=False,
364+
),
365+
] = None,
329366
geom_filter_file: Annotated[
330367
Optional[str],
331368
typer.Option(
332369
help=(
333370
"Geometry to use as a filter in the"
334371
" [bold dark_orange]file[/bold dark_orange] format - any that can be opened by"
335372
" GeoPandas. Will return the unary union of the geometries in the file."
336-
" Cannot be used together with"
337-
" [bold bright_cyan]geom-filter-geocode[/bold bright_cyan] or"
338-
" [bold bright_cyan]geom-filter-geojson[/bold bright_cyan] or"
339-
" [bold bright_cyan]geom-filter-index-geohash[/bold bright_cyan] or"
340-
" [bold bright_cyan]geom-filter-index-h3[/bold bright_cyan] or"
341-
" [bold bright_cyan]geom-filter-index-s2[/bold bright_cyan] or"
342-
" [bold bright_cyan]geom-filter-wkt[/bold bright_cyan]."
373+
" Cannot be used together with other"
374+
" [bold bright_cyan]geom-filter-...[/bold bright_cyan] parameters."
343375
),
344376
click_type=GeoFileGeometryParser(),
377+
show_default=False,
345378
),
346379
] = None,
347380
geom_filter_geocode: Annotated[
@@ -351,15 +384,11 @@ def main(
351384
"Geometry to use as a filter in the"
352385
" [bold dark_orange]string to geocode[/bold dark_orange] format - it will be"
353386
" geocoded to the geometry using Nominatim API (GeoPy library)."
354-
" Cannot be used together with"
355-
" [bold bright_cyan]geom-filter-file[/bold bright_cyan] or"
356-
" [bold bright_cyan]geom-filter-geojson[/bold bright_cyan] or"
357-
" [bold bright_cyan]geom-filter-index-geohash[/bold bright_cyan] or"
358-
" [bold bright_cyan]geom-filter-index-h3[/bold bright_cyan] or"
359-
" [bold bright_cyan]geom-filter-index-s2[/bold bright_cyan] or"
360-
" [bold bright_cyan]geom-filter-wkt[/bold bright_cyan]."
387+
" Cannot be used together with other"
388+
" [bold bright_cyan]geom-filter-...[/bold bright_cyan] parameters."
361389
),
362390
click_type=GeocodeGeometryParser(),
391+
show_default=False,
363392
),
364393
] = None,
365394
geom_filter_geojson: Annotated[
@@ -368,15 +397,11 @@ def main(
368397
help=(
369398
"Geometry to use as a filter in the [bold dark_orange]GeoJSON[/bold dark_orange]"
370399
" format."
371-
" Cannot be used used together with"
372-
" [bold bright_cyan]geom-filter-file[/bold bright_cyan] or"
373-
" [bold bright_cyan]geom-filter-geocode[/bold bright_cyan] or"
374-
" [bold bright_cyan]geom-filter-index-geohash[/bold bright_cyan] or"
375-
" [bold bright_cyan]geom-filter-index-h3[/bold bright_cyan] or"
376-
" [bold bright_cyan]geom-filter-index-s2[/bold bright_cyan] or"
377-
" [bold bright_cyan]geom-filter-wkt[/bold bright_cyan]."
400+
" Cannot be used together with other"
401+
" [bold bright_cyan]geom-filter-...[/bold bright_cyan] parameters."
378402
),
379403
click_type=GeoJsonGeometryParser(),
404+
show_default=False,
380405
),
381406
] = None,
382407
geom_filter_index_geohash: Annotated[
@@ -386,15 +411,11 @@ def main(
386411
"Geometry to use as a filter in the"
387412
" [bold dark_orange]Geohash index[/bold dark_orange]"
388413
" format. Separate multiple values with a comma."
389-
" Cannot be used used together with"
390-
" [bold bright_cyan]geom-filter-file[/bold bright_cyan] or"
391-
" [bold bright_cyan]geom-filter-geocode[/bold bright_cyan] or"
392-
" [bold bright_cyan]geom-filter-geojson[/bold bright_cyan] or"
393-
" [bold bright_cyan]geom-filter-index-h3[/bold bright_cyan] or"
394-
" [bold bright_cyan]geom-filter-index-s2[/bold bright_cyan] or"
395-
" [bold bright_cyan]geom-filter-wkt[/bold bright_cyan]."
414+
" Cannot be used together with other"
415+
" [bold bright_cyan]geom-filter-...[/bold bright_cyan] parameters."
396416
),
397417
click_type=GeohashGeometryParser(),
418+
show_default=False,
398419
),
399420
] = None,
400421
geom_filter_index_h3: Annotated[
@@ -403,15 +424,11 @@ def main(
403424
help=(
404425
"Geometry to use as a filter in the [bold dark_orange]H3 index[/bold dark_orange]"
405426
" format. Separate multiple values with a comma."
406-
" Cannot be used used together with"
407-
" [bold bright_cyan]geom-filter-file[/bold bright_cyan] or"
408-
" [bold bright_cyan]geom-filter-geocode[/bold bright_cyan] or"
409-
" [bold bright_cyan]geom-filter-geojson[/bold bright_cyan] or"
410-
" [bold bright_cyan]geom-filter-index-geohash[/bold bright_cyan] or"
411-
" [bold bright_cyan]geom-filter-index-s2[/bold bright_cyan] or"
412-
" [bold bright_cyan]geom-filter-wkt[/bold bright_cyan]."
427+
" Cannot be used together with other"
428+
" [bold bright_cyan]geom-filter-...[/bold bright_cyan] parameters."
413429
),
414430
click_type=H3GeometryParser(),
431+
show_default=False,
415432
),
416433
] = None,
417434
geom_filter_index_s2: Annotated[
@@ -420,15 +437,11 @@ def main(
420437
help=(
421438
"Geometry to use as a filter in the [bold dark_orange]S2 index[/bold dark_orange]"
422439
" format. Separate multiple values with a comma."
423-
" Cannot be used used together with"
424-
" [bold bright_cyan]geom-filter-file[/bold bright_cyan] or"
425-
" [bold bright_cyan]geom-filter-geocode[/bold bright_cyan] or"
426-
" [bold bright_cyan]geom-filter-geojson[/bold bright_cyan] or"
427-
" [bold bright_cyan]geom-filter-index-geohash[/bold bright_cyan] or"
428-
" [bold bright_cyan]geom-filter-index-h3[/bold bright_cyan] or"
429-
" [bold bright_cyan]geom-filter-wkt[/bold bright_cyan]."
440+
" Cannot be used together with other"
441+
" [bold bright_cyan]geom-filter-...[/bold bright_cyan] parameters."
430442
),
431443
click_type=S2GeometryParser(),
444+
show_default=False,
432445
),
433446
] = None,
434447
geom_filter_wkt: Annotated[
@@ -437,15 +450,11 @@ def main(
437450
help=(
438451
"Geometry to use as a filter in the [bold dark_orange]WKT[/bold dark_orange]"
439452
" format."
440-
" Cannot be used together with"
441-
" [bold bright_cyan]geom-filter-file[/bold bright_cyan] or"
442-
" [bold bright_cyan]geom-filter-geocode[/bold bright_cyan] or"
443-
" [bold bright_cyan]geom-filter-geojson[/bold bright_cyan] or"
444-
" [bold bright_cyan]geom-filter-index-geohash[/bold bright_cyan] or"
445-
" [bold bright_cyan]geom-filter-index-h3[/bold bright_cyan] or"
446-
" [bold bright_cyan]geom-filter-index-s2[/bold bright_cyan]."
453+
" Cannot be used together with other"
454+
" [bold bright_cyan]geom-filter-...[/bold bright_cyan] parameters."
447455
),
448456
click_type=WktGeometryParser(),
457+
show_default=False,
449458
),
450459
] = None,
451460
osm_extract_query: Annotated[
@@ -457,6 +466,7 @@ def main(
457466
"Can be used instead of [bold yellow]PBF file path[/bold yellow] argument."
458467
),
459468
case_sensitive=False,
469+
show_default=False,
460470
),
461471
] = None,
462472
osm_extract_source: Annotated[
@@ -505,6 +515,7 @@ def main(
505515
" Can be [bold green].parquet[/bold green] or"
506516
" [bold green].db[/bold green] or [bold green].duckdb[/bold green] extension."
507517
),
518+
show_default=False,
508519
),
509520
] = None,
510521
duckdb: Annotated[
@@ -558,6 +569,7 @@ def main(
558569
" for file generation."
559570
),
560571
callback=_empty_path_callback,
572+
show_default=False,
561573
),
562574
] = None,
563575
filter_osm_ids: Annotated[
@@ -570,6 +582,7 @@ def main(
570582
" Separate multiple values with a comma."
571583
),
572584
callback=_filter_osm_ids_callback,
585+
show_default=False,
573586
),
574587
] = None,
575588
wkt_result: Annotated[
@@ -655,6 +668,7 @@ def main(
655668
number_of_geometries_provided = sum(
656669
geom is not None
657670
for geom in (
671+
geom_filter_bbox,
658672
geom_filter_file,
659673
geom_filter_geocode,
660674
geom_filter_geojson,
@@ -668,7 +682,8 @@ def main(
668682
raise typer.BadParameter("Provided more than one geometry for filtering")
669683

670684
geometry_filter_value = (
671-
geom_filter_file
685+
geom_filter_bbox
686+
or geom_filter_file
672687
or geom_filter_geocode
673688
or geom_filter_geojson
674689
or geom_filter_index_geohash
@@ -685,7 +700,7 @@ def main(
685700
message=(
686701
"QuackOSM requires either the path to the pbf file,"
687702
" an OSM extract query (--osm-extract-query) or a geometry filter"
688-
" (one of --geom-filter-file, --geom-filter-geocode,"
703+
" (one of --geom-filter-bbox, --geom-filter-file, --geom-filter-geocode,"
689704
" --geom-filter-geojson, --geom-filter-index-geohash,"
690705
" --geom-filter-index-h3, --geom-filter-index-s2, --geom-filter-wkt)"
691706
" to download the file automatically. All three cannot be empty at once."

tests/base/conftest.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@
1717
]
1818

1919

20+
def bbox() -> tuple[float, float, float, float]:
21+
"""Bounding Box."""
22+
return (7.416486207767861, 43.7310867041912, 7.421931388477276, 43.73370705597216)
23+
24+
2025
def geometry_box() -> Polygon:
2126
"""Geometry box."""
22-
return box(
23-
minx=7.416486207767861,
24-
miny=43.7310867041912,
25-
maxx=7.421931388477276,
26-
maxy=43.73370705597216,
27-
)
27+
return box(*bbox())
2828

2929

3030
def geometry_wkt() -> str:

tests/base/test_cli.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,11 @@ def test_proper_args_with_pbf(
280280

281281

282282
@P.parameters("args", "expected_result") # type: ignore
283+
@P.case(
284+
"Geometry BBOX filter",
285+
["--geom-filter-bbox", "7.416486,43.731086,7.421931,43.733707"],
286+
"files/b9115f99abe0ba4933e368fdf5e7d4ffbcc6fa48ba5ce03c4c50f95165fd3291_nofilter_compact.parquet",
287+
) # type: ignore
283288
@P.case(
284289
"Geometry WKT filter",
285290
["--geom-filter-wkt", geometry_wkt()],
@@ -645,6 +650,8 @@ def test_proper_args_with_pbf_url() -> None:
645650
@P.case("Geometry H3 filter with S2", ["--geom-filter-index-h3", "12cdc28bc"]) # type: ignore
646651
@P.case("Geometry H3 filter with random string", ["--geom-filter-index-h3", random_str()]) # type: ignore
647652
@P.case("Geometry S2 filter with random string", ["--geom-filter-index-s2", random_str()]) # type: ignore
653+
@P.case("Geometry BBOX filter with wrong values", ["--geom-filter-bbox", random_str()]) # type: ignore
654+
@P.case("Geometry BBOX filter with spaces", ["--geom-filter-bbox", "10,", "-5,", "6,", "17"]) # type: ignore
648655
@P.case(
649656
"Geometry two filters",
650657
["--geom-filter-geojson", geometry_geojson(), "--geom-filter-wkt", geometry_wkt()],

0 commit comments

Comments
 (0)