Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pretty_gpx/rendering_modes/city/data/bridges.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def create_bridge(cls,
outer_members = [member.geometry for member in way_or_relation.members
if isinstance(member, RelationWay) and member.geometry
and member.role == "outer"]

merged_ways = merge_ways(outer_members)
if len(merged_ways) > 1:
logger.error("Multiple geometries found")
Expand Down Expand Up @@ -152,6 +153,7 @@ def _extract_intersection_coordinates(intersection: BaseGeometry) -> tuple[list[
if isinstance(intersection, GeometryCollection | MultiLineString):
coords = [(x, y) for geom in intersection.geoms
if isinstance(geom, LineString) for x, y in geom.coords]

if not coords:
return None
x_coords, y_coords = zip(*coords)
Expand Down
61 changes: 31 additions & 30 deletions pretty_gpx/rendering_modes/city/data/roads.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#!/usr/bin/python3
"""Roads."""
import os
from enum import auto
from enum import Enum
from enum import IntEnum

from tqdm import tqdm

Expand All @@ -11,6 +10,7 @@
from pretty_gpx.common.request.gpx_data_cache_handler import GpxDataCacheHandler
from pretty_gpx.common.request.overpass_processing import get_ways_coordinates_from_results
from pretty_gpx.common.request.overpass_request import OverpassQuery
from pretty_gpx.common.utils.asserts import assert_same_keys
from pretty_gpx.common.utils.logger import logger
from pretty_gpx.common.utils.pickle_io import read_pickle
from pretty_gpx.common.utils.pickle_io import write_pickle
Expand All @@ -20,31 +20,32 @@
ROADS_CACHE = GpxDataCacheHandler(name='roads', extension='.pkl')


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

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

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

QUERY_NAME_PER_CITY_ROAD_TYPE = {
CityRoadType.HIGHWAY: "highway",
CityRoadType.SECONDARY_ROAD: "secondary_roads",
CityRoadType.STREET: "street",
CityRoadType.ACCESS_ROAD: "access_roads"
}

assert HIGHWAY_TAGS_PER_CITY_ROAD_TYPE.keys() == QUERY_NAME_PER_CITY_ROAD_TYPE.keys()
ROAD_HIGHWAY_TAGS: dict[CityRoadPrecision, list[str]] = {
CityRoadPrecision.LOW: ["motorway", "trunk", "primary"],
CityRoadPrecision.MEDIUM: ["tertiary", "secondary"],
CityRoadPrecision.HIGH: ["residential", "living_street"],
CityRoadPrecision.VERY_HIGH: ["unclassified", "service"]
}

CityRoads = dict[CityRoadType, list[ListLonLat]]
assert_same_keys(ROAD_HIGHWAY_TAGS, CityRoadPrecision)


@profile
Expand All @@ -65,9 +66,9 @@ def prepare_download_city_roads(query: OverpassQuery,
query.add_cached_result(ROADS_CACHE.name, cache_file=cache_pkl)
return

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

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

with Profiling.Scope("Process City Roads"):
roads = dict()
for city_road_type, query_name in QUERY_NAME_PER_CITY_ROAD_TYPE.items():
logger.debug(f"Query name : {query_name}")
result = query.get_query_result(query_name)
roads[city_road_type] = get_ways_coordinates_from_results(result)
for city_road_precision in CityRoadPrecision:
logger.debug(f"Query precision : {city_road_precision.name}")
result = query.get_query_result(city_road_precision.name)
roads[city_road_precision] = get_ways_coordinates_from_results(result)

cache_pkl = ROADS_CACHE.get_path(bounds)
write_pickle(cache_pkl, roads)
Expand Down
25 changes: 13 additions & 12 deletions pretty_gpx/rendering_modes/city/drawing/city_background.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,17 @@
from pretty_gpx.rendering_modes.city.data.forests import process_city_forests
from pretty_gpx.rendering_modes.city.data.rivers import prepare_download_city_rivers
from pretty_gpx.rendering_modes.city.data.rivers import process_city_rivers
from pretty_gpx.rendering_modes.city.data.roads import CityRoadType
from pretty_gpx.rendering_modes.city.data.roads import CityRoadPrecision
from pretty_gpx.rendering_modes.city.data.roads import prepare_download_city_roads
from pretty_gpx.rendering_modes.city.data.roads import process_city_roads


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

city_road_max_precision: CityRoadPrecision
city_dark_mode: bool
city_background_color: str
city_farmland_color: str
Expand All @@ -38,12 +39,12 @@ class CityBackground:
"""Drawing Component for a City Background."""
union_bounds: GpxBounds

full_roads: dict[CityRoadType, list[ListLonLat]]
full_roads: dict[CityRoadPrecision, list[ListLonLat]]
full_rivers: SurfacePolygons
full_forests: SurfacePolygons
full_farmlands: SurfacePolygons

paper_roads: dict[CityRoadType, list[ListLonLat]] | None
paper_roads: dict[CityRoadPrecision, list[ListLonLat]] | None
paper_rivers: SurfacePolygons | None
paper_forests: SurfacePolygons | None
paper_farmlands: SurfacePolygons | None
Expand All @@ -53,10 +54,9 @@ class CityBackground:
def from_union_bounds(union_bounds: GpxBounds) -> 'CityBackground':
"""Initialize the City Background from the Union Bounds."""
total_query = OverpassQuery()
for prepare_func in [prepare_download_city_roads,
prepare_download_city_rivers,
prepare_download_city_forests]:
prepare_func(total_query, union_bounds)
prepare_download_city_roads(total_query, union_bounds)
prepare_download_city_rivers(total_query, union_bounds)
prepare_download_city_forests(total_query, union_bounds)

total_query.launch_queries()

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

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

fig.background_color(params.city_background_color)
16 changes: 9 additions & 7 deletions pretty_gpx/rendering_modes/city/drawing/city_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from pretty_gpx.common.drawing.utils.fonts import FontEnum
from pretty_gpx.common.drawing.utils.plt_marker import MarkerType
from pretty_gpx.common.drawing.utils.scatter_point import ScatterPointCategory
from pretty_gpx.rendering_modes.city.data.roads import CityRoadType
from pretty_gpx.rendering_modes.city.data.roads import CityRoadPrecision
from pretty_gpx.rendering_modes.city.drawing.city_colors import CITY_COLOR_THEMES


Expand All @@ -28,7 +28,8 @@ class CityParams:
annot_ha: str
annot_va: str

city_roads_lw: dict[CityRoadType, MetersFloat]
city_roads_lw: dict[CityRoadPrecision, MetersFloat]
city_road_max_precision: CityRoadPrecision
city_dark_mode: bool
city_background_color: str
city_farmland_color: str
Expand Down Expand Up @@ -88,11 +89,12 @@ def default() -> "CityParams":
annot_ha="center",
annot_va="center",
city_roads_lw={
CityRoadType.HIGHWAY: MetersFloat(m=40.0),
CityRoadType.SECONDARY_ROAD: MetersFloat(m=20),
CityRoadType.STREET: MetersFloat(m=10),
CityRoadType.ACCESS_ROAD: MetersFloat(m=5)
CityRoadPrecision.LOW: MetersFloat(m=40.0),
CityRoadPrecision.MEDIUM: MetersFloat(m=20),
CityRoadPrecision.HIGH: MetersFloat(m=10),
CityRoadPrecision.VERY_HIGH: MetersFloat(m=5)
},
city_road_max_precision=CityRoadPrecision.MEDIUM,
city_dark_mode=CITY_COLOR_THEMES[DarkTheme.BLUE_PURPLE_YELLOW].dark_mode,
city_background_color=CITY_COLOR_THEMES[DarkTheme.BLUE_PURPLE_YELLOW].background_color,
city_farmland_color=CITY_COLOR_THEMES[DarkTheme.BLUE_PURPLE_YELLOW].farmland_color,
Expand Down Expand Up @@ -122,5 +124,5 @@ def default() -> "CityParams":

centered_title_font_color="cyan",
centered_title_font_size=A4Float(mm=20),
centered_title_fontproperties=FontEnum.TITLE.value
centered_title_fontproperties=FontEnum.TITLE.value,
)
9 changes: 9 additions & 0 deletions pretty_gpx/ui/pages/city/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
from dataclasses import dataclass

from pretty_gpx.common.drawing.utils.scatter_point import ScatterPointCategory
from pretty_gpx.rendering_modes.city.data.roads import CityRoadPrecision
from pretty_gpx.rendering_modes.city.drawing.city_colors import CITY_COLOR_THEMES
from pretty_gpx.rendering_modes.city.drawing.city_drawer import CityDrawer
from pretty_gpx.rendering_modes.city.drawing.city_params import CityParams
from pretty_gpx.ui.pages.template.ui_input import UiInputInt
from pretty_gpx.ui.pages.template.ui_manager import UiManager
from pretty_gpx.ui.pages.template.ui_toggle import UiToggle


def city_page() -> None:
Expand All @@ -19,6 +21,7 @@ def city_page() -> None:
class CityUiManager(UiManager[CityDrawer]):
"""City Ui Manager."""
uphill: UiInputInt
road_max_precision: UiToggle[CityRoadPrecision]

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

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

self.drawer.params.track_color = theme.track_color

self.drawer.params.city_road_max_precision = self.road_max_precision.value
self.drawer.params.city_background_color = theme.background_color
self.drawer.params.city_farmland_color = theme.farmland_color
self.drawer.params.city_rivers_color = theme.rivers_color
Expand Down
4 changes: 3 additions & 1 deletion pretty_gpx/ui/pages/mountain/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ def __init__(self) -> None:
with self.subclass_column:
self.uphill = UiInputInt.create(label='D+ (m)', value="", on_enter=self.on_click_update,
tooltip="Press Enter to override elevation from GPX")
self.azimuth = UiToggle[int].create(mapping=AZIMUTHS, on_change=self.on_click_update)
self.azimuth = UiToggle[int].create(mapping=AZIMUTHS,
tooltip="Select the azimuth angle for the hillshading",
on_change=self.on_click_update)

@staticmethod
def get_chat_msg() -> list[str]:
Expand Down
4 changes: 3 additions & 1 deletion pretty_gpx/ui/pages/multi_mountain/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ def __init__(self) -> None:
with self.subclass_column:
self.uphill = UiInputInt.create(label='D+ (m)', value="", on_enter=self.on_click_update,
tooltip="Press Enter to override elevation from GPX")
self.azimuth = UiToggle[int].create(mapping=AZIMUTHS, on_change=self.on_click_update)
self.azimuth = UiToggle[int].create(mapping=AZIMUTHS,
tooltip="Select the azimuth angle for the hillshading",
on_change=self.on_click_update)
self.hut_icon = UiIconToggle(markers=[MarkerType.HOUSE, MarkerType.CAMPING],
on_change=self.on_click_update)

Expand Down
17 changes: 14 additions & 3 deletions pretty_gpx/ui/pages/template/ui_icon_toggle.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/python3
"""Ui Icon Toggle."""
"""Ui Icon Toggle, to the let the user trigger an action by selecting an icon."""
import os
from collections.abc import Awaitable
from collections.abc import Callable
Expand All @@ -14,9 +14,20 @@


class UiIconToggle:
"""NiceGUI Icon Toggle."""
"""NiceGUI Icon Toggle.

def __init__(self, markers: list[MarkerType],
This component is used to the let the user trigger an action by selecting an icon.

This UI toggle displays a row of buttons, each represented by an icon loaded
from a corresponding SVG file. The icons are linked to values of type `MarkerType`.

When a button is clicked, the selected icon is visually highlighted, and a
provided async callback (`on_change`) is triggered. The currently selected
value can be accessed via the `value` property.
"""

def __init__(self,
markers: list[MarkerType],
start_idx: int = 0,
*, on_change: Callable[[], Awaitable[None]]) -> None:
self.markers = markers
Expand Down
24 changes: 18 additions & 6 deletions pretty_gpx/ui/pages/template/ui_input.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/python3
"""Ui Input."""
"""Ui Input, to capture text input from the user."""
from collections.abc import Awaitable
from collections.abc import Callable
from dataclasses import dataclass
Expand All @@ -12,7 +12,10 @@

@dataclass
class UiInput:
"""NiceGUI Input Wrapper."""
"""NiceGUI Input Wrapper.

This component is used to capture text input from the user.
"""
input: ui.input

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

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

@dataclass
class UiInputStr(UiInput):
"""NiceGUI Str Input Wrapper."""
"""NiceGUI Str Input Wrapper.

This component is used to capture text input from the user as a string.
"""

@property
def value(self) -> str | None:
Expand All @@ -46,7 +52,10 @@ def value(self) -> str | None:

@dataclass
class UiInputFloat(UiInput):
"""NiceGUI Float Input Wrapper."""
"""NiceGUI Float Input Wrapper.

This component is used to capture text input from the user as a float.
"""

@property
def value(self) -> float | None:
Expand All @@ -59,7 +68,10 @@ def value(self) -> float | None:

@dataclass
class UiInputInt(UiInput):
"""NiceGUI Int Input Wrapper."""
"""NiceGUI Int Input Wrapper.

This component is used to capture text input from the user as an int.
"""

@property
def value(self) -> int | None:
Expand Down
Loading