feat: refine supply map layers and workbench branding
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||
import { mockPipeVectorTiles } from "./support/mock-pipe-vector-tiles";
|
||||
|
||||
declare global {
|
||||
interface Window { __layerFadeSamples: number[]; __layerFadePixels: number[]; }
|
||||
}
|
||||
|
||||
async function openLayerFixture(page: Page, vectorPipes = false) {
|
||||
await mockWorkbenchContext(page);
|
||||
if (vectorPipes) await mockPipeVectorTiles(page);
|
||||
await page.route("https://workbench-map.invalid/geoserver/tjwater_next/ows?**", (route) => {
|
||||
const type = new URL(route.request().url()).searchParams.get("typeNames");
|
||||
return route.fulfill({ contentType: "application/xml",
|
||||
body: `<wfs:FeatureCollection xmlns:wfs="http://www.opengis.net/wfs/2.0" numberMatched="${type?.endsWith(":reservoirs") || type?.endsWith(":water_quality_monitoring_points") ? 1 : 0}"/>` });
|
||||
});
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await expect.poll(() => page.evaluate(() => Boolean(
|
||||
window.__waterNetworkMap?.getLayer("reservoirs-symbol")
|
||||
)), { timeout: 15_000 }).toBe(true);
|
||||
await page.evaluate((vectorPipes) => {
|
||||
const map = window.__waterNetworkMap!;
|
||||
for (const sourceId of ["pipes", "reservoirs", "water_quality_monitoring_points"]) {
|
||||
if (vectorPipes && sourceId === "pipes") continue;
|
||||
const layers = map.getStyle().layers.filter((layer) => "source" in layer && layer.source === sourceId);
|
||||
[...layers].reverse().forEach((layer) => map.removeLayer(layer.id));
|
||||
map.removeSource(sourceId);
|
||||
map.addSource(sourceId, { type: "geojson", data: { type: "FeatureCollection", features: [{
|
||||
type: "Feature", properties: { id: "fade-fixture", diameter: 800 },
|
||||
geometry: sourceId === "pipes"
|
||||
? { type: "LineString", coordinates: [[121.45, 30.91], [121.48, 30.91]] }
|
||||
: { type: "Point", coordinates: [sourceId === "reservoirs" ? 121.46 : 121.47, 30.915] }
|
||||
}] } });
|
||||
layers.forEach((layer) => {
|
||||
const copy = { ...layer };
|
||||
if ("source-layer" in copy) delete copy["source-layer"];
|
||||
map.addLayer(copy);
|
||||
});
|
||||
}
|
||||
const scada = map.getSource("scada");
|
||||
if (scada && "setData" in scada) scada.setData({ type: "FeatureCollection", features: [{
|
||||
type: "Feature", id: "pressure-size-fixture",
|
||||
properties: { device_id: "pressure-size-fixture", device_type: "pressure" },
|
||||
geometry: { type: "Point", coordinates: [121.475, 30.915] }
|
||||
}] });
|
||||
map.jumpTo({ center: [121.465, 30.913], zoom: 13, pitch: 0, bearing: 0 });
|
||||
}, vectorPipes);
|
||||
await page.getByRole("button", { name: /图层:管理地图图层/ }).click();
|
||||
}
|
||||
|
||||
for (const [label, layerId, property] of [
|
||||
["管线", "pipes-flow", "line-opacity"],
|
||||
["水库", "reservoirs-symbol", "icon-opacity"],
|
||||
["水质监测点", "water_quality_monitoring_points-symbol", "icon-opacity"]
|
||||
] as const) {
|
||||
test(`${label} fades on toggle and restores its original paint`, async ({ page }, testInfo) => {
|
||||
await openLayerFixture(page);
|
||||
const original = await page.evaluate(({ layerId, property }) => {
|
||||
const map = window.__waterNetworkMap!;
|
||||
const original = map.getPaintProperty(layerId, property);
|
||||
const setPaint = map.setPaintProperty.bind(map);
|
||||
window.__layerFadeSamples = [];
|
||||
map.setPaintProperty = (id, name, value, options) => {
|
||||
if (id === layerId && name === property) {
|
||||
if (typeof value === "number" && typeof original === "number") {
|
||||
window.__layerFadeSamples.push(value / original);
|
||||
}
|
||||
const output = Array.isArray(value) && value[0] === "interpolate" ? value[4] : value;
|
||||
if (Array.isArray(output) && output[0] === "*") {
|
||||
window.__layerFadeSamples.push(Number(output.at(-1)));
|
||||
}
|
||||
}
|
||||
return setPaint(id, name, value, options);
|
||||
};
|
||||
return original;
|
||||
}, { layerId, property });
|
||||
const toggle = page.getByRole("button", { name: new RegExp(`^${label},`) });
|
||||
await toggle.click();
|
||||
await expect.poll(() => page.evaluate((id) => window.__waterNetworkMap!
|
||||
.getLayoutProperty(id, "visibility"), layerId)).toBe("none");
|
||||
const hiddenSamples = await page.evaluate(() => window.__layerFadeSamples);
|
||||
expect(hiddenSamples.some((value) => value > 0 && value < 1)).toBe(true);
|
||||
expect(await page.evaluate(({ layerId, property }) => window.__waterNetworkMap!
|
||||
.getPaintProperty(layerId, property), { layerId, property })).toEqual(original);
|
||||
await toggle.click();
|
||||
await expect.poll(() => page.evaluate(({ layerId, property }) => window.__waterNetworkMap!
|
||||
.getPaintProperty(layerId, property), { layerId, property })).toEqual(original);
|
||||
await page.screenshot({ path: testInfo.outputPath("layer-visible.png") });
|
||||
await toggle.click();
|
||||
await toggle.click();
|
||||
await expect.poll(() => page.evaluate((id) => window.__waterNetworkMap!
|
||||
.getLayoutProperty(id, "visibility"), layerId)).toBe("visible");
|
||||
await expect.poll(() => page.evaluate(({ layerId, property }) => window.__waterNetworkMap!
|
||||
.getPaintProperty(layerId, property), { layerId, property })).toEqual(original);
|
||||
});
|
||||
}
|
||||
|
||||
test("reduced motion switches layers without a fade", async ({ page }) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await openLayerFixture(page);
|
||||
await page.getByRole("button", { name: /^管线,/ }).click();
|
||||
expect(await page.evaluate(() => window.__waterNetworkMap!
|
||||
.getLayoutProperty("pipes-flow", "visibility"))).toBe("none");
|
||||
});
|
||||
|
||||
for (const [label, layerId, property, longitude, latitude] of [
|
||||
["管线", "pipes-flow", "line-opacity", 121.465, 30.91],
|
||||
["水库", "reservoirs-symbol", "icon-opacity", 121.46, 30.915],
|
||||
["水质监测点", "water_quality_monitoring_points-symbol", "icon-opacity", 121.47, 30.915]
|
||||
] as const) {
|
||||
test(`${label} has intermediate rendered pixels while fading in and out`, async ({ page }, testInfo) => {
|
||||
await openLayerFixture(page, label === "管线");
|
||||
await expect.poll(() => page.evaluate((id) => window.__waterNetworkMap!
|
||||
.queryRenderedFeatures({ layers: [id] }).length, layerId)).toBeGreaterThan(0);
|
||||
await page.evaluate(() => new Promise<void>((resolve) => {
|
||||
const map = window.__waterNetworkMap!;
|
||||
map.once("idle", () => resolve());
|
||||
map.triggerRepaint();
|
||||
}));
|
||||
await page.screenshot({ path: testInfo.outputPath("fade-before.png") });
|
||||
const original = await page.evaluate(({ layerId, property, longitude, latitude }) => {
|
||||
const map = window.__waterNetworkMap!;
|
||||
const canvas = map.getCanvas();
|
||||
const gl = canvas.getContext("webgl2") ?? canvas.getContext("webgl");
|
||||
if (!gl) throw new Error("No WebGL context");
|
||||
window.__layerFadePixels = [];
|
||||
map.on("render", () => {
|
||||
const point = map.project([longitude, latitude]);
|
||||
const ratio = canvas.width / canvas.clientWidth;
|
||||
const pixels = new Uint8Array(7 * 7 * 4);
|
||||
gl.readPixels(Math.round(point.x * ratio) - 3,
|
||||
canvas.height - Math.round(point.y * ratio) - 3, 7, 7, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
let darkest = 0;
|
||||
for (let index = 0; index < pixels.length; index += 4) {
|
||||
darkest = Math.max(darkest, 765 - pixels[index] - pixels[index + 1] - pixels[index + 2]);
|
||||
}
|
||||
window.__layerFadePixels.push(darkest);
|
||||
});
|
||||
map.triggerRepaint();
|
||||
return map.getPaintProperty(layerId, property);
|
||||
}, { layerId, property, longitude, latitude });
|
||||
const toggle = page.getByRole("button", { name: new RegExp(`^${label},`) });
|
||||
await toggle.click();
|
||||
await expect.poll(() => page.evaluate((id) => window.__waterNetworkMap!
|
||||
.getLayoutProperty(id, "visibility"), layerId)).toBe("none");
|
||||
const hiding = await page.evaluate(() => window.__layerFadePixels);
|
||||
const min = Math.min(...hiding);
|
||||
const max = Math.max(...hiding);
|
||||
await testInfo.attach("hiding-pixels", { body: JSON.stringify(hiding), contentType: "application/json" });
|
||||
expect(max - min).toBeGreaterThan(50);
|
||||
// Require multiple rendered intermediate alpha states without prescribing a GPU frame rate.
|
||||
expect(new Set(hiding.filter((pixel) => pixel > min + 10 && pixel < max - 10)).size).toBeGreaterThan(1);
|
||||
await page.evaluate(() => { window.__layerFadePixels = []; });
|
||||
await toggle.click();
|
||||
await expect.poll(() => page.evaluate(({ layerId, property }) => window.__waterNetworkMap!
|
||||
.getPaintProperty(layerId, property), { layerId, property })).toEqual(original);
|
||||
const showing = await page.evaluate(() => window.__layerFadePixels);
|
||||
expect(new Set(showing.filter((pixel) => pixel > min + 10 && pixel < max - 10)).size).toBeGreaterThan(1);
|
||||
await testInfo.attach("rendered-opacity", {
|
||||
body: JSON.stringify({ hiding, showing }), contentType: "application/json"
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -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") });
|
||||
});
|
||||
}
|
||||
@@ -36,6 +36,12 @@ test("loads SCADA from the backend API and aligns map features by device_id", as
|
||||
|
||||
const deviceRequest = await deviceRequestPromise;
|
||||
expect(deviceRequest.headers()["x-project-id"]).toBe(TEST_PROJECT_ID);
|
||||
await expect.poll(() => page.evaluate(() => Boolean(
|
||||
globalThis.__waterNetworkMap?.getLayer("scada-pressure-symbol")
|
||||
))).toBe(true);
|
||||
await page.evaluate(() => globalThis.__waterNetworkMap!.jumpTo({
|
||||
center: [121.5, 30.9], zoom: 13
|
||||
}));
|
||||
await expect.poll(async () => page.evaluate(() => {
|
||||
const map = globalThis.__waterNetworkMap;
|
||||
if (!map) return [];
|
||||
@@ -74,6 +80,7 @@ test("shows data-dependent layer controls only when GeoServer reports features",
|
||||
await expect(layerTool).toBeEnabled();
|
||||
await layerTool.click();
|
||||
await expect(page.getByRole("button", { name: /^阀门/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /^阀门/ })).toContainText("符号自动避让");
|
||||
await expect(page.getByRole("button", { name: /^水库/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /^水泵/ })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /^水箱/ })).toBeVisible();
|
||||
@@ -111,6 +118,12 @@ test("shows data-dependent layer controls only when GeoServer reports features",
|
||||
});
|
||||
expect(layerOrder[0]).toBeGreaterThanOrEqual(0);
|
||||
expect(layerOrder[0]).toBeLessThan(layerOrder[1]);
|
||||
const valveMinZooms = await page.evaluate(() => {
|
||||
const map = globalThis.__waterNetworkMap;
|
||||
return ["valves-symbol", "valves-hit", "workbench-value-label-valves"]
|
||||
.map((layerId) => map?.getLayer(layerId)?.minzoom);
|
||||
});
|
||||
expect(valveMinZooms).toEqual([13, 13, 13]);
|
||||
});
|
||||
|
||||
test("loads required map layers while optional layer probes are still pending", async ({ page }) => {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { createRequire } from "node:module";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
const requireFromMapLibre = createRequire(import.meta.resolve("maplibre-gl"));
|
||||
const { fromVectorTileJs } = requireFromMapLibre("@maplibre/vt-pbf") as {
|
||||
fromVectorTileJs(tile: unknown): Uint8Array;
|
||||
};
|
||||
|
||||
// Exercise the same MVT worker path as GeoServer, including a dense pipe inventory.
|
||||
export async function mockPipeVectorTiles(page: Page) {
|
||||
const tiles = new Map<string, Buffer>();
|
||||
await page.route("**/tjwater_next:pipes/WebMercatorQuad/**", (route) => {
|
||||
const path = new URL(route.request().url()).pathname.split("/");
|
||||
const [zoom, tileY, tileX] = path.slice(-3).map(Number);
|
||||
const key = `${zoom}/${tileY}/${tileX}`;
|
||||
let body = tiles.get(key);
|
||||
if (!body) {
|
||||
const project = (longitude: number, latitude: number) => {
|
||||
const sin = Math.sin(latitude * Math.PI / 180);
|
||||
return {
|
||||
x: Math.round(((longitude + 180) / 360 * 2 ** zoom - tileX) * 4096),
|
||||
y: Math.round(((0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI)) * 2 ** zoom - tileY) * 4096)
|
||||
};
|
||||
};
|
||||
const features = Array.from({ length: 2001 }, (_, index) => {
|
||||
const latitude = index === 0 ? 30.91 : 30.918 + Math.floor((index - 1) / 100) * 0.0004;
|
||||
const longitude = index === 0 ? 121.45 : 121.45 + (index - 1) % 100 * 0.0003;
|
||||
const geometry = [[project(longitude, latitude),
|
||||
project(index === 0 ? 121.48 : longitude + 0.00025, latitude)]];
|
||||
const [start, end] = geometry[0];
|
||||
if (start.y < -256 || start.y > 4352 || end.x < -256 || start.x > 4352) return [];
|
||||
return [{ type: 2, id: index + 1, extent: 4096,
|
||||
properties: { id: index + 1, diameter: index === 0 ? 800 : 100,
|
||||
status: index % 5 === 4 ? "closed" : "open" },
|
||||
loadGeometry: () => geometry }];
|
||||
}).flat();
|
||||
body = Buffer.from(fromVectorTileJs({ layers: { pipes: {
|
||||
version: 2, name: "pipes", extent: 4096, length: features.length,
|
||||
feature: (index: number) => features[index]
|
||||
} } }));
|
||||
tiles.set(key, body);
|
||||
}
|
||||
return route.fulfill({ body, contentType: "application/vnd.mapbox-vector-tile",
|
||||
headers: { "Access-Control-Allow-Origin": "*" } });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user