Fix onlineboard empty results and flights-map polyline zoom hazard

Online board: the /board endpoint treats dateFrom/dateTo as a half-open
interval, so sending the same date for both yielded zero rows on routes
that obviously have flights (e.g. SVO-LED). Mirror Angular's
OnlineBoardApiService.getFlightsByRoute and use dateTo = date + 1 day.

Flights map: two stacked problems made arcs disappear on zoom.
 - syncPolylines gated endpoints on map.hasLayer(marker); when
   syncVisibility removed a zoom-tier layer, its arc went with it.
 - The zoomend and toggle effects both called syncPolylines, which
   captured a stale closure from the first render (polylines = []) and
   wiped the layer. Polyline coords are geographic — Leaflet rescales
   them on zoom — so the rebuild was never necessary.
Arcs now render once per polylines prop change and stay put through
zoom and filter toggles.
This commit is contained in:
2026-04-17 22:48:18 +03:00
parent 18ab969e1c
commit 79fcf2bdc1
3 changed files with 41 additions and 15 deletions
@@ -452,7 +452,10 @@ describe("MapCanvas — polylines (C.3)", () => {
expect(createdPolylines.length).toBe(0);
});
it("skips intermediate cities whose marker is not on the map", () => {
it("draws multi-hop polylines through every city in the index", () => {
// Route/domestic filtering happens upstream in filterRoutes — by the time
// a polyline reaches MapCanvas, every city on its path is expected to be
// drawn, independent of zoom-tier visibility.
render(
<MapCanvas
markers={[cm("A"), cm("B", "other"), cm("C")]}
@@ -461,11 +464,6 @@ describe("MapCanvas — polylines (C.3)", () => {
tileUrl="t"
/>,
);
const map = createdMaps[0]!;
// Reset so we count only polylines drawn by the next sync cycle.
createdPolylines.length = 0;
map.setZoom(6);
map.fireZoomend();
expect(createdPolylines.length).toBe(1);
});
@@ -487,7 +485,7 @@ describe("MapCanvas — polylines (C.3)", () => {
expect(opts?.dashArray).toBe("4 14");
});
it("rebuilds polylines on zoomend", () => {
it("does not rebuild polylines on zoomend (they are zoom-independent)", () => {
render(
<MapCanvas
markers={[cm("A"), cm("B")]}
@@ -496,14 +494,17 @@ describe("MapCanvas — polylines (C.3)", () => {
/>,
);
const map = createdMaps[0]!;
map.setZoom(6);
map.fireZoomend();
const before = createdPolylines.length;
const afterInitialRender = createdPolylines.length;
map.setZoom(4);
map.fireZoomend();
map.setZoom(6);
map.fireZoomend();
expect(createdPolylines.length).toBeGreaterThan(before);
// Polyline coords are fixed by city lat/lng — Leaflet's SVG pane rescales
// them on zoom without us recomputing. Rebuilding from the zoomend
// handler also carried a stale-closure hazard.
expect(createdPolylines.length).toBe(afterInitialRender);
});
it("silently skips polylines with unknown city codes", () => {
@@ -324,10 +324,11 @@ export const MapCanvas: FC<MapCanvasProps> = ({
}
zoomLayersRef.current = zoomLayers;
// Rerun visibility + tooltip rules on every zoom change.
// Rerun visibility + tooltip rules on every zoom change. Polylines are
// zoom-independent (Leaflet rescales the SVG automatically) so no rebuild
// is needed here — rebuilding from a stale closure in fact wiped them.
map.on("zoomend", () => {
syncVisibility();
syncPolylines();
syncTooltips();
});
@@ -441,7 +442,6 @@ export const MapCanvas: FC<MapCanvasProps> = ({
// --- Re-sync visibility + tooltips when toggles change ---
useEffect(() => {
syncVisibility();
syncPolylines();
syncTooltips();
}, [domestic, international]);
@@ -61,6 +61,30 @@ function formatDateForApi(yyyymmdd: string): string {
return yyyymmdd.includes("T") ? yyyymmdd : `${yyyymmdd}T00:00:00`;
}
/**
* Return the yyyyMMdd date one day after `yyyymmdd`. The API treats the
* board range as a half-open interval: `dateFrom=D, dateTo=D` yields zero
* rows. Matching Angular's OnlineBoardApiService.getFlightsByRoute.
*/
function addOneDayYyyymmdd(yyyymmdd: string): string {
const iso = yyyymmdd.includes("T") ? yyyymmdd.split("T")[0]! : yyyymmdd;
let year: number, month: number, day: number;
if (iso.includes("-")) {
const [y, m, d] = iso.split("-");
year = Number(y); month = Number(m) - 1; day = Number(d);
} else {
year = Number(iso.slice(0, 4));
month = Number(iso.slice(4, 6)) - 1;
day = Number(iso.slice(6, 8));
}
const dt = new Date(year, month, day);
dt.setDate(dt.getDate() + 1);
const y = dt.getFullYear().toString();
const m = (dt.getMonth() + 1).toString().padStart(2, "0");
const d = dt.getDate().toString().padStart(2, "0");
return `${y}${m}${d}`;
}
/**
* Convert parsed online board URL params into API search params.
* The API expects dateFrom/dateTo in yyyy-MM-ddT00:00:00 format.
@@ -69,9 +93,10 @@ function toSearchParams(
params: OnlineBoardSearchPageProps["params"],
): SearchFlightsParams {
const apiDate = formatDateForApi(params.date);
const apiDateTo = formatDateForApi(addOneDayYyyymmdd(params.date));
const base: SearchFlightsParams = {
dateFrom: apiDate,
dateTo: apiDate,
dateTo: apiDateTo,
};
switch (params.type) {