Files
next-tjwater-drainage-frontend/features/workbench/map/layers.test.ts
T

174 lines
7.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
INTERACTIVE_HIT_LAYER_IDS,
waterNetworkLayers
} from "./layers";
import { MAP_STYLE_TOKENS } from "./map-colors";
import { DRAINAGE_LAYER_VISUALS, MAP_MAX_ZOOM } from "./map-layer-visuals";
describe("drainage network interaction layers", () => {
it("adds one invisible hit layer for every interactive source", () => {
expect(INTERACTIVE_HIT_LAYER_IDS).toEqual([
"conduits-hit",
"junctions-hit",
"orifices-hit",
"pumps-hit",
"outfalls-hit"
]);
const hitLayers = INTERACTIVE_HIT_LAYER_IDS.map((layerId) => {
const layer = waterNetworkLayers.find((candidate) => candidate.id === layerId);
expect(layer).toBeDefined();
return layer!;
});
expect(hitLayers.map((layer) => "source" in layer ? layer.source : null)).toEqual([
"conduits",
"junctions",
"orifices",
"pumps",
"outfalls"
]);
hitLayers.forEach((layer) => {
if (layer.type === "line") {
expect(layer.paint?.["line-width"]).toBe(DRAINAGE_LAYER_VISUALS.conduits.hitWidth);
expect(layer.paint?.["line-opacity"]).toBe(0);
} else if (layer.type === "circle") {
expect(layer.paint?.["circle-radius"]).toBe(DRAINAGE_LAYER_VISUALS.junctions.hitRadius);
expect(layer.paint?.["circle-opacity"]).toBe(0);
} else {
throw new Error(`Unexpected hit layer type: ${layer.type}`);
}
});
});
it("orders smaller facilities above conduits for overlapping hits", () => {
const layerIds = waterNetworkLayers.map((layer) => layer.id);
const hitLayerIndexes = INTERACTIVE_HIT_LAYER_IDS.map((layerId) => layerIds.indexOf(layerId));
expect(hitLayerIndexes).toEqual([...hitLayerIndexes].sort((first, second) => first - second));
expect(hitLayerIndexes.every((index) => index >= 0)).toBe(true);
});
it("provides persistent selected layers for every source", () => {
const selectedLayerIds = [
"pipes-selected-outline",
"junctions-selected",
"orifices-selected-outline",
"pumps-selected-outline",
"outfalls-selected"
];
selectedLayerIds.forEach((layerId) => {
const layer = waterNetworkLayers.find((candidate) => candidate.id === layerId);
expect(layer && "filter" in layer ? layer.filter : undefined).toBeUndefined();
const paint = layer?.paint as Record<string, unknown> | undefined;
expect(JSON.stringify(paint)).toContain("feature-state");
expect(JSON.stringify(paint)).toContain("selected");
});
});
it("renders selected features as opaque hollow outlines", () => {
const expectedLineOutlines = [
["pipes-selected-outline", DRAINAGE_LAYER_VISUALS.conduits],
["orifices-selected-outline", DRAINAGE_LAYER_VISUALS.orifices],
["pumps-selected-outline", DRAINAGE_LAYER_VISUALS.pumps]
] as const;
expectedLineOutlines.forEach(([layerId, visuals]) => {
const layer = waterNetworkLayers.find((candidate) => candidate.id === layerId);
const paint = layer?.paint as Record<string, unknown> | undefined;
expect(paint?.["line-color"]).toBe(MAP_STYLE_TOKENS.state.selected);
expect(paint?.["line-gap-width"]).toBe(visuals.selectedWidth);
expect(paint?.["line-width"]).toBe(visuals.selectedBorderWidth);
expect(paint?.["line-opacity"]).toEqual([
"case",
["any", ["boolean", ["feature-state", "selected"], false], ["boolean", ["feature-state", "highlighted"], false]],
1,
0
]);
});
const expectedPointOutlines = [
"junctions-selected",
"outfalls-selected"
] as const;
expectedPointOutlines.forEach((layerId) => {
const layer = waterNetworkLayers.find((candidate) => candidate.id === layerId);
const paint = layer?.paint as Record<string, unknown> | undefined;
expect(paint?.["circle-color"]).toBe(MAP_STYLE_TOKENS.canvas.transparent);
expect(JSON.stringify(paint?.["circle-opacity"])).toContain("feature-state");
expect(paint?.["circle-stroke-color"]).toBe(MAP_STYLE_TOKENS.state.selected);
expect(JSON.stringify(paint?.["circle-stroke-opacity"])).toContain("selected");
});
});
it("keeps source geometry readable through zoom level 24", () => {
expect(MAP_MAX_ZOOM).toBe(24);
expect(DRAINAGE_LAYER_VISUALS.junctions.minzoom).toBe(12);
expect(DRAINAGE_LAYER_VISUALS.junctions.radius).toEqual([
"interpolate", ["linear"], ["zoom"], 12, 0.6, 14, 1, 15, 1.5, 16, 3, 20, 5.5, 24, 8
]);
expect(DRAINAGE_LAYER_VISUALS.outfalls.radius).toEqual([
"interpolate", ["linear"], ["zoom"], 11, 3, 16, 5, 20, 7, 24, 9
]);
expect(DRAINAGE_LAYER_VISUALS.orifices.width).toEqual([
"interpolate", ["linear"], ["zoom"], 11, 1.6, 17, 4.5, 22, 6, 24, 7
]);
expect(DRAINAGE_LAYER_VISUALS.pumps.width).toEqual([
"interpolate", ["linear"], ["zoom"], 11, 2.4, 17, 6.2, 22, 8, 24, 9
]);
expect((DRAINAGE_LAYER_VISUALS.conduits.width as unknown[]).at(-2)).toBe(24);
expect((DRAINAGE_LAYER_VISUALS.conduits.hitWidth as unknown[]).at(-1)).toBe(24);
expect((DRAINAGE_LAYER_VISUALS.junctions.hitRadius as unknown[]).at(-1)).toBe(14);
});
it("prioritizes continuous conduits before full junction detail at low zoom", () => {
expect(DRAINAGE_LAYER_VISUALS.conduits.width).toEqual([
"interpolate", ["linear"], ["zoom"],
9, 1.1,
13, 2,
17, ["+", 2.5, ["/", ["coalesce", ["get", "diameter"], 100], 360]],
22, ["+", 3.5, ["/", ["coalesce", ["get", "diameter"], 100], 300]],
24, ["+", 4, ["/", ["coalesce", ["get", "diameter"], 100], 280]]
]);
expect(DRAINAGE_LAYER_VISUALS.junctions.haloOpacity).toEqual([
"interpolate", ["linear"], ["zoom"], 12, 0, 14, 0, 15, 1
]);
const junctionHalo = waterNetworkLayers.find((layer) => layer.id === "junctions-halo");
const orifices = waterNetworkLayers.find((layer) => layer.id === "orifices");
const pumps = waterNetworkLayers.find((layer) => layer.id === "pumps");
expect((junctionHalo?.paint as Record<string, unknown>)?.["circle-opacity"]).toBe(DRAINAGE_LAYER_VISUALS.junctions.haloOpacity);
expect((orifices?.paint as Record<string, unknown>)?.["line-dasharray"]).toEqual([4, 2]);
expect((pumps?.paint as Record<string, unknown>)?.["line-dasharray"]).toEqual([6, 2, 1.5, 2]);
});
it("uses the centralized geometry tokens in rendered layers", () => {
const expectedGeometry = [
["pipes-flow", "line-width", DRAINAGE_LAYER_VISUALS.conduits.width],
["pipes-selected-outline", "line-width", DRAINAGE_LAYER_VISUALS.conduits.selectedBorderWidth],
["junctions", "circle-radius", DRAINAGE_LAYER_VISUALS.junctions.radius],
["junctions-selected", "circle-radius", DRAINAGE_LAYER_VISUALS.junctions.selectedRadius],
["orifices", "line-width", DRAINAGE_LAYER_VISUALS.orifices.width],
["orifices-selected-outline", "line-width", DRAINAGE_LAYER_VISUALS.orifices.selectedBorderWidth],
["pumps", "line-width", DRAINAGE_LAYER_VISUALS.pumps.width],
["pumps-selected-outline", "line-width", DRAINAGE_LAYER_VISUALS.pumps.selectedBorderWidth],
["outfalls", "circle-radius", DRAINAGE_LAYER_VISUALS.outfalls.radius],
["outfalls-selected", "circle-radius", DRAINAGE_LAYER_VISUALS.outfalls.selectedRadius]
] as const;
expectedGeometry.forEach(([layerId, paintProperty, visualToken]) => {
const layer = waterNetworkLayers.find((candidate) => candidate.id === layerId);
const paint = layer?.paint as Record<string, unknown> | undefined;
expect(paint?.[paintProperty]).toBe(visualToken);
});
["junctions-halo", "junctions", "junctions-hover", "junctions-hit"].forEach((layerId) => {
const layer = waterNetworkLayers.find((candidate) => candidate.id === layerId);
expect(layer && "minzoom" in layer ? layer.minzoom : undefined).toBe(12);
});
});
});