File size: 8,546 Bytes
f00d669
 
 
 
 
 
 
 
 
 
 
 
 
b34c92a
f00d669
 
 
b34c92a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f00d669
 
 
 
 
 
 
 
 
 
 
b34c92a
 
 
 
 
 
 
f00d669
 
 
b34c92a
 
 
 
 
 
 
 
 
f00d669
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b34c92a
 
f00d669
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2c5a5d1
b34c92a
2c5a5d1
b34c92a
 
2c5a5d1
 
 
 
b34c92a
 
 
 
 
 
 
 
 
 
f00d669
 
b34c92a
f00d669
b34c92a
f00d669
b34c92a
f00d669
b34c92a
f00d669
 
 
 
 
b34c92a
 
f00d669
 
 
 
 
 
b34c92a
 
f00d669
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b34c92a
f00d669
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
"""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 = ('&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> '
              'contributors &copy; <a href="https://carto.com/attributions">CARTO</a>')

# 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 = """
<div style="position:absolute; bottom:18px; left:12px; z-index:9999;
     background:#FFFCF5; border:1px solid #E7DAC0; border-radius:14px;
     padding:9px 13px; font-family:'DM Sans',system-ui,sans-serif; font-size:12px;
     color:#2B2620; box-shadow:0 8px 22px -12px rgba(43,38,32,.45); line-height:1.9;">
  <span style="display:inline-block;width:18px;height:4px;border-radius:2px;
        background:#2FA463;vertical-align:middle;margin-right:7px;"></span>Discovery route<br>
  <span style="display:inline-block;width:18px;height:4px;border-radius:2px;
        background:#2F5DF4;vertical-align:middle;margin-right:7px;"></span>Fastest route<br>
  <span style="display:inline-block;width:10px;height:10px;border-radius:50%;
        background:#2FA463;vertical-align:middle;margin-right:7px;margin-left:4px;"></span>Green space
  <span style="display:inline-block;width:10px;height:10px;border-radius:50%;
        background:#2F5DF4;vertical-align:middle;margin-right:7px;margin-left:10px;"></span>Water &amp; wayfinding<br>
  <span style="display:inline-block;width:10px;height:10px;border-radius:50%;
        background:#FF6A52;vertical-align:middle;margin-right:7px;margin-left:4px;"></span>Culture
  <span style="display:inline-block;width:10px;height:10px;border-radius:50%;
        background:#E89E1C;vertical-align:middle;margin-right:7px;margin-left:10px;"></span>Cozy stops
</div>
"""

# 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 = """
<style>
.leaflet-tile-pane{ filter: saturate(1.12) sepia(0.10) brightness(1.02); }
.leaflet-container{ background:#F6ECD9; }
</style>
"""


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"""
    <div style="position:absolute; inset:0; z-index:9999; display:grid; place-items:center;
         pointer-events:none; background:rgba(246,236,217,.45);">
      <div style="background:#FFFCF5; border:1px solid #E7DAC0; border-radius:22px;
           padding:20px 26px; text-align:center; max-width:300px;
           box-shadow:0 18px 44px -18px rgba(43,38,32,.4);">
        <svg width="72" height="58" viewBox="0 0 90 70" fill="none" aria-hidden="true">
          <polygon points="12,14 36,6 36,56 12,64" fill="#8FD6A8"/>
          <polygon points="36,6 60,14 60,64 36,56" fill="#BDE6CD"/>
          <polygon points="60,14 82,6 82,56 60,64" fill="#8FD6A8"/>
          <path d="M20 30 Q36 20 50 32 T76 30" stroke="#2F5DF4" stroke-width="3.5"
                stroke-dasharray="1 7" stroke-linecap="round" fill="none"/>
          <circle cx="58" cy="38" r="13" fill="none" stroke="#FF6A52" stroke-width="5"/>
          <path d="M67 48 L78 60" stroke="#FF6A52" stroke-width="6" stroke-linecap="round"/>
        </svg>
        <div style="font-family:'Fredoka',system-ui,sans-serif; font-weight:600; font-size:16.5px;
             color:#2B2620; margin-top:6px;">{message}</div>
      </div>
    </div>
    """
    fmap.get_root().html.add_child(Element(_TILE_WARMTH_CSS))
    fmap.get_root().html.add_child(Element(overlay))
    return fmap._repr_html_()