Fix E2E select locator, remove maxspeed mode (data unavailable)

E2E fixes:
- Color mode select locator uses option[value='highway'] filter
  instead of fragile page.locator("select").last()

Removed maxspeed color mode:
- BRouter does not expose maxspeed in WayTags output despite the tag
  existing in its lookup table. The dummyUsage profile trick only
  affects cost calculation, not tiledesc output.
- Can revisit if BRouter adds maxspeed to WayTags in a future version
  or if we fork/patch the BRouter profiles more deeply.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-11 09:06:30 +02:00
parent 4bcd6137de
commit cec613d989
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
8 changed files with 9 additions and 122 deletions

View file

@ -2,14 +2,13 @@ import { useMemo } from "react";
import { Polyline } from "react-leaflet";
import type L from "leaflet";
export type ColorMode = "plain" | "elevation" | "surface" | "grade" | "highway" | "maxspeed";
export type ColorMode = "plain" | "elevation" | "surface" | "grade" | "highway";
interface ColoredRouteProps {
coordinates: [number, number, number][]; // [lon, lat, ele]
colorMode: ColorMode;
surfaces?: string[];
highways?: string[];
maxspeeds?: string[];
}
const SURFACE_COLORS: Record<string, string> = {
@ -64,22 +63,6 @@ const HIGHWAY_COLORS: Record<string, string> = {
const DEFAULT_HIGHWAY_COLOR = "#9ca3af";
export function maxspeedColor(speed: string): string {
const num = parseInt(speed, 10);
if (isNaN(num)) {
// Handle text values: "walk", "none", "unknown"
if (speed === "walk") return "#22c55e";
if (speed === "none") return "#991b1b";
return "#9ca3af";
}
if (num <= 20) return "#22c55e"; // green: walking pace
if (num <= 30) return "#16a34a"; // green: slow zone
if (num <= 50) return "#eab308"; // yellow: urban
if (num <= 70) return "#f97316"; // orange: suburban
if (num <= 100) return "#ef4444"; // red: rural
return "#991b1b"; // dark red: highway
}
export function routeGradeColor(grade: number): string {
const absGrade = Math.abs(grade);
if (absGrade < 3) return "#22c55e";
@ -99,7 +82,7 @@ export function elevationColor(t: number): string {
return `rgb(255, ${g}, 50)`;
}
export function ColoredRoute({ coordinates, colorMode, surfaces, highways, maxspeeds }: ColoredRouteProps) {
export function ColoredRoute({ coordinates, colorMode, surfaces, highways }: ColoredRouteProps) {
const segments = useMemo(() => {
if (colorMode === "plain" || coordinates.length < 2) {
return null;
@ -164,23 +147,6 @@ export function ColoredRoute({ coordinates, colorMode, surfaces, highways, maxsp
return result;
}
// maxspeed mode
if (colorMode === "maxspeed") {
if (!maxspeeds || maxspeeds.length < coordinates.length) return null;
const result: { positions: L.LatLngExpression[]; color: string }[] = [];
for (let i = 0; i < coordinates.length - 1; i++) {
result.push({
positions: [
[coordinates[i]![1], coordinates[i]![0]],
[coordinates[i + 1]![1], coordinates[i + 1]![0]],
],
color: maxspeedColor(maxspeeds[i] ?? "unknown"),
});
}
return result;
}
// surface mode
if (!surfaces || surfaces.length < coordinates.length) return null;
@ -196,7 +162,7 @@ export function ColoredRoute({ coordinates, colorMode, surfaces, highways, maxsp
});
}
return result;
}, [coordinates, colorMode, surfaces, highways, maxspeeds]);
}, [coordinates, colorMode, surfaces, highways]);
const plainPositions = useMemo(
() => coordinates.map((c) => [c[1], c[0]] as L.LatLngExpression),

View file

@ -2,7 +2,7 @@ import { useEffect, useState, useRef, useCallback } from "react";
import { useTranslation } from "react-i18next";
import type { DayStage } from "@trails-cool/gpx";
import type { YjsState } from "~/lib/use-yjs";
import { elevationColor, SURFACE_COLORS, DEFAULT_SURFACE_COLOR, HIGHWAY_COLORS, DEFAULT_HIGHWAY_COLOR, maxspeedColor, type ColorMode } from "~/components/ColoredRoute";
import { elevationColor, SURFACE_COLORS, DEFAULT_SURFACE_COLOR, HIGHWAY_COLORS, DEFAULT_HIGHWAY_COLOR, type ColorMode } from "~/components/ColoredRoute";
function gradeColor(grade: number): string {
const absGrade = Math.abs(grade);
@ -76,7 +76,6 @@ export function ElevationChart({ yjs, onHover, days }: ElevationChartProps) {
const [colorMode, setColorMode] = useState<ColorMode>("plain");
const [surfaces, setSurfaces] = useState<string[]>([]);
const [highways, setHighways] = useState<string[]>([]);
const [maxspeeds, setMaxspeeds] = useState<string[]>([]);
const canvasRef = useRef<HTMLCanvasElement>(null);
const pointsRef = useRef<ElevationPoint[]>([]);
pointsRef.current = points;
@ -103,12 +102,6 @@ export function ElevationChart({ yjs, onHover, days }: ElevationChartProps) {
} else {
setHighways([]);
}
const maxspeedsJson = yjs.routeData.get("maxspeeds") as string | undefined;
if (maxspeedsJson) {
try { setMaxspeeds(JSON.parse(maxspeedsJson)); } catch { setMaxspeeds([]); }
} else {
setMaxspeeds([]);
}
};
yjs.routeData.observe(update);
update();
@ -241,29 +234,6 @@ export function ElevationChart({ yjs, onHover, days }: ElevationChartProps) {
ctx.fillStyle = color + "40";
ctx.fill();
ctx.beginPath();
ctx.moveTo(toX(p0.distance), toY(p0.elevation));
ctx.lineTo(toX(p1.distance), toY(p1.elevation));
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
}
} else if (colorMode === "maxspeed" && maxspeeds.length >= points.length) {
// Maxspeed-colored segments
for (let i = 0; i < points.length - 1; i++) {
const p0 = points[i]!;
const p1 = points[i + 1]!;
const color = maxspeedColor(maxspeeds[i] ?? "unknown");
ctx.beginPath();
ctx.moveTo(toX(p0.distance), PADDING.top + chartH);
ctx.lineTo(toX(p0.distance), toY(p0.elevation));
ctx.lineTo(toX(p1.distance), toY(p1.elevation));
ctx.lineTo(toX(p1.distance), PADDING.top + chartH);
ctx.closePath();
ctx.fillStyle = color + "40";
ctx.fill();
ctx.beginPath();
ctx.moveTo(toX(p0.distance), toY(p0.elevation));
ctx.lineTo(toX(p1.distance), toY(p1.elevation));
@ -369,15 +339,12 @@ export function ElevationChart({ yjs, onHover, days }: ElevationChartProps) {
if (colorMode === "highway" && highways[highlightIdx]) {
label += ` · ${highways[highlightIdx]}`;
}
if (colorMode === "maxspeed" && maxspeeds[highlightIdx]) {
label += ` · ${maxspeeds[highlightIdx]} km/h`;
}
const labelX = hx + 8 > w - 80 ? hx - 8 : hx + 8;
ctx.textAlign = hx + 8 > w - 80 ? "right" : "left";
ctx.fillText(label, labelX, PADDING.top + 10);
}
},
[points, colorMode, surfaces, highways, maxspeeds, days, t],
[points, colorMode, surfaces, highways, days, t],
);
useEffect(() => {
@ -436,7 +403,7 @@ export function ElevationChart({ yjs, onHover, days }: ElevationChartProps) {
<div className="border-t border-gray-200 px-2 py-2">
<div className="mb-1 flex items-center gap-2 px-2">
<p className="shrink-0 text-xs font-medium text-gray-500">
{colorMode === "grade" ? t("elevation.grade") : colorMode === "highway" ? t("elevation.highway") : colorMode === "maxspeed" ? t("elevation.maxspeed") : t("elevation.profile")}
{colorMode === "grade" ? t("elevation.grade") : colorMode === "highway" ? t("elevation.highway") : t("elevation.profile")}
</p>
<div className="flex flex-1 items-center justify-center gap-1.5 text-[10px] text-gray-400">
{colorMode === "grade" && (<>
@ -469,13 +436,6 @@ export function ElevationChart({ yjs, onHover, days }: ElevationChartProps) {
))}
{[...new Set(highways)].length > 6 && <span>+{[...new Set(highways)].length - 6}</span>}
</>)}
{colorMode === "maxspeed" && (<>
<span className="flex items-center gap-0.5"><span className="inline-block h-1.5 w-2.5 rounded-sm" style={{ background: "#22c55e" }} />30</span>
<span className="flex items-center gap-0.5"><span className="inline-block h-1.5 w-2.5 rounded-sm" style={{ background: "#eab308" }} />50</span>
<span className="flex items-center gap-0.5"><span className="inline-block h-1.5 w-2.5 rounded-sm" style={{ background: "#f97316" }} />70</span>
<span className="flex items-center gap-0.5"><span className="inline-block h-1.5 w-2.5 rounded-sm" style={{ background: "#ef4444" }} />100</span>
<span className="flex items-center gap-0.5"><span className="inline-block h-1.5 w-2.5 rounded-sm" style={{ background: "#991b1b" }} />100+</span>
</>)}
</div>
<select
value={colorMode}
@ -487,7 +447,6 @@ export function ElevationChart({ yjs, onHover, days }: ElevationChartProps) {
<option value="surface">{t("colorMode.surface")}</option>
<option value="grade">{t("colorMode.grade")}</option>
<option value="highway">{t("colorMode.highway")}</option>
<option value="maxspeed">{t("colorMode.maxspeed")}</option>
</select>
</div>
<canvas

View file

@ -376,7 +376,6 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, highlighted
const [segmentBoundaries, setSegmentBoundaries] = useState<number[]>([]);
const [surfaces, setSurfaces] = useState<string[]>([]);
const [highways, setHighways] = useState<string[]>([]);
const [maxspeeds, setMaxspeeds] = useState<string[]>([]);
const [colorMode, setColorMode] = useState<ColorMode>("plain");
const [noGoDrawing, setNoGoDrawing] = useState(false);
const toggleNoGoDraw = useCallback(() => setNoGoDrawing((v) => !v), []);
@ -451,13 +450,6 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, highlighted
setHighways([]);
}
const maxspeedsJson = yjs.routeData.get("maxspeeds") as string | undefined;
if (maxspeedsJson) {
try { setMaxspeeds(JSON.parse(maxspeedsJson)); } catch { setMaxspeeds([]); }
} else {
setMaxspeeds([]);
}
if (modeVal) setColorMode(modeVal);
};
@ -739,7 +731,6 @@ export function PlannerMap({ yjs, onRouteRequest, highlightPosition, highlighted
colorMode={colorMode}
surfaces={surfaces}
highways={highways}
maxspeeds={maxspeeds}
/>
<RouteInteraction
coordinates={routeCoordinates}

View file

@ -17,7 +17,6 @@ export interface EnrichedRoute {
segmentBoundaries: number[]; // coordinate index where each waypoint segment starts
surfaces: string[]; // surface type per coordinate point (e.g. "asphalt", "gravel")
highways: string[]; // highway classification per coordinate point (e.g. "cycleway", "residential")
maxspeeds: string[]; // speed limit per coordinate point (e.g. "30", "50", "none")
totalLength: number;
totalAscend: number;
totalTime: number;
@ -97,7 +96,6 @@ export function mergeGeoJsonSegments(segments: Record<string, unknown>[]): Enric
const allCoords: [number, number, number][] = [];
const allSurfaces: string[] = [];
const allHighways: string[] = [];
const allMaxspeeds: string[] = [];
const segmentBoundaries: number[] = [];
let totalLength = 0;
let totalAscend = 0;
@ -122,7 +120,6 @@ export function mergeGeoJsonSegments(segments: Record<string, unknown>[]): Enric
allCoords.push([c[0]!, c[1]!, c[2] ?? 0]);
allSurfaces.push(wayTagData.surfaces.get(j) ?? wayTagData.surfaces.get(j - 1) ?? "unknown");
allHighways.push(wayTagData.highways.get(j) ?? wayTagData.highways.get(j - 1) ?? "unknown");
allMaxspeeds.push(wayTagData.maxspeeds.get(j) ?? wayTagData.maxspeeds.get(j - 1) ?? "unknown");
}
// Accumulate stats
@ -156,7 +153,6 @@ export function mergeGeoJsonSegments(segments: Record<string, unknown>[]): Enric
segmentBoundaries,
surfaces: allSurfaces,
highways: allHighways,
maxspeeds: allMaxspeeds,
totalLength,
totalAscend,
totalTime,
@ -167,7 +163,6 @@ export function mergeGeoJsonSegments(segments: Record<string, unknown>[]): Enric
interface WayTagData {
surfaces: Map<number, string>;
highways: Map<number, string>;
maxspeeds: Map<number, string>;
}
/**
@ -178,13 +173,12 @@ interface WayTagData {
function extractWayTagData(properties: Record<string, unknown>): WayTagData {
const surfaces = new Map<number, string>();
const highways = new Map<number, string>();
const maxspeeds = new Map<number, string>();
const messages = properties.messages as string[][] | undefined;
if (!messages || messages.length < 2) return { surfaces, highways, maxspeeds };
if (!messages || messages.length < 2) return { surfaces, highways };
const headers = messages[0]!;
const wayTagsIdx = headers.indexOf("WayTags");
if (wayTagsIdx === -1) return { surfaces, highways, maxspeeds };
if (wayTagsIdx === -1) return { surfaces, highways };
for (let i = 1; i < messages.length; i++) {
const row = messages[i]!;
@ -193,17 +187,8 @@ function extractWayTagData(properties: Record<string, unknown>): WayTagData {
surfaces.set(i - 1, surfaceMatch ? surfaceMatch[1]! : "unknown");
const highwayMatch = tags.match(/highway=(\S+)/);
highways.set(i - 1, highwayMatch ? highwayMatch[1]! : "unknown");
// Handle maxspeed variants: maxspeed:forward, maxspeed:backward, maxspeed
const hasReverse = tags.includes("reversedirection=yes");
const forwardMatch = tags.match(/maxspeed:forward=(\S+)/);
const backwardMatch = tags.match(/maxspeed:backward=(\S+)/);
const plainMatch = tags.match(/(?<![:\w])maxspeed=(\S+)/);
const speed = hasReverse
? (backwardMatch?.[1] ?? plainMatch?.[1] ?? "unknown")
: (forwardMatch?.[1] ?? plainMatch?.[1] ?? "unknown");
maxspeeds.set(i - 1, speed);
}
return { surfaces, highways, maxspeeds };
return { surfaces, highways };
}
/**

View file

@ -129,9 +129,6 @@ export function useRouting(yjs: YjsState | null) {
if (enriched.highways?.length) {
yjs.routeData.set("highways", JSON.stringify(enriched.highways));
}
if (enriched.maxspeeds?.length) {
yjs.routeData.set("maxspeeds", JSON.stringify(enriched.maxspeeds));
}
});
} catch {
setRouteError("failed");

View file

@ -40,10 +40,6 @@ const MOCK_ROUTE_3WP = {
"residential", "residential", "cycleway", "cycleway", "path",
"path", "tertiary", "tertiary", "residential", "residential",
],
maxspeeds: [
"30", "30", "unknown", "unknown", "unknown",
"unknown", "50", "50", "30", "30",
],
};
/**
@ -80,9 +76,6 @@ const MOCK_ROUTE_2WP = {
highways: [
"secondary", "secondary", "cycleway", "cycleway", "residential", "residential",
],
maxspeeds: [
"50", "50", "unknown", "unknown", "30", "30",
],
};
/**

View file

@ -103,7 +103,6 @@ export default {
surface: "Untergrund",
grade: "Steigung",
highway: "Straßentyp",
maxspeed: "Tempolimit",
surfaceLegend: "Farbe nach Straßenbelag",
surfaceUnavailable: "Untergrunddaten für dieses Profil nicht verfügbar",
},
@ -114,7 +113,6 @@ export default {
profile: "Höhenprofil",
grade: "Steigungsprofil",
highway: "Straßentypenprofil",
maxspeed: "Tempolimitprofil",
low: "Tief",
high: "Hoch",
},

View file

@ -103,7 +103,6 @@ export default {
surface: "Surface",
grade: "Grade",
highway: "Road Type",
maxspeed: "Speed Limit",
surfaceLegend: "Color by road surface type",
surfaceUnavailable: "Surface data not available for this profile",
},
@ -114,7 +113,6 @@ export default {
profile: "Elevation Profile",
grade: "Grade Profile",
highway: "Road Type Profile",
maxspeed: "Speed Limit Profile",
low: "Low",
high: "High",
},