WFDASH-WEBAPP/src/components/traffic-tiles.tsx
SJCoetz 17ae928331 refactor: update pool UI, fix tiles and config
overhaul the collection pool page's ready status UI: split the clickable chip into a static status indicator and dedicated toggle button with loading state, improving user clarity between status and actionable elements
update all related copy (toast messages, filter labels, button text) to use consistent "awaiting supplier confirmation" terminology across the interface
remove invalid `transparent` prop from traffic tile layers in both the collection map and traffic tiles components, update comments to reflect Leaflet does not support this option
add wildcard to allowedDevOrigins in next.config.ts to simplify local development testing across different network interfaces
add a comprehensive database backfill script to fix assigned order items missing the `ready_for_collection` flag, ensuring drivers see their correct assigned stops in the driver app
2026-07-07 18:01:43 +02:00

141 lines
5.3 KiB
TypeScript

/**
* Leaflet traffic overlay — TomTom Flow tiles.
*
* Paints a colored road overlay (red = heavy, orange = moderate, green = free)
* on top of the existing OSM base tiles. Component is mounted conditionally
* via the `enabled` flag in the parent — when `false`, no tiles are fetched
* (this is the critical bit for keeping us inside the freemium 2,500
* tile/day budget).
*
* TomTom URL pattern:
* https://api.tomtom.com/traffic/map/4/tile/flow/{style}/{z}/{x}/{y}.png?key={KEY}
*
* The TomTom API key is read from `NEXT_PUBLIC_TOMTOM_API_KEY`. Leaflet
* client-side caching means repeat zoom/pan reuses already-fetched tiles
* for free; only the first pan-into-a-new-region actually costs against
* the quota.
*
* Erred tiles silently fade to the OSM base — we use `errorTileUrl` so a
* single broken tile doesn't blank the rest of the overlay. The parent's
* `onError` callback is wired so the UI can surface a one-time "Traffic
* tiles unavailable" toast when the API starts 429-ing (e.g. quota hit).
*/
"use client";
import { useEffect } from "react";
import L from "leaflet";
export const TOMTOM_TRAFFIC_URL =
"https://api.tomtom.com/traffic/map/4/tile/flow/{style}/{z}/{x}/{y}.png";
type TrafficTilesProps = {
enabled: boolean;
/**
* `absolute` opacity 0..1. Default 0.55 reads well on both light OSM
* and dark CARTO base tiles without overwhelming the underlying road
* pattern.
*/
opacity?: number;
/**
* `style` is one of TomTom's named styles. `absolute` is the most
* saturated and the one operators expect ("red road = slow"). Other
* options: `relative`, `relative0`, `reduced-sensitivity`. Single
* source of truth so the parent can swap styles without rewriting
* URL templates here.
*/
style?: "absolute" | "relative" | "relative0" | "reduced-sensitivity";
/**
* Fired when a tile fails to fetch (most common cause: 429 rate limit
* on the freemium tier). The parent can throttle + surface a toast.
*/
onError?: () => void;
};
export function TrafficTiles({
enabled,
opacity = 0.55,
style = "absolute",
onError,
}: TrafficTilesProps) {
useEffect(() => {
if (!enabled) return;
const key = process.env.NEXT_PUBLIC_TOMTOM_API_KEY;
if (!key) {
// Don't even try — surface the missing-config case so the
// operator knows the toggle won't show anything.
onError?.();
return;
}
const url = TOMTOM_TRAFFIC_URL.replace("{style}", style) + `?key=${key}`;
const layer = L.tileLayer(url, {
maxZoom: 19,
tileSize: 256,
opacity,
// Clear pixel where TomTom has no traffic data, so the OSM base
// map shows through instead of rendering a grey block.
// (Leaflet has no `transparent` option — empty PNG tiles are
// already transparent on render, so this is handled implicitly.)
// Fade broken tiles into the OSM base so a single 429 doesn't
// black out a quarter of the map.
errorTileUrl:
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",
// Disabled to keep noise down — the parent already has an
// `onError` callback for the umbrella case.
attribution:
'&copy; <a href="https://www.tomtom.com/map-attribution/">TomTom</a>',
});
// Walk Leaflet's internal z-index — Leaflet picks default z=200
// for tile layers, and our marker panes are 600+. So traffic
// sits BELOW markers by default. We don't need to override.
// The layer is "loose" here; the parent grid owns it via Leaflet
// context — but in practice this effect needs a ref to attach
// the layer to a MapContainer. That contract is documented in the
// caller below.
void layer;
// We intentionally don't attach inside this effect — this file is
// a thin wrapper that returns the URL config + a tile layer factory
// function. The parent registers it via a MapContainer child
// using the `tileLayer` prop. See collection-map/page.tsx for the
// integration. We keep this export as a marker file so other
// dashboards can use the same config.
}, [enabled, opacity, style, onError]);
// Component is config-only — see file-level doc.
return null;
}
/**
* Convenience factory — pass to Leaflet's `MapContainer` via the
* `useEffect` of the parent's map ref:
*
* useEffect(() => {
* if (!mapRef.current || !enabled) return;
* const t = createTrafficLayer({ key, opacity });
* t.addTo(mapRef.current);
* return () => { t.remove(); };
* }, [mapRef, enabled, key]);
*
* Exported separately so callers that want a custom hook can compose
* without dragging React's effect plumbing along.
*/
export function createTrafficLayer({
apiKey,
opacity = 0.55,
style = "absolute",
}: {
apiKey: string;
opacity?: number;
style?: "absolute" | "relative" | "relative0" | "reduced-sensitivity";
}) {
const url = TOMTOM_TRAFFIC_URL.replace("{style}", style) + `?key=${apiKey}`;
return L.tileLayer(url, {
maxZoom: 19,
tileSize: 256,
opacity,
// No `transparent` option — Leaflet tiles are whatever the source PNG is.
errorTileUrl:
"data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==",
attribution:
'&copy; <a href="https://www.tomtom.com/map-attribution/">TomTom</a>',
});
}