feat(map): refine SCADA feature experience
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import type { GeoJSONSourceSpecification, StyleSpecification } from "maplibre-gl";
|
||||
import { annotationPoints, impactAreaPolygon } from "../data/workbench-simulation";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
|
||||
export const SIMULATION_SOURCE_IDS = {
|
||||
impactArea: "simulation-impact-area",
|
||||
@@ -23,8 +24,8 @@ export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
||||
type: "fill",
|
||||
source: SIMULATION_SOURCE_IDS.impactArea,
|
||||
paint: {
|
||||
"fill-color": "#ef4444",
|
||||
"fill-opacity": 0.18
|
||||
"fill-color": MAP_STYLE_TOKENS.state.risk,
|
||||
"fill-opacity": MAP_STYLE_TOKENS.opacity.area
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -32,7 +33,7 @@ export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
||||
type: "line",
|
||||
source: SIMULATION_SOURCE_IDS.impactArea,
|
||||
paint: {
|
||||
"line-color": "#ef4444",
|
||||
"line-color": MAP_STYLE_TOKENS.state.risk,
|
||||
"line-width": 1.5,
|
||||
"line-dasharray": [2, 2],
|
||||
"line-opacity": 0.72
|
||||
@@ -44,9 +45,9 @@ export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
||||
source: SIMULATION_SOURCE_IDS.annotations,
|
||||
filter: ["==", ["get", "kind"], "burst"],
|
||||
paint: {
|
||||
"circle-color": "#fef2f2",
|
||||
"circle-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-radius": 18,
|
||||
"circle-stroke-color": "#ef4444",
|
||||
"circle-stroke-color": MAP_STYLE_TOKENS.state.incident,
|
||||
"circle-stroke-width": 3,
|
||||
"circle-opacity": 0.88
|
||||
}
|
||||
@@ -57,9 +58,9 @@ export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
||||
source: SIMULATION_SOURCE_IDS.annotations,
|
||||
filter: ["==", ["get", "kind"], "burst"],
|
||||
paint: {
|
||||
"circle-color": "#ef4444",
|
||||
"circle-color": MAP_STYLE_TOKENS.state.incident,
|
||||
"circle-radius": 7,
|
||||
"circle-stroke-color": "#ffffff",
|
||||
"circle-stroke-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-stroke-width": 3
|
||||
}
|
||||
},
|
||||
@@ -75,8 +76,8 @@ export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
||||
"text-allow-overlap": true
|
||||
},
|
||||
paint: {
|
||||
"text-color": "#ffffff",
|
||||
"text-halo-color": "#2563eb",
|
||||
"text-color": MAP_STYLE_TOKENS.label.inverse,
|
||||
"text-halo-color": MAP_STYLE_TOKENS.state.agent,
|
||||
"text-halo-width": 9
|
||||
}
|
||||
},
|
||||
@@ -93,8 +94,8 @@ export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
||||
"text-allow-overlap": true
|
||||
},
|
||||
paint: {
|
||||
"text-color": "#dc2626",
|
||||
"text-halo-color": "#ffffff",
|
||||
"text-color": MAP_STYLE_TOKENS.state.risk,
|
||||
"text-halo-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"text-halo-width": 4
|
||||
}
|
||||
},
|
||||
@@ -110,8 +111,8 @@ export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
||||
"text-allow-overlap": true
|
||||
},
|
||||
paint: {
|
||||
"text-color": "#334155",
|
||||
"text-halo-color": "#ffffff",
|
||||
"text-color": MAP_STYLE_TOKENS.label.secondary,
|
||||
"text-halo-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"text-halo-width": 3
|
||||
}
|
||||
},
|
||||
@@ -129,8 +130,8 @@ export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
||||
"text-allow-overlap": true
|
||||
},
|
||||
paint: {
|
||||
"text-color": "#0f172a",
|
||||
"text-halo-color": "#ffffff",
|
||||
"text-color": MAP_STYLE_TOKENS.label.primary,
|
||||
"text-halo-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"text-halo-width": 5
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,18 +5,16 @@ import {
|
||||
getContrastRatio
|
||||
} from "./color-contrast";
|
||||
import {
|
||||
CLOSED_CONDUIT_COLOR,
|
||||
DRAINAGE_SELECTED_OUTLINE_COLORS,
|
||||
DRAINAGE_SOURCE_COLORS,
|
||||
DRAINAGE_SOURCE_OPACITIES,
|
||||
MAP_BACKGROUND_COLORS
|
||||
MAP_STYLE_TOKENS
|
||||
} from "./map-colors";
|
||||
import { createBaseStyle } from "./sources";
|
||||
|
||||
type DrainageColorId = keyof typeof DRAINAGE_SOURCE_COLORS;
|
||||
|
||||
const drainageColorIds = Object.keys(DRAINAGE_SOURCE_COLORS) as DrainageColorId[];
|
||||
const mapBackgrounds = Object.values(MAP_BACKGROUND_COLORS);
|
||||
const sourceColors = Object.values(MAP_STYLE_TOKENS.asset);
|
||||
const mapBackgrounds = [
|
||||
MAP_STYLE_TOKENS.canvas.primary,
|
||||
MAP_STYLE_TOKENS.canvas.secondary,
|
||||
MAP_STYLE_TOKENS.canvas.secondaryAlt
|
||||
];
|
||||
|
||||
describe("map color contrast", () => {
|
||||
it("calculates WCAG contrast and alpha composites", () => {
|
||||
@@ -32,12 +30,12 @@ describe("map color contrast", () => {
|
||||
});
|
||||
|
||||
it("keeps rendered source colors above 3:1 on every map background", () => {
|
||||
drainageColorIds.forEach((sourceId) => {
|
||||
sourceColors.forEach((sourceColor) => {
|
||||
mapBackgrounds.forEach((background) => {
|
||||
const renderedColor = compositeHexColor(
|
||||
DRAINAGE_SOURCE_COLORS[sourceId],
|
||||
sourceColor,
|
||||
background,
|
||||
DRAINAGE_SOURCE_OPACITIES[sourceId]
|
||||
MAP_STYLE_TOKENS.opacity.core
|
||||
);
|
||||
|
||||
expect(
|
||||
@@ -48,14 +46,14 @@ describe("map color contrast", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps selected outlines above 3:1 on every map background", () => {
|
||||
drainageColorIds.forEach((sourceId) => {
|
||||
it("keeps selected outlines above 4.5:1 on every map background", () => {
|
||||
sourceColors.forEach(() => {
|
||||
mapBackgrounds.forEach((background) => {
|
||||
const selectedColor = DRAINAGE_SELECTED_OUTLINE_COLORS[sourceId];
|
||||
const selectedColor = MAP_STYLE_TOKENS.state.selected;
|
||||
expect(
|
||||
getContrastRatio(selectedColor, background),
|
||||
createCoolorsContrastUrl(selectedColor, background)
|
||||
).toBeGreaterThanOrEqual(3);
|
||||
).toBeGreaterThanOrEqual(4.5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -63,9 +61,9 @@ describe("map color contrast", () => {
|
||||
it("keeps closed conduits above 3:1 and applies the primary map background", () => {
|
||||
mapBackgrounds.forEach((background) => {
|
||||
const renderedColor = compositeHexColor(
|
||||
CLOSED_CONDUIT_COLOR,
|
||||
MAP_STYLE_TOKENS.state.inactive,
|
||||
background,
|
||||
DRAINAGE_SOURCE_OPACITIES.conduits
|
||||
MAP_STYLE_TOKENS.opacity.core
|
||||
);
|
||||
expect(getContrastRatio(renderedColor, background)).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
@@ -74,6 +72,6 @@ describe("map color contrast", () => {
|
||||
if (backgroundLayer?.type !== "background") {
|
||||
throw new Error("Expected the base style to contain a background layer.");
|
||||
}
|
||||
expect(backgroundLayer.paint?.["background-color"]).toBe(MAP_BACKGROUND_COLORS.primary);
|
||||
expect(backgroundLayer.paint?.["background-color"]).toBe(MAP_STYLE_TOKENS.canvas.primary);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,8 @@ import type {
|
||||
} from "maplibre-gl";
|
||||
import type { FeatureCollection, Geometry, LineString, Point, Polygon } from "geojson";
|
||||
import { createCircleCoordinates } from "./geo";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import { WORKBENCH_INTERACTION_BEFORE_ID } from "./layers";
|
||||
|
||||
export const DRAWING_SOURCE_IDS = {
|
||||
features: "workbench-drawing-features",
|
||||
@@ -54,7 +56,7 @@ export const drawingLayers: StyleSpecification["layers"] = [
|
||||
source: DRAWING_SOURCE_IDS.features,
|
||||
filter: ["==", ["geometry-type"], "Polygon"],
|
||||
paint: {
|
||||
"fill-color": "#2563eb",
|
||||
"fill-color": MAP_STYLE_TOKENS.tool.geometry,
|
||||
"fill-opacity": 0.16
|
||||
}
|
||||
},
|
||||
@@ -64,7 +66,7 @@ export const drawingLayers: StyleSpecification["layers"] = [
|
||||
source: DRAWING_SOURCE_IDS.features,
|
||||
filter: ["==", ["geometry-type"], "Polygon"],
|
||||
paint: {
|
||||
"line-color": "#2563eb",
|
||||
"line-color": MAP_STYLE_TOKENS.tool.geometry,
|
||||
"line-width": 2,
|
||||
"line-opacity": 0.82
|
||||
}
|
||||
@@ -75,7 +77,7 @@ export const drawingLayers: StyleSpecification["layers"] = [
|
||||
source: DRAWING_SOURCE_IDS.features,
|
||||
filter: ["==", ["geometry-type"], "LineString"],
|
||||
paint: {
|
||||
"line-color": "#2563eb",
|
||||
"line-color": MAP_STYLE_TOKENS.tool.geometry,
|
||||
"line-width": 3,
|
||||
"line-opacity": 0.88
|
||||
}
|
||||
@@ -86,9 +88,9 @@ export const drawingLayers: StyleSpecification["layers"] = [
|
||||
source: DRAWING_SOURCE_IDS.features,
|
||||
filter: ["==", ["geometry-type"], "Point"],
|
||||
paint: {
|
||||
"circle-color": "#2563eb",
|
||||
"circle-color": MAP_STYLE_TOKENS.tool.geometry,
|
||||
"circle-radius": 6,
|
||||
"circle-stroke-color": "#ffffff",
|
||||
"circle-stroke-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-stroke-width": 2
|
||||
}
|
||||
},
|
||||
@@ -98,7 +100,7 @@ export const drawingLayers: StyleSpecification["layers"] = [
|
||||
source: DRAWING_SOURCE_IDS.features,
|
||||
filter: ["all", ["==", ["geometry-type"], "Polygon"], ["==", ["get", "id"], ""]],
|
||||
paint: {
|
||||
"line-color": "#ff7a45",
|
||||
"line-color": MAP_STYLE_TOKENS.state.selected,
|
||||
"line-width": 4,
|
||||
"line-opacity": 0.86
|
||||
}
|
||||
@@ -109,7 +111,7 @@ export const drawingLayers: StyleSpecification["layers"] = [
|
||||
source: DRAWING_SOURCE_IDS.features,
|
||||
filter: ["all", ["==", ["geometry-type"], "LineString"], ["==", ["get", "id"], ""]],
|
||||
paint: {
|
||||
"line-color": "#ff7a45",
|
||||
"line-color": MAP_STYLE_TOKENS.state.selected,
|
||||
"line-width": 6,
|
||||
"line-opacity": 0.72
|
||||
}
|
||||
@@ -120,10 +122,10 @@ export const drawingLayers: StyleSpecification["layers"] = [
|
||||
source: DRAWING_SOURCE_IDS.features,
|
||||
filter: ["all", ["==", ["geometry-type"], "Point"], ["==", ["get", "id"], ""]],
|
||||
paint: {
|
||||
"circle-color": "#ff7a45",
|
||||
"circle-color": MAP_STYLE_TOKENS.state.selected,
|
||||
"circle-radius": 10,
|
||||
"circle-opacity": 0.2,
|
||||
"circle-stroke-color": "#ff7a45",
|
||||
"circle-stroke-color": MAP_STYLE_TOKENS.state.selected,
|
||||
"circle-stroke-width": 2
|
||||
}
|
||||
},
|
||||
@@ -133,7 +135,7 @@ export const drawingLayers: StyleSpecification["layers"] = [
|
||||
source: DRAWING_SOURCE_IDS.draft,
|
||||
filter: ["==", ["geometry-type"], "Polygon"],
|
||||
paint: {
|
||||
"fill-color": "#ff7a45",
|
||||
"fill-color": MAP_STYLE_TOKENS.tool.geometry,
|
||||
"fill-opacity": 0.12
|
||||
}
|
||||
},
|
||||
@@ -143,7 +145,7 @@ export const drawingLayers: StyleSpecification["layers"] = [
|
||||
source: DRAWING_SOURCE_IDS.draft,
|
||||
filter: ["any", ["==", ["geometry-type"], "LineString"], ["==", ["geometry-type"], "Polygon"]],
|
||||
paint: {
|
||||
"line-color": "#ff7a45",
|
||||
"line-color": MAP_STYLE_TOKENS.tool.geometry,
|
||||
"line-width": 2,
|
||||
"line-dasharray": [2, 2],
|
||||
"line-opacity": 0.9
|
||||
@@ -155,9 +157,9 @@ export const drawingLayers: StyleSpecification["layers"] = [
|
||||
source: DRAWING_SOURCE_IDS.draft,
|
||||
filter: ["==", ["geometry-type"], "Point"],
|
||||
paint: {
|
||||
"circle-color": "#ff7a45",
|
||||
"circle-color": MAP_STYLE_TOKENS.state.selected,
|
||||
"circle-radius": 4,
|
||||
"circle-stroke-color": "#ffffff",
|
||||
"circle-stroke-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-stroke-width": 1.5
|
||||
}
|
||||
}
|
||||
@@ -167,7 +169,7 @@ export function ensureDrawingLayers(map: {
|
||||
getSource: (id: string) => unknown;
|
||||
addSource: (id: string, source: GeoJSONSourceSpecification) => void;
|
||||
getLayer: (id: string) => unknown;
|
||||
addLayer: (layer: NonNullable<StyleSpecification["layers"]>[number]) => void;
|
||||
addLayer: (layer: NonNullable<StyleSpecification["layers"]>[number], beforeId?: string) => void;
|
||||
}) {
|
||||
if (!map.getSource(DRAWING_SOURCE_IDS.features)) {
|
||||
map.addSource(DRAWING_SOURCE_IDS.features, drawingSources.features);
|
||||
@@ -179,7 +181,7 @@ export function ensureDrawingLayers(map: {
|
||||
|
||||
drawingLayers.forEach((layer) => {
|
||||
if (!map.getLayer(layer.id)) {
|
||||
map.addLayer(layer);
|
||||
map.addLayer(layer, WORKBENCH_INTERACTION_BEFORE_ID);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import maplibregl, { type Map as MapLibreMap, type StyleSpecification } from "maplibre-gl";
|
||||
import type { MapFeatureInteractionState } from "../hooks/use-map-interactions";
|
||||
|
||||
const MAX_EXPORT_DIMENSION = 4096;
|
||||
const EXPORT_IDLE_TIMEOUT_MS = 6000;
|
||||
@@ -7,9 +8,10 @@ export type ExportMapViewOptions = {
|
||||
scale?: number;
|
||||
targetLongEdge?: number;
|
||||
filename?: string;
|
||||
selectedFeature?: Pick<MapFeatureInteractionState, "source" | "sourceLayer" | "id"> | null;
|
||||
};
|
||||
|
||||
export async function exportMapViewImage(map: MapLibreMap, { scale, targetLongEdge, filename }: ExportMapViewOptions = {}) {
|
||||
export async function exportMapViewImage(map: MapLibreMap, { scale, targetLongEdge, filename, selectedFeature }: ExportMapViewOptions = {}) {
|
||||
const canvas = map.getCanvas();
|
||||
const sourceWidth = canvas.clientWidth || canvas.width;
|
||||
const sourceHeight = canvas.clientHeight || canvas.height;
|
||||
@@ -47,6 +49,12 @@ export async function exportMapViewImage(map: MapLibreMap, { scale, targetLongEd
|
||||
padding: 0,
|
||||
duration: 0
|
||||
});
|
||||
if (selectedFeature) {
|
||||
exportMap.setFeatureState(
|
||||
{ source: selectedFeature.source, sourceLayer: selectedFeature.sourceLayer, id: selectedFeature.id },
|
||||
{ selected: true, hovered: false }
|
||||
);
|
||||
}
|
||||
await waitForMapIdle(exportMap);
|
||||
|
||||
const link = document.createElement("a");
|
||||
|
||||
@@ -64,5 +64,10 @@ function getFeaturePresentation(
|
||||
title: `排放口 ${displayId}`,
|
||||
subtitle: `类型 ${formatValue(properties.outfall_type)} · 井底高程 ${formatValue(properties.invert_elevation)} m`
|
||||
};
|
||||
case "scada":
|
||||
return {
|
||||
title: String(properties.point_name || `SCADA 测点 ${displayId}`),
|
||||
subtitle: `${formatValue(properties.device_type_name)} · 设备 ${formatValue(properties.device_external_id)}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,7 @@ import {
|
||||
INTERACTIVE_HIT_LAYER_IDS,
|
||||
waterNetworkLayers
|
||||
} from "./layers";
|
||||
import {
|
||||
DRAINAGE_SELECTED_OUTLINE_COLORS,
|
||||
DRAINAGE_SOURCE_COLORS
|
||||
} from "./map-colors";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import { DRAINAGE_LAYER_VISUALS, MAP_MAX_ZOOM } from "./map-layer-visuals";
|
||||
|
||||
describe("drainage network interaction layers", () => {
|
||||
@@ -56,61 +53,49 @@ describe("drainage network interaction layers", () => {
|
||||
|
||||
it("provides persistent selected layers for every source", () => {
|
||||
const selectedLayerIds = [
|
||||
"pipes-selected-halo",
|
||||
"pipes-selected-outline",
|
||||
"pipes-selected",
|
||||
"junctions-selected-halo",
|
||||
"junctions-selected",
|
||||
"orifices-selected-halo",
|
||||
"orifices-selected-outline",
|
||||
"orifices-selected",
|
||||
"pumps-selected-halo",
|
||||
"pumps-selected-outline",
|
||||
"pumps-selected",
|
||||
"outfalls-selected-halo",
|
||||
"outfalls-selected"
|
||||
];
|
||||
|
||||
selectedLayerIds.forEach((layerId) => {
|
||||
const layer = waterNetworkLayers.find((candidate) => candidate.id === layerId);
|
||||
expect(layer && "filter" in layer ? layer.filter : undefined).toEqual(["==", ["get", "id"], ""]);
|
||||
expect(layer && "minzoom" in layer ? layer.minzoom : undefined).toBeUndefined();
|
||||
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 a halo, complementary outline, and source-color core", () => {
|
||||
const expectedSelectedColors = [
|
||||
["pipes-selected-outline", "line-color", DRAINAGE_SELECTED_OUTLINE_COLORS.conduits],
|
||||
["pipes-selected", "line-color", DRAINAGE_SOURCE_COLORS.conduits],
|
||||
["junctions-selected", "circle-stroke-color", DRAINAGE_SELECTED_OUTLINE_COLORS.junctions],
|
||||
["junctions-selected", "circle-color", DRAINAGE_SOURCE_COLORS.junctions],
|
||||
["orifices-selected-outline", "line-color", DRAINAGE_SELECTED_OUTLINE_COLORS.orifices],
|
||||
["orifices-selected", "line-color", DRAINAGE_SOURCE_COLORS.orifices],
|
||||
["pumps-selected-outline", "line-color", DRAINAGE_SELECTED_OUTLINE_COLORS.pumps],
|
||||
["pumps-selected", "line-color", DRAINAGE_SOURCE_COLORS.pumps],
|
||||
["outfalls-selected", "circle-stroke-color", DRAINAGE_SELECTED_OUTLINE_COLORS.outfalls],
|
||||
["outfalls-selected", "circle-color", DRAINAGE_SOURCE_COLORS.outfalls]
|
||||
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;
|
||||
|
||||
expectedSelectedColors.forEach(([layerId, paintProperty, color]) => {
|
||||
expectedLineOutlines.forEach(([layerId, visuals]) => {
|
||||
const layer = waterNetworkLayers.find((candidate) => candidate.id === layerId);
|
||||
const paint = layer?.paint as Record<string, unknown> | undefined;
|
||||
expect(paint?.[paintProperty]).toBe(color);
|
||||
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", ["boolean", ["feature-state", "selected"], false], 1, 0]);
|
||||
});
|
||||
|
||||
const layerIds = waterNetworkLayers.map((layer) => layer.id);
|
||||
["pipes", "orifices", "pumps"].forEach((prefix) => {
|
||||
expect(layerIds.indexOf(`${prefix}-selected-halo`)).toBeLessThan(
|
||||
layerIds.indexOf(`${prefix}-selected-outline`)
|
||||
);
|
||||
expect(layerIds.indexOf(`${prefix}-selected-outline`)).toBeLessThan(
|
||||
layerIds.indexOf(`${prefix}-selected`)
|
||||
);
|
||||
});
|
||||
["junctions", "outfalls"].forEach((prefix) => {
|
||||
expect(layerIds.indexOf(`${prefix}-selected-halo`)).toBeLessThan(
|
||||
layerIds.indexOf(`${prefix}-selected`)
|
||||
);
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -118,7 +103,7 @@ describe("drainage network interaction layers", () => {
|
||||
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, 1, 16, 3, 20, 5.5, 24, 8
|
||||
"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
|
||||
@@ -134,18 +119,39 @@ describe("drainage network interaction layers", () => {
|
||||
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-halo", "line-width", DRAINAGE_LAYER_VISUALS.conduits.selectedHaloWidth],
|
||||
["pipes-selected-outline", "line-width", DRAINAGE_LAYER_VISUALS.conduits.selectedBorderWidth],
|
||||
["junctions", "circle-radius", DRAINAGE_LAYER_VISUALS.junctions.radius],
|
||||
["junctions-selected-halo", "circle-radius", DRAINAGE_LAYER_VISUALS.junctions.selectedHaloRadius],
|
||||
["junctions-selected", "circle-radius", DRAINAGE_LAYER_VISUALS.junctions.selectedRadius],
|
||||
["orifices", "line-width", DRAINAGE_LAYER_VISUALS.orifices.width],
|
||||
["orifices-selected", "line-width", DRAINAGE_LAYER_VISUALS.orifices.selectedWidth],
|
||||
["orifices-selected-outline", "line-width", DRAINAGE_LAYER_VISUALS.orifices.selectedBorderWidth],
|
||||
["pumps", "line-width", DRAINAGE_LAYER_VISUALS.pumps.width],
|
||||
["pumps-selected", "line-width", DRAINAGE_LAYER_VISUALS.pumps.selectedWidth],
|
||||
["pumps-selected-outline", "line-width", DRAINAGE_LAYER_VISUALS.pumps.selectedBorderWidth],
|
||||
["outfalls", "circle-radius", DRAINAGE_LAYER_VISUALS.outfalls.radius],
|
||||
["outfalls-selected-halo", "circle-radius", DRAINAGE_LAYER_VISUALS.outfalls.selectedHaloRadius]
|
||||
["outfalls-selected", "circle-radius", DRAINAGE_LAYER_VISUALS.outfalls.selectedRadius]
|
||||
] as const;
|
||||
|
||||
expectedGeometry.forEach(([layerId, paintProperty, visualToken]) => {
|
||||
|
||||
@@ -1,493 +1,90 @@
|
||||
import type { StyleSpecification } from "maplibre-gl";
|
||||
import {
|
||||
CLOSED_CONDUIT_COLOR,
|
||||
DRAINAGE_SELECTED_OUTLINE_COLORS,
|
||||
DRAINAGE_SOURCE_COLORS,
|
||||
DRAINAGE_SOURCE_OPACITIES
|
||||
} from "./map-colors";
|
||||
import type { ExpressionSpecification, StyleSpecification } from "maplibre-gl";
|
||||
import { DRAINAGE_LAYER_VISUALS } from "./map-layer-visuals";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import { SOURCE_LAYERS } from "./sources";
|
||||
|
||||
export { DRAINAGE_SOURCE_COLORS } from "./map-colors";
|
||||
|
||||
export const INTERACTIVE_HIT_LAYER_IDS = [
|
||||
"conduits-hit",
|
||||
"junctions-hit",
|
||||
"orifices-hit",
|
||||
"pumps-hit",
|
||||
"outfalls-hit"
|
||||
];
|
||||
"conduits-hit", "junctions-hit", "orifices-hit", "pumps-hit", "outfalls-hit"
|
||||
] as const;
|
||||
|
||||
export const waterNetworkLayers: StyleSpecification["layers"] = [
|
||||
{
|
||||
id: "pipes-casing",
|
||||
type: "line",
|
||||
source: "conduits",
|
||||
"source-layer": SOURCE_LAYERS.conduits,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.conduits.minzoom,
|
||||
paint: {
|
||||
"line-color": "rgba(255,255,255,0.86)",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.conduits.casingWidth,
|
||||
"line-opacity": 0.9
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pipes-flow",
|
||||
type: "line",
|
||||
source: "conduits",
|
||||
"source-layer": SOURCE_LAYERS.conduits,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.conduits.minzoom,
|
||||
paint: {
|
||||
"line-color": [
|
||||
"case",
|
||||
["==", ["get", "status"], "Closed"],
|
||||
CLOSED_CONDUIT_COLOR,
|
||||
DRAINAGE_SOURCE_COLORS.conduits
|
||||
],
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.conduits.width,
|
||||
"line-opacity": DRAINAGE_SOURCE_OPACITIES.conduits
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pipes-risk-glow",
|
||||
type: "line",
|
||||
source: "conduits",
|
||||
"source-layer": SOURCE_LAYERS.conduits,
|
||||
minzoom: 12,
|
||||
filter: [">=", ["coalesce", ["get", "diameter"], 0], 600],
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SOURCE_COLORS.conduits,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.conduits.riskGlowWidth,
|
||||
"line-blur": 2.5,
|
||||
"line-opacity": 0.22
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pipes-hover-outline",
|
||||
type: "line",
|
||||
source: "conduits",
|
||||
"source-layer": SOURCE_LAYERS.conduits,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.conduits.minzoom,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": "#ffffff",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.conduits.hoverOutlineWidth,
|
||||
"line-opacity": 0.92
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pipes-hover",
|
||||
type: "line",
|
||||
source: "conduits",
|
||||
"source-layer": SOURCE_LAYERS.conduits,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.conduits.minzoom,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SOURCE_COLORS.conduits,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.conduits.hoverWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pipes-selected-halo",
|
||||
type: "line",
|
||||
source: "conduits",
|
||||
"source-layer": SOURCE_LAYERS.conduits,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": "#ffffff",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.conduits.selectedHaloWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pipes-selected-outline",
|
||||
type: "line",
|
||||
source: "conduits",
|
||||
"source-layer": SOURCE_LAYERS.conduits,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SELECTED_OUTLINE_COLORS.conduits,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.conduits.selectedOutlineWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pipes-selected",
|
||||
type: "line",
|
||||
source: "conduits",
|
||||
"source-layer": SOURCE_LAYERS.conduits,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SOURCE_COLORS.conduits,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.conduits.selectedWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "junctions-halo",
|
||||
type: "circle",
|
||||
source: "junctions",
|
||||
"source-layer": SOURCE_LAYERS.junctions,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.junctions.minzoom,
|
||||
paint: {
|
||||
"circle-color": "#ffffff",
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.junctions.haloRadius,
|
||||
"circle-opacity": 0.76,
|
||||
"circle-stroke-color": "rgba(15,23,42,0.22)",
|
||||
"circle-stroke-width": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "junctions",
|
||||
type: "circle",
|
||||
source: "junctions",
|
||||
"source-layer": SOURCE_LAYERS.junctions,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.junctions.minzoom,
|
||||
paint: {
|
||||
"circle-color": DRAINAGE_SOURCE_COLORS.junctions,
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.junctions.radius,
|
||||
"circle-opacity": DRAINAGE_SOURCE_OPACITIES.junctions
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "junctions-hover",
|
||||
type: "circle",
|
||||
source: "junctions",
|
||||
"source-layer": SOURCE_LAYERS.junctions,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.junctions.minzoom,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"circle-color": DRAINAGE_SOURCE_COLORS.junctions,
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.junctions.hoverRadius,
|
||||
"circle-opacity": 0.24,
|
||||
"circle-stroke-color": DRAINAGE_SOURCE_COLORS.junctions,
|
||||
"circle-stroke-width": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "junctions-selected-halo",
|
||||
type: "circle",
|
||||
source: "junctions",
|
||||
"source-layer": SOURCE_LAYERS.junctions,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"circle-color": "#ffffff",
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.junctions.selectedHaloRadius,
|
||||
"circle-opacity": 0.92
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "junctions-selected",
|
||||
type: "circle",
|
||||
source: "junctions",
|
||||
"source-layer": SOURCE_LAYERS.junctions,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"circle-color": DRAINAGE_SOURCE_COLORS.junctions,
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.junctions.selectedRadius,
|
||||
"circle-opacity": 1,
|
||||
"circle-stroke-color": DRAINAGE_SELECTED_OUTLINE_COLORS.junctions,
|
||||
"circle-stroke-width": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "orifices-casing",
|
||||
type: "line",
|
||||
source: "orifices",
|
||||
"source-layer": SOURCE_LAYERS.orifices,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.orifices.minzoom,
|
||||
paint: {
|
||||
"line-color": "#ffffff",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.orifices.casingWidth,
|
||||
"line-opacity": 0.9
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "orifices",
|
||||
type: "line",
|
||||
source: "orifices",
|
||||
"source-layer": SOURCE_LAYERS.orifices,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.orifices.minzoom,
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SOURCE_COLORS.orifices,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.orifices.width,
|
||||
"line-opacity": DRAINAGE_SOURCE_OPACITIES.orifices
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "orifices-hover-outline",
|
||||
type: "line",
|
||||
source: "orifices",
|
||||
"source-layer": SOURCE_LAYERS.orifices,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.orifices.minzoom,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": "#ffffff",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.orifices.hoverOutlineWidth,
|
||||
"line-opacity": 0.92
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "orifices-hover",
|
||||
type: "line",
|
||||
source: "orifices",
|
||||
"source-layer": SOURCE_LAYERS.orifices,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.orifices.minzoom,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SOURCE_COLORS.orifices,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.orifices.hoverWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "orifices-selected-halo",
|
||||
type: "line",
|
||||
source: "orifices",
|
||||
"source-layer": SOURCE_LAYERS.orifices,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": "#ffffff",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.orifices.selectedHaloWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "orifices-selected-outline",
|
||||
type: "line",
|
||||
source: "orifices",
|
||||
"source-layer": SOURCE_LAYERS.orifices,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SELECTED_OUTLINE_COLORS.orifices,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.orifices.selectedOutlineWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "orifices-selected",
|
||||
type: "line",
|
||||
source: "orifices",
|
||||
"source-layer": SOURCE_LAYERS.orifices,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SOURCE_COLORS.orifices,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.orifices.selectedWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pumps-casing",
|
||||
type: "line",
|
||||
source: "pumps",
|
||||
"source-layer": SOURCE_LAYERS.pumps,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.pumps.minzoom,
|
||||
paint: {
|
||||
"line-color": "#ffffff",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.pumps.casingWidth,
|
||||
"line-opacity": 0.9
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pumps",
|
||||
type: "line",
|
||||
source: "pumps",
|
||||
"source-layer": SOURCE_LAYERS.pumps,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.pumps.minzoom,
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SOURCE_COLORS.pumps,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.pumps.width,
|
||||
"line-opacity": DRAINAGE_SOURCE_OPACITIES.pumps
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pumps-hover-outline",
|
||||
type: "line",
|
||||
source: "pumps",
|
||||
"source-layer": SOURCE_LAYERS.pumps,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.pumps.minzoom,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": "#ffffff",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.pumps.hoverOutlineWidth,
|
||||
"line-opacity": 0.92
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pumps-hover",
|
||||
type: "line",
|
||||
source: "pumps",
|
||||
"source-layer": SOURCE_LAYERS.pumps,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.pumps.minzoom,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SOURCE_COLORS.pumps,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.pumps.hoverWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pumps-selected-halo",
|
||||
type: "line",
|
||||
source: "pumps",
|
||||
"source-layer": SOURCE_LAYERS.pumps,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": "#ffffff",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.pumps.selectedHaloWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pumps-selected-outline",
|
||||
type: "line",
|
||||
source: "pumps",
|
||||
"source-layer": SOURCE_LAYERS.pumps,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SELECTED_OUTLINE_COLORS.pumps,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.pumps.selectedOutlineWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pumps-selected",
|
||||
type: "line",
|
||||
source: "pumps",
|
||||
"source-layer": SOURCE_LAYERS.pumps,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"line-color": DRAINAGE_SOURCE_COLORS.pumps,
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.pumps.selectedWidth,
|
||||
"line-opacity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "outfalls-halo",
|
||||
type: "circle",
|
||||
source: "outfalls",
|
||||
"source-layer": SOURCE_LAYERS.outfalls,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.outfalls.minzoom,
|
||||
paint: {
|
||||
"circle-color": "#ffffff",
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.outfalls.haloRadius,
|
||||
"circle-opacity": 0.9
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "outfalls",
|
||||
type: "circle",
|
||||
source: "outfalls",
|
||||
"source-layer": SOURCE_LAYERS.outfalls,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.outfalls.minzoom,
|
||||
paint: {
|
||||
"circle-color": DRAINAGE_SOURCE_COLORS.outfalls,
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.outfalls.radius,
|
||||
"circle-stroke-color": "#7F1D2D",
|
||||
"circle-stroke-width": 1,
|
||||
"circle-opacity": DRAINAGE_SOURCE_OPACITIES.outfalls
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "outfalls-hover",
|
||||
type: "circle",
|
||||
source: "outfalls",
|
||||
"source-layer": SOURCE_LAYERS.outfalls,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.outfalls.minzoom,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"circle-color": DRAINAGE_SOURCE_COLORS.outfalls,
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.outfalls.hoverRadius,
|
||||
"circle-opacity": 0.24,
|
||||
"circle-stroke-color": DRAINAGE_SOURCE_COLORS.outfalls,
|
||||
"circle-stroke-width": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "outfalls-selected-halo",
|
||||
type: "circle",
|
||||
source: "outfalls",
|
||||
"source-layer": SOURCE_LAYERS.outfalls,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"circle-color": "#ffffff",
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.outfalls.selectedHaloRadius,
|
||||
"circle-opacity": 0.92
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "outfalls-selected",
|
||||
type: "circle",
|
||||
source: "outfalls",
|
||||
"source-layer": SOURCE_LAYERS.outfalls,
|
||||
filter: ["==", ["get", "id"], ""],
|
||||
paint: {
|
||||
"circle-color": DRAINAGE_SOURCE_COLORS.outfalls,
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.outfalls.selectedRadius,
|
||||
"circle-opacity": 1,
|
||||
"circle-stroke-color": DRAINAGE_SELECTED_OUTLINE_COLORS.outfalls,
|
||||
"circle-stroke-width": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "conduits-hit",
|
||||
type: "line",
|
||||
source: "conduits",
|
||||
"source-layer": SOURCE_LAYERS.conduits,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.conduits.minzoom,
|
||||
paint: {
|
||||
"line-color": "#000000",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.conduits.hitWidth,
|
||||
"line-opacity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "junctions-hit",
|
||||
type: "circle",
|
||||
source: "junctions",
|
||||
"source-layer": SOURCE_LAYERS.junctions,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.junctions.minzoom,
|
||||
paint: {
|
||||
"circle-color": "#000000",
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.junctions.hitRadius,
|
||||
"circle-opacity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "orifices-hit",
|
||||
type: "line",
|
||||
source: "orifices",
|
||||
"source-layer": SOURCE_LAYERS.orifices,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.orifices.minzoom,
|
||||
paint: {
|
||||
"line-color": "#000000",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.orifices.hitWidth,
|
||||
"line-opacity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "pumps-hit",
|
||||
type: "line",
|
||||
source: "pumps",
|
||||
"source-layer": SOURCE_LAYERS.pumps,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.pumps.minzoom,
|
||||
paint: {
|
||||
"line-color": "#000000",
|
||||
"line-width": DRAINAGE_LAYER_VISUALS.pumps.hitWidth,
|
||||
"line-opacity": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "outfalls-hit",
|
||||
type: "circle",
|
||||
source: "outfalls",
|
||||
"source-layer": SOURCE_LAYERS.outfalls,
|
||||
minzoom: DRAINAGE_LAYER_VISUALS.outfalls.minzoom,
|
||||
paint: {
|
||||
"circle-color": "#000000",
|
||||
"circle-radius": DRAINAGE_LAYER_VISUALS.outfalls.hitRadius,
|
||||
"circle-opacity": 0
|
||||
}
|
||||
}
|
||||
const hoveredOpacity: ExpressionSpecification = [
|
||||
"case",
|
||||
["all", ["boolean", ["feature-state", "hovered"], false], ["!", ["boolean", ["feature-state", "selected"], false]]],
|
||||
1,
|
||||
0
|
||||
];
|
||||
const selectedOpacity: ExpressionSpecification = [
|
||||
"case", ["boolean", ["feature-state", "selected"], false], 1, 0
|
||||
];
|
||||
const stateColor = (sourceColor: string): ExpressionSpecification => [
|
||||
"case",
|
||||
["in", ["downcase", ["to-string", ["coalesce", ["get", "status"], ""]]], ["literal", ["closed", "off", "inactive"]]],
|
||||
MAP_STYLE_TOKENS.state.inactive,
|
||||
sourceColor
|
||||
];
|
||||
const opacityTransition = { duration: 120, delay: 0 } as const;
|
||||
|
||||
type Layer = NonNullable<StyleSpecification["layers"]>[number];
|
||||
type AuthoredLayer = { id: string; type: string; [key: string]: unknown };
|
||||
const layers: AuthoredLayer[] = [];
|
||||
|
||||
// Business sources: white casing, then category-colored core.
|
||||
layers.push(
|
||||
{ id: "pipes-casing", type: "line", source: "conduits", "source-layer": SOURCE_LAYERS.conduits, minzoom: DRAINAGE_LAYER_VISUALS.conduits.minzoom, paint: { "line-color": MAP_STYLE_TOKENS.canvas.casing, "line-width": DRAINAGE_LAYER_VISUALS.conduits.casingWidth, "line-opacity": MAP_STYLE_TOKENS.opacity.casing } },
|
||||
{ id: "pipes-flow", type: "line", source: "conduits", "source-layer": SOURCE_LAYERS.conduits, minzoom: DRAINAGE_LAYER_VISUALS.conduits.minzoom, paint: { "line-color": stateColor(MAP_STYLE_TOKENS.asset.conduit), "line-width": DRAINAGE_LAYER_VISUALS.conduits.width, "line-opacity": MAP_STYLE_TOKENS.opacity.core } },
|
||||
{ id: "junctions-halo", type: "circle", source: "junctions", "source-layer": SOURCE_LAYERS.junctions, minzoom: DRAINAGE_LAYER_VISUALS.junctions.minzoom, paint: { "circle-color": MAP_STYLE_TOKENS.canvas.casing, "circle-radius": DRAINAGE_LAYER_VISUALS.junctions.haloRadius, "circle-opacity": DRAINAGE_LAYER_VISUALS.junctions.haloOpacity } },
|
||||
{ id: "junctions", type: "circle", source: "junctions", "source-layer": SOURCE_LAYERS.junctions, minzoom: DRAINAGE_LAYER_VISUALS.junctions.minzoom, paint: { "circle-color": stateColor(MAP_STYLE_TOKENS.asset.junction), "circle-radius": DRAINAGE_LAYER_VISUALS.junctions.radius, "circle-opacity": MAP_STYLE_TOKENS.opacity.core } },
|
||||
{ id: "orifices-casing", type: "line", source: "orifices", "source-layer": SOURCE_LAYERS.orifices, minzoom: DRAINAGE_LAYER_VISUALS.orifices.minzoom, paint: { "line-color": MAP_STYLE_TOKENS.canvas.casing, "line-width": DRAINAGE_LAYER_VISUALS.orifices.casingWidth, "line-opacity": 1 } },
|
||||
{ id: "orifices", type: "line", source: "orifices", "source-layer": SOURCE_LAYERS.orifices, minzoom: DRAINAGE_LAYER_VISUALS.orifices.minzoom, paint: { "line-color": stateColor(MAP_STYLE_TOKENS.asset.orifice), "line-width": DRAINAGE_LAYER_VISUALS.orifices.width, "line-dasharray": [4, 2], "line-opacity": MAP_STYLE_TOKENS.opacity.core } },
|
||||
{ id: "pumps-casing", type: "line", source: "pumps", "source-layer": SOURCE_LAYERS.pumps, minzoom: DRAINAGE_LAYER_VISUALS.pumps.minzoom, paint: { "line-color": MAP_STYLE_TOKENS.canvas.casing, "line-width": DRAINAGE_LAYER_VISUALS.pumps.casingWidth, "line-opacity": 1 } },
|
||||
{ id: "pumps", type: "line", source: "pumps", "source-layer": SOURCE_LAYERS.pumps, minzoom: DRAINAGE_LAYER_VISUALS.pumps.minzoom, paint: { "line-color": stateColor(MAP_STYLE_TOKENS.asset.pump), "line-width": DRAINAGE_LAYER_VISUALS.pumps.width, "line-dasharray": [6, 2, 1.5, 2], "line-opacity": MAP_STYLE_TOKENS.opacity.core } },
|
||||
{ id: "outfalls-halo", type: "circle", source: "outfalls", "source-layer": SOURCE_LAYERS.outfalls, minzoom: DRAINAGE_LAYER_VISUALS.outfalls.minzoom, paint: { "circle-color": MAP_STYLE_TOKENS.canvas.casing, "circle-radius": DRAINAGE_LAYER_VISUALS.outfalls.haloRadius, "circle-opacity": 1 } },
|
||||
{ id: "outfalls", type: "circle", source: "outfalls", "source-layer": SOURCE_LAYERS.outfalls, minzoom: DRAINAGE_LAYER_VISUALS.outfalls.minzoom, paint: { "circle-color": MAP_STYLE_TOKENS.canvas.casing, "circle-radius": DRAINAGE_LAYER_VISUALS.outfalls.radius, "circle-opacity": 1, "circle-stroke-color": stateColor(MAP_STYLE_TOKENS.asset.outfall), "circle-stroke-width": 2 } }
|
||||
);
|
||||
|
||||
const lineInteractions = [
|
||||
["pipes", "conduits", SOURCE_LAYERS.conduits, DRAINAGE_LAYER_VISUALS.conduits, MAP_STYLE_TOKENS.asset.conduit],
|
||||
["orifices", "orifices", SOURCE_LAYERS.orifices, DRAINAGE_LAYER_VISUALS.orifices, MAP_STYLE_TOKENS.asset.orifice],
|
||||
["pumps", "pumps", SOURCE_LAYERS.pumps, DRAINAGE_LAYER_VISUALS.pumps, MAP_STYLE_TOKENS.asset.pump]
|
||||
] as const;
|
||||
|
||||
lineInteractions.forEach(([prefix, source, sourceLayer, visual, color]) => {
|
||||
layers.push(
|
||||
{ id: `${prefix}-hover-outline`, type: "line", source, "source-layer": sourceLayer, minzoom: visual.minzoom, paint: { "line-color": MAP_STYLE_TOKENS.canvas.casing, "line-width": visual.hoverOutlineWidth, "line-opacity": hoveredOpacity, "line-opacity-transition": opacityTransition } },
|
||||
{ id: `${prefix}-hover`, type: "line", source, "source-layer": sourceLayer, minzoom: visual.minzoom, paint: { "line-color": stateColor(color), "line-width": visual.hoverWidth, "line-opacity": hoveredOpacity, "line-opacity-transition": opacityTransition } },
|
||||
{ id: `${prefix}-selected-outer`, type: "line", source, "source-layer": sourceLayer, minzoom: visual.minzoom, paint: { "line-color": MAP_STYLE_TOKENS.canvas.casing, "line-gap-width": visual.selectedWidth, "line-width": 4.5, "line-opacity": selectedOpacity, "line-opacity-transition": opacityTransition } },
|
||||
{ id: `${prefix}-selected-outline`, type: "line", source, "source-layer": sourceLayer, minzoom: visual.minzoom, paint: { "line-color": MAP_STYLE_TOKENS.state.selected, "line-gap-width": visual.selectedWidth, "line-width": visual.selectedBorderWidth, "line-opacity": selectedOpacity, "line-opacity-transition": opacityTransition } },
|
||||
{ id: `${prefix}-selected-separator`, type: "line", source, "source-layer": sourceLayer, minzoom: visual.minzoom, paint: { "line-color": MAP_STYLE_TOKENS.canvas.casing, "line-gap-width": visual.selectedWidth, "line-width": 1, "line-offset": -1.5, "line-opacity": selectedOpacity, "line-opacity-transition": opacityTransition } }
|
||||
);
|
||||
});
|
||||
|
||||
const pointInteractions = [
|
||||
["junctions", "junctions", SOURCE_LAYERS.junctions, DRAINAGE_LAYER_VISUALS.junctions, MAP_STYLE_TOKENS.asset.junction],
|
||||
["outfalls", "outfalls", SOURCE_LAYERS.outfalls, DRAINAGE_LAYER_VISUALS.outfalls, MAP_STYLE_TOKENS.asset.outfall]
|
||||
] as const;
|
||||
pointInteractions.forEach(([prefix, source, sourceLayer, visual, color]) => {
|
||||
layers.push(
|
||||
{ id: `${prefix}-hover`, type: "circle", source, "source-layer": sourceLayer, minzoom: visual.minzoom, paint: { "circle-color": stateColor(color), "circle-radius": visual.hoverRadius, "circle-opacity": hoveredOpacity, "circle-stroke-color": MAP_STYLE_TOKENS.canvas.casing, "circle-stroke-width": 2, "circle-opacity-transition": opacityTransition, "circle-stroke-opacity": hoveredOpacity, "circle-stroke-opacity-transition": opacityTransition } },
|
||||
{ id: `${prefix}-selected-outer`, type: "circle", source, "source-layer": sourceLayer, minzoom: visual.minzoom, paint: { "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-radius": visual.selectedRadius, "circle-opacity": selectedOpacity, "circle-stroke-color": MAP_STYLE_TOKENS.canvas.casing, "circle-stroke-width": 6, "circle-stroke-opacity": selectedOpacity, "circle-opacity-transition": opacityTransition, "circle-stroke-opacity-transition": opacityTransition } },
|
||||
{ id: `${prefix}-selected`, type: "circle", source, "source-layer": sourceLayer, minzoom: visual.minzoom, paint: { "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-radius": visual.selectedRadius, "circle-opacity": selectedOpacity, "circle-stroke-color": MAP_STYLE_TOKENS.state.selected, "circle-stroke-width": 3, "circle-stroke-opacity": selectedOpacity, "circle-opacity-transition": opacityTransition, "circle-stroke-opacity-transition": opacityTransition } }
|
||||
);
|
||||
});
|
||||
|
||||
// Transparent hit layers stay last so pointer targeting is independent of paint order.
|
||||
layers.push(
|
||||
{ id: "conduits-hit", type: "line", source: "conduits", "source-layer": SOURCE_LAYERS.conduits, minzoom: DRAINAGE_LAYER_VISUALS.conduits.minzoom, paint: { "line-color": MAP_STYLE_TOKENS.canvas.transparent, "line-width": DRAINAGE_LAYER_VISUALS.conduits.hitWidth, "line-opacity": 0 } },
|
||||
{ id: "junctions-hit", type: "circle", source: "junctions", "source-layer": SOURCE_LAYERS.junctions, minzoom: DRAINAGE_LAYER_VISUALS.junctions.minzoom, paint: { "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-radius": DRAINAGE_LAYER_VISUALS.junctions.hitRadius, "circle-opacity": 0 } },
|
||||
{ id: "orifices-hit", type: "line", source: "orifices", "source-layer": SOURCE_LAYERS.orifices, minzoom: DRAINAGE_LAYER_VISUALS.orifices.minzoom, paint: { "line-color": MAP_STYLE_TOKENS.canvas.transparent, "line-width": DRAINAGE_LAYER_VISUALS.orifices.hitWidth, "line-opacity": 0 } },
|
||||
{ id: "pumps-hit", type: "line", source: "pumps", "source-layer": SOURCE_LAYERS.pumps, minzoom: DRAINAGE_LAYER_VISUALS.pumps.minzoom, paint: { "line-color": MAP_STYLE_TOKENS.canvas.transparent, "line-width": DRAINAGE_LAYER_VISUALS.pumps.hitWidth, "line-opacity": 0 } },
|
||||
{ id: "outfalls-hit", type: "circle", source: "outfalls", "source-layer": SOURCE_LAYERS.outfalls, minzoom: DRAINAGE_LAYER_VISUALS.outfalls.minzoom, paint: { "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-radius": DRAINAGE_LAYER_VISUALS.outfalls.hitRadius, "circle-opacity": 0 } }
|
||||
);
|
||||
|
||||
export const waterNetworkLayers: StyleSpecification["layers"] = layers as Layer[];
|
||||
|
||||
const interactionIndex = layers.findIndex((layer) => layer.id === "pipes-hover-outline");
|
||||
const hitIndex = layers.findIndex((layer) => layer.id === INTERACTIVE_HIT_LAYER_IDS[0]);
|
||||
|
||||
export const waterNetworkBusinessLayers = layers.slice(0, interactionIndex) as Layer[];
|
||||
export const waterNetworkInteractionLayers = layers.slice(interactionIndex, hitIndex) as Layer[];
|
||||
export const waterNetworkHitLayers = layers.slice(hitIndex) as Layer[];
|
||||
export const WORKBENCH_INTERACTION_BEFORE_ID = "pipes-hover-outline";
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
export const MAP_BACKGROUND_COLORS = {
|
||||
primary: "#F9FAFB",
|
||||
secondary: "#F0F1F3",
|
||||
secondaryAlt: "#FAFBFC"
|
||||
/** Runtime map colors. Keep literal colors in this file only. */
|
||||
export const MAP_STYLE_TOKENS = {
|
||||
canvas: {
|
||||
primary: "#F9FAFB",
|
||||
secondary: "#F0F1F3",
|
||||
secondaryAlt: "#FAFBFC",
|
||||
casing: "#FFFFFF",
|
||||
transparent: "rgba(0,0,0,0)"
|
||||
},
|
||||
asset: {
|
||||
conduit: "#0F766E",
|
||||
junction: "#0369A1",
|
||||
orifice: "#475569",
|
||||
pump: "#155E75",
|
||||
outfall: "#334155"
|
||||
},
|
||||
state: {
|
||||
selected: "#2563EB",
|
||||
inactive: "#64748B",
|
||||
risk: "#C2410C",
|
||||
incident: "#B91C1C",
|
||||
success: "#15803D",
|
||||
agent: "#6D28D9"
|
||||
},
|
||||
tool: {
|
||||
geometry: "#334155"
|
||||
},
|
||||
label: {
|
||||
primary: "#0F172A",
|
||||
secondary: "#334155",
|
||||
inverse: "#FFFFFF"
|
||||
},
|
||||
opacity: {
|
||||
core: 0.9,
|
||||
casing: 1,
|
||||
area: 0.14
|
||||
}
|
||||
} as const;
|
||||
|
||||
export const DRAINAGE_SOURCE_COLORS = {
|
||||
conduits: "#0F766E",
|
||||
junctions: "#B45309",
|
||||
orifices: "#6D28D9",
|
||||
pumps: "#1D4ED8",
|
||||
outfalls: "#B91C1C"
|
||||
} as const;
|
||||
|
||||
export const DRAINAGE_SOURCE_OPACITIES = {
|
||||
conduits: 0.88,
|
||||
junctions: 0.9,
|
||||
orifices: 0.92,
|
||||
pumps: 0.92,
|
||||
outfalls: 0.95
|
||||
} as const;
|
||||
|
||||
export const DRAINAGE_SELECTED_OUTLINE_COLORS = {
|
||||
conduits: "#BE123C",
|
||||
junctions: "#1D4ED8",
|
||||
orifices: "#3F6212",
|
||||
pumps: "#C2410C",
|
||||
outfalls: "#0E7490"
|
||||
} as const;
|
||||
|
||||
export const CLOSED_CONDUIT_COLOR = "#64748B";
|
||||
|
||||
@@ -16,7 +16,8 @@ describe("workbench source layer controls", () => {
|
||||
"junctions",
|
||||
"orifices",
|
||||
"outfalls",
|
||||
"pumps"
|
||||
"pumps",
|
||||
"scada"
|
||||
]);
|
||||
expect(items.slice(0, 5).map((item) => item.description)).toEqual([
|
||||
"lingang:geo_conduits_mat",
|
||||
@@ -37,16 +38,16 @@ describe("workbench source layer controls", () => {
|
||||
"orifices",
|
||||
"orifices-hover-outline",
|
||||
"orifices-hover",
|
||||
"orifices-selected-halo",
|
||||
"orifices-selected-outer",
|
||||
"orifices-selected-outline",
|
||||
"orifices-selected",
|
||||
"orifices-selected-separator",
|
||||
"orifices-hit"
|
||||
]);
|
||||
expect(getWorkbenchLayerIds(map, "outfalls")).toEqual([
|
||||
"outfalls-halo",
|
||||
"outfalls",
|
||||
"outfalls-hover",
|
||||
"outfalls-selected-halo",
|
||||
"outfalls-selected-outer",
|
||||
"outfalls-selected",
|
||||
"outfalls-hit"
|
||||
]);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import type { BaseLayerOption } from "@/features/map/core/components/base-layers-control";
|
||||
import type { MapLayerControlItem } from "@/features/map/core/components/layer-control";
|
||||
import type { MapLegendItem } from "@/features/map/core/components/legend";
|
||||
import { DRAINAGE_SOURCE_COLORS } from "./map-colors";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import {
|
||||
SOURCE_LAYERS,
|
||||
WATER_NETWORK_SOURCE_IDS,
|
||||
@@ -28,6 +28,7 @@ export const INITIAL_LAYER_VISIBILITY: Record<string, boolean> = {
|
||||
orifices: true,
|
||||
outfalls: true,
|
||||
pumps: true,
|
||||
scada: true,
|
||||
simulation: false
|
||||
};
|
||||
|
||||
@@ -36,16 +37,24 @@ const SOURCE_CONTROL_LABELS: Record<WaterNetworkSourceId, { label: string; descr
|
||||
junctions: { label: "检查井", description: `lingang:${SOURCE_LAYERS.junctions}` },
|
||||
orifices: { label: "孔口", description: `lingang:${SOURCE_LAYERS.orifices}` },
|
||||
outfalls: { label: "排放口", description: `lingang:${SOURCE_LAYERS.outfalls}` },
|
||||
pumps: { label: "泵", description: `lingang:${SOURCE_LAYERS.pumps}` }
|
||||
pumps: { label: "泵", description: `lingang:${SOURCE_LAYERS.pumps}` },
|
||||
scada: { label: "SCADA 测点", description: `lingang:${SOURCE_LAYERS.scada}` }
|
||||
};
|
||||
|
||||
export const MAP_LEGEND_ITEMS: MapLegendItem[] = [
|
||||
{ id: "conduit", label: "管渠(线宽表示口径)", color: DRAINAGE_SOURCE_COLORS.conduits, shape: "line" },
|
||||
{ id: "junction", label: "检查井", color: DRAINAGE_SOURCE_COLORS.junctions, shape: "dot" },
|
||||
{ id: "orifice", label: "孔口", color: DRAINAGE_SOURCE_COLORS.orifices, shape: "line" },
|
||||
{ id: "pump", label: "泵", color: DRAINAGE_SOURCE_COLORS.pumps, shape: "line" },
|
||||
{ id: "outfall", label: "排放口", color: DRAINAGE_SOURCE_COLORS.outfalls, shape: "dot" },
|
||||
{ id: "impact-area", label: "影响范围", color: "#ef4444", shape: "square" }
|
||||
{ id: "conduit", label: "管渠(线宽表示口径)", color: MAP_STYLE_TOKENS.asset.conduit, shape: "line" },
|
||||
{ id: "junction", label: "检查井", color: MAP_STYLE_TOKENS.asset.junction, shape: "dot" },
|
||||
{ id: "orifice", label: "孔口", color: MAP_STYLE_TOKENS.asset.orifice, shape: "dashed-line" },
|
||||
{ id: "pump", label: "泵", color: MAP_STYLE_TOKENS.asset.pump, shape: "dash-dot-line" },
|
||||
{ id: "outfall", label: "排放口", color: MAP_STYLE_TOKENS.asset.outfall, shape: "ring" },
|
||||
{ id: "scada-quality", label: "水质仪", color: "#0F766E", imageSrc: "/icons/scada-water-quality.svg" },
|
||||
{ id: "scada-level", label: "雷达液位计", color: "#2563EB", imageSrc: "/icons/scada-radar-level.svg" },
|
||||
{ id: "scada-flow", label: "超声流量计", color: "#EA580C", imageSrc: "/icons/scada-ultrasonic-flow.svg" },
|
||||
{ id: "inactive", label: "停用 / 关闭", color: MAP_STYLE_TOKENS.state.inactive, shape: "line" },
|
||||
{ id: "impact-area", label: "风险影响范围", color: MAP_STYLE_TOKENS.state.risk, shape: "square" },
|
||||
{ id: "incident", label: "事故 / 爆管", color: MAP_STYLE_TOKENS.state.incident, shape: "dot" },
|
||||
{ id: "agent", label: "Agent 推断", color: MAP_STYLE_TOKENS.state.agent, shape: "dot" },
|
||||
{ id: "selected", label: "当前选中", color: MAP_STYLE_TOKENS.state.selected, shape: "ring" }
|
||||
];
|
||||
|
||||
export const BASE_LAYER_IDS = ["mapbox-light", "mapbox-satellite"] as const;
|
||||
|
||||
@@ -41,34 +41,32 @@ export const DRAINAGE_LAYER_VISUALS = {
|
||||
minzoom: 9,
|
||||
casingWidth: [
|
||||
"interpolate", ["linear"], ["zoom"],
|
||||
9, 1.8,
|
||||
13, 3,
|
||||
9, 1.7,
|
||||
13, 2.8,
|
||||
17, ["+", 4.5, ["/", ["coalesce", ["get", "diameter"], 100], 300]],
|
||||
22, ["+", 6, ["/", ["coalesce", ["get", "diameter"], 100], 260]],
|
||||
24, ["+", 7, ["/", ["coalesce", ["get", "diameter"], 100], 240]]
|
||||
] as LineWidth,
|
||||
width: [
|
||||
"interpolate", ["linear"], ["zoom"],
|
||||
9, 0.9,
|
||||
13, 1.8,
|
||||
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]]
|
||||
] as LineWidth,
|
||||
riskGlowWidth: ["interpolate", ["linear"], ["zoom"], 12, 2.6, 17, 7.5, 22, 9.5, 24, 11] as LineWidth,
|
||||
hoverOutlineWidth: ["interpolate", ["linear"], ["zoom"], 9, 5.5, 13, 8, 17, 12.5, 22, 15, 24, 17] as LineWidth,
|
||||
hoverWidth: ["interpolate", ["linear"], ["zoom"], 9, 3, 13, 5.2, 17, 9, 22, 11.5, 24, 13] as LineWidth,
|
||||
selectedHaloWidth: ["interpolate", ["linear"], ["zoom"], 9, 7, 13, 10, 17, 14.5, 22, 17.5, 24, 20] as LineWidth,
|
||||
selectedOutlineWidth: ["interpolate", ["linear"], ["zoom"], 9, 5.5, 13, 8.2, 17, 12.5, 22, 15.5, 24, 18] as LineWidth,
|
||||
selectedWidth: ["interpolate", ["linear"], ["zoom"], 9, 4, 13, 6.5, 17, 10.5, 22, 13.5, 24, 16] as LineWidth,
|
||||
selectedBorderWidth: ["interpolate", ["linear"], ["zoom"], 9, 1.2, 13, 1.5, 17, 2, 22, 2.5, 24, 3] as LineWidth,
|
||||
hitWidth: lineHitWidth
|
||||
},
|
||||
junctions: {
|
||||
minzoom: 12,
|
||||
haloRadius: ["interpolate", ["linear"], ["zoom"], 12, 2.5, 16, 4.5, 20, 7, 24, 9.5] as CircleRadius,
|
||||
radius: ["interpolate", ["linear"], ["zoom"], 12, 1, 16, 3, 20, 5.5, 24, 8] as CircleRadius,
|
||||
haloRadius: ["interpolate", ["linear"], ["zoom"], 12, 1.2, 14, 1.8, 15, 2.8, 16, 4.5, 20, 7, 24, 9.5] as CircleRadius,
|
||||
haloOpacity: ["interpolate", ["linear"], ["zoom"], 12, 0, 14, 0, 15, 1] as const,
|
||||
radius: ["interpolate", ["linear"], ["zoom"], 12, 0.6, 14, 1, 15, 1.5, 16, 3, 20, 5.5, 24, 8] as CircleRadius,
|
||||
hoverRadius: ["interpolate", ["linear"], ["zoom"], 12, 5, 16, 7, 20, 10, 24, 12] as CircleRadius,
|
||||
selectedHaloRadius: ["interpolate", ["linear"], ["zoom"], 12, 10.5, 16, 12.5, 20, 15.5, 24, 18] as CircleRadius,
|
||||
selectedRadius: ["interpolate", ["linear"], ["zoom"], 12, 5.5, 16, 7.5, 20, 10.5, 24, 13] as CircleRadius,
|
||||
hitRadius: pointHitRadius
|
||||
},
|
||||
@@ -78,9 +76,8 @@ export const DRAINAGE_LAYER_VISUALS = {
|
||||
width: ["interpolate", ["linear"], ["zoom"], 11, 1.6, 17, 4.5, 22, 6, 24, 7] as LineWidth,
|
||||
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,
|
||||
selectedHaloWidth: ["interpolate", ["linear"], ["zoom"], 11, 8, 17, 13, 22, 16, 24, 18] as LineWidth,
|
||||
selectedOutlineWidth: ["interpolate", ["linear"], ["zoom"], 11, 6.75, 17, 11, 22, 14, 24, 16] as LineWidth,
|
||||
selectedWidth: ["interpolate", ["linear"], ["zoom"], 11, 5.5, 17, 9, 22, 12, 24, 14] as LineWidth,
|
||||
selectedBorderWidth: ["interpolate", ["linear"], ["zoom"], 11, 1.25, 17, 1.75, 22, 2.25, 24, 2.5] as LineWidth,
|
||||
hitWidth: lineHitWidth
|
||||
},
|
||||
pumps: {
|
||||
@@ -89,9 +86,8 @@ export const DRAINAGE_LAYER_VISUALS = {
|
||||
width: ["interpolate", ["linear"], ["zoom"], 11, 2.4, 17, 6.2, 22, 8, 24, 9] as LineWidth,
|
||||
hoverOutlineWidth: ["interpolate", ["linear"], ["zoom"], 11, 7, 17, 12, 22, 14, 24, 16] as LineWidth,
|
||||
hoverWidth: ["interpolate", ["linear"], ["zoom"], 11, 4.5, 17, 8.5, 22, 10.5, 24, 12] as LineWidth,
|
||||
selectedHaloWidth: ["interpolate", ["linear"], ["zoom"], 11, 9, 17, 14, 22, 17, 24, 19] as LineWidth,
|
||||
selectedOutlineWidth: ["interpolate", ["linear"], ["zoom"], 11, 7.5, 17, 12, 22, 15, 24, 17] as LineWidth,
|
||||
selectedWidth: ["interpolate", ["linear"], ["zoom"], 11, 6, 17, 10, 22, 13, 24, 15] as LineWidth,
|
||||
selectedBorderWidth: ["interpolate", ["linear"], ["zoom"], 11, 1.25, 17, 1.75, 22, 2.25, 24, 2.5] as LineWidth,
|
||||
hitWidth: lineHitWidth
|
||||
},
|
||||
outfalls: {
|
||||
@@ -99,7 +95,6 @@ export const DRAINAGE_LAYER_VISUALS = {
|
||||
haloRadius: ["interpolate", ["linear"], ["zoom"], 11, 5, 16, 7, 20, 9, 24, 11] as CircleRadius,
|
||||
radius: ["interpolate", ["linear"], ["zoom"], 11, 3, 16, 5, 20, 7, 24, 9] as CircleRadius,
|
||||
hoverRadius: ["interpolate", ["linear"], ["zoom"], 11, 6, 16, 8, 20, 11, 24, 13] as CircleRadius,
|
||||
selectedHaloRadius: ["interpolate", ["linear"], ["zoom"], 11, 12.5, 16, 14.5, 20, 17.5, 24, 20] as CircleRadius,
|
||||
selectedRadius: ["interpolate", ["linear"], ["zoom"], 11, 7.5, 16, 9.5, 20, 12.5, 24, 15] as CircleRadius,
|
||||
hitRadius: pointHitRadius
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import type {
|
||||
} from "maplibre-gl";
|
||||
import type { FeatureCollection, Geometry, LineString, Point, Polygon } from "geojson";
|
||||
import { SOURCE_LAYERS } from "./sources";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
import { WORKBENCH_INTERACTION_BEFORE_ID } from "./layers";
|
||||
|
||||
export const MEASUREMENT_SOURCE_ID = "workbench-measurement";
|
||||
export const MEASUREMENT_SELECTED_PIPES_LAYER_ID = "workbench-measurement-selected-pipes";
|
||||
@@ -31,7 +33,7 @@ export const measurementLayers: StyleSpecification["layers"] = [
|
||||
minzoom: 9,
|
||||
filter: ["in", ["get", "id"], ["literal", []]],
|
||||
paint: {
|
||||
"line-color": "#2563eb",
|
||||
"line-color": MAP_STYLE_TOKENS.state.selected,
|
||||
"line-width": ["interpolate", ["linear"], ["zoom"], 9, 4, 13, 7, 17, 11],
|
||||
"line-opacity": 0.68
|
||||
}
|
||||
@@ -42,7 +44,7 @@ export const measurementLayers: StyleSpecification["layers"] = [
|
||||
source: MEASUREMENT_SOURCE_ID,
|
||||
filter: ["==", ["get", "kind"], "polygon"],
|
||||
paint: {
|
||||
"fill-color": "#2563eb",
|
||||
"fill-color": MAP_STYLE_TOKENS.tool.geometry,
|
||||
"fill-opacity": 0.12
|
||||
}
|
||||
},
|
||||
@@ -52,7 +54,7 @@ export const measurementLayers: StyleSpecification["layers"] = [
|
||||
source: MEASUREMENT_SOURCE_ID,
|
||||
filter: ["any", ["==", ["get", "kind"], "line"], ["==", ["get", "kind"], "polygon"]],
|
||||
paint: {
|
||||
"line-color": "#2563eb",
|
||||
"line-color": MAP_STYLE_TOKENS.tool.geometry,
|
||||
"line-width": 2.5,
|
||||
"line-dasharray": [2, 1.5],
|
||||
"line-opacity": 0.9
|
||||
@@ -64,9 +66,9 @@ export const measurementLayers: StyleSpecification["layers"] = [
|
||||
source: MEASUREMENT_SOURCE_ID,
|
||||
filter: ["==", ["get", "kind"], "vertex"],
|
||||
paint: {
|
||||
"circle-color": "#2563eb",
|
||||
"circle-color": MAP_STYLE_TOKENS.state.selected,
|
||||
"circle-radius": 4,
|
||||
"circle-stroke-color": "#ffffff",
|
||||
"circle-stroke-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||
"circle-stroke-width": 1.5
|
||||
}
|
||||
}
|
||||
@@ -76,7 +78,7 @@ export function ensureMeasurementLayers(map: {
|
||||
getSource: (id: string) => unknown;
|
||||
addSource: (id: string, source: GeoJSONSourceSpecification) => void;
|
||||
getLayer: (id: string) => unknown;
|
||||
addLayer: (layer: NonNullable<StyleSpecification["layers"]>[number]) => void;
|
||||
addLayer: (layer: NonNullable<StyleSpecification["layers"]>[number], beforeId?: string) => void;
|
||||
}) {
|
||||
if (!map.getSource(MEASUREMENT_SOURCE_ID)) {
|
||||
map.addSource(MEASUREMENT_SOURCE_ID, measurementSource);
|
||||
@@ -84,7 +86,7 @@ export function ensureMeasurementLayers(map: {
|
||||
|
||||
measurementLayers.forEach((layer) => {
|
||||
if (!map.getLayer(layer.id)) {
|
||||
map.addLayer(layer);
|
||||
map.addLayer(layer, WORKBENCH_INTERACTION_BEFORE_ID);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
SCADA_HIT_LAYER_ID,
|
||||
SCADA_ICON_EXPRESSION,
|
||||
SCADA_IMAGE_IDS,
|
||||
SCADA_MAP_ICON_PATHS,
|
||||
registerScadaImages,
|
||||
scadaFallbackLayers,
|
||||
scadaLayers
|
||||
} from "./scada";
|
||||
|
||||
describe("SCADA map presentation", () => {
|
||||
it("maps the three authoritative device types and falls back to unknown", () => {
|
||||
expect(SCADA_ICON_EXPRESSION).toEqual([
|
||||
"match", ["to-number", ["get", "device_type_id"]],
|
||||
72, SCADA_IMAGE_IDS.waterQuality,
|
||||
58, SCADA_IMAGE_IDS.radarLevel,
|
||||
45, SCADA_IMAGE_IDS.ultrasonicFlow,
|
||||
SCADA_IMAGE_IDS.unknown
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps the hit target last in normal and fallback presentations", () => {
|
||||
expect(scadaLayers.at(-1)?.id).toBe(SCADA_HIT_LAYER_ID);
|
||||
expect(scadaFallbackLayers.at(-1)?.id).toBe(SCADA_HIT_LAYER_ID);
|
||||
});
|
||||
|
||||
it("uses MapLibre-compatible PNG assets", () => {
|
||||
expect(Object.values(SCADA_MAP_ICON_PATHS)).toHaveLength(4);
|
||||
Object.values(SCADA_MAP_ICON_PATHS).forEach((path) => expect(path).toMatch(/\.png$/));
|
||||
});
|
||||
|
||||
it("registers all map images at two-times pixel density", async () => {
|
||||
const image = { width: 64, height: 64 };
|
||||
const map = createImageMap(() => Promise.resolve({ data: image }));
|
||||
|
||||
await expect(registerScadaImages(map)).resolves.toEqual({ status: "ready", failedImageIds: [] });
|
||||
expect(map.loadImage).toHaveBeenCalledTimes(4);
|
||||
expect(map.addImage).toHaveBeenCalledTimes(4);
|
||||
expect(map.addImage).toHaveBeenCalledWith(expect.any(String), image, { pixelRatio: 2 });
|
||||
});
|
||||
|
||||
it("falls back to the unknown image when one category fails", async () => {
|
||||
const image = { width: 64, height: 64 };
|
||||
const map = createImageMap((path) => path.includes("radar-level")
|
||||
? Promise.reject(new Error("decode failed"))
|
||||
: Promise.resolve({ data: image }));
|
||||
|
||||
await expect(registerScadaImages(map)).resolves.toEqual({
|
||||
status: "degraded",
|
||||
failedImageIds: [SCADA_IMAGE_IDS.radarLevel]
|
||||
});
|
||||
expect(map.addImage).toHaveBeenCalledWith(SCADA_IMAGE_IDS.radarLevel, image, { pixelRatio: 2 });
|
||||
});
|
||||
|
||||
it("returns unavailable instead of rejecting when the fallback image fails", async () => {
|
||||
const map = createImageMap(() => Promise.reject(new Error("unsupported image")));
|
||||
|
||||
await expect(registerScadaImages(map)).resolves.toEqual({
|
||||
status: "unavailable",
|
||||
failedImageIds: Object.values(SCADA_IMAGE_IDS)
|
||||
});
|
||||
expect(scadaFallbackLayers[0]?.id).toBe("scada-fallback");
|
||||
});
|
||||
});
|
||||
|
||||
function createImageMap(loadImage: (path: string) => Promise<{ data: { width: number; height: number } }>) {
|
||||
const loadedImageIds = new Set<string>();
|
||||
return {
|
||||
loadImage: vi.fn(loadImage),
|
||||
hasImage: vi.fn((imageId: string) => loadedImageIds.has(imageId)),
|
||||
addImage: vi.fn((imageId: string) => loadedImageIds.add(imageId))
|
||||
} as unknown as Pick<MapLibreMap, "loadImage" | "addImage" | "hasImage"> & {
|
||||
loadImage: ReturnType<typeof vi.fn>;
|
||||
addImage: ReturnType<typeof vi.fn>;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import type { ExpressionSpecification, Map as MapLibreMap, StyleSpecification } from "maplibre-gl";
|
||||
|
||||
export const SCADA_SOURCE_ID = "scada";
|
||||
export const SCADA_SOURCE_LAYER = "scada_points";
|
||||
export const SCADA_HIT_LAYER_ID = "scada-hit";
|
||||
|
||||
export const SCADA_ICON_PATHS = {
|
||||
waterQuality: "/icons/scada-water-quality.svg",
|
||||
radarLevel: "/icons/scada-radar-level.svg",
|
||||
ultrasonicFlow: "/icons/scada-ultrasonic-flow.svg",
|
||||
unknown: "/icons/scada-unknown.svg"
|
||||
} as const;
|
||||
|
||||
export const SCADA_MAP_ICON_PATHS = {
|
||||
waterQuality: "/icons/scada-water-quality.png",
|
||||
radarLevel: "/icons/scada-radar-level.png",
|
||||
ultrasonicFlow: "/icons/scada-ultrasonic-flow.png",
|
||||
unknown: "/icons/scada-unknown.png"
|
||||
} as const;
|
||||
|
||||
export const SCADA_IMAGE_IDS = {
|
||||
waterQuality: "scada-water-quality",
|
||||
radarLevel: "scada-radar-level",
|
||||
ultrasonicFlow: "scada-ultrasonic-flow",
|
||||
unknown: "scada-unknown"
|
||||
} as const;
|
||||
|
||||
export const SCADA_ICON_EXPRESSION: ExpressionSpecification = [
|
||||
"match", ["to-number", ["get", "device_type_id"]],
|
||||
72, SCADA_IMAGE_IDS.waterQuality,
|
||||
58, SCADA_IMAGE_IDS.radarLevel,
|
||||
45, SCADA_IMAGE_IDS.ultrasonicFlow,
|
||||
SCADA_IMAGE_IDS.unknown
|
||||
];
|
||||
|
||||
const stateOpacity = (state: "hovered" | "selected"): ExpressionSpecification => [
|
||||
"case", ["boolean", ["feature-state", state], false], 1, 0
|
||||
];
|
||||
|
||||
type Layer = NonNullable<StyleSpecification["layers"]>[number];
|
||||
|
||||
export type ScadaImageRegistrationResult = {
|
||||
status: "ready" | "degraded" | "unavailable";
|
||||
failedImageIds: string[];
|
||||
};
|
||||
|
||||
const scadaSymbolLayer: Layer = {
|
||||
id: "scada-symbol", type: "symbol", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12,
|
||||
layout: { "icon-image": SCADA_ICON_EXPRESSION, "icon-size": ["interpolate", ["linear"], ["zoom"], 12, 0.625, 20, 0.875], "icon-allow-overlap": true, "icon-ignore-placement": true }
|
||||
};
|
||||
|
||||
const scadaFallbackLayer: Layer = {
|
||||
id: "scada-fallback", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12,
|
||||
paint: { "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 5, 20, 8], "circle-color": "#0E7490", "circle-stroke-color": "white", "circle-stroke-width": 2 }
|
||||
};
|
||||
|
||||
const scadaInteractionLayers: Layer[] = [
|
||||
{ id: "scada-hover", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12, paint: { "circle-radius": 14, "circle-color": "rgba(255,255,255,0)", "circle-stroke-color": "#0E7490", "circle-stroke-width": 2, "circle-opacity": stateOpacity("hovered"), "circle-stroke-opacity": stateOpacity("hovered") } },
|
||||
{ id: "scada-selected-outer", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12, paint: { "circle-radius": 16, "circle-color": "rgba(255,255,255,0)", "circle-stroke-color": "white", "circle-stroke-width": 6, "circle-opacity": stateOpacity("selected"), "circle-stroke-opacity": stateOpacity("selected") } },
|
||||
{ id: "scada-selected", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12, paint: { "circle-radius": 16, "circle-color": "rgba(255,255,255,0)", "circle-stroke-color": "#2563EB", "circle-stroke-width": 3, "circle-opacity": stateOpacity("selected"), "circle-stroke-opacity": stateOpacity("selected") } },
|
||||
{ id: SCADA_HIT_LAYER_ID, type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12, paint: { "circle-radius": 16, "circle-color": "rgba(0,0,0,0)", "circle-opacity": 0 } }
|
||||
];
|
||||
|
||||
export const scadaLayers: Layer[] = [
|
||||
scadaSymbolLayer,
|
||||
...scadaInteractionLayers
|
||||
];
|
||||
|
||||
export const scadaFallbackLayers: Layer[] = [
|
||||
scadaFallbackLayer,
|
||||
...scadaInteractionLayers
|
||||
];
|
||||
|
||||
export async function registerScadaImages(
|
||||
map: Pick<MapLibreMap, "loadImage" | "addImage" | "hasImage">
|
||||
): Promise<ScadaImageRegistrationResult> {
|
||||
let unknown: Awaited<ReturnType<typeof loadImage>>;
|
||||
|
||||
try {
|
||||
unknown = await loadImage(map, SCADA_MAP_ICON_PATHS.unknown);
|
||||
addImageIfMissing(map, SCADA_IMAGE_IDS.unknown, unknown);
|
||||
} catch {
|
||||
return { status: "unavailable", failedImageIds: Object.values(SCADA_IMAGE_IDS) };
|
||||
}
|
||||
|
||||
const failedImageIds: string[] = [];
|
||||
for (const [key, path] of Object.entries(SCADA_MAP_ICON_PATHS)) {
|
||||
if (key === "unknown") continue;
|
||||
const imageId = SCADA_IMAGE_IDS[key as Exclude<keyof typeof SCADA_IMAGE_IDS, "unknown">];
|
||||
if (map.hasImage(imageId)) continue;
|
||||
|
||||
try {
|
||||
const image = await loadImage(map, path);
|
||||
addImageIfMissing(map, imageId, image);
|
||||
} catch {
|
||||
failedImageIds.push(imageId);
|
||||
try {
|
||||
addImageIfMissing(map, imageId, unknown);
|
||||
} catch {
|
||||
return { status: "unavailable", failedImageIds };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: failedImageIds.length > 0 ? "degraded" : "ready",
|
||||
failedImageIds
|
||||
};
|
||||
}
|
||||
|
||||
async function loadImage(map: Pick<MapLibreMap, "loadImage">, path: string) {
|
||||
const response = await map.loadImage(path);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function addImageIfMissing(
|
||||
map: Pick<MapLibreMap, "addImage" | "hasImage">,
|
||||
imageId: string,
|
||||
image: Awaited<ReturnType<typeof loadImage>>
|
||||
) {
|
||||
if (!map.hasImage(imageId)) map.addImage(imageId, image, { pixelRatio: 2 });
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { VectorSourceSpecification, StyleSpecification } from "maplibre-gl";
|
||||
import { MAP_BACKGROUND_COLORS } from "./map-colors";
|
||||
import { MAP_URL } from "../../../lib/config";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
|
||||
export const WATER_NETWORK_GLOBAL_VIEW = {
|
||||
bbox3857: [
|
||||
@@ -15,7 +16,8 @@ export const SOURCE_LAYERS = {
|
||||
junctions: "geo_junctions_mat",
|
||||
orifices: "geo_orifices_mat",
|
||||
outfalls: "geo_outfalls_mat",
|
||||
pumps: "geo_pumps_mat"
|
||||
pumps: "geo_pumps_mat",
|
||||
scada: "scada_points"
|
||||
} as const;
|
||||
|
||||
export const WATER_NETWORK_SOURCE_IDS = [
|
||||
@@ -23,12 +25,13 @@ export const WATER_NETWORK_SOURCE_IDS = [
|
||||
"junctions",
|
||||
"orifices",
|
||||
"outfalls",
|
||||
"pumps"
|
||||
"pumps",
|
||||
"scada"
|
||||
] as const;
|
||||
|
||||
export type WaterNetworkSourceId = (typeof WATER_NETWORK_SOURCE_IDS)[number];
|
||||
|
||||
const GEOSERVER_WMTS_ROOT = "https://geoserver.waternetwork.cn/geoserver/gwc/service/wmts/rest";
|
||||
const GEOSERVER_WMTS_ROOT = `${MAP_URL}/gwc/service/wmts/rest`;
|
||||
|
||||
export function createWaterNetworkSources() {
|
||||
return {
|
||||
@@ -56,7 +59,8 @@ export function createWaterNetworkSources() {
|
||||
SOURCE_LAYERS.pumps,
|
||||
"line",
|
||||
[121.73585149761436, 30.861759757577705, 121.9498481645973, 30.983213202338085]
|
||||
)
|
||||
),
|
||||
scada: createLingangVectorSource(SOURCE_LAYERS.scada, "point", [121.7350340307058, 30.84636502815, 121.97051839928491, 30.994737681416165])
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,6 +71,7 @@ function createLingangVectorSource(
|
||||
): VectorSourceSpecification {
|
||||
return {
|
||||
type: "vector",
|
||||
promoteId: "id",
|
||||
tiles: [
|
||||
`${GEOSERVER_WMTS_ROOT}/lingang:${sourceLayer}/${style}/WebMercatorQuad/{z}/{y}/{x}?format=application/vnd.mapbox-vector-tile`
|
||||
],
|
||||
@@ -83,7 +88,7 @@ export function createBaseStyle(mapboxToken?: string): StyleSpecification {
|
||||
id: "background",
|
||||
type: "background",
|
||||
paint: {
|
||||
"background-color": MAP_BACKGROUND_COLORS.primary
|
||||
"background-color": MAP_STYLE_TOKENS.canvas.primary
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user