| from __future__ import annotations | |
| import html | |
| from .models import GeoPoint | |
| def build_route_svg(points: list[GeoPoint], route_indices: list[int]) -> str: | |
| if not points or not route_indices: | |
| return "" | |
| width = 760 | |
| height = 420 | |
| padding = 46 | |
| lats = [point.lat for point in points] | |
| lons = [point.lon for point in points] | |
| min_lat, max_lat = min(lats), max(lats) | |
| min_lon, max_lon = min(lons), max(lons) | |
| lat_span = max(max_lat - min_lat, 0.001) | |
| lon_span = max(max_lon - min_lon, 0.001) | |
| def project(point: GeoPoint) -> tuple[float, float]: | |
| x = padding + (point.lon - min_lon) / lon_span * (width - 2 * padding) | |
| y = height - padding - (point.lat - min_lat) / lat_span * (height - 2 * padding) | |
| return x, y | |
| projected = [project(point) for point in points] | |
| route_points = [projected[idx] for idx in route_indices] | |
| polyline = " ".join(f"{x:.1f},{y:.1f}" for x, y in route_points) | |
| marker_svg = [] | |
| first_visit_number: dict[int, int] = {} | |
| for order, idx in enumerate(route_indices, start=1): | |
| first_visit_number.setdefault(idx, order) | |
| for idx, point in enumerate(points): | |
| x, y = projected[idx] | |
| order = first_visit_number.get(idx, idx + 1) | |
| is_start = idx == 0 | |
| fill = "#0f766e" if is_start else "#2563eb" | |
| marker_svg.append( | |
| f""" | |
| <g> | |
| <circle cx="{x:.1f}" cy="{y:.1f}" r="15" fill="{fill}" stroke="#ffffff" stroke-width="3"/> | |
| <text x="{x:.1f}" y="{y + 4:.1f}" text-anchor="middle" font-size="12" font-weight="700" fill="#ffffff">{order}</text> | |
| <text x="{x + 19:.1f}" y="{y - 16:.1f}" font-size="12" font-weight="600" fill="#111827">{html.escape(point.name)}</text> | |
| </g> | |
| """ | |
| ) | |
| return f""" | |
| <div style="width: 100%;"> | |
| <svg viewBox="0 0 {width} {height}" role="img" aria-label="Route diagram" style="width:100%;height:auto;border:1px solid #d9e2ec;border-radius:8px;"> | |
| <defs> | |
| <marker id="arrow" markerWidth="10" markerHeight="10" refX="7" refY="3" orient="auto" markerUnits="strokeWidth"> | |
| <path d="M0,0 L0,6 L8,3 z" fill="#334155" /> | |
| </marker> | |
| </defs> | |
| <rect x="0" y="0" width="{width}" height="{height}" rx="8" fill="#f8fafc"/> | |
| <path d="M {polyline.replace(' ', ' L ')}" fill="none" stroke="#334155" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" marker-end="url(#arrow)"/> | |
| {''.join(marker_svg)} | |
| </svg> | |
| </div> | |
| """ | |