Give each map leg its own colour

Legs cycle through the shared 8-hue palette by index: polylines (air
stays dashed), km-label accents, and the Legs-list number badges all
match, so multiple car routes are distinguishable. Badges use dark text
after a contrast check ruled out white on these hues.
This commit is contained in:
2026-07-20 11:03:11 +07:00
parent 4cedab0cf6
commit 9369c82e56
2 changed files with 29 additions and 14 deletions
+1 -1
View File
@@ -283,7 +283,7 @@ textarea.input { resize: vertical; }
.map-pin-wrap { background: none; border: none; }
.map-pin { display: inline-flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%; color: #fff; font-weight: 700; font-size: 0.8rem; box-shadow: 0 2px 6px rgba(0,0,0,0.35); border: 2px solid #fff; }
.km-label { background: rgba(37, 99, 235, 0.92); color: #fff; font-size: 0.7rem; font-weight: 700; padding: 0.1rem 0.4rem; border-radius: 999px; white-space: nowrap; box-shadow: 0 1px 3px rgba(0,0,0,0.3); }
.km-label { background: rgba(37, 99, 235, 0.92); color: #fff; font-size: 0.7rem; font-weight: 700; padding: 0.1rem 0.4rem 0.1rem 0.55rem; border-radius: 999px; white-space: nowrap; box-shadow: 0 1px 3px rgba(0,0,0,0.3); border-left: 4px solid var(--leg-accent, transparent); }
.map-popup strong { font-size: 0.95rem; }
.map-popup-sub { color: #64748b; font-size: 0.8rem; margin-top: 0.15rem; }
.map-popup-loc { font-size: 0.82rem; margin-top: 0.15rem; }
+28 -13
View File
@@ -4,6 +4,14 @@
import { el } from '../dom.js';
import { typeInfo, entryIcon } from '../format.js';
import { api } from '../api.js';
import { STAY_PALETTE } from './stayBands.js';
// Each route leg gets its own colour from the same palette used for stay
// bands/area dots, so several ground legs (e.g. multiple car routes) read
// apart instead of blending into one generic "ground" colour.
function legColor(i) {
return STAY_PALETTE[((i % STAY_PALETTE.length) + STAY_PALETTE.length) % STAY_PALETTE.length];
}
export function renderMap(tctx) {
const route = tctx.route || { stops: [], legs: [], totalKm: 0 };
@@ -91,20 +99,22 @@ function initMap(mapDiv, stops, legs, kmEls) {
.bindPopup(popupHtml(stop, info, entryIcon(stop)));
});
// One polyline per measured leg, styled by mode (air = dashed blue, ground =
// solid teal), each with a km label at its midpoint (straight great-circle
// line to start — the map is never empty while road geometry loads).
const legLayers = legs.map(({ leg, a, b }) => {
// One polyline per measured leg, each in its own colour from the shared
// palette (air legs keep the dashed pattern), each with a km label at its
// midpoint (straight great-circle line to start — the map is never empty
// while road geometry loads).
const legLayers = legs.map(({ leg, a, b }, i) => {
const air = leg.mode === 'air';
const color = legColor(i);
const line = L.polyline([[a.lat, a.lng], [b.lat, b.lng]], {
color: air ? '#2563eb' : '#0f766e',
color,
weight: 3,
opacity: 0.75,
dashArray: air ? '6 6' : null,
}).addTo(map);
const mid = [(a.lat + b.lat) / 2, (a.lng + b.lng) / 2];
const label = L.marker(mid, { icon: kmLabel(L, leg.km), interactive: false }).addTo(map);
return { leg, a, b, line, label };
const label = L.marker(mid, { icon: kmLabel(L, leg.km, false, color), interactive: false }).addTo(map);
return { leg, a, b, line, label, color };
});
if (points.length === 1) {
@@ -122,17 +132,18 @@ function initMap(mapDiv, stops, legs, kmEls) {
// already drawn, so a slow/failed fetch just leaves those in place — no
// error UI. Each leg fetches (and fails) independently.
function fetchRoadGeometry(L, map, mapDiv, legLayers, kmEls) {
legLayers.forEach(({ leg, a, b, line, label }, i) => {
legLayers.forEach(({ leg, a, b, line, label, color }, i) => {
if (leg.mode !== 'ground') return;
api.directions({ lat: a.lat, lng: a.lng }, { lat: b.lat, lng: b.lng })
.then((res) => {
// The trip view may have been re-rendered (refreshTrip) while this
// was in flight — the old map/section is detached from the DOM and
// its layers must not be touched.
// its layers must not be touched. Only latlngs/label are swapped —
// the polyline keeps its per-leg colour/style from creation.
if (!mapDiv.isConnected || !map.hasLayer(line) || !map.hasLayer(label)) return;
line.setLatLngs(res.geometry);
label.setLatLng(pathMidpoint(res.geometry) || label.getLatLng());
label.setIcon(kmLabel(L, res.km, true));
label.setIcon(kmLabel(L, res.km, true, color));
const kmEl = kmEls[i];
if (kmEl) kmEl.textContent = `${fmtKm(res.km)} km · road`;
})
@@ -166,10 +177,11 @@ function numberedIcon(L, n, color) {
});
}
function kmLabel(L, km, road) {
function kmLabel(L, km, road, color) {
const accent = color ? ` style="--leg-accent:${color}"` : '';
return L.divIcon({
className: 'km-label-wrap',
html: `<span class="km-label">${fmtKm(km)} km${road ? ' · road' : ''}</span>`,
html: `<span class="km-label"${accent}>${fmtKm(km)} km${road ? ' · road' : ''}</span>`,
iconSize: [0, 0],
});
}
@@ -217,7 +229,10 @@ function legList(legs) {
el(
'div',
{ class: 'leg-row' },
el('span', { class: 'leg-index' }, String(i + 1)),
// Dark text, not white: the palette's mid-tone hues (amber, lime, …)
// don't clear AA contrast with white at full strength — the same
// reason stay bands use dark text on these colours (see stayBands.js).
el('span', { class: 'leg-index', style: { background: legColor(i), color: '#334155' } }, String(i + 1)),
el('span', { class: 'leg-mode', title: air ? 'Flight' : 'Ground' }, air ? '✈️' : '🚗'),
el('span', { class: 'leg-path' }, stopLabel(a), ' → ', stopLabel(b)),
kmEl,