feat: refine supply map layers and workbench branding
This commit is contained in:
+2
-2
@@ -17,8 +17,8 @@ test("renders the migrated WebGIS workbench shell", async ({ page }) => {
|
||||
});
|
||||
|
||||
await page.goto("/");
|
||||
await expect(page.getByText("供水管网智能调度系统")).toBeVisible();
|
||||
await expect(page.getByText("调度工作台")).toBeVisible();
|
||||
await expect(page.getByText("供水管网智慧运维系统")).toBeVisible();
|
||||
await expect(page.getByText("运维工作台")).toBeVisible();
|
||||
await expect(page.getByLabel("Agent 折叠栏")).toBeVisible();
|
||||
await expect(page.getByLabel("Agent 命令面板")).toHaveCount(0);
|
||||
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
||||
|
||||
@@ -117,11 +117,11 @@ export function WorkbenchTopBar({
|
||||
<Droplets size={18} aria-hidden="true" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h1 aria-label="供水管网智能调度系统" className="truncate text-sm font-semibold leading-5 text-slate-950">
|
||||
<h1 aria-label="供水管网智慧运维系统" className="truncate text-sm font-semibold leading-5 text-slate-950">
|
||||
<span className="sm:hidden">供水</span>
|
||||
<span className="hidden sm:inline">供水管网智能调度系统</span>
|
||||
<span className="hidden sm:inline">供水管网智慧运维系统</span>
|
||||
</h1>
|
||||
<p className="hidden truncate text-xs leading-4 text-[var(--ink-muted)] sm:block">调度工作台</p>
|
||||
<p className="hidden truncate text-xs leading-4 text-[var(--ink-muted)] sm:block">运维工作台</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -356,7 +356,7 @@ export function useWorkbenchMap({
|
||||
).forEach((layer) => map.addLayer(layer));
|
||||
scadaHitLayers.forEach((layer) => map.addLayer(layer));
|
||||
removeCityScene = installCityScene(map);
|
||||
setSimulationLayersVisibility(map, impactVisibleRef.current);
|
||||
setSimulationLayersVisibility(map, impactVisibleRef.current, false);
|
||||
setMapReady(true);
|
||||
updateSourceStatus(
|
||||
setSourceStatuses,
|
||||
|
||||
@@ -102,6 +102,7 @@ import {
|
||||
} from "./layout/workbench-layout";
|
||||
import { getResponsivePadding, getResponsiveWorkbenchPadding } from "./map/camera";
|
||||
import { exportMapViewImage } from "./map/export-view";
|
||||
import { setMapLayersVisibility } from "./map/layer-visibility";
|
||||
import { parseScadaAnalysisItems } from "./map/scada-analysis";
|
||||
import {
|
||||
BASE_LAYER_OPTIONS,
|
||||
@@ -724,9 +725,7 @@ function MapWorkbenchContent({
|
||||
return;
|
||||
}
|
||||
|
||||
getWorkbenchLayerIds(map, layerControlId).forEach((layerId) => {
|
||||
map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
|
||||
});
|
||||
setMapLayersVisibility(map, getWorkbenchLayerIds(map, layerControlId), visible);
|
||||
}
|
||||
function handleTogglePresentationLayer(id: string, visible: boolean) {
|
||||
const before = presentationLayerChangesRef.current.get(id) ?? (layerVisibility[id] !== false);
|
||||
|
||||
@@ -5,9 +5,7 @@ type LayoutPropertyName = Parameters<MapLibreMap["setLayoutProperty"]>[1];
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import {
|
||||
createSupplyActiveOpacity,
|
||||
createSupplyStateColor,
|
||||
supplyFacilityOpacityExpression,
|
||||
supplyPipeColorExpression,
|
||||
supplyPipeDashExpression
|
||||
} from "./supply-style-expressions";
|
||||
@@ -60,8 +58,8 @@ const POINT_DEFAULT_COLORS = {
|
||||
} as const;
|
||||
|
||||
const POINT_DEFAULT_OPACITIES = {
|
||||
junctions: 0.9,
|
||||
valves: supplyFacilityOpacityExpression,
|
||||
junctions: SUPPLY_LAYER_VISUALS.junctions.opacity,
|
||||
valves: SUPPLY_LAYER_VISUALS.valves.opacity,
|
||||
reservoirs: 0
|
||||
} as const;
|
||||
|
||||
@@ -107,7 +105,7 @@ function applyLineStyle(map: MapLibreMap, groupId: "pipes", patch: LineLayerGrou
|
||||
const prefix = groupId;
|
||||
const coreId = LINE_CORE_LAYERS[groupId];
|
||||
if (patch.color !== undefined) setPaint(map, coreId, "line-color", createSupplyStateColor(patch.color));
|
||||
if (patch.opacity !== undefined) setPaint(map, coreId, "line-opacity", createSupplyActiveOpacity(patch.opacity));
|
||||
if (patch.opacity !== undefined) setPaint(map, coreId, "line-opacity", patch.opacity);
|
||||
if (patch.dash !== undefined) {
|
||||
const dash = patch.dash === "solid"
|
||||
? [1, 0]
|
||||
@@ -127,7 +125,7 @@ function applyLineStyle(map: MapLibreMap, groupId: "pipes", patch: LineLayerGrou
|
||||
function resetLineStyle(map: MapLibreMap, groupId: "pipes") {
|
||||
const coreId = LINE_CORE_LAYERS[groupId];
|
||||
setPaint(map, coreId, "line-color", supplyPipeColorExpression);
|
||||
setPaint(map, coreId, "line-opacity", createSupplyActiveOpacity(0.92));
|
||||
setPaint(map, coreId, "line-opacity", 0.92);
|
||||
setPaint(map, coreId, "line-dasharray", supplyPipeDashExpression);
|
||||
setPaint(map, `${groupId}-casing`, "line-dasharray", supplyPipeDashExpression);
|
||||
for (const [id, value] of Object.entries(LINE_DEFAULT_WIDTHS)) {
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { LAYER_VISIBILITY_FADE_MS, setMapLayersVisibility } from "./layer-visibility";
|
||||
|
||||
describe("map layer visibility fades", () => {
|
||||
let now = 0;
|
||||
let nextFrame = 0;
|
||||
let frames: Map<number, FrameRequestCallback>;
|
||||
|
||||
beforeEach(() => {
|
||||
now = 0;
|
||||
nextFrame = 0;
|
||||
frames = new Map();
|
||||
vi.spyOn(performance, "now").mockImplementation(() => now);
|
||||
vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
|
||||
frames.set(++nextFrame, callback);
|
||||
return nextFrame;
|
||||
});
|
||||
vi.stubGlobal("cancelAnimationFrame", (frame: number) => frames.delete(frame));
|
||||
vi.stubGlobal("matchMedia", () => ({ matches: false }));
|
||||
});
|
||||
|
||||
afterEach(() => { vi.restoreAllMocks(); vi.unstubAllGlobals(); });
|
||||
|
||||
function advance(time: number) {
|
||||
now = time;
|
||||
const callbacks = [...frames.values()];
|
||||
frames.clear();
|
||||
callbacks.forEach((callback) => callback(time));
|
||||
}
|
||||
|
||||
it("fades before hiding and restores the authored opacity and transition", () => {
|
||||
const fixture = createMap();
|
||||
setMapLayersVisibility(fixture.map, ["pipes", "pipes-hit"], false);
|
||||
expect(fixture.visibility.get("pipes")).toBe("visible");
|
||||
expect(fixture.visibility.get("pipes-hit")).toBe("none");
|
||||
advance(LAYER_VISIBILITY_FADE_MS / 2);
|
||||
expect(fixture.paint.get("pipes:line-opacity")).toBeGreaterThan(0);
|
||||
expect(fixture.paint.get("pipes:line-opacity")).toBeLessThan(0.92);
|
||||
advance(LAYER_VISIBILITY_FADE_MS);
|
||||
expect(fixture.visibility.get("pipes")).toBe("none");
|
||||
expect(fixture.paint.get("pipes:line-opacity")).toBe(0.92);
|
||||
expect(fixture.paint.get("pipes:line-opacity-transition")).toEqual({ duration: 120 });
|
||||
});
|
||||
|
||||
it("reverses an in-flight fade without hiding the layer or stacking expressions", () => {
|
||||
const fixture = createMap();
|
||||
const camera = ["interpolate", ["linear"], ["zoom"], 8, 0.5, 16, 1];
|
||||
fixture.paint.set("pipes:line-opacity", camera);
|
||||
setMapLayersVisibility(fixture.map, ["pipes"], false);
|
||||
advance(80);
|
||||
setMapLayersVisibility(fixture.map, ["pipes"], true);
|
||||
advance(80);
|
||||
advance(180);
|
||||
expect((fixture.paint.get("pipes:line-opacity") as unknown[])[0]).toBe("interpolate");
|
||||
expect(fixture.visibility.get("pipes")).toBe("visible");
|
||||
advance(80 + LAYER_VISIBILITY_FADE_MS);
|
||||
expect(fixture.paint.get("pipes:line-opacity")).toEqual(camera);
|
||||
expect(frames.size).toBe(0);
|
||||
});
|
||||
|
||||
it("enables picking after showing and respects reduced motion", () => {
|
||||
const fixture = createMap();
|
||||
fixture.visibility.set("pipes", "none");
|
||||
fixture.visibility.set("pipes-hit", "none");
|
||||
setMapLayersVisibility(fixture.map, ["pipes", "pipes-hit"], true);
|
||||
advance(0);
|
||||
expect(fixture.paint.get("pipes:line-opacity")).toBe(0);
|
||||
expect(fixture.visibility.get("pipes-hit")).toBe("none");
|
||||
advance(LAYER_VISIBILITY_FADE_MS);
|
||||
expect(fixture.visibility.get("pipes-hit")).toBe("visible");
|
||||
vi.stubGlobal("matchMedia", () => ({ matches: true }));
|
||||
setMapLayersVisibility(fixture.map, ["pipes"], false);
|
||||
expect(fixture.visibility.get("pipes")).toBe("none");
|
||||
expect(frames.size).toBe(0);
|
||||
});
|
||||
|
||||
it("cancels frames when the owning map is removed", () => {
|
||||
const fixture = createMap();
|
||||
setMapLayersVisibility(fixture.map, ["pipes"], false);
|
||||
fixture.remove();
|
||||
expect(frames.size).toBe(0);
|
||||
});
|
||||
|
||||
it("starts fading in after vector tiles are ready", () => {
|
||||
const fixture = createMap();
|
||||
let loaded = false;
|
||||
fixture.map.isSourceLoaded = () => loaded;
|
||||
fixture.visibility.set("pipes", "none");
|
||||
setMapLayersVisibility(fixture.map, ["pipes"], true);
|
||||
advance(LAYER_VISIBILITY_FADE_MS * 2);
|
||||
expect(fixture.paint.get("pipes:line-opacity")).toBe(0);
|
||||
loaded = true;
|
||||
advance(LAYER_VISIBILITY_FADE_MS * 3);
|
||||
expect(fixture.paint.get("pipes:line-opacity")).toBe(0);
|
||||
advance(LAYER_VISIBILITY_FADE_MS * 3.5);
|
||||
expect(fixture.paint.get("pipes:line-opacity")).toBeGreaterThan(0);
|
||||
expect(fixture.paint.get("pipes:line-opacity")).toBeLessThan(0.92);
|
||||
advance(LAYER_VISIBILITY_FADE_MS * 4);
|
||||
expect(fixture.paint.get("pipes:line-opacity")).toBe(0.92);
|
||||
});
|
||||
});
|
||||
|
||||
function createMap() {
|
||||
const layers = new Map([ ["pipes", { type: "line", source: "pipes" }], ["pipes-hit", { type: "line", source: "pipes" }] ]);
|
||||
const visibility = new Map([ ["pipes", "visible"], ["pipes-hit", "visible"] ]);
|
||||
const paint = new Map<string, unknown>([
|
||||
["pipes:line-opacity", 0.92], ["pipes:line-opacity-transition", { duration: 120 }],
|
||||
["pipes-hit:line-opacity", 0]
|
||||
]);
|
||||
let remove = () => {};
|
||||
const map = {
|
||||
once: (event: string, callback: () => void) => {
|
||||
if (event === "remove") remove = callback;
|
||||
else if (event === "render") callback();
|
||||
},
|
||||
getLayer: (id: string) => layers.get(id),
|
||||
isSourceLoaded: () => true,
|
||||
getLayoutProperty: (id: string) => visibility.get(id),
|
||||
getPaintProperty: (id: string, name: string) => paint.get(`${id}:${name}`),
|
||||
setLayoutProperty: (id: string, _name: string, value: string) => visibility.set(id, value),
|
||||
setPaintProperty: (id: string, name: string, value: unknown) => paint.set(`${id}:${name}`, value)
|
||||
} as unknown as MapLibreMap;
|
||||
return { map, visibility, paint, remove: () => remove() };
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
|
||||
export const LAYER_VISIBILITY_FADE_MS = 320;
|
||||
|
||||
type PaintProperty = Parameters<MapLibreMap["getPaintProperty"]>[1];
|
||||
type OpacityProperty = { name: PaintProperty; transitionName: PaintProperty; value: unknown; transition: unknown };
|
||||
type LayerFade = {
|
||||
visible: boolean;
|
||||
opacity: number;
|
||||
frame: number;
|
||||
properties: OpacityProperty[];
|
||||
};
|
||||
|
||||
const fadesByMap = new WeakMap<MapLibreMap, Map<string, LayerFade>>();
|
||||
const opacityProperties: Record<string, readonly PaintProperty[]> = {
|
||||
line: ["line-opacity"],
|
||||
circle: ["circle-opacity", "circle-stroke-opacity"],
|
||||
symbol: ["icon-opacity", "text-opacity"],
|
||||
fill: ["fill-opacity"],
|
||||
"fill-extrusion": ["fill-extrusion-opacity"],
|
||||
raster: ["raster-opacity"],
|
||||
heatmap: ["heatmap-opacity"]
|
||||
};
|
||||
|
||||
export function setMapLayersVisibility(
|
||||
map: MapLibreMap,
|
||||
layerIds: readonly string[],
|
||||
visible: boolean,
|
||||
animate = true
|
||||
) {
|
||||
let fades = fadesByMap.get(map);
|
||||
if (!fades) {
|
||||
fades = new Map();
|
||||
fadesByMap.set(map, fades);
|
||||
const ownedFades = fades;
|
||||
map.once("remove", () => {
|
||||
ownedFades.forEach((fade) => cancelAnimationFrame(fade.frame));
|
||||
ownedFades.clear();
|
||||
fadesByMap.delete(map);
|
||||
});
|
||||
}
|
||||
const reducedMotion = typeof matchMedia === "function"
|
||||
&& matchMedia("(prefers-reduced-motion: reduce)").matches;
|
||||
const activeFades = fades;
|
||||
|
||||
for (const layerId of layerIds) {
|
||||
const layer = map.getLayer(layerId);
|
||||
if (!layer) continue;
|
||||
const previous = activeFades.get(layerId);
|
||||
if (previous?.visible === visible) continue;
|
||||
const wasVisible = map.getLayoutProperty(layerId, "visibility") !== "none";
|
||||
if (!previous && wasVisible === visible) continue;
|
||||
if (previous) cancelAnimationFrame(previous.frame);
|
||||
|
||||
const names = opacityProperties[layer.type] ?? [];
|
||||
const properties = previous?.properties ?? names.map((name) => {
|
||||
const transitionName = `${name}-transition` as PaintProperty;
|
||||
return { name, transitionName, value: map.getPaintProperty(layerId, name),
|
||||
transition: map.getPaintProperty(layerId, transitionName) };
|
||||
});
|
||||
const restorePaint = () => {
|
||||
for (const property of properties) {
|
||||
map.setPaintProperty(layerId, property.name, (property.value ?? null) as never);
|
||||
map.setPaintProperty(layerId, property.transitionName, (property.transition ?? null) as never);
|
||||
}
|
||||
};
|
||||
const finish = () => {
|
||||
map.setLayoutProperty(layerId, "visibility", visible ? "visible" : "none");
|
||||
restorePaint();
|
||||
activeFades.delete(layerId);
|
||||
};
|
||||
// Picking stops immediately on hide and starts after the visual fade on show.
|
||||
const hitTarget = layerId.endsWith("-hit");
|
||||
const deferred = hitTarget || properties.some((property) => readsFeatureState(property.value));
|
||||
if ((deferred && !visible) || !animate || reducedMotion || names.length === 0) {
|
||||
finish();
|
||||
continue;
|
||||
}
|
||||
|
||||
const startOpacity = previous?.opacity ?? (wasVisible ? 1 : 0);
|
||||
const targetOpacity = visible ? 1 : 0;
|
||||
const fade: LayerFade = { visible, opacity: startOpacity, frame: 0, properties };
|
||||
activeFades.set(layerId, fade);
|
||||
const paint = () => {
|
||||
if (deferred) return;
|
||||
for (const property of properties) {
|
||||
map.setPaintProperty(layerId, property.name, scaleOpacity(property.value ?? 1, fade.opacity) as never);
|
||||
}
|
||||
};
|
||||
if (!deferred) {
|
||||
for (const property of properties) {
|
||||
map.setPaintProperty(layerId, property.transitionName, { duration: 0, delay: 0 });
|
||||
}
|
||||
}
|
||||
paint();
|
||||
map.setLayoutProperty(layerId, "visibility", deferred ? "none" : "visible");
|
||||
let startedAt: number | undefined = visible ? undefined : performance.now();
|
||||
let rendered = !visible;
|
||||
if (visible) map.once("render", () => { rendered = true; });
|
||||
const tick = (now: number) => {
|
||||
if (map.getLayer(layerId) !== layer) {
|
||||
activeFades.delete(layerId);
|
||||
return;
|
||||
}
|
||||
// A hidden vector source may need its tiles again. Start the fade when it can draw,
|
||||
// rather than spending the entire animation while its worker is still loading.
|
||||
if (startedAt === undefined && (!rendered
|
||||
|| (typeof layer.source === "string" && !map.isSourceLoaded(layer.source)))) {
|
||||
fade.frame = requestAnimationFrame(tick);
|
||||
return;
|
||||
}
|
||||
startedAt ??= now;
|
||||
const progress = Math.min(1, (now - startedAt) / LAYER_VISIBILITY_FADE_MS);
|
||||
fade.opacity = startOpacity + (targetOpacity - startOpacity) * progress;
|
||||
if (progress >= 1) finish();
|
||||
else {
|
||||
paint();
|
||||
fade.frame = requestAnimationFrame(tick);
|
||||
}
|
||||
};
|
||||
fade.frame = requestAnimationFrame(tick);
|
||||
}
|
||||
}
|
||||
|
||||
function readsFeatureState(value: unknown): boolean {
|
||||
return Array.isArray(value) && (value[0] === "feature-state" || value.some(readsFeatureState));
|
||||
}
|
||||
|
||||
function scaleOpacity(value: unknown, opacity: number): unknown {
|
||||
if (typeof value === "number") return value * opacity;
|
||||
if (Array.isArray(value)) {
|
||||
// Camera expressions must keep zoom at the top level of the paint expression.
|
||||
const cameraInterpolate = value[0] === "interpolate" && value[2]?.[0] === "zoom";
|
||||
const cameraStep = value[0] === "step" && value[1]?.[0] === "zoom";
|
||||
if (cameraInterpolate || cameraStep) {
|
||||
return value.map((entry, index) => index >= (cameraStep ? 2 : 4) && index % 2 === 0
|
||||
? scaleOpacity(entry, opacity) : entry);
|
||||
}
|
||||
}
|
||||
return ["*", value, opacity];
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import { SUPPLY_ASSET_IMAGE_IDS } from "./supply-icons";
|
||||
import {
|
||||
supplyFacilityOpacityExpression,
|
||||
supplyInactiveExpression,
|
||||
supplyPipeColorExpression,
|
||||
supplyPipeDashExpression
|
||||
} from "./supply-style-expressions";
|
||||
@@ -21,6 +21,8 @@ describe("supply network layer styling", () => {
|
||||
expect(paint?.["line-color"]).toEqual(supplyPipeColorExpression);
|
||||
expect(paint?.["line-width"]).toEqual(SUPPLY_LAYER_VISUALS.pipes.width);
|
||||
expect(paint?.["line-dasharray"]).toEqual(supplyPipeDashExpression);
|
||||
expect(paint?.["line-opacity"]).toBe(0.92);
|
||||
expect(pipe && "filter" in pipe ? pipe.filter : undefined).toEqual(["!", supplyInactiveExpression]);
|
||||
expect(JSON.stringify(paint?.["line-width"])).toContain("diameter");
|
||||
expect(JSON.stringify(paint?.["line-width"])).toContain("zoom");
|
||||
expect(waterNetworkBusinessLayers.some((layer) => layer.id === "pipes-risk-glow")).toBe(false);
|
||||
@@ -35,6 +37,8 @@ describe("supply network layer styling", () => {
|
||||
const sourcePipePaint = sourcePipe?.paint as Record<string, unknown> | undefined;
|
||||
const inventoryPipe = getLayer("fx_20200811_dn100_pipes-line");
|
||||
const inventoryPaint = inventoryPipe?.paint as Record<string, unknown> | undefined;
|
||||
expect(sourcePipe).toMatchObject({ minzoom: 8.5 });
|
||||
expect(inventoryPipe).toMatchObject({ minzoom: 8.5 });
|
||||
|
||||
expect(sourcePipePaint?.["line-width"]).toEqual(SUPPLY_LAYER_VISUALS.pipes.width);
|
||||
expect(JSON.stringify(sourcePipePaint?.["line-color"])).toContain("diameter");
|
||||
@@ -49,55 +53,58 @@ describe("supply network layer styling", () => {
|
||||
|
||||
it("uses distinct engineering symbols for every source GIS facility type", () => {
|
||||
const sourceFacilitySymbols = [
|
||||
["fx_20200423_valves-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceValve, 14],
|
||||
["fx_20200423_drain_valves-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceDrainValve, 10],
|
||||
["fx_20200423_flow_meters-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceFlowMeter, 10],
|
||||
["fx_20200423_hydrants-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceHydrant, 13],
|
||||
["fx_20200423_service_connections-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceConnection, 13],
|
||||
["fx_20200423_customer_meters-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceCustomerMeter, 13],
|
||||
["fx_20200423_valves-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceValve],
|
||||
["fx_20200423_drain_valves-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceDrainValve],
|
||||
["fx_20200423_flow_meters-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceFlowMeter],
|
||||
["fx_20200423_hydrants-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceHydrant],
|
||||
["fx_20200423_service_connections-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceConnection],
|
||||
["fx_20200423_customer_meters-symbol", SUPPLY_ASSET_IMAGE_IDS.sourceCustomerMeter],
|
||||
[
|
||||
"fx_20200423_non_residential_meters-symbol",
|
||||
SUPPLY_ASSET_IMAGE_IDS.sourceNonResidentialMeter,
|
||||
15
|
||||
SUPPLY_ASSET_IMAGE_IDS.sourceNonResidentialMeter
|
||||
]
|
||||
] as const;
|
||||
|
||||
sourceFacilitySymbols.forEach(([layerId, imageId, minZoom]) => {
|
||||
sourceFacilitySymbols.forEach(([layerId, imageId]) => {
|
||||
expect(getLayer(layerId)).toMatchObject({
|
||||
type: "symbol",
|
||||
layout: {
|
||||
"icon-image": imageId,
|
||||
"icon-size": ["interpolate", ["linear"], ["zoom"], minZoom, 0.36, 18, 0.54]
|
||||
"icon-size": SUPPLY_LAYER_VISUALS.valves.iconSize,
|
||||
"icon-allow-overlap": false
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps facilities visible from overview while deferring ordinary junctions", () => {
|
||||
expect(getLayer("valves")).toMatchObject({ minzoom: 8.5 });
|
||||
expect(getLayer("valves-symbol")).toMatchObject({ minzoom: 8.5 });
|
||||
it("keeps facilities available from overview with quieter detail markers", () => {
|
||||
expect(getLayer("valves")).toMatchObject({ minzoom: 13 });
|
||||
expect(getLayer("valves-symbol")).toMatchObject({ minzoom: 13 });
|
||||
expect(getLayer("valves-hit")).toMatchObject({ minzoom: 13 });
|
||||
expect(getLayer("fx_20200423_valves-symbol")).toMatchObject({ minzoom: 13 });
|
||||
expect(getLayer("fx_20200423_drain_valves-symbol")).toMatchObject({ minzoom: 13 });
|
||||
expect(getLayer("reservoirs")).toMatchObject({ minzoom: 8.5 });
|
||||
expect(getLayer("reservoirs-symbol")).toMatchObject({
|
||||
minzoom: 8.5,
|
||||
layout: { "icon-image": SUPPLY_ASSET_IMAGE_IDS.reservoir }
|
||||
});
|
||||
expect(getLayer("pumps-symbol")).toMatchObject({
|
||||
minzoom: 11,
|
||||
minzoom: 8.5,
|
||||
layout: { "icon-image": SUPPLY_ASSET_IMAGE_IDS.pump }
|
||||
});
|
||||
expect(getLayer("tanks-symbol")).toMatchObject({
|
||||
minzoom: 11,
|
||||
minzoom: 8.5,
|
||||
layout: { "icon-image": SUPPLY_ASSET_IMAGE_IDS.tank }
|
||||
});
|
||||
expect(getLayer("water_quality_monitoring_points-symbol")).toMatchObject({
|
||||
type: "symbol",
|
||||
minzoom: 8,
|
||||
minzoom: 11,
|
||||
layout: { "icon-image": SUPPLY_ASSET_IMAGE_IDS.waterQuality }
|
||||
});
|
||||
expect(getLayer("junctions")).toMatchObject({ minzoom: 15 });
|
||||
expect(getLayer("junctions")).toMatchObject({ minzoom: 12 });
|
||||
|
||||
const valvePaint = getLayer("valves")?.paint as Record<string, unknown> | undefined;
|
||||
expect(valvePaint?.["circle-opacity"]).toEqual(supplyFacilityOpacityExpression);
|
||||
expect(valvePaint?.["circle-opacity"]).toEqual(SUPPLY_LAYER_VISUALS.valves.opacity);
|
||||
|
||||
const reservoirPaint = getLayer("reservoirs")?.paint as Record<string, unknown> | undefined;
|
||||
expect(reservoirPaint?.["circle-opacity"]).toBe(0);
|
||||
|
||||
@@ -14,11 +14,8 @@ import {
|
||||
} from "./sources";
|
||||
import { SUPPLY_ASSET_IMAGE_IDS } from "./supply-icons";
|
||||
import {
|
||||
createSupplyActiveOpacity,
|
||||
createSupplyStateColor,
|
||||
supplyFacilityOpacityExpression,
|
||||
supplyInactiveExpression,
|
||||
supplyIconOpacityExpression,
|
||||
supplyPipeColorExpression,
|
||||
supplyPipeDashExpression
|
||||
} from "./supply-style-expressions";
|
||||
@@ -66,10 +63,11 @@ layers.push(
|
||||
source: "pipes",
|
||||
"source-layer": SOURCE_LAYERS.pipes,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.pipes.minzoom,
|
||||
filter: ["!", supplyInactiveExpression],
|
||||
paint: {
|
||||
"line-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"line-width": SUPPLY_LAYER_VISUALS.pipes.casingWidth,
|
||||
"line-opacity": createSupplyActiveOpacity(0.86),
|
||||
"line-opacity": 0.86,
|
||||
"line-dasharray": supplyPipeDashExpression
|
||||
}
|
||||
},
|
||||
@@ -79,10 +77,11 @@ layers.push(
|
||||
source: "pipes",
|
||||
"source-layer": SOURCE_LAYERS.pipes,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.pipes.minzoom,
|
||||
filter: ["!", supplyInactiveExpression],
|
||||
paint: {
|
||||
"line-color": supplyPipeColorExpression,
|
||||
"line-width": SUPPLY_LAYER_VISUALS.pipes.width,
|
||||
"line-opacity": createSupplyActiveOpacity(0.92),
|
||||
"line-opacity": 0.92,
|
||||
"line-dasharray": supplyPipeDashExpression
|
||||
}
|
||||
},
|
||||
@@ -136,7 +135,7 @@ layers.push(
|
||||
paint: {
|
||||
"circle-color": MAP_STYLE_TOKENS.supply.junction,
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.junctions.radius,
|
||||
"circle-opacity": 0.9
|
||||
"circle-opacity": SUPPLY_LAYER_VISUALS.junctions.opacity
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -148,7 +147,7 @@ layers.push(
|
||||
paint: {
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.valves.haloRadius,
|
||||
"circle-opacity": 0.94
|
||||
"circle-opacity": SUPPLY_LAYER_VISUALS.valves.haloOpacity
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -160,7 +159,7 @@ layers.push(
|
||||
paint: {
|
||||
"circle-color": createSupplyStateColor(MAP_STYLE_TOKENS.supply.valve),
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.valves.radius,
|
||||
"circle-opacity": supplyFacilityOpacityExpression,
|
||||
"circle-opacity": SUPPLY_LAYER_VISUALS.valves.opacity,
|
||||
"circle-stroke-width": 0
|
||||
}
|
||||
},
|
||||
@@ -173,11 +172,12 @@ layers.push(
|
||||
layout: {
|
||||
"icon-image": SUPPLY_ASSET_IMAGE_IDS.valve,
|
||||
"icon-size": SUPPLY_LAYER_VISUALS.valves.iconSize,
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true
|
||||
"icon-padding": 5,
|
||||
"icon-allow-overlap": false,
|
||||
"icon-ignore-placement": false
|
||||
},
|
||||
paint: {
|
||||
"icon-opacity": supplyIconOpacityExpression
|
||||
"icon-opacity": SUPPLY_LAYER_VISUALS.valves.iconOpacity
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -189,7 +189,7 @@ layers.push(
|
||||
paint: {
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.reservoirs.haloRadius,
|
||||
"circle-opacity": 0.94
|
||||
"circle-opacity": SUPPLY_LAYER_VISUALS.reservoirs.haloOpacity
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -214,11 +214,12 @@ layers.push(
|
||||
layout: {
|
||||
"icon-image": SUPPLY_ASSET_IMAGE_IDS.reservoir,
|
||||
"icon-size": SUPPLY_LAYER_VISUALS.reservoirs.iconSize,
|
||||
"icon-padding": 6,
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true
|
||||
"icon-ignore-placement": false
|
||||
},
|
||||
paint: {
|
||||
"icon-opacity": supplyIconOpacityExpression
|
||||
"icon-opacity": SUPPLY_LAYER_VISUALS.reservoirs.iconOpacity
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -230,11 +231,12 @@ layers.push(
|
||||
layout: {
|
||||
"icon-image": SUPPLY_ASSET_IMAGE_IDS.pump,
|
||||
"icon-size": SUPPLY_LAYER_VISUALS.pumps.iconSize,
|
||||
"icon-padding": 6,
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true
|
||||
"icon-ignore-placement": false
|
||||
},
|
||||
paint: {
|
||||
"icon-opacity": supplyIconOpacityExpression
|
||||
"icon-opacity": SUPPLY_LAYER_VISUALS.pumps.iconOpacity
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -246,11 +248,12 @@ layers.push(
|
||||
layout: {
|
||||
"icon-image": SUPPLY_ASSET_IMAGE_IDS.tank,
|
||||
"icon-size": SUPPLY_LAYER_VISUALS.tanks.iconSize,
|
||||
"icon-padding": 6,
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true
|
||||
"icon-ignore-placement": false
|
||||
},
|
||||
paint: {
|
||||
"icon-opacity": supplyIconOpacityExpression
|
||||
"icon-opacity": SUPPLY_LAYER_VISUALS.tanks.iconOpacity
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -66,10 +66,11 @@ describe("workbench supply layer controls", () => {
|
||||
collection: "fx-water-assets-20200423",
|
||||
collectionLabel: "2020-04-23 · 全网除头桥快照",
|
||||
featureCount: 215_797,
|
||||
minZoom: 10,
|
||||
minZoom: 8.5,
|
||||
visible: false
|
||||
});
|
||||
expect(items.find((item) => item.id === "fx_20200811_dn100_pipes")).toMatchObject({
|
||||
minZoom: 8.5,
|
||||
collection: "fx-pipe-inventory-20200811",
|
||||
featureCount: 122_686,
|
||||
visual: { kind: "line", dashed: true }
|
||||
@@ -77,9 +78,15 @@ describe("workbench supply layer controls", () => {
|
||||
expect(items.find((item) => item.id === "water_quality_monitoring_points")).toMatchObject({
|
||||
group: "monitoring",
|
||||
groupLabel: "监测点",
|
||||
minZoom: 11,
|
||||
visual: { kind: "image", imageSrc: "/map/water-quality-monitoring.svg" },
|
||||
visible: true
|
||||
});
|
||||
expect(items.find((item) => item.id === "fx_20200423_flow_meters")).toMatchObject({ minZoom: 11 });
|
||||
for (const id of ["fx_20200423_hydrants", "fx_20200423_service_connections",
|
||||
"fx_20200423_customer_meters", "fx_20200423_non_residential_meters"]) {
|
||||
expect(items.find((item) => item.id === id)).toMatchObject({ minZoom: 12 });
|
||||
}
|
||||
});
|
||||
|
||||
it("omits data-dependent controls when their GeoServer layers are empty", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
MapLegendItem
|
||||
} from "@/features/map/core";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import { SOURCE_GIS_MIN_ZOOM, SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import {
|
||||
AVAILABLE_WATER_NETWORK_SOURCE_IDS,
|
||||
SUPPLY_LAYER_CATALOG,
|
||||
@@ -67,13 +68,13 @@ const LAYER_CONTROL_PRESENTATION: Record<
|
||||
}
|
||||
},
|
||||
junctions: {
|
||||
description: "拓扑连接点",
|
||||
minZoom: 15,
|
||||
description: "拓扑连接点 · 随缩放细化",
|
||||
minZoom: SUPPLY_LAYER_VISUALS.junctions.minzoom,
|
||||
visual: { kind: "point", color: MAP_STYLE_TOKENS.supply.junction }
|
||||
},
|
||||
valves: {
|
||||
description: "控制设施",
|
||||
minZoom: 8.5,
|
||||
description: "控制设施 · 符号自动避让",
|
||||
minZoom: SUPPLY_LAYER_VISUALS.valves.minzoom,
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.valve }
|
||||
},
|
||||
reservoirs: {
|
||||
@@ -83,22 +84,22 @@ const LAYER_CONTROL_PRESENTATION: Record<
|
||||
},
|
||||
pumps: {
|
||||
description: "加压设施",
|
||||
minZoom: 11,
|
||||
minZoom: SUPPLY_LAYER_VISUALS.pumps.minzoom,
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.pump }
|
||||
},
|
||||
tanks: {
|
||||
description: "调蓄设施",
|
||||
minZoom: 11,
|
||||
minZoom: SUPPLY_LAYER_VISUALS.tanks.minzoom,
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.tank }
|
||||
},
|
||||
water_quality_monitoring_points: {
|
||||
description: "灵敏度分析选点",
|
||||
minZoom: 8,
|
||||
minZoom: SUPPLY_LAYER_VISUALS.waterQuality.minzoom,
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.waterQuality }
|
||||
},
|
||||
fx_20200423_pipes: {
|
||||
description: "全口径管线",
|
||||
minZoom: 10,
|
||||
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_pipes,
|
||||
collection: "fx-water-assets-20200423",
|
||||
collectionLabel: "2020-04-23 · 全网除头桥快照",
|
||||
visual: {
|
||||
@@ -112,7 +113,7 @@ const LAYER_CONTROL_PRESENTATION: Record<
|
||||
},
|
||||
fx_20200811_dn100_pipes: {
|
||||
description: "DN100 及以上",
|
||||
minZoom: 9,
|
||||
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200811_dn100_pipes,
|
||||
collection: "fx-pipe-inventory-20200811",
|
||||
collectionLabel: "2020-08-11 · 专项管线台账",
|
||||
visual: {
|
||||
@@ -123,49 +124,49 @@ const LAYER_CONTROL_PRESENTATION: Record<
|
||||
},
|
||||
fx_20200423_valves: {
|
||||
description: "源设施台账",
|
||||
minZoom: 14,
|
||||
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_valves,
|
||||
collection: "fx-water-assets-20200423",
|
||||
collectionLabel: "2020-04-23 · 全网除头桥快照",
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceValve }
|
||||
},
|
||||
fx_20200423_drain_valves: {
|
||||
description: "源设施台账",
|
||||
minZoom: 10,
|
||||
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_drain_valves,
|
||||
collection: "fx-water-assets-20200423",
|
||||
collectionLabel: "2020-04-23 · 全网除头桥快照",
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceDrainValve }
|
||||
},
|
||||
fx_20200423_flow_meters: {
|
||||
description: "源仪表台账",
|
||||
minZoom: 10,
|
||||
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_flow_meters,
|
||||
collection: "fx-water-assets-20200423",
|
||||
collectionLabel: "2020-04-23 · 全网除头桥快照",
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceFlowMeter }
|
||||
},
|
||||
fx_20200423_hydrants: {
|
||||
description: "源设施台账",
|
||||
minZoom: 13,
|
||||
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_hydrants,
|
||||
collection: "fx-water-assets-20200423",
|
||||
collectionLabel: "2020-04-23 · 全网除头桥快照",
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceHydrant }
|
||||
},
|
||||
fx_20200423_service_connections: {
|
||||
description: "源设施台账",
|
||||
minZoom: 13,
|
||||
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_service_connections,
|
||||
collection: "fx-water-assets-20200423",
|
||||
collectionLabel: "2020-04-23 · 全网除头桥快照",
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceConnection }
|
||||
},
|
||||
fx_20200423_customer_meters: {
|
||||
description: "源水表台账",
|
||||
minZoom: 13,
|
||||
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_customer_meters,
|
||||
collection: "fx-water-assets-20200423",
|
||||
collectionLabel: "2020-04-23 · 全网除头桥快照",
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceCustomerMeter }
|
||||
},
|
||||
fx_20200423_non_residential_meters: {
|
||||
description: "源水表台账",
|
||||
minZoom: 15,
|
||||
minZoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_non_residential_meters,
|
||||
collection: "fx-water-assets-20200423",
|
||||
collectionLabel: "2020-04-23 · 全网除头桥快照",
|
||||
visual: { kind: "image", imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceNonResidentialMeter }
|
||||
@@ -526,7 +527,7 @@ export function createLayerControlItems(
|
||||
id: item.id,
|
||||
label: item.label,
|
||||
description: item.description,
|
||||
minZoom: 8.5,
|
||||
minZoom: item.minZoom,
|
||||
visual: {
|
||||
kind: "image" as const,
|
||||
imageSrc:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ExpressionSpecification, StyleSpecification } from "maplibre-gl";
|
||||
import { createSupplyZoomOpacity } from "./supply-style-expressions";
|
||||
|
||||
type LayerSpecification = StyleSpecification["layers"][number];
|
||||
type LineLayerSpecification = Extract<LayerSpecification, { type: "line" }>;
|
||||
@@ -9,6 +10,7 @@ type CircleRadius = NonNullable<NonNullable<CircleLayerSpecification["paint"]>["
|
||||
type IconSize = NonNullable<NonNullable<SymbolLayerSpecification["layout"]>["icon-size"]>;
|
||||
|
||||
export const MAP_MAX_ZOOM = 24;
|
||||
export const MONITORING_POINT_MIN_ZOOM = 11;
|
||||
|
||||
const lineHitWidth = [
|
||||
"interpolate",
|
||||
@@ -58,17 +60,17 @@ const supplyPipeWidth = [
|
||||
["linear"],
|
||||
["zoom"],
|
||||
9,
|
||||
pipeWidthAtZoom(0.7, 1, 1.4),
|
||||
pipeWidthAtZoom(0.4, 0.7, 1.1),
|
||||
11,
|
||||
pipeWidthAtZoom(0.9, 1.3, 1.8),
|
||||
pipeWidthAtZoom(0.65, 1.05, 1.6),
|
||||
14,
|
||||
pipeWidthAtZoom(1.2, 1.8, 2.6),
|
||||
pipeWidthAtZoom(1.3, 2, 2.9),
|
||||
17,
|
||||
pipeWidthAtZoom(1.8, 2.8, 4),
|
||||
pipeWidthAtZoom(2.2, 3.4, 4.8),
|
||||
20,
|
||||
pipeWidthAtZoom(2.6, 4.2, 6),
|
||||
pipeWidthAtZoom(3.2, 5, 7),
|
||||
24,
|
||||
pipeWidthAtZoom(3.2, 5.2, 7)
|
||||
pipeWidthAtZoom(4.2, 6.8, 9.2)
|
||||
] as unknown as LineWidth;
|
||||
|
||||
const supplyPipeCasingWidth = [
|
||||
@@ -76,39 +78,19 @@ const supplyPipeCasingWidth = [
|
||||
["linear"],
|
||||
["zoom"],
|
||||
9,
|
||||
pipeWidthAtZoom(2.1, 2.4, 2.8),
|
||||
pipeWidthAtZoom(1.2, 1.5, 1.9),
|
||||
11,
|
||||
pipeWidthAtZoom(2.3, 2.7, 3.2),
|
||||
pipeWidthAtZoom(1.65, 2.05, 2.6),
|
||||
14,
|
||||
pipeWidthAtZoom(2.7, 3.3, 4.1),
|
||||
pipeWidthAtZoom(2.7, 3.4, 4.3),
|
||||
17,
|
||||
pipeWidthAtZoom(3.6, 4.6, 5.8),
|
||||
pipeWidthAtZoom(4, 5.2, 6.6),
|
||||
20,
|
||||
pipeWidthAtZoom(4.5, 6.1, 7.9),
|
||||
pipeWidthAtZoom(5.2, 7, 9),
|
||||
24,
|
||||
pipeWidthAtZoom(5.2, 7.2, 9)
|
||||
pipeWidthAtZoom(6.6, 9.2, 11.6)
|
||||
] as unknown as LineWidth;
|
||||
|
||||
const facilityRadius = [
|
||||
"interpolate", ["linear"], ["zoom"],
|
||||
8.5, 3,
|
||||
10, 4.2,
|
||||
12, 5.5,
|
||||
16, 7,
|
||||
20, 9,
|
||||
24, 11
|
||||
] as CircleRadius;
|
||||
|
||||
const facilityCasingRadius = [
|
||||
"interpolate", ["linear"], ["zoom"],
|
||||
8.5, 4,
|
||||
10, 5.4,
|
||||
12, 7,
|
||||
16, 8.5,
|
||||
20, 10.5,
|
||||
24, 12.5
|
||||
] as CircleRadius;
|
||||
|
||||
const facilityHoverRadius = [
|
||||
"interpolate", ["linear"], ["zoom"],
|
||||
8.5, 5,
|
||||
@@ -131,12 +113,26 @@ const facilitySelectedRadius = [
|
||||
|
||||
const facilityIconSize = [
|
||||
"interpolate", ["linear"], ["zoom"],
|
||||
8.5, 0.18,
|
||||
10, 0.23,
|
||||
12, 0.29,
|
||||
16, 0.36,
|
||||
20, 0.46,
|
||||
24, 0.56
|
||||
8.5, 0.1,
|
||||
12, 0.16,
|
||||
16, 0.28,
|
||||
20, 0.4,
|
||||
24, 0.5
|
||||
] as IconSize;
|
||||
|
||||
const importantFacilityIconOpacity = createSupplyZoomOpacity([
|
||||
[8, 0.78], [12, 0.88], [16, 0.98]
|
||||
]);
|
||||
const importantFacilityHaloOpacity = createSupplyZoomOpacity([
|
||||
[8, 0.35], [12, 0.55], [16, 0.8]
|
||||
]);
|
||||
|
||||
const monitoringPointRadius = ["interpolate", ["linear"], ["zoom"], 8.5, 1.5, 12, 2, 16, 3, 20, 4.5, 24, 5.5] as CircleRadius;
|
||||
const monitoringPointHaloRadius = ["interpolate", ["linear"], ["zoom"], 8.5, 2.5, 12, 3.5, 16, 5, 20, 7, 24, 8] as CircleRadius;
|
||||
const monitoringIconStops = [[8.5, 0.18], [12, 0.26], [16, 0.4], [20, 0.52], [24, 0.64]] as const;
|
||||
const monitoringIconSize = (pixelRatio: number) => [
|
||||
"interpolate", ["linear"], ["zoom"],
|
||||
...monitoringIconStops.flatMap(([zoom, size]) => [zoom, size * pixelRatio])
|
||||
] as IconSize;
|
||||
|
||||
export const SUPPLY_LAYER_VISUALS = {
|
||||
@@ -151,20 +147,24 @@ export const SUPPLY_LAYER_VISUALS = {
|
||||
hitWidth: lineHitWidth
|
||||
},
|
||||
junctions: {
|
||||
minzoom: 15,
|
||||
haloRadius: ["interpolate", ["linear"], ["zoom"], 13, 2.8, 17, 6] as CircleRadius,
|
||||
haloOpacity: ["interpolate", ["linear"], ["zoom"], 15, 0, 17, 0, 19, 0.55, 24, 0.82] as const,
|
||||
radius: ["interpolate", ["linear"], ["zoom"], 13, 1.5, 17, 3.8] as CircleRadius,
|
||||
minzoom: 12,
|
||||
haloRadius: ["interpolate", ["linear"], ["zoom"], 8.5, 0.6, 13, 1.4, 17, 3.5, 24, 6] as CircleRadius,
|
||||
haloOpacity: ["interpolate", ["linear"], ["zoom"], 8.5, 0, 15, 0.08, 19, 0.45, 24, 0.7] as const,
|
||||
radius: ["interpolate", ["linear"], ["zoom"], 8.5, 0.15, 12, 0.4, 15, 1, 18, 2.6, 24, 3.8] as CircleRadius,
|
||||
opacity: createSupplyZoomOpacity([[8.5, 0.08], [12, 0.18], [15, 0.45], [18, 0.82]]),
|
||||
hoverRadius: ["interpolate", ["linear"], ["zoom"], 13, 5, 17, 10] as CircleRadius,
|
||||
selectedRadius: ["interpolate", ["linear"], ["zoom"], 13, 5.5, 17, 10.5] as CircleRadius,
|
||||
hitRadius: pointHitRadius
|
||||
hitRadius: ["interpolate", ["linear"], ["zoom"], 8.5, 0.5, 12, 1, 15, 3, 18, 7, 24, 10] as CircleRadius
|
||||
},
|
||||
valves: {
|
||||
minzoom: 8.5,
|
||||
minzoom: 13,
|
||||
casingWidth: ["interpolate", ["linear"], ["zoom"], 11, 3.2, 17, 7.5, 22, 9, 24, 10] as LineWidth,
|
||||
width: ["interpolate", ["linear"], ["zoom"], 11, 1.6, 17, 4.4, 22, 6, 24, 7] as LineWidth,
|
||||
haloRadius: facilityCasingRadius,
|
||||
radius: facilityRadius,
|
||||
haloRadius: ["interpolate", ["linear"], ["zoom"], 8.5, 1, 12, 1.6, 16, 3, 20, 4.5, 24, 5.5] as CircleRadius,
|
||||
haloOpacity: createSupplyZoomOpacity([[8.5, 0.12], [12, 0.25], [16, 0.5], [20, 0.7]]),
|
||||
radius: ["interpolate", ["linear"], ["zoom"], 8.5, 0.6, 12, 1, 16, 2, 20, 3.2, 24, 4] as CircleRadius,
|
||||
opacity: createSupplyZoomOpacity([[8.5, 0.18], [12, 0.32], [16, 0.6], [20, 0.8]]),
|
||||
iconOpacity: createSupplyZoomOpacity([[8.5, 0.3], [12, 0.5], [16, 0.9], [20, 0.98]]),
|
||||
hoverOutlineWidth: ["interpolate", ["linear"], ["zoom"], 11, 6, 17, 10.5, 22, 12.5, 24, 14] as LineWidth,
|
||||
hoverWidth: ["interpolate", ["linear"], ["zoom"], 11, 4, 17, 7, 22, 9, 24, 10.5] as LineWidth,
|
||||
hoverRadius: facilityHoverRadius,
|
||||
@@ -177,6 +177,8 @@ export const SUPPLY_LAYER_VISUALS = {
|
||||
},
|
||||
reservoirs: {
|
||||
minzoom: 8.5,
|
||||
iconOpacity: importantFacilityIconOpacity,
|
||||
haloOpacity: importantFacilityHaloOpacity,
|
||||
haloRadius: ["interpolate", ["linear"], ["zoom"], 8.5, 7, 10, 8.5, 12, 9.5, 16, 11.5, 20, 13.5, 24, 15.5] as CircleRadius,
|
||||
radius: ["interpolate", ["linear"], ["zoom"], 8.5, 5.5, 10, 7, 12, 8, 16, 10, 20, 12, 24, 14] as CircleRadius,
|
||||
hoverRadius: ["interpolate", ["linear"], ["zoom"], 8.5, 8, 10, 9.5, 12, 10.5, 16, 12.5, 20, 14.5, 24, 16.5] as CircleRadius,
|
||||
@@ -185,26 +187,53 @@ export const SUPPLY_LAYER_VISUALS = {
|
||||
iconSize: ["interpolate", ["linear"], ["zoom"], 8.5, 0.28, 10, 0.36, 12, 0.42, 16, 0.52, 20, 0.64, 24, 0.76] as IconSize
|
||||
},
|
||||
scada: {
|
||||
minzoom: 8.5,
|
||||
haloRadius: ["interpolate", ["linear"], ["zoom"], 8.5, 5.5, 10, 7, 12, 8.5, 16, 10, 20, 12, 24, 14] as CircleRadius,
|
||||
radius: ["interpolate", ["linear"], ["zoom"], 8.5, 4, 10, 5.5, 12, 7, 16, 8.5, 20, 10.5, 24, 12.5] as CircleRadius,
|
||||
minzoom: MONITORING_POINT_MIN_ZOOM,
|
||||
haloRadius: monitoringPointHaloRadius,
|
||||
radius: monitoringPointRadius,
|
||||
haloOpacity: createSupplyZoomOpacity([[8.5, 0.45], [12, 0.55], [16, 0.7]]),
|
||||
opacity: createSupplyZoomOpacity([[8.5, 0.55], [12, 0.65], [16, 0.8]]),
|
||||
iconOpacity: createSupplyZoomOpacity([[8.5, 0.75], [12, 0.85], [16, 0.98]]),
|
||||
hoverRadius: ["interpolate", ["linear"], ["zoom"], 8.5, 6.5, 10, 8, 12, 9.5, 16, 11, 20, 13, 24, 15] as CircleRadius,
|
||||
selectedRadius: ["interpolate", ["linear"], ["zoom"], 8.5, 7, 10, 8.5, 12, 10, 16, 11.5, 20, 13.5, 24, 15.5] as CircleRadius,
|
||||
hitRadius: pointHitRadius,
|
||||
iconSize: ["interpolate", ["linear"], ["zoom"], 8.5, 0.25, 10, 0.32, 12, 0.4, 16, 0.48, 20, 0.6, 24, 0.72] as IconSize
|
||||
iconSize: monitoringIconSize(1)
|
||||
},
|
||||
pumps: {
|
||||
minzoom: 11,
|
||||
minzoom: 8.5,
|
||||
iconOpacity: importantFacilityIconOpacity,
|
||||
hoverRadius: 14,
|
||||
selectedRadius: 12.5,
|
||||
hitRadius: 16,
|
||||
iconSize: ["interpolate", ["linear"], ["zoom"], 11, 0.44, 16, 0.66, 20, 0.82, 24, 0.96] as const
|
||||
iconSize: ["interpolate", ["linear"], ["zoom"], 8.5, 0.3, 12, 0.44, 16, 0.66, 20, 0.82, 24, 0.96] as const
|
||||
},
|
||||
tanks: {
|
||||
minzoom: 11,
|
||||
minzoom: 8.5,
|
||||
iconOpacity: importantFacilityIconOpacity,
|
||||
hoverRadius: 14,
|
||||
selectedRadius: 12.5,
|
||||
hitRadius: 16,
|
||||
iconSize: ["interpolate", ["linear"], ["zoom"], 11, 0.44, 16, 0.66, 20, 0.82, 24, 0.96] as const
|
||||
iconSize: ["interpolate", ["linear"], ["zoom"], 8.5, 0.3, 12, 0.44, 16, 0.66, 20, 0.82, 24, 0.96] as const
|
||||
},
|
||||
waterQuality: {
|
||||
minzoom: MONITORING_POINT_MIN_ZOOM,
|
||||
iconOpacity: importantFacilityIconOpacity,
|
||||
haloOpacity: importantFacilityHaloOpacity,
|
||||
opacity: createSupplyZoomOpacity([[8, 0.55], [12, 0.7], [16, 0.9]]),
|
||||
radius: monitoringPointRadius,
|
||||
haloRadius: monitoringPointHaloRadius,
|
||||
// Water-quality artwork is 128px; SCADA artwork is 96px, both registered at 2x.
|
||||
iconSize: monitoringIconSize(96 / 128)
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const SOURCE_GIS_MIN_ZOOM = {
|
||||
fx_20200423_pipes: SUPPLY_LAYER_VISUALS.pipes.minzoom,
|
||||
fx_20200811_dn100_pipes: SUPPLY_LAYER_VISUALS.pipes.minzoom,
|
||||
fx_20200423_valves: SUPPLY_LAYER_VISUALS.valves.minzoom,
|
||||
fx_20200423_drain_valves: SUPPLY_LAYER_VISUALS.valves.minzoom,
|
||||
fx_20200423_flow_meters: MONITORING_POINT_MIN_ZOOM,
|
||||
fx_20200423_hydrants: SUPPLY_LAYER_VISUALS.junctions.minzoom,
|
||||
fx_20200423_service_connections: SUPPLY_LAYER_VISUALS.junctions.minzoom,
|
||||
fx_20200423_customer_meters: SUPPLY_LAYER_VISUALS.junctions.minzoom,
|
||||
fx_20200423_non_residential_meters: SUPPLY_LAYER_VISUALS.junctions.minzoom
|
||||
} as const;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ExpressionSpecification, StyleSpecification } from "maplibre-gl";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import { SOURCE_GIS_MIN_ZOOM, SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import {
|
||||
SOURCE_LAYERS,
|
||||
type GeoServerWaterNetworkSourceId
|
||||
@@ -24,19 +24,19 @@ const definitions: PublishedLayerDefinition[] = [
|
||||
sourceId: "water_quality_monitoring_points",
|
||||
geometry: "point",
|
||||
color: MAP_STYLE_TOKENS.supply.waterQuality,
|
||||
minzoom: 8,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.waterQuality.minzoom,
|
||||
radius: 5.5,
|
||||
imageId: SUPPLY_ASSET_IMAGE_IDS.waterQuality
|
||||
},
|
||||
{ sourceId: "fx_20200423_pipes", geometry: "line", color: MAP_STYLE_TOKENS.supply.sourcePipe, minzoom: 10, lineVariant: "network" },
|
||||
{ sourceId: "fx_20200811_dn100_pipes", geometry: "line", color: MAP_STYLE_TOKENS.supply.sourcePipeInventory, minzoom: 9, lineVariant: "inventory" },
|
||||
{ sourceId: "fx_20200423_valves", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceValve, minzoom: 14, radius: 3.2, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceValve },
|
||||
{ sourceId: "fx_20200423_drain_valves", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceDrainValve, minzoom: 10, radius: 3.6, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceDrainValve },
|
||||
{ sourceId: "fx_20200423_flow_meters", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceFlowMeter, minzoom: 10, radius: 4.2, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceFlowMeter },
|
||||
{ sourceId: "fx_20200423_hydrants", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceHydrant, minzoom: 13, radius: 3.5, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceHydrant },
|
||||
{ sourceId: "fx_20200423_service_connections", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceConnection, minzoom: 13, radius: 3, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceConnection },
|
||||
{ sourceId: "fx_20200423_customer_meters", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceCustomerMeter, minzoom: 13, radius: 3, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceCustomerMeter },
|
||||
{ sourceId: "fx_20200423_non_residential_meters", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceNonResidentialMeter, minzoom: 15, radius: 2.8, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceNonResidentialMeter }
|
||||
{ sourceId: "fx_20200423_pipes", geometry: "line", color: MAP_STYLE_TOKENS.supply.sourcePipe, minzoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_pipes, lineVariant: "network" },
|
||||
{ sourceId: "fx_20200811_dn100_pipes", geometry: "line", color: MAP_STYLE_TOKENS.supply.sourcePipeInventory, minzoom: SOURCE_GIS_MIN_ZOOM.fx_20200811_dn100_pipes, lineVariant: "inventory" },
|
||||
{ sourceId: "fx_20200423_valves", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceValve, minzoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_valves, radius: 3.2, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceValve },
|
||||
{ sourceId: "fx_20200423_drain_valves", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceDrainValve, minzoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_drain_valves, radius: 3.6, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceDrainValve },
|
||||
{ sourceId: "fx_20200423_flow_meters", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceFlowMeter, minzoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_flow_meters, radius: 4.2, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceFlowMeter },
|
||||
{ sourceId: "fx_20200423_hydrants", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceHydrant, minzoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_hydrants, radius: 3.5, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceHydrant },
|
||||
{ sourceId: "fx_20200423_service_connections", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceConnection, minzoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_service_connections, radius: 3, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceConnection },
|
||||
{ sourceId: "fx_20200423_customer_meters", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceCustomerMeter, minzoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_customer_meters, radius: 3, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceCustomerMeter },
|
||||
{ sourceId: "fx_20200423_non_residential_meters", geometry: "point", color: MAP_STYLE_TOKENS.supply.sourceNonResidentialMeter, minzoom: SOURCE_GIS_MIN_ZOOM.fx_20200423_non_residential_meters, radius: 2.8, imageId: SUPPLY_ASSET_IMAGE_IDS.sourceNonResidentialMeter }
|
||||
];
|
||||
|
||||
const sourcePipeColorExpression: ExpressionSpecification = [
|
||||
@@ -102,9 +102,10 @@ function createBusinessLayers(definition: PublishedLayerDefinition): Layer[] {
|
||||
}
|
||||
|
||||
const radius = definition.radius ?? 3.2;
|
||||
const iconSize = definition.sourceId === "water_quality_monitoring_points"
|
||||
? ["interpolate", ["linear"], ["zoom"], definition.minzoom, 0.25, 18, 0.42] as ExpressionSpecification
|
||||
: ["interpolate", ["linear"], ["zoom"], definition.minzoom, 0.36, 18, 0.54] as ExpressionSpecification;
|
||||
const important = definition.sourceId === "water_quality_monitoring_points";
|
||||
const iconSize = important
|
||||
? SUPPLY_LAYER_VISUALS.waterQuality.iconSize
|
||||
: SUPPLY_LAYER_VISUALS.valves.iconSize;
|
||||
const layers: Layer[] = [
|
||||
{
|
||||
id: `${definition.sourceId}-halo`,
|
||||
@@ -114,8 +115,9 @@ function createBusinessLayers(definition: PublishedLayerDefinition): Layer[] {
|
||||
minzoom: definition.minzoom,
|
||||
paint: {
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-radius": radius + 1.8,
|
||||
"circle-opacity": 0.9
|
||||
"circle-radius": important ? SUPPLY_LAYER_VISUALS.waterQuality.haloRadius
|
||||
: ["interpolate", ["linear"], ["zoom"], 8.5, radius * 0.3, 12, radius * 0.5, 18, radius + 1],
|
||||
"circle-opacity": important ? SUPPLY_LAYER_VISUALS.waterQuality.haloOpacity : SUPPLY_LAYER_VISUALS.valves.haloOpacity
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -126,8 +128,10 @@ function createBusinessLayers(definition: PublishedLayerDefinition): Layer[] {
|
||||
minzoom: definition.minzoom,
|
||||
paint: {
|
||||
"circle-color": definition.color,
|
||||
"circle-radius": ["interpolate", ["linear"], ["zoom"], definition.minzoom, radius, 18, radius * 1.35],
|
||||
"circle-opacity": 0.9,
|
||||
"circle-radius": important
|
||||
? SUPPLY_LAYER_VISUALS.waterQuality.radius
|
||||
: ["interpolate", ["linear"], ["zoom"], 8.5, radius * 0.18, 12, radius * 0.3, 18, radius],
|
||||
"circle-opacity": important ? SUPPLY_LAYER_VISUALS.waterQuality.opacity : SUPPLY_LAYER_VISUALS.valves.opacity,
|
||||
"circle-stroke-color": definition.sourceId === "water_quality_monitoring_points"
|
||||
? MAP_STYLE_TOKENS.canvas.casing
|
||||
: definition.color,
|
||||
@@ -145,11 +149,12 @@ function createBusinessLayers(definition: PublishedLayerDefinition): Layer[] {
|
||||
layout: {
|
||||
"icon-image": definition.imageId,
|
||||
"icon-size": iconSize,
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true
|
||||
"icon-padding": important ? 6 : 5,
|
||||
"icon-allow-overlap": important,
|
||||
"icon-ignore-placement": false
|
||||
},
|
||||
paint: {
|
||||
"icon-opacity": 0.96
|
||||
"icon-opacity": important ? SUPPLY_LAYER_VISUALS.waterQuality.iconOpacity : SUPPLY_LAYER_VISUALS.valves.iconOpacity
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import {
|
||||
createSupplyStateColor,
|
||||
supplyFacilityOpacityExpression,
|
||||
supplyIconOpacityExpression
|
||||
} from "./supply-style-expressions";
|
||||
|
||||
@@ -34,6 +33,7 @@ export const SCADA_LAYER_CONTROL_ITEMS = [
|
||||
label: "压力监测点",
|
||||
description: "83 个压力监测点",
|
||||
deviceType: "pressure",
|
||||
minZoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
imageId: SCADA_IMAGE_IDS.pressure,
|
||||
color: MAP_STYLE_TOKENS.supply.scadaPressure
|
||||
},
|
||||
@@ -42,6 +42,7 @@ export const SCADA_LAYER_CONTROL_ITEMS = [
|
||||
label: "流量监测点",
|
||||
description: "35 个流量监测点",
|
||||
deviceType: "pipe_flow",
|
||||
minZoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
imageId: SCADA_IMAGE_IDS.flow,
|
||||
color: MAP_STYLE_TOKENS.supply.scadaFlow
|
||||
}
|
||||
@@ -80,11 +81,11 @@ function createScadaLayers(definition: (typeof SCADA_LAYER_CONTROL_ITEMS)[number
|
||||
type: "circle",
|
||||
source: SCADA_SOURCE_ID,
|
||||
filter,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
minzoom: definition.minZoom,
|
||||
paint: {
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.scada.haloRadius,
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-opacity": 0.94
|
||||
"circle-opacity": SUPPLY_LAYER_VISUALS.scada.haloOpacity
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -92,11 +93,11 @@ function createScadaLayers(definition: (typeof SCADA_LAYER_CONTROL_ITEMS)[number
|
||||
type: "circle",
|
||||
source: SCADA_SOURCE_ID,
|
||||
filter,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
minzoom: definition.minZoom,
|
||||
paint: {
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.scada.radius,
|
||||
"circle-color": color,
|
||||
"circle-opacity": supplyFacilityOpacityExpression
|
||||
"circle-opacity": SUPPLY_LAYER_VISUALS.scada.opacity
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -104,14 +105,15 @@ function createScadaLayers(definition: (typeof SCADA_LAYER_CONTROL_ITEMS)[number
|
||||
type: "symbol",
|
||||
source: SCADA_SOURCE_ID,
|
||||
filter,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
minzoom: definition.minZoom,
|
||||
layout: {
|
||||
"icon-image": definition.imageId,
|
||||
"icon-size": SUPPLY_LAYER_VISUALS.scada.iconSize,
|
||||
"icon-allow-overlap": true,
|
||||
"icon-ignore-placement": true
|
||||
"icon-padding": 7,
|
||||
"icon-allow-overlap": false,
|
||||
"icon-ignore-placement": false
|
||||
},
|
||||
paint: { "icon-opacity": supplyIconOpacityExpression }
|
||||
paint: { "icon-opacity": SUPPLY_LAYER_VISUALS.scada.iconOpacity }
|
||||
}
|
||||
];
|
||||
const fallback: Layer[] = [
|
||||
@@ -121,7 +123,7 @@ function createScadaLayers(definition: (typeof SCADA_LAYER_CONTROL_ITEMS)[number
|
||||
type: "circle",
|
||||
source: SCADA_SOURCE_ID,
|
||||
filter,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
minzoom: definition.minZoom,
|
||||
paint: {
|
||||
"circle-radius": ["interpolate", ["linear"], ["zoom"], 9, 1.7, 12, 2.1, 16, 2.7, 20, 3.4, 24, 4.2],
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
@@ -135,7 +137,7 @@ function createScadaLayers(definition: (typeof SCADA_LAYER_CONTROL_ITEMS)[number
|
||||
type: "circle",
|
||||
source: SCADA_SOURCE_ID,
|
||||
filter,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
minzoom: definition.minZoom,
|
||||
paint: {
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.scada.hoverRadius,
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.transparent,
|
||||
@@ -150,7 +152,7 @@ function createScadaLayers(definition: (typeof SCADA_LAYER_CONTROL_ITEMS)[number
|
||||
type: "circle",
|
||||
source: SCADA_SOURCE_ID,
|
||||
filter,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
minzoom: definition.minZoom,
|
||||
paint: {
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.scada.selectedRadius,
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.transparent,
|
||||
@@ -165,7 +167,7 @@ function createScadaLayers(definition: (typeof SCADA_LAYER_CONTROL_ITEMS)[number
|
||||
type: "circle",
|
||||
source: SCADA_SOURCE_ID,
|
||||
filter,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
minzoom: definition.minZoom,
|
||||
paint: {
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.scada.selectedRadius,
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.transparent,
|
||||
@@ -181,7 +183,7 @@ function createScadaLayers(definition: (typeof SCADA_LAYER_CONTROL_ITEMS)[number
|
||||
type: "circle",
|
||||
source: SCADA_SOURCE_ID,
|
||||
filter,
|
||||
minzoom: SUPPLY_LAYER_VISUALS.scada.minzoom,
|
||||
minzoom: definition.minZoom,
|
||||
paint: {
|
||||
"circle-radius": SUPPLY_LAYER_VISUALS.scada.hitRadius,
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.transparent,
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { simulationLayerIds } from "./annotation-layers";
|
||||
import { setMapLayersVisibility } from "./layer-visibility";
|
||||
|
||||
export function setSimulationLayersVisibility(map: MapLibreMap, visible: boolean) {
|
||||
const visibility = visible ? "visible" : "none";
|
||||
|
||||
simulationLayerIds.forEach((layerId) => {
|
||||
if (map.getLayer(layerId)) {
|
||||
map.setLayoutProperty(layerId, "visibility", visibility);
|
||||
}
|
||||
});
|
||||
export function setSimulationLayersVisibility(map: MapLibreMap, visible: boolean, animate = true) {
|
||||
setMapLayersVisibility(map, simulationLayerIds, visible, animate);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import { SCADA_ICON_PATHS } from "./scada";
|
||||
|
||||
import {
|
||||
SUPPLY_ASSET_ICON_PATHS,
|
||||
@@ -9,6 +12,19 @@ import {
|
||||
} from "./supply-icons";
|
||||
|
||||
describe("supply facility map icons", () => {
|
||||
it("matches water-quality and pressure icon pixel widths at every zoom stop", () => {
|
||||
const imageWidth = (path: string) => readFileSync(`public${path}`).readUInt32BE(16);
|
||||
const pressureWidth = imageWidth(SCADA_ICON_PATHS.pressure);
|
||||
const waterQualityWidth = imageWidth(SUPPLY_ASSET_MAP_ICON_PATHS.waterQuality);
|
||||
const pressure = SUPPLY_LAYER_VISUALS.scada.iconSize as unknown as unknown[];
|
||||
const waterQuality = SUPPLY_LAYER_VISUALS.waterQuality.iconSize as unknown as unknown[];
|
||||
for (let index = 3; index < pressure.length; index += 2) {
|
||||
expect(waterQuality[index]).toBe(pressure[index]);
|
||||
expect(Number(waterQuality[index + 1]) * waterQualityWidth)
|
||||
.toBeCloseTo(Number(pressure[index + 1]) * pressureWidth);
|
||||
}
|
||||
});
|
||||
|
||||
it("uses MapLibre-compatible PNG assets from the public map directory", () => {
|
||||
expect(Object.keys(SUPPLY_ASSET_ICON_PATHS)).toEqual([
|
||||
"valve",
|
||||
|
||||
@@ -54,6 +54,17 @@ export const supplyIconOpacityExpression: ExpressionSpecification = [
|
||||
0.98
|
||||
];
|
||||
|
||||
export function createSupplyZoomOpacity(
|
||||
stops: readonly (readonly [number, number])[]
|
||||
): ExpressionSpecification {
|
||||
return [
|
||||
"interpolate", ["linear"], ["zoom"],
|
||||
...stops.flatMap(([zoom, opacity]) => [
|
||||
zoom, ["case", supplyInactiveExpression, opacity * 0.65, opacity]
|
||||
])
|
||||
] as ExpressionSpecification;
|
||||
}
|
||||
|
||||
export function createSupplyStateColor(
|
||||
activeColor: string | ExpressionSpecification
|
||||
): ExpressionSpecification {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type {
|
||||
SymbolLayerSpecification
|
||||
} from "maplibre-gl";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import { SOURCE_LAYERS } from "./sources";
|
||||
|
||||
export const VALUE_LABEL_LAYER_IDS = {
|
||||
@@ -36,12 +37,12 @@ export function createValueLabelLayers(): SymbolLayerSpecification[] {
|
||||
type: "symbol",
|
||||
source: sourceId,
|
||||
"source-layer": SOURCE_LAYERS[sourceId],
|
||||
minzoom: sourceId === "junctions" ? 15 : 10,
|
||||
minzoom: Math.max(10, SUPPLY_LAYER_VISUALS[sourceId].minzoom),
|
||||
layout: {
|
||||
visibility: "none",
|
||||
"symbol-placement": line ? "line-center" : "point",
|
||||
"text-field": "",
|
||||
"text-size": 12,
|
||||
"text-size": createValueLabelTextSize(12),
|
||||
"text-offset": line ? [0, 0] : [0, -1.35],
|
||||
"text-anchor": line ? "center" : "bottom",
|
||||
"text-padding": 5,
|
||||
@@ -54,7 +55,7 @@ export function createValueLabelLayers(): SymbolLayerSpecification[] {
|
||||
"text-halo-color": MAP_STYLE_TOKENS.label.inverse,
|
||||
"text-halo-width": 2,
|
||||
"text-halo-blur": 0.4,
|
||||
"text-opacity": 0.96
|
||||
"text-opacity": ["interpolate", ["linear"], ["zoom"], 10, 0.3, 14, 0.8, 17, 0.96]
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -80,7 +81,7 @@ export function applyValueLabelStyle(
|
||||
"text-field",
|
||||
createValueLabelTextExpression(style)
|
||||
);
|
||||
map.setLayoutProperty(layerId, "text-size", style.textSize);
|
||||
map.setLayoutProperty(layerId, "text-size", createValueLabelTextSize(style.textSize));
|
||||
map.setPaintProperty(layerId, "text-color", style.textColor);
|
||||
map.setLayoutProperty(layerId, "visibility", "visible");
|
||||
}
|
||||
@@ -98,6 +99,10 @@ export function clearValueLabels(
|
||||
});
|
||||
}
|
||||
|
||||
function createValueLabelTextSize(textSize: number): ExpressionSpecification {
|
||||
return ["interpolate", ["linear"], ["zoom"], 10, Math.max(10, textSize * 0.8), 16, textSize];
|
||||
}
|
||||
|
||||
function createValueLabelTextExpression(
|
||||
style: ValueLabelStyle
|
||||
): ExpressionSpecification {
|
||||
|
||||
Reference in New Issue
Block a user