style: refine acrylic workbench surfaces
This commit is contained in:
@@ -1,20 +1,57 @@
|
||||
import type { Map as MapLibreMap } from "maplibre-gl";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fitNetworkBounds } from "./camera";
|
||||
import {
|
||||
fitNetworkBounds,
|
||||
getResponsiveWorkbenchPadding,
|
||||
getWorkbenchPadding
|
||||
} from "./camera";
|
||||
import { WATER_NETWORK_GLOBAL_VIEW } from "./sources";
|
||||
|
||||
function createMap(width = 1280, height = 720) {
|
||||
const fitBounds = vi.fn();
|
||||
const map = {
|
||||
fitBounds,
|
||||
getCanvas: () => ({ clientWidth: width, clientHeight: height, width, height })
|
||||
} as unknown as MapLibreMap;
|
||||
|
||||
return { fitBounds, map };
|
||||
}
|
||||
|
||||
describe("workbench camera padding", () => {
|
||||
it.each([
|
||||
{ agentOpen: false, conditionOpen: false, expected: { top: 72, right: 72, bottom: 32, left: 160 } },
|
||||
{ agentOpen: true, conditionOpen: false, expected: { top: 72, right: 72, bottom: 32, left: 484 } },
|
||||
{ agentOpen: false, conditionOpen: true, expected: { top: 72, right: 504, bottom: 32, left: 160 } },
|
||||
{ agentOpen: true, conditionOpen: true, expected: { top: 72, right: 504, bottom: 32, left: 484 } }
|
||||
])("covers agent=$agentOpen and condition=$conditionOpen", ({ agentOpen, conditionOpen, expected }) => {
|
||||
expect(getWorkbenchPadding(agentOpen, conditionOpen)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("caps combined padding on constrained desktop widths", () => {
|
||||
const { map } = createMap(1024, 720);
|
||||
const padding = getResponsiveWorkbenchPadding(map, true, true);
|
||||
|
||||
expect(padding.left + padding.right).toBeLessThanOrEqual(Math.floor(1024 * 0.58) + 1);
|
||||
expect(padding.top + padding.bottom).toBeLessThanOrEqual(Math.floor(720 * 0.58) + 1);
|
||||
expect(padding.left).toBeGreaterThan(padding.top);
|
||||
expect(padding.right).toBeGreaterThan(padding.top);
|
||||
});
|
||||
|
||||
it("uses mobile-safe padding below the dock breakpoint", () => {
|
||||
const { map } = createMap(375, 667);
|
||||
|
||||
expect(getResponsiveWorkbenchPadding(map, true, true)).toEqual({
|
||||
top: 72,
|
||||
right: 24,
|
||||
bottom: 72,
|
||||
left: 24
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fitNetworkBounds", () => {
|
||||
it("fits the global WebMercator bbox through MapLibre lng/lat bounds", () => {
|
||||
const fitBounds = vi.fn();
|
||||
const map = {
|
||||
fitBounds,
|
||||
getCanvas: () => ({
|
||||
clientWidth: 1280,
|
||||
clientHeight: 720,
|
||||
width: 1280,
|
||||
height: 720
|
||||
})
|
||||
} as unknown as MapLibreMap;
|
||||
it("preserves the original global extent for initial and home views", () => {
|
||||
const { fitBounds, map } = createMap();
|
||||
|
||||
fitNetworkBounds(map, WATER_NETWORK_GLOBAL_VIEW);
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { Map as MapLibreMap, PaddingOptions } from "maplibre-gl";
|
||||
import {
|
||||
getWorkbenchCameraPadding
|
||||
} from "../layout/workbench-layout";
|
||||
import { WATER_NETWORK_GLOBAL_VIEW } from "./sources";
|
||||
|
||||
type LngLatBounds = [[number, number], [number, number]];
|
||||
@@ -11,14 +14,11 @@ export type NetworkViewParams = {
|
||||
const WEB_MERCATOR_RADIUS = 6378137;
|
||||
const GLOBAL_VIEW_MAX_ZOOM = 12;
|
||||
const GLOBAL_VIEW_PADDING: PaddingOptions = { top: 48, right: 48, bottom: 48, left: 48 };
|
||||
|
||||
export function getWorkbenchPadding(leftPanelOpen: boolean, rightPanelOpen: boolean): PaddingOptions {
|
||||
return {
|
||||
top: 88,
|
||||
left: leftPanelOpen ? 530 : 88,
|
||||
right: rightPanelOpen ? 430 : 120,
|
||||
bottom: 96
|
||||
};
|
||||
return getWorkbenchCameraPadding(1280, {
|
||||
agentOpen: leftPanelOpen,
|
||||
conditionOpen: rightPanelOpen
|
||||
});
|
||||
}
|
||||
|
||||
export function getResponsiveWorkbenchPadding(
|
||||
@@ -26,7 +26,14 @@ export function getResponsiveWorkbenchPadding(
|
||||
leftPanelOpen: boolean,
|
||||
rightPanelOpen: boolean
|
||||
): PaddingOptions {
|
||||
return getResponsivePadding(map, getWorkbenchPadding(leftPanelOpen, rightPanelOpen));
|
||||
const width = getMapViewportSize(map).width;
|
||||
return getResponsivePadding(
|
||||
map,
|
||||
getWorkbenchCameraPadding(width, {
|
||||
agentOpen: leftPanelOpen,
|
||||
conditionOpen: rightPanelOpen
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function fitNetworkBounds(
|
||||
@@ -44,10 +51,8 @@ export function fitNetworkBounds(
|
||||
);
|
||||
}
|
||||
|
||||
function getResponsivePadding(map: MapLibreMap, padding: PaddingOptions): PaddingOptions {
|
||||
const canvas = map.getCanvas();
|
||||
const width = canvas.clientWidth || canvas.width;
|
||||
const height = canvas.clientHeight || canvas.height;
|
||||
export function getResponsivePadding(map: MapLibreMap, padding: PaddingOptions): PaddingOptions {
|
||||
const { width, height } = getMapViewportSize(map);
|
||||
const horizontalPadding = padding.left + padding.right;
|
||||
const verticalPadding = padding.top + padding.bottom;
|
||||
const maxHorizontalPadding = width * 0.58;
|
||||
@@ -68,6 +73,14 @@ function getResponsivePadding(map: MapLibreMap, padding: PaddingOptions): Paddin
|
||||
};
|
||||
}
|
||||
|
||||
function getMapViewportSize(map: MapLibreMap) {
|
||||
const canvas = map.getCanvas();
|
||||
return {
|
||||
width: canvas.clientWidth || canvas.width,
|
||||
height: canvas.clientHeight || canvas.height
|
||||
};
|
||||
}
|
||||
|
||||
function getLngLatBoundsFromBbox3857(bbox3857: WebMercatorBbox): LngLatBounds {
|
||||
const [minX, minY, maxX, maxY] = bbox3857;
|
||||
|
||||
|
||||
@@ -60,4 +60,26 @@ describe("drainage feature adapter", () => {
|
||||
"Unsupported water network source layer: other"
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the promoted SCADA point id for feature-state interactions", () => {
|
||||
const detail = toDetailFeature({
|
||||
id: "aaedc54a-e5b4-5791-b729-4b7e838a8d00",
|
||||
sourceLayer: SOURCE_LAYERS.scada,
|
||||
properties: {
|
||||
id: "4bfa834b-cbbb-5f35-9523-7487ac021ed2",
|
||||
point_id: "aaedc54a-e5b4-5791-b729-4b7e838a8d00",
|
||||
point_external_id: "36455",
|
||||
point_name: "DY22东市南街环城南路西段交叉口",
|
||||
device_external_id: "26825171",
|
||||
device_type_name: "2代水质仪"
|
||||
}
|
||||
} as unknown as MapGeoJSONFeature);
|
||||
|
||||
expect(detail).toMatchObject({
|
||||
id: "aaedc54a-e5b4-5791-b729-4b7e838a8d00",
|
||||
layer: "scada",
|
||||
title: "DY22东市南街环城南路西段交叉口",
|
||||
subtitle: "2代水质仪 · 设备 26825171"
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,11 @@ import { formatValue } from "../utils/format-value";
|
||||
import { SOURCE_LAYERS, type WaterNetworkSourceId } from "./sources";
|
||||
|
||||
export function getFeatureId(feature: MapGeoJSONFeature) {
|
||||
const raw = feature.properties?.id ?? feature.id;
|
||||
const layer = getWaterNetworkSourceId(feature);
|
||||
const raw = layer === "scada"
|
||||
? feature.id ?? feature.properties?.id ?? feature.properties?.point_id ?? feature.properties?.point_external_id
|
||||
: feature.id ?? feature.properties?.id;
|
||||
|
||||
return raw === undefined || raw === null ? "" : String(raw);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
scadaFallbackLayers,
|
||||
scadaLayers
|
||||
} from "./scada";
|
||||
import { waterNetworkLayers } from "./layers";
|
||||
|
||||
describe("SCADA map presentation", () => {
|
||||
it("maps the three authoritative device types and falls back to unknown", () => {
|
||||
@@ -26,20 +27,29 @@ describe("SCADA map presentation", () => {
|
||||
expect(scadaFallbackLayers.at(-1)?.id).toBe(SCADA_HIT_LAYER_ID);
|
||||
});
|
||||
|
||||
it("keeps the icon and selected ring consistent across zoom levels", () => {
|
||||
it("keeps the icon and selected ring consistent with junction selection", () => {
|
||||
const symbolLayer = scadaLayers.find((layer) => layer.id === "scada-symbol");
|
||||
const outerLayer = scadaLayers.find((layer) => layer.id === "scada-selected-outer");
|
||||
const selectedLayer = scadaLayers.find((layer) => layer.id === "scada-selected");
|
||||
const junctionOuterLayer = waterNetworkLayers.find((layer) => layer.id === "junctions-selected-outer");
|
||||
const junctionSelectedLayer = waterNetworkLayers.find((layer) => layer.id === "junctions-selected");
|
||||
const junctionOuterPaint = junctionOuterLayer?.paint as Record<string, unknown> | undefined;
|
||||
const junctionSelectedPaint = junctionSelectedLayer?.paint as Record<string, unknown> | undefined;
|
||||
|
||||
expect(symbolLayer).toMatchObject({ type: "symbol", layout: { "icon-size": 0.75 } });
|
||||
expect(outerLayer).toMatchObject({
|
||||
type: "circle",
|
||||
paint: {
|
||||
"circle-radius": 12.5,
|
||||
"circle-stroke-width": 3
|
||||
"circle-stroke-width": junctionOuterPaint?.["circle-stroke-width"]
|
||||
}
|
||||
});
|
||||
expect(selectedLayer).toMatchObject({
|
||||
type: "circle",
|
||||
paint: {
|
||||
"circle-stroke-width": junctionSelectedPaint?.["circle-stroke-width"]
|
||||
}
|
||||
});
|
||||
expect(selectedLayer).toMatchObject({ type: "circle", paint: { "circle-stroke-width": 2 } });
|
||||
});
|
||||
|
||||
it("uses MapLibre-compatible PNG assets", () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ExpressionSpecification, Map as MapLibreMap, StyleSpecification } from "maplibre-gl";
|
||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||
|
||||
export const SCADA_SOURCE_ID = "scada";
|
||||
export const SCADA_SOURCE_LAYER = "scada_points";
|
||||
@@ -39,6 +40,8 @@ const stateOpacity = (state: "hovered" | "selected"): ExpressionSpecification =>
|
||||
|
||||
const SCADA_ICON_SIZE = 0.75;
|
||||
const SCADA_SELECTED_RADIUS = 12.5;
|
||||
const SCADA_SELECTED_OUTER_STROKE_WIDTH = 6;
|
||||
const SCADA_SELECTED_STROKE_WIDTH = 3;
|
||||
|
||||
type Layer = NonNullable<StyleSpecification["layers"]>[number];
|
||||
|
||||
@@ -59,8 +62,8 @@ const scadaFallbackLayer: Layer = {
|
||||
|
||||
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": SCADA_SELECTED_RADIUS, "circle-color": "rgba(255,255,255,0)", "circle-stroke-color": "white", "circle-stroke-width": 3, "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": SCADA_SELECTED_RADIUS, "circle-color": "rgba(255,255,255,0)", "circle-stroke-color": "#2563EB", "circle-stroke-width": 2, "circle-opacity": stateOpacity("selected"), "circle-stroke-opacity": stateOpacity("selected") } },
|
||||
{ id: "scada-selected-outer", type: "circle", source: SCADA_SOURCE_ID, "source-layer": SCADA_SOURCE_LAYER, minzoom: 12, paint: { "circle-radius": SCADA_SELECTED_RADIUS, "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-stroke-color": MAP_STYLE_TOKENS.canvas.casing, "circle-stroke-width": SCADA_SELECTED_OUTER_STROKE_WIDTH, "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": SCADA_SELECTED_RADIUS, "circle-color": MAP_STYLE_TOKENS.canvas.transparent, "circle-stroke-color": MAP_STYLE_TOKENS.state.selected, "circle-stroke-width": SCADA_SELECTED_STROKE_WIDTH, "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 } }
|
||||
];
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { RasterLayerSpecification } from "maplibre-gl";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createBaseStyle, createWaterNetworkSources, SOURCE_LAYERS } from "./sources";
|
||||
|
||||
describe("createWaterNetworkSources", () => {
|
||||
it("loads SCADA tiles from the lingang:scada_points GeoServer layer", () => {
|
||||
const scada = createWaterNetworkSources().scada;
|
||||
|
||||
expect(SOURCE_LAYERS.scada).toBe("scada_points");
|
||||
expect(scada.tiles).toEqual([
|
||||
expect.stringContaining("/lingang:scada_points/point/WebMercatorQuad/")
|
||||
]);
|
||||
expect(scada.promoteId).toBe("point_id");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createBaseStyle", () => {
|
||||
it("calibrates light and satellite rasters for solid workbench surfaces", () => {
|
||||
const style = createBaseStyle("test-token");
|
||||
const light = style.layers.find((layer) => layer.id === "mapbox-light") as RasterLayerSpecification;
|
||||
const satellite = style.layers.find((layer) => layer.id === "mapbox-satellite") as RasterLayerSpecification;
|
||||
|
||||
expect(light.paint).toMatchObject({
|
||||
"raster-opacity": 0.96,
|
||||
"raster-saturation": -0.1,
|
||||
"raster-contrast": 0.06
|
||||
});
|
||||
expect(satellite.paint).toMatchObject({
|
||||
"raster-opacity": 1,
|
||||
"raster-saturation": -0.24,
|
||||
"raster-contrast": -0.06
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -60,18 +60,24 @@ export function createWaterNetworkSources() {
|
||||
"line",
|
||||
[121.73585149761436, 30.861759757577705, 121.9498481645973, 30.983213202338085]
|
||||
),
|
||||
scada: createLingangVectorSource(SOURCE_LAYERS.scada, "point", [121.7350340307058, 30.84636502815, 121.97051839928491, 30.994737681416165])
|
||||
scada: createLingangVectorSource(
|
||||
SOURCE_LAYERS.scada,
|
||||
"point",
|
||||
[121.7350340307058, 30.84636502815, 121.97051839928491, 30.994737681416165],
|
||||
"point_id"
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
function createLingangVectorSource(
|
||||
sourceLayer: (typeof SOURCE_LAYERS)[keyof typeof SOURCE_LAYERS],
|
||||
style: "line" | "point",
|
||||
bounds: [number, number, number, number]
|
||||
bounds: [number, number, number, number],
|
||||
promoteId = "id"
|
||||
): VectorSourceSpecification {
|
||||
return {
|
||||
type: "vector",
|
||||
promoteId: "id",
|
||||
promoteId,
|
||||
tiles: [
|
||||
`${GEOSERVER_WMTS_ROOT}/lingang:${sourceLayer}/${style}/WebMercatorQuad/{z}/{y}/{x}?format=application/vnd.mapbox-vector-tile`
|
||||
],
|
||||
@@ -118,9 +124,9 @@ export function createBaseStyle(mapboxToken?: string): StyleSpecification {
|
||||
type: "raster",
|
||||
source: "mapbox-light",
|
||||
paint: {
|
||||
"raster-opacity": 0.82,
|
||||
"raster-saturation": -0.22,
|
||||
"raster-contrast": 0.04
|
||||
"raster-opacity": 0.96,
|
||||
"raster-saturation": -0.1,
|
||||
"raster-contrast": 0.06
|
||||
}
|
||||
});
|
||||
layers.push({
|
||||
@@ -131,9 +137,9 @@ export function createBaseStyle(mapboxToken?: string): StyleSpecification {
|
||||
visibility: "none"
|
||||
},
|
||||
paint: {
|
||||
"raster-opacity": 0.86,
|
||||
"raster-saturation": -0.16,
|
||||
"raster-contrast": 0.02
|
||||
"raster-opacity": 1,
|
||||
"raster-saturation": -0.24,
|
||||
"raster-contrast": -0.06
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user