feat: refine supply map layers and workbench branding

This commit is contained in:
2026-10-27 17:56:38 +08:00
parent 6983ba03f1
commit 4f0f34f460
23 changed files with 948 additions and 171 deletions
+199
View File
@@ -0,0 +1,199 @@
import { expect, test } from "@playwright/test";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
test("keeps pipes and SCADA visible across fractional zoom changes", async ({ page }, testInfo) => {
await mockWorkbenchContext(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect.poll(() => page.evaluate(() => Boolean(
window.__waterNetworkMap?.getLayer("pipes-flow")
&& window.__waterNetworkMap?.getLayer("scada-pressure-symbol")
)), { timeout: 15_000 }).toBe(true);
await page.evaluate(() => {
const map = window.__waterNetworkMap!;
map.addSource("scale-fixture", {
type: "geojson",
data: {
type: "FeatureCollection",
features: [
...[800, 400, 100, null].map((diameter, index) => ({
type: "Feature" as const,
id: `pipe-${index}`,
properties: { diameter, fixture_id: `pipe-${index}` },
geometry: {
type: "LineString" as const,
coordinates: [[121.46, 30.91 + index * 0.001], [121.47, 30.91 + index * 0.001]]
}
})),
...["pipe_flow", "pressure"].map((deviceType, index) => ({
type: "Feature" as const,
id: deviceType,
properties: { device_type: deviceType, fixture_id: deviceType },
geometry: {
type: "Point" as const,
coordinates: [121.462 + index * 0.008, 30.915]
}
}))
]
}
});
for (const id of ["pipes-casing", "pipes-flow", "pipes-hit", "scada-flow-symbol", "scada-pressure-symbol"]) {
const original = map.getStyle().layers.find((layer) => layer.id === id)!;
const copy = { ...original };
if ("source-layer" in copy) delete copy["source-layer"];
map.addLayer({
...copy,
id: `scale-fixture-${id}`,
source: "scale-fixture",
filter: ["all", ["==", ["geometry-type"], original.type === "line" ? "LineString" : "Point"],
"filter" in original ? original.filter ?? true : true]
});
}
map.jumpTo({ center: [121.465, 30.913], zoom: 11.7, pitch: 0, bearing: 0 });
});
const pipeIds = ["pipe-0", "pipe-1", "pipe-2", "pipe-3"];
for (const [zoom, scadaIds] of [
[11.7, ["pipe_flow", "pressure"]],
[12, ["pipe_flow", "pressure"]],
[13, ["pipe_flow", "pressure"]],
[14, ["pipe_flow", "pressure"]],
[16, ["pipe_flow", "pressure"]],
[11.7, ["pipe_flow", "pressure"]]
] as const) {
await page.evaluate((value) => window.__waterNetworkMap!.jumpTo({ zoom: value }), zoom);
await expect.poll(() => page.evaluate(() => {
const map = window.__waterNetworkMap!;
const ids = (layers: string[]) => [...new Set(
map.queryRenderedFeatures({ layers }).map((feature) => String(feature.properties.fixture_id))
)].sort();
return {
pipes: ids(["scale-fixture-pipes-flow"]),
hits: ids(["scale-fixture-pipes-hit"]),
scada: ids(["scale-fixture-scada-flow-symbol", "scale-fixture-scada-pressure-symbol"])
};
})).toEqual({ pipes: [...pipeIds], hits: [...pipeIds], scada: [...scadaIds] });
await page.screenshot({ path: testInfo.outputPath(`zoom-${zoom}.png`) });
}
});
for (const width of [1440, 390]) {
test(`declutters dense facilities and preserves selection at width ${width}`, async ({ page }, testInfo) => {
await page.setViewportSize({ width, height: 900 });
await mockWorkbenchContext(page);
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect.poll(() => page.evaluate(() => Boolean(
window.__waterNetworkMap?.getLayer("pipes-flow")
)), { timeout: 15_000 }).toBe(true);
await page.evaluate(async () => {
const layerModuleUrl = "/src/features/workbench/map/layers.ts";
const { waterNetworkLayers } = await import(layerModuleUrl);
const scadaModuleUrl = "/src/features/workbench/map/scada.ts";
const { scadaLayers } = await import(scadaModuleUrl);
const layers = [...waterNetworkLayers, ...scadaLayers];
const map = window.__waterNetworkMap!;
map.addSource("density-fixture", {
type: "geojson",
promoteId: "fixture_id",
data: {
type: "FeatureCollection",
features: [
...Array.from({ length: 70 }, (_, index) => ({
type: "Feature" as const,
properties: { fixture_id: `valve-${index}`, kind: "valve" },
geometry: {
type: "Point" as const,
coordinates: [121.4635 + index % 10 * 0.00035, 30.912 + Math.floor(index / 10) * 0.00035]
}
})),
{
type: "Feature" as const,
properties: { fixture_id: "reservoir", kind: "reservoir" },
geometry: { type: "Point" as const, coordinates: [121.465, 30.913] }
},
...[
["pump", [121.464, 30.9114]],
["tank", [121.466, 30.9114]],
["water-quality", [121.465, 30.9098]],
["pressure", [121.505, 30.913]],
["junction", [121.4648, 30.9138]],
["gis-detail", [121.466, 30.9138]],
["gis-flow", [121.467, 30.9138]]
].map(([kind, coordinates]) => ({
type: "Feature" as const,
properties: { fixture_id: kind, kind },
geometry: { type: "Point" as const, coordinates }
}))
]
}
});
const kinds: Record<string, string> = {
"reservoirs-symbol": "reservoir", "pumps-symbol": "pump", "tanks-symbol": "tank",
"water_quality_monitoring_points-symbol": "water-quality",
"water_quality_monitoring_points-hit": "water-quality",
"scada-pressure-symbol": "pressure",
junctions: "junction", "junctions-hit": "junction",
"fx_20200423_hydrants-point": "gis-detail", "fx_20200423_hydrants-hit": "gis-detail",
"fx_20200423_flow_meters-point": "gis-flow", "fx_20200423_flow_meters-hit": "gis-flow"
};
for (const id of ["valves-casing", "valves", "valves-symbol", ...Object.keys(kinds),
"valves-selected-outer", "valves-selected", "valves-hit"]) {
const original = layers.find((layer: { id: string }) => layer.id === id);
const copy = { ...original };
delete copy["source-layer"];
map.addLayer({
...copy, id: `density-${id}`, source: "density-fixture",
filter: ["==", ["get", "kind"], kinds[id] ?? "valve"]
});
}
map.jumpTo({ center: [121.465, 30.913], zoom: 13.1, pitch: 0, bearing: 0 });
});
const renderedCounts = () => page.evaluate(() => {
const map = window.__waterNetworkMap!;
const count = (id: string) => new Set(
map.queryRenderedFeatures({ layers: [id] }).map((feature) => feature.properties.fixture_id)
).size;
return { dots: count("density-valves"), symbols: count("density-valves-symbol"), hits: count("density-valves-hit"),
reservoir: count("density-reservoirs-symbol"), pump: count("density-pumps-symbol"),
tank: count("density-tanks-symbol"), waterQuality: count("density-water_quality_monitoring_points-symbol"),
waterQualityHit: count("density-water_quality_monitoring_points-hit"), pressure: count("density-scada-pressure-symbol"),
nodes: count("density-junctions"), nodeHits: count("density-junctions-hit"),
gisDetails: count("density-fx_20200423_hydrants-point"), gisDetailHits: count("density-fx_20200423_hydrants-hit"),
gisFlow: count("density-fx_20200423_flow_meters-point"), gisFlowHits: count("density-fx_20200423_flow_meters-hit") };
});
await expect.poll(renderedCounts).toMatchObject({ dots: 70, reservoir: 1, pump: 1, tank: 1, waterQuality: 1 });
const overview = await renderedCounts();
expect(overview.symbols).toBeLessThan(overview.dots / 3);
await page.screenshot({ path: testInfo.outputPath("dense-overview.png") });
await page.evaluate(() => window.__waterNetworkMap!.jumpTo({ zoom: 10.99 }));
await expect.poll(renderedCounts).toMatchObject({ waterQuality: 0, waterQualityHit: 0, pressure: 0, gisFlow: 0, gisFlowHits: 0 });
await page.evaluate(() => window.__waterNetworkMap!.jumpTo({ zoom: 11 }));
await expect.poll(renderedCounts).toMatchObject({ waterQuality: 1, waterQualityHit: 1, pressure: 1, gisFlow: 1, gisFlowHits: 1 });
await page.evaluate(() => window.__waterNetworkMap!.jumpTo({ zoom: 11.99 }));
await expect.poll(renderedCounts).toMatchObject({ nodes: 0, nodeHits: 0, gisDetails: 0, gisDetailHits: 0 });
await page.evaluate(() => window.__waterNetworkMap!.jumpTo({ zoom: 12 }));
await expect.poll(renderedCounts).toMatchObject({ nodes: 1, nodeHits: 1, gisDetails: 1, gisDetailHits: 1 });
await page.evaluate(() => window.__waterNetworkMap!.jumpTo({ zoom: 12.9 }));
await expect.poll(renderedCounts).toMatchObject({ dots: 0, symbols: 0, hits: 0, waterQuality: 1 });
await page.evaluate(() => window.__waterNetworkMap!.jumpTo({ zoom: 13 }));
await expect.poll(renderedCounts).toMatchObject({ dots: 70, hits: 70 });
await page.evaluate(() => window.__waterNetworkMap!.jumpTo({ zoom: 15.5 }));
await expect.poll(async () => (await renderedCounts()).symbols).toBeGreaterThan(overview.symbols);
await page.screenshot({ path: testInfo.outputPath("dense-detail.png") });
await page.evaluate(() => {
const map = window.__waterNetworkMap!;
map.setFeatureState({ source: "density-fixture", id: "valve-35" }, { selected: true });
map.jumpTo({ zoom: 13.1 });
});
await expect.poll(() => page.evaluate(() => window.__waterNetworkMap!
.getFeatureState({ source: "density-fixture", id: "valve-35" }).selected)).toBe(true);
await page.screenshot({ path: testInfo.outputPath("dense-selected.png") });
});
}