"""Folium map rendering: plain route, discovery route, and POI markers. Returns an HTML string suitable for a Gradio ``gr.HTML`` component. Styled per the design handoff: cobalt plain route, grass discovery route (with an in-iframe draw-on animation), coral POI markers that pop in, and a legend. """ from __future__ import annotations import folium from branca.element import Element from discoverroute import config from discoverroute.routing.graph import Route from discoverroute.ui import design, markers PLAIN_COLOR = "#2F5DF4" # cobalt — the plain/fastest route DISCOVERY_COLOR = "#2FA463" # grass — the discovery route # Warmer, livelier basemap than the pale Positron — CARTO Voyager (keyless). _TILE_URL = "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png" _TILE_ATTR = ('© OpenStreetMap ' 'contributors © CARTO') # POI marker color by category family (design palette). _CATEGORY_COLORS = { # nature & calm — grass "park_garden": "#2FA463", "water_feature": "#2FA463", "viewpoint": "#2FA463", # culture & history — cobalt "monument_historic": "#2F5DF4", "museum_gallery": "#2F5DF4", "place_of_worship": "#2F5DF4", "library": "#2F5DF4", "theatre_cinema": "#2F5DF4", "attraction": "#2F5DF4", # food & drink — sun "cafe": "#E89E1C", "bakery_food_shop": "#E89E1C", "restaurant": "#E89E1C", "bar_pub": "#E89E1C", "market": "#E89E1C", # art & finds — coral "artwork": "#FF6A52", "bookshop": "#FF6A52", "specialty_shop": "#FF6A52", } _DEFAULT_POI_COLOR = "#FF6A52" _LEGEND_HTML = """
Discovery route
Fastest route
Green space Water & wayfinding
Culture Cozy stops
""" # Gentle warm grade on the tiles so the basemap sits inside the cream design # instead of fighting it (applies inside the folium iframe). _TILE_WARMTH_CSS = """ """ def _fit_bounds(fmap: folium.Map, coords: list[tuple[float, float]]) -> None: if not coords: return lats = [c[0] for c in coords] lons = [c[1] for c in coords] fmap.fit_bounds([[min(lats), min(lons)], [max(lats), max(lons)]], padding=(28, 28)) def render_routes( plain: Route | None = None, discovery: Route | None = None, pois=None, start: tuple[float, float] | None = None, end: tuple[float, float] | None = None, ) -> str: """Render routes + markers and return the map as standalone HTML.""" center = start or config.PARIS_CENTER fmap = folium.Map(location=list(center), zoom_start=14, tiles=_TILE_URL, attr=_TILE_ATTR) all_coords: list[tuple[float, float]] = [] if plain is not None and plain.coords: folium.PolyLine( plain.coords, color=PLAIN_COLOR, weight=4, opacity=0.55, dash_array="7 9", tooltip=f"Fastest route · {plain.distance_m/1000:.2f} km · {plain.time_min:.0f} min", ).add_to(fmap) all_coords.extend(plain.coords) if discovery is not None and discovery.coords: # under-glow + main stroke; class_name lets the iframe script draw it on folium.PolyLine( discovery.coords, color=DISCOVERY_COLOR, weight=10, opacity=0.18, ).add_to(fmap) folium.PolyLine( discovery.coords, color=DISCOVERY_COLOR, weight=5, opacity=0.95, class_name="route-disc", tooltip=f"Discovery route · {discovery.distance_m/1000:.2f} km · {discovery.time_min:.0f} min", ).add_to(fmap) all_coords.extend(discovery.coords) if pois: from discoverroute.data import taxonomy for i, poi in enumerate(pois): name = getattr(poi, "name", None) cat = getattr(poi, "category", "") icon = markers.poi_icon(cat, index=i) if name and str(name).strip(): tooltip = f"{name} · {taxonomy.pretty_category(cat)}" if cat else str(name) else: # unnamed → a single natural label, never raw snake_case tooltip = taxonomy.display_label(poi) if icon is not None: folium.Marker([poi.lat, poi.lon], icon=icon, tooltip=tooltip).add_to(fmap) else: # icon file missing — fall back to a colored dot folium.CircleMarker( location=[poi.lat, poi.lon], radius=7, color="#FFFCF5", weight=2, fill=True, fill_opacity=1.0, fill_color=_CATEGORY_COLORS.get(cat, _DEFAULT_POI_COLOR), class_name="dr-poi", tooltip=tooltip, ).add_to(fmap) if start is not None: icon = markers.endpoint_icon("start") folium.Marker(list(start), tooltip="Start", icon=icon or folium.Icon(color="blue", icon="play")).add_to(fmap) if end is not None: icon = markers.endpoint_icon("dest") folium.Marker(list(end), tooltip="Destination", icon=icon or folium.Icon(color="red", icon="flag")).add_to(fmap) _fit_bounds(fmap, all_coords or [c for c in (start, end) if c]) root = fmap.get_root() root.html.add_child(Element(_LEGEND_HTML)) root.html.add_child(Element(_TILE_WARMTH_CSS)) root.html.add_child(Element(markers.MARKER_CSS)) root.html.add_child(Element(design.MAP_ANIMATION_JS)) return fmap._repr_html_() def empty_map(message: str = design.EMPTY_STATE_LABEL) -> str: """A blank Paris map with a friendly sticker overlay (empty/error state).""" fmap = folium.Map(location=list(config.PARIS_CENTER), zoom_start=12, tiles=_TILE_URL, attr=_TILE_ATTR) overlay = f"""
{message}
""" fmap.get_root().html.add_child(Element(_TILE_WARMTH_CSS)) fmap.get_root().html.add_child(Element(overlay)) return fmap._repr_html_()