Skip to content

Commit 76160a7

Browse files
feat: Select road level of precision (#107)
Co-authored-by: Jules Lecoustre <jules@lecoustre.org>
1 parent b6ba959 commit 76160a7

File tree

12 files changed

+193
-68
lines changed

12 files changed

+193
-68
lines changed

pretty_gpx/rendering_modes/city/data/bridges.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def create_bridge(cls,
105105
outer_members = [member.geometry for member in way_or_relation.members
106106
if isinstance(member, RelationWay) and member.geometry
107107
and member.role == "outer"]
108+
108109
merged_ways = merge_ways(outer_members)
109110
if len(merged_ways) > 1:
110111
logger.error("Multiple geometries found")
@@ -152,6 +153,7 @@ def _extract_intersection_coordinates(intersection: BaseGeometry) -> tuple[list[
152153
if isinstance(intersection, GeometryCollection | MultiLineString):
153154
coords = [(x, y) for geom in intersection.geoms
154155
if isinstance(geom, LineString) for x, y in geom.coords]
156+
155157
if not coords:
156158
return None
157159
x_coords, y_coords = zip(*coords)

pretty_gpx/rendering_modes/city/data/roads.py

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
#!/usr/bin/python3
22
"""Roads."""
33
import os
4-
from enum import auto
5-
from enum import Enum
4+
from enum import IntEnum
65

76
from tqdm import tqdm
87

@@ -11,6 +10,7 @@
1110
from pretty_gpx.common.request.gpx_data_cache_handler import GpxDataCacheHandler
1211
from pretty_gpx.common.request.overpass_processing import get_ways_coordinates_from_results
1312
from pretty_gpx.common.request.overpass_request import OverpassQuery
13+
from pretty_gpx.common.utils.asserts import assert_same_keys
1414
from pretty_gpx.common.utils.logger import logger
1515
from pretty_gpx.common.utils.pickle_io import read_pickle
1616
from pretty_gpx.common.utils.pickle_io import write_pickle
@@ -20,31 +20,32 @@
2020
ROADS_CACHE = GpxDataCacheHandler(name='roads', extension='.pkl')
2121

2222

23-
class CityRoadType(Enum):
24-
"""City Road Type."""
25-
HIGHWAY = auto()
26-
SECONDARY_ROAD = auto()
27-
STREET = auto()
28-
ACCESS_ROAD = auto()
23+
class CityRoadPrecision(IntEnum):
24+
"""Enum defining different road precision levels."""
25+
VERY_HIGH = 3 # Access roads
26+
HIGH = 2 # Streets
27+
MEDIUM = 1 # Secondary roads
28+
LOW = 0 # Highways
2929

30+
@property
31+
def pretty_name(self) -> str:
32+
"""Human-friendly name, e.g. 'Very-High'."""
33+
return self.name.replace("_", "-").title()
3034

31-
HIGHWAY_TAGS_PER_CITY_ROAD_TYPE = {
32-
CityRoadType.HIGHWAY: ["motorway", "trunk", "primary"],
33-
CityRoadType.SECONDARY_ROAD: ["tertiary", "secondary"],
34-
CityRoadType.STREET: ["residential", "living_street"],
35-
CityRoadType.ACCESS_ROAD: ["unclassified", "service"]
36-
}
35+
@staticmethod
36+
def coarse_to_fine() -> list["CityRoadPrecision"]:
37+
"""Return the list of road precision from coarse to fine."""
38+
return sorted(CityRoadPrecision, key=lambda p: p.value)
3739

38-
QUERY_NAME_PER_CITY_ROAD_TYPE = {
39-
CityRoadType.HIGHWAY: "highway",
40-
CityRoadType.SECONDARY_ROAD: "secondary_roads",
41-
CityRoadType.STREET: "street",
42-
CityRoadType.ACCESS_ROAD: "access_roads"
43-
}
4440

45-
assert HIGHWAY_TAGS_PER_CITY_ROAD_TYPE.keys() == QUERY_NAME_PER_CITY_ROAD_TYPE.keys()
41+
ROAD_HIGHWAY_TAGS: dict[CityRoadPrecision, list[str]] = {
42+
CityRoadPrecision.LOW: ["motorway", "trunk", "primary"],
43+
CityRoadPrecision.MEDIUM: ["tertiary", "secondary"],
44+
CityRoadPrecision.HIGH: ["residential", "living_street"],
45+
CityRoadPrecision.VERY_HIGH: ["unclassified", "service"]
46+
}
4647

47-
CityRoads = dict[CityRoadType, list[ListLonLat]]
48+
assert_same_keys(ROAD_HIGHWAY_TAGS, CityRoadPrecision)
4849

4950

5051
@profile
@@ -65,9 +66,9 @@ def prepare_download_city_roads(query: OverpassQuery,
6566
query.add_cached_result(ROADS_CACHE.name, cache_file=cache_pkl)
6667
return
6768

68-
for city_road_type in tqdm(CityRoadType):
69-
highway_tags_str = "|".join(HIGHWAY_TAGS_PER_CITY_ROAD_TYPE[city_road_type])
70-
query.add_overpass_query(QUERY_NAME_PER_CITY_ROAD_TYPE[city_road_type],
69+
for city_road_precision in tqdm(CityRoadPrecision):
70+
highway_tags_str = "|".join(ROAD_HIGHWAY_TAGS[city_road_precision])
71+
query.add_overpass_query(city_road_precision.name,
7172
[f"way['highway'~'({highway_tags_str})']"],
7273
bounds,
7374
include_way_nodes=True,
@@ -76,18 +77,18 @@ def prepare_download_city_roads(query: OverpassQuery,
7677

7778
@profile
7879
def process_city_roads(query: OverpassQuery,
79-
bounds: GpxBounds) -> dict[CityRoadType, list[ListLonLat]]:
80+
bounds: GpxBounds) -> dict[CityRoadPrecision, list[ListLonLat]]:
8081
"""Query the overpass API to get the roads of a city."""
8182
if query.is_cached(ROADS_CACHE.name):
8283
cache_file = query.get_cache_file(ROADS_CACHE.name)
8384
return read_pickle(cache_file)
8485

8586
with Profiling.Scope("Process City Roads"):
8687
roads = dict()
87-
for city_road_type, query_name in QUERY_NAME_PER_CITY_ROAD_TYPE.items():
88-
logger.debug(f"Query name : {query_name}")
89-
result = query.get_query_result(query_name)
90-
roads[city_road_type] = get_ways_coordinates_from_results(result)
88+
for city_road_precision in CityRoadPrecision:
89+
logger.debug(f"Query precision : {city_road_precision.name}")
90+
result = query.get_query_result(city_road_precision.name)
91+
roads[city_road_precision] = get_ways_coordinates_from_results(result)
9192

9293
cache_pkl = ROADS_CACHE.get_path(bounds)
9394
write_pickle(cache_pkl, roads)

pretty_gpx/rendering_modes/city/drawing/city_background.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@
1616
from pretty_gpx.rendering_modes.city.data.forests import process_city_forests
1717
from pretty_gpx.rendering_modes.city.data.rivers import prepare_download_city_rivers
1818
from pretty_gpx.rendering_modes.city.data.rivers import process_city_rivers
19-
from pretty_gpx.rendering_modes.city.data.roads import CityRoadType
19+
from pretty_gpx.rendering_modes.city.data.roads import CityRoadPrecision
2020
from pretty_gpx.rendering_modes.city.data.roads import prepare_download_city_roads
2121
from pretty_gpx.rendering_modes.city.data.roads import process_city_roads
2222

2323

2424
class CityBackgroundParamsProtocol(Protocol):
2525
"""Protocol for City Background Parameters."""
2626
@property
27-
def city_roads_lw(self) -> dict[CityRoadType, MetersFloat]: ... # noqa: D102
27+
def city_roads_lw(self) -> dict[CityRoadPrecision, MetersFloat]: ... # noqa: D102
2828

29+
city_road_max_precision: CityRoadPrecision
2930
city_dark_mode: bool
3031
city_background_color: str
3132
city_farmland_color: str
@@ -38,12 +39,12 @@ class CityBackground:
3839
"""Drawing Component for a City Background."""
3940
union_bounds: GpxBounds
4041

41-
full_roads: dict[CityRoadType, list[ListLonLat]]
42+
full_roads: dict[CityRoadPrecision, list[ListLonLat]]
4243
full_rivers: SurfacePolygons
4344
full_forests: SurfacePolygons
4445
full_farmlands: SurfacePolygons
4546

46-
paper_roads: dict[CityRoadType, list[ListLonLat]] | None
47+
paper_roads: dict[CityRoadPrecision, list[ListLonLat]] | None
4748
paper_rivers: SurfacePolygons | None
4849
paper_forests: SurfacePolygons | None
4950
paper_farmlands: SurfacePolygons | None
@@ -53,10 +54,9 @@ class CityBackground:
5354
def from_union_bounds(union_bounds: GpxBounds) -> 'CityBackground':
5455
"""Initialize the City Background from the Union Bounds."""
5556
total_query = OverpassQuery()
56-
for prepare_func in [prepare_download_city_roads,
57-
prepare_download_city_rivers,
58-
prepare_download_city_forests]:
59-
prepare_func(total_query, union_bounds)
57+
prepare_download_city_roads(total_query, union_bounds)
58+
prepare_download_city_rivers(total_query, union_bounds)
59+
prepare_download_city_forests(total_query, union_bounds)
6060

6161
total_query.launch_queries()
6262

@@ -93,9 +93,10 @@ def draw(self, fig: DrawingFigure, params: CityBackgroundParamsProtocol) -> None
9393
color_background=params.city_background_color)
9494

9595
road_color = "black" if params.city_dark_mode else "white"
96-
for priority, roads in safe(self.paper_roads).items():
97-
fig.line_collection(lon_lat_lines=roads,
98-
lw=params.city_roads_lw[priority],
99-
color=road_color)
96+
for road_precision in CityRoadPrecision: # Filter roads by precision
97+
if road_precision <= params.city_road_max_precision:
98+
fig.line_collection(lon_lat_lines=safe(self.paper_roads)[road_precision],
99+
lw=params.city_roads_lw[road_precision],
100+
color=road_color)
100101

101102
fig.background_color(params.city_background_color)

pretty_gpx/rendering_modes/city/drawing/city_params.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from pretty_gpx.common.drawing.utils.fonts import FontEnum
1313
from pretty_gpx.common.drawing.utils.plt_marker import MarkerType
1414
from pretty_gpx.common.drawing.utils.scatter_point import ScatterPointCategory
15-
from pretty_gpx.rendering_modes.city.data.roads import CityRoadType
15+
from pretty_gpx.rendering_modes.city.data.roads import CityRoadPrecision
1616
from pretty_gpx.rendering_modes.city.drawing.city_colors import CITY_COLOR_THEMES
1717

1818

@@ -28,7 +28,8 @@ class CityParams:
2828
annot_ha: str
2929
annot_va: str
3030

31-
city_roads_lw: dict[CityRoadType, MetersFloat]
31+
city_roads_lw: dict[CityRoadPrecision, MetersFloat]
32+
city_road_max_precision: CityRoadPrecision
3233
city_dark_mode: bool
3334
city_background_color: str
3435
city_farmland_color: str
@@ -88,11 +89,12 @@ def default() -> "CityParams":
8889
annot_ha="center",
8990
annot_va="center",
9091
city_roads_lw={
91-
CityRoadType.HIGHWAY: MetersFloat(m=40.0),
92-
CityRoadType.SECONDARY_ROAD: MetersFloat(m=20),
93-
CityRoadType.STREET: MetersFloat(m=10),
94-
CityRoadType.ACCESS_ROAD: MetersFloat(m=5)
92+
CityRoadPrecision.LOW: MetersFloat(m=40.0),
93+
CityRoadPrecision.MEDIUM: MetersFloat(m=20),
94+
CityRoadPrecision.HIGH: MetersFloat(m=10),
95+
CityRoadPrecision.VERY_HIGH: MetersFloat(m=5)
9596
},
97+
city_road_max_precision=CityRoadPrecision.MEDIUM,
9698
city_dark_mode=CITY_COLOR_THEMES[DarkTheme.BLUE_PURPLE_YELLOW].dark_mode,
9799
city_background_color=CITY_COLOR_THEMES[DarkTheme.BLUE_PURPLE_YELLOW].background_color,
98100
city_farmland_color=CITY_COLOR_THEMES[DarkTheme.BLUE_PURPLE_YELLOW].farmland_color,
@@ -122,5 +124,5 @@ def default() -> "CityParams":
122124

123125
centered_title_font_color="cyan",
124126
centered_title_font_size=A4Float(mm=20),
125-
centered_title_fontproperties=FontEnum.TITLE.value
127+
centered_title_fontproperties=FontEnum.TITLE.value,
126128
)

pretty_gpx/ui/pages/city/page.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
from dataclasses import dataclass
44

55
from pretty_gpx.common.drawing.utils.scatter_point import ScatterPointCategory
6+
from pretty_gpx.rendering_modes.city.data.roads import CityRoadPrecision
67
from pretty_gpx.rendering_modes.city.drawing.city_colors import CITY_COLOR_THEMES
78
from pretty_gpx.rendering_modes.city.drawing.city_drawer import CityDrawer
89
from pretty_gpx.rendering_modes.city.drawing.city_params import CityParams
910
from pretty_gpx.ui.pages.template.ui_input import UiInputInt
1011
from pretty_gpx.ui.pages.template.ui_manager import UiManager
12+
from pretty_gpx.ui.pages.template.ui_toggle import UiToggle
1113

1214

1315
def city_page() -> None:
@@ -19,6 +21,7 @@ def city_page() -> None:
1921
class CityUiManager(UiManager[CityDrawer]):
2022
"""City Ui Manager."""
2123
uphill: UiInputInt
24+
road_max_precision: UiToggle[CityRoadPrecision]
2225

2326
def __init__(self) -> None:
2427
drawer = CityDrawer(params=CityParams.default(), top_ratio=0.18, bot_ratio=0.22, margin_ratio=0.1)
@@ -29,6 +32,11 @@ def __init__(self) -> None:
2932
with self.subclass_column:
3033
self.uphill = UiInputInt.create(label='D+ (m)', value="", on_enter=self.on_click_update,
3134
tooltip="Press Enter to override elevation from GPX")
35+
self.road_max_precision = UiToggle[CityRoadPrecision].create({p.pretty_name: p
36+
for p in CityRoadPrecision.coarse_to_fine()},
37+
tooltip="Change the roads level of details",
38+
on_change=self.on_click_update,
39+
start_key=CityRoadPrecision.MEDIUM.pretty_name)
3240

3341
@staticmethod
3442
def get_chat_msg() -> list[str]:
@@ -43,6 +51,7 @@ def update_drawer_params(self) -> None:
4351

4452
self.drawer.params.track_color = theme.track_color
4553

54+
self.drawer.params.city_road_max_precision = self.road_max_precision.value
4655
self.drawer.params.city_background_color = theme.background_color
4756
self.drawer.params.city_farmland_color = theme.farmland_color
4857
self.drawer.params.city_rivers_color = theme.rivers_color

pretty_gpx/ui/pages/mountain/page.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ def __init__(self) -> None:
3232
with self.subclass_column:
3333
self.uphill = UiInputInt.create(label='D+ (m)', value="", on_enter=self.on_click_update,
3434
tooltip="Press Enter to override elevation from GPX")
35-
self.azimuth = UiToggle[int].create(mapping=AZIMUTHS, on_change=self.on_click_update)
35+
self.azimuth = UiToggle[int].create(mapping=AZIMUTHS,
36+
tooltip="Select the azimuth angle for the hillshading",
37+
on_change=self.on_click_update)
3638

3739
@staticmethod
3840
def get_chat_msg() -> list[str]:

pretty_gpx/ui/pages/multi_mountain/page.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ def __init__(self) -> None:
3636
with self.subclass_column:
3737
self.uphill = UiInputInt.create(label='D+ (m)', value="", on_enter=self.on_click_update,
3838
tooltip="Press Enter to override elevation from GPX")
39-
self.azimuth = UiToggle[int].create(mapping=AZIMUTHS, on_change=self.on_click_update)
39+
self.azimuth = UiToggle[int].create(mapping=AZIMUTHS,
40+
tooltip="Select the azimuth angle for the hillshading",
41+
on_change=self.on_click_update)
4042
self.hut_icon = UiIconToggle(markers=[MarkerType.HOUSE, MarkerType.CAMPING],
4143
on_change=self.on_click_update)
4244

pretty_gpx/ui/pages/template/ui_icon_toggle.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/python3
2-
"""Ui Icon Toggle."""
2+
"""Ui Icon Toggle, to the let the user trigger an action by selecting an icon."""
33
import os
44
from collections.abc import Awaitable
55
from collections.abc import Callable
@@ -14,9 +14,20 @@
1414

1515

1616
class UiIconToggle:
17-
"""NiceGUI Icon Toggle."""
17+
"""NiceGUI Icon Toggle.
1818
19-
def __init__(self, markers: list[MarkerType],
19+
This component is used to the let the user trigger an action by selecting an icon.
20+
21+
This UI toggle displays a row of buttons, each represented by an icon loaded
22+
from a corresponding SVG file. The icons are linked to values of type `MarkerType`.
23+
24+
When a button is clicked, the selected icon is visually highlighted, and a
25+
provided async callback (`on_change`) is triggered. The currently selected
26+
value can be accessed via the `value` property.
27+
"""
28+
29+
def __init__(self,
30+
markers: list[MarkerType],
2031
start_idx: int = 0,
2132
*, on_change: Callable[[], Awaitable[None]]) -> None:
2233
self.markers = markers

pretty_gpx/ui/pages/template/ui_input.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/python3
2-
"""Ui Input."""
2+
"""Ui Input, to capture text input from the user."""
33
from collections.abc import Awaitable
44
from collections.abc import Callable
55
from dataclasses import dataclass
@@ -12,7 +12,10 @@
1212

1313
@dataclass
1414
class UiInput:
15-
"""NiceGUI Input Wrapper."""
15+
"""NiceGUI Input Wrapper.
16+
17+
This component is used to capture text input from the user.
18+
"""
1619
input: ui.input
1720

1821
@classmethod
@@ -23,7 +26,7 @@ def create(cls,
2326
tooltip: str,
2427
on_enter: Callable[[], Awaitable[None]]) -> Self:
2528
"""Create NiceGUI Input element and add a tooltip."""
26-
with ui.input(label=label, value=value).on('keydown.enter', on_enter) as input:
29+
with ui.input(label=label, value=value).on('keydown.enter', on_enter).style('width: 100%') as input:
2730
ui.tooltip(tooltip)
2831
return cls(input)
2932

@@ -36,7 +39,10 @@ def _value_str(self) -> str | None:
3639

3740
@dataclass
3841
class UiInputStr(UiInput):
39-
"""NiceGUI Str Input Wrapper."""
42+
"""NiceGUI Str Input Wrapper.
43+
44+
This component is used to capture text input from the user as a string.
45+
"""
4046

4147
@property
4248
def value(self) -> str | None:
@@ -46,7 +52,10 @@ def value(self) -> str | None:
4652

4753
@dataclass
4854
class UiInputFloat(UiInput):
49-
"""NiceGUI Float Input Wrapper."""
55+
"""NiceGUI Float Input Wrapper.
56+
57+
This component is used to capture text input from the user as a float.
58+
"""
5059

5160
@property
5261
def value(self) -> float | None:
@@ -59,7 +68,10 @@ def value(self) -> float | None:
5968

6069
@dataclass
6170
class UiInputInt(UiInput):
62-
"""NiceGUI Int Input Wrapper."""
71+
"""NiceGUI Int Input Wrapper.
72+
73+
This component is used to capture text input from the user as an int.
74+
"""
6375

6476
@property
6577
def value(self) -> int | None:

0 commit comments

Comments
 (0)