Skip to content

Commit 4f44216

Browse files
committed
add a dropdown to select priority to plot
1 parent 21385bf commit 4f44216

File tree

8 files changed

+258
-53
lines changed

8 files changed

+258
-53
lines changed

pretty_gpx/rendering_modes/city/data/bridges.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def create_bridge(cls,
101101
bridge_coords = get_way_coordinates(way_or_relation)
102102
elif isinstance(way_or_relation, Relation) and way_or_relation.members:
103103
outer_members = [member.geometry for member in way_or_relation.members
104-
if isinstance(member, RelationWay) and member.geometry
104+
if isinstance(member, RelationWay) and member.geometry
105105
and member.role == "outer"]
106106
merged_ways = merge_ways(outer_members)
107107
if len(merged_ways) > 1:
@@ -147,7 +147,7 @@ def _calculate_intersection_length(intersection: BaseGeometry) -> float:
147147
def _extract_intersection_coordinates(intersection: BaseGeometry) -> tuple[list[float], list[float]] | None:
148148
"""Extract x,y coordinates from intersection geometry."""
149149
if isinstance(intersection, GeometryCollection | MultiLineString):
150-
coords = [(x, y) for geom in intersection.geoms
150+
coords = [(x, y) for geom in intersection.geoms
151151
if isinstance(geom, LineString) for x, y in geom.coords]
152152
if not coords:
153153
return None
@@ -192,7 +192,7 @@ def analyze_track_bridge_crossing(cls, track: GpxTrack, bridges: list[Bridge]) -
192192

193193
intersection_direction = get_average_straight_line(coords[0], coords[1])[1]
194194
angle = cls._calculate_crossing_angle(intersection_direction, bridge.direction)
195-
195+
196196
if angle < cls.ANGLE_THRESHOLD:
197197
logger.debug(f"{bridge.name} crossed, angle : {angle}")
198198
crossed_bridges.append(bridge)
@@ -233,7 +233,7 @@ def process_city_bridges(query: OverpassQuery, track: GpxTrack) -> list[ScatterP
233233
bridges_direction: dict[str, tuple[float, LineString]] = {}
234234
bridges_stats = {}
235235
bridges_to_process = []
236-
236+
237237
for way in query.get_query_result(BRIDGES_WAYS_ARRAY_NAME).ways:
238238
if way.tags.get("bridge") and "man_made" not in way.tags:
239239
line = LineString(get_way_coordinates(way))
@@ -252,7 +252,7 @@ def process_city_bridges(query: OverpassQuery, track: GpxTrack) -> list[ScatterP
252252
bridges = [BridgeApproximation.create_bridge(way, bridges_stats) for way in bridges_to_process]
253253
bridges.extend(BridgeApproximation.create_bridge(rel, bridges_stats)
254254
for rel in query.get_query_result(BRIDGES_RELATIONS_ARRAY_NAME).relations)
255-
255+
256256
crossed_bridges = BridgeCrossingAnalyzer.analyze_track_bridge_crossing(track, [b for b in bridges if b])
257257
result = [ScatterPoint(name=b.name, lat=b.center.y, lon=b.center.x, category=ScatterPointCategory.CITY_BRIDGE)
258258
for b in crossed_bridges]
Lines changed: 122 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#!/usr/bin/python3
22
"""Roads."""
33
import os
4+
from dataclasses import dataclass
45
from enum import auto
56
from enum import Enum
7+
from typing import Any
68

79
from tqdm import tqdm
810

@@ -28,69 +30,151 @@ class CityRoadType(Enum):
2830
ACCESS_ROAD = auto()
2931

3032

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"]
33+
class RoadPrecisionLevel:
34+
"""City road precision level."""
35+
def __init__(self, name: str, priority: int):
36+
self.name = name
37+
self.priority = priority
38+
39+
def __str__(self) -> str:
40+
return self.name
41+
42+
def __eq__(self, other: Any) -> bool:
43+
if isinstance(other, RoadPrecisionLevel):
44+
return self.priority == other.priority
45+
return False
46+
47+
def __lt__(self, other: 'RoadPrecisionLevel') -> bool:
48+
if isinstance(other, RoadPrecisionLevel):
49+
return self.priority < other.priority
50+
return NotImplemented
51+
52+
53+
class CityRoadPrecision(Enum):
54+
"""Enum defining different road precision levels."""
55+
VERY_HIGH = RoadPrecisionLevel(name="Very-High", priority=3)
56+
HIGH = RoadPrecisionLevel(name="High", priority=2)
57+
MEDIUM = RoadPrecisionLevel(name="Medium", priority=1)
58+
LOW = RoadPrecisionLevel(name="Low", priority=0)
59+
60+
@classmethod
61+
def from_string(cls, precision_name: str) -> 'CityRoadPrecision':
62+
"""Convert a string representation to a CityRoadPrecision enum value."""
63+
for precision in cls:
64+
if precision.value.name.lower() == precision_name.lower():
65+
return precision
66+
raise ValueError(f"Invalid precision level: {precision_name}")
67+
68+
@property
69+
def priority(self) -> int:
70+
"""Get the priority value for the precision level."""
71+
return self.value.priority
72+
73+
74+
@dataclass(frozen=True)
75+
class RoadTypeData:
76+
"""Data for each type of city road."""
77+
tags: list[str]
78+
priority: int
79+
query_name: str
80+
81+
# Dictionary of RoadTypeData for each CityRoadType, ordered by priority
82+
ROAD_TYPE_DATA: dict[CityRoadType, RoadTypeData] = {
83+
CityRoadType.HIGHWAY: RoadTypeData(tags=["motorway", "trunk", "primary"], priority=0, query_name="highway"),
84+
CityRoadType.SECONDARY_ROAD: RoadTypeData(tags=["tertiary", "secondary"], priority=1, query_name="secondary_roads"),
85+
CityRoadType.STREET: RoadTypeData(tags=["residential", "living_street"], priority=2, query_name="street"),
86+
CityRoadType.ACCESS_ROAD: RoadTypeData(tags=["unclassified", "service"], priority=3, query_name="access_roads")
3687
}
3788

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-
}
44-
45-
assert HIGHWAY_TAGS_PER_CITY_ROAD_TYPE.keys() == QUERY_NAME_PER_CITY_ROAD_TYPE.keys()
89+
# Automatically check that ROAD_TYPE_DATA is sorted by priority
90+
assert list(ROAD_TYPE_DATA.keys()) == sorted(ROAD_TYPE_DATA.keys(),
91+
key=lambda road_type: ROAD_TYPE_DATA[road_type].priority)
4692

47-
CityRoads = dict[CityRoadType, list[ListLonLat]]
4893

94+
def get_city_roads_with_priority_better_than(precision: CityRoadPrecision) -> list[CityRoadType]:
95+
"""Returns a list of CityRoadType with a priority better than the given x."""
96+
# Filter ROAD_TYPE_DATA to get only those with priority less than x
97+
return [
98+
road_type
99+
for road_type, data in ROAD_TYPE_DATA.items()
100+
if data.priority <= precision.priority
101+
]
49102

50103
@profile
51104
def prepare_download_city_roads(query: OverpassQuery,
52-
bounds: GpxBounds) -> None:
105+
bounds: GpxBounds,
106+
road_precision: CityRoadPrecision) -> list[CityRoadType]:
53107
"""Download roads map from OpenStreetMap.
54108
55109
Args:
56110
query: OverpassQuery class that merge all queries into a single one
57111
bounds: GPX bounds
112+
road_precision: string with the road precision desired
58113
59114
Returns:
60115
List of roads (sequence of lon, lat coordinates) for each road type
61116
"""
62117
cache_pkl = ROADS_CACHE.get_path(bounds)
63118

119+
logger.debug(f"Road precision: {road_precision.name}")
120+
121+
roads_to_plot: list[CityRoadType] = get_city_roads_with_priority_better_than(road_precision)
122+
64123
if os.path.isfile(cache_pkl):
124+
cityroads_cache: dict[CityRoadType, list[ListLonLat]] = read_pickle(file_path=cache_pkl)
125+
roads_types_cache = list(cityroads_cache.keys())
65126
query.add_cached_result(ROADS_CACHE.name, cache_file=cache_pkl)
66-
return
67127

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],
71-
[f"way['highway'~'({highway_tags_str})']"],
72-
bounds,
73-
include_way_nodes=True,
74-
add_relative_margin=None)
128+
if all(key in roads_types_cache for key in roads_to_plot):
129+
logger.debug("Roads needed already downloaded")
130+
roads_to_plot = []
131+
else:
132+
logger.debug("Downloading additionnal roads")
133+
roads_to_plot = [key for key in roads_to_plot if key not in roads_types_cache]
134+
135+
if len(roads_to_plot) > 0:
136+
for city_road_type in tqdm(roads_to_plot):
137+
highway_tags_str = "|".join(ROAD_TYPE_DATA[city_road_type].tags)
138+
query_name = ROAD_TYPE_DATA[city_road_type].query_name
139+
query.add_overpass_query(query_name,
140+
[f"way['highway'~'({highway_tags_str})']"],
141+
bounds,
142+
include_way_nodes=True,
143+
add_relative_margin=None)
144+
return roads_to_plot
75145

76146

77147
@profile
78148
def process_city_roads(query: OverpassQuery,
79-
bounds: GpxBounds) -> dict[CityRoadType, list[ListLonLat]]:
149+
bounds: GpxBounds,
150+
city_roads_downloaded: list[CityRoadType],
151+
road_precision: CityRoadPrecision) -> dict[CityRoadType,list[ListLonLat]]:
80152
"""Query the overpass API to get the roads of a city."""
81-
if query.is_cached(ROADS_CACHE.name):
82-
cache_file = query.get_cache_file(ROADS_CACHE.name)
83-
return read_pickle(cache_file)
84-
85-
with Profiling.Scope("Process City Roads"):
86-
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)
153+
roads_to_plot: list[CityRoadType] = get_city_roads_with_priority_better_than(road_precision)
154+
155+
if len(city_roads_downloaded) > 0:
156+
# We need to process some downloaded road types.
157+
if query.is_cached(ROADS_CACHE.name):
158+
cache_file = query.get_cache_file(ROADS_CACHE.name)
159+
roads = read_pickle(cache_file)
160+
else:
161+
roads = dict()
162+
with Profiling.Scope("Process City Roads"):
163+
for city_road_type in city_roads_downloaded:
164+
query_name = ROAD_TYPE_DATA[city_road_type].query_name
165+
result = query.get_query_result(query_name)
166+
roads[city_road_type] = get_ways_coordinates_from_results(result)
167+
cache_pkl = ROADS_CACHE.get_path(bounds)
168+
write_pickle(cache_pkl, roads)
169+
query.add_cached_result(ROADS_CACHE.name, cache_file=cache_pkl)
170+
roads_to_return = {road_type: roads[road_type] for road_type in roads_to_plot}
171+
return roads_to_return
91172

92-
cache_pkl = ROADS_CACHE.get_path(bounds)
93-
write_pickle(cache_pkl, roads)
94-
query.add_cached_result(ROADS_CACHE.name, cache_file=cache_pkl)
173+
elif query.is_cached(ROADS_CACHE.name):
174+
cache_file = query.get_cache_file(ROADS_CACHE.name)
175+
roads = read_pickle(cache_file)
176+
roads_to_return = {road_type: roads[road_type] for road_type in roads_to_plot}
177+
return roads_to_return
95178

96-
return roads
179+
else:
180+
raise FileNotFoundError("Query is supposed to be cached but it is not.")

pretty_gpx/rendering_modes/city/drawing/city_background.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
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 CityRoadPrecision
1920
from pretty_gpx.rendering_modes.city.data.roads import CityRoadType
2021
from pretty_gpx.rendering_modes.city.data.roads import prepare_download_city_roads
2122
from pretty_gpx.rendering_modes.city.data.roads import process_city_roads
@@ -50,18 +51,18 @@ class CityBackground:
5051

5152
@staticmethod
5253
@profile
53-
def from_union_bounds(union_bounds: GpxBounds) -> 'CityBackground':
54+
def from_union_bounds(union_bounds: GpxBounds,
55+
road_precision: CityRoadPrecision) -> 'CityBackground':
5456
"""Initialize the City Background from the Union Bounds."""
5557
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)
58+
roads_downloaded = prepare_download_city_roads(total_query, union_bounds, road_precision)
59+
prepare_download_city_rivers(total_query, union_bounds)
60+
prepare_download_city_forests(total_query, union_bounds)
6061

6162
total_query.launch_queries()
6263

6364
# Retrieve the data
64-
roads = process_city_roads(total_query, union_bounds)
65+
roads = process_city_roads(total_query, union_bounds, roads_downloaded, road_precision)
6566
rivers = process_city_rivers(total_query, union_bounds)
6667
forests, farmlands = process_city_forests(total_query, union_bounds)
6768
forests.interior_polygons = []

pretty_gpx/rendering_modes/city/drawing/city_drawer.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from pretty_gpx.common.layout.vertical_layout import VerticalLayoutUnion
1818
from pretty_gpx.common.request.overpass_request import OverpassQuery
1919
from pretty_gpx.common.utils.profile import profile
20+
from pretty_gpx.common.utils.profile import profile_parallel
2021
from pretty_gpx.rendering_modes.city.data.bridges import prepare_download_city_bridges
2122
from pretty_gpx.rendering_modes.city.data.bridges import process_city_bridges
2223
from pretty_gpx.rendering_modes.city.data.city_pois import prepare_download_city_pois
@@ -68,7 +69,7 @@ def change_gpx(self, gpx_path: str | bytes, paper: PaperSize) -> None:
6869
scatter_points += process_city_pois(total_query, gpx_track)
6970
# TODO(upgrade): Draw the POIs as well. This is currently disabled because text allocation fails when there
7071
# are too many overlapping scatter points. Need to filter out the points that are too close to each other.
71-
background = CityBackground.from_union_bounds(layouts.union_bounds)
72+
background = CityBackground.from_union_bounds(layouts.union_bounds, self.params.user_road_precision)
7273

7374
layout = layouts.layouts[paper]
7475
background.change_papersize(paper, layout.background_bounds)
@@ -86,6 +87,22 @@ def change_gpx(self, gpx_path: str | bytes, paper: PaperSize) -> None:
8687
mid_track=track_data,
8788
paper=paper)
8889

90+
@profile
91+
def update_background(self, paper: PaperSize) -> None:
92+
"""Update the background (for example when road priority changes)."""
93+
assert self.data is not None
94+
gpx_track = self.data.mid_track.track
95+
assert isinstance(gpx_track, GpxTrack)
96+
layouts = VerticalLayoutUnion.from_track(gpx_track,
97+
top_ratio=self.top_ratio,
98+
bot_ratio=self.bot_ratio,
99+
margin_ratio=self.margin_ratio)
100+
101+
background = CityBackground.from_union_bounds(layouts.union_bounds, self.params.user_road_precision)
102+
layout = layouts.layouts[paper]
103+
background.change_papersize(paper, layout.background_bounds)
104+
self.data.background = background
105+
89106
@profile
90107
def change_papersize(self, paper: PaperSize) -> None:
91108
"""Change Papersize of the poster."""
@@ -112,3 +129,13 @@ def draw(self, fig: Figure, ax: Axes, high_resolution: bool) -> None:
112129
self.data.top.draw(f, self.params)
113130
self.data.mid_track.draw(f, self.params)
114131
self.data.mid_scatter.draw(f, self.params)
132+
133+
@profile_parallel
134+
def _update_city_background(drawer: CityDrawer, paper: PaperSize) -> CityDrawer:
135+
"""Process the GPX file and return the new drawer."""
136+
# This function is designed for parallel execution and will be pickled.
137+
# Defining it as a global function avoids pickling the entire UiManager class,
138+
# which contains non-picklable elements like local lambdas and UI components.
139+
assert isinstance(drawer, DrawerSingleTrack)
140+
drawer.update_background(paper)
141+
return drawer

pretty_gpx/rendering_modes/city/drawing/city_params.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +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 CityRoadPrecision
1516
from pretty_gpx.rendering_modes.city.data.roads import CityRoadType
1617
from pretty_gpx.rendering_modes.city.drawing.city_colors import CITY_COLOR_THEMES
1718

@@ -48,6 +49,7 @@ class CityParams:
4849
user_title: str | None = None
4950
user_uphill_m: int | None = None
5051
user_dist_km: float | None = None
52+
user_road_precision: CityRoadPrecision = CityRoadPrecision.MEDIUM
5153

5254
@staticmethod
5355
def default() -> "CityParams":

0 commit comments

Comments
 (0)