feat(map): manage agent overlays in layer panel
This commit is contained in:
@@ -46,17 +46,10 @@ type LayerVisibilitySectionProps = {
|
|||||||
|
|
||||||
function LayerVisibilitySection({ items, onToggleLayer }: LayerVisibilitySectionProps) {
|
function LayerVisibilitySection({ items, onToggleLayer }: LayerVisibilitySectionProps) {
|
||||||
const groups = groupLayerItems(items);
|
const groups = groupLayerItems(items);
|
||||||
const [openGroupIds, setOpenGroupIds] = useState(
|
const [groupOpenStates, setGroupOpenStates] = useState<Record<string, boolean>>({});
|
||||||
() => new Set(groups.filter((group) => group.defaultOpen).map((group) => group.id))
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleGroupToggle = (groupId: string, open: boolean) => {
|
const handleGroupToggle = (groupId: string, open: boolean) => {
|
||||||
setOpenGroupIds((current) => {
|
setGroupOpenStates((current) => ({ ...current, [groupId]: open }));
|
||||||
const next = new Set(current);
|
|
||||||
if (open) next.add(groupId);
|
|
||||||
else next.delete(groupId);
|
|
||||||
return next;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -64,7 +57,7 @@ function LayerVisibilitySection({ items, onToggleLayer }: LayerVisibilitySection
|
|||||||
{groups.map((group) => (
|
{groups.map((group) => (
|
||||||
<details
|
<details
|
||||||
key={group.id}
|
key={group.id}
|
||||||
open={openGroupIds.has(group.id)}
|
open={groupOpenStates[group.id] ?? group.defaultOpen}
|
||||||
onToggle={(event) => handleGroupToggle(group.id, event.currentTarget.open)}
|
onToggle={(event) => handleGroupToggle(group.id, event.currentTarget.open)}
|
||||||
className="surface-well group/layer overflow-hidden rounded-xl border"
|
className="surface-well group/layer overflow-hidden rounded-xl border"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import type { MapAnnotation } from "../types";
|
|
||||||
|
|
||||||
export const mapAnnotations: MapAnnotation[] = [
|
|
||||||
{
|
|
||||||
id: "burst",
|
|
||||||
label: "演示爆管位置",
|
|
||||||
coordinate: [121.5215, 30.9084],
|
|
||||||
kind: "burst"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "dn600",
|
|
||||||
label: "演示 DN600",
|
|
||||||
coordinate: [121.512, 30.9102],
|
|
||||||
kind: "pipe-label"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "east-gate",
|
|
||||||
label: "演示片区",
|
|
||||||
coordinate: [121.545, 30.951],
|
|
||||||
kind: "district"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "impact",
|
|
||||||
label: "演示影响范围 约 1.82 km²",
|
|
||||||
coordinate: [121.567, 30.896],
|
|
||||||
kind: "tooltip"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const impactAreaPolygon = {
|
|
||||||
type: "FeatureCollection" as const,
|
|
||||||
features: [
|
|
||||||
{
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: {
|
|
||||||
id: "impact-area",
|
|
||||||
label: "演示影响范围",
|
|
||||||
area: "1.82 km²"
|
|
||||||
},
|
|
||||||
geometry: {
|
|
||||||
type: "Polygon" as const,
|
|
||||||
coordinates: [
|
|
||||||
[
|
|
||||||
[121.5205, 30.9084],
|
|
||||||
[121.548, 30.928],
|
|
||||||
[121.604, 30.918],
|
|
||||||
[121.626, 30.886],
|
|
||||||
[121.584, 30.862],
|
|
||||||
[121.538, 30.873],
|
|
||||||
[121.506, 30.895],
|
|
||||||
[121.5205, 30.9084]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
export const annotationPoints = {
|
|
||||||
type: "FeatureCollection" as const,
|
|
||||||
features: mapAnnotations.map((annotation) => ({
|
|
||||||
type: "Feature" as const,
|
|
||||||
properties: {
|
|
||||||
id: annotation.id,
|
|
||||||
label: annotation.label,
|
|
||||||
kind: annotation.kind
|
|
||||||
},
|
|
||||||
geometry: {
|
|
||||||
type: "Point" as const,
|
|
||||||
coordinates: annotation.coordinate
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
@@ -70,7 +70,6 @@ type UseWorkbenchMapOptions = {
|
|||||||
onSelectFeature: (feature: DetailFeature) => void;
|
onSelectFeature: (feature: DetailFeature) => void;
|
||||||
selectedFeature: DetailFeature | null;
|
selectedFeature: DetailFeature | null;
|
||||||
simulationOverlay: FeatureCollection<Geometry> | null;
|
simulationOverlay: FeatureCollection<Geometry> | null;
|
||||||
allowDemoSimulation: boolean;
|
|
||||||
networkView?: NetworkViewParams | null;
|
networkView?: NetworkViewParams | null;
|
||||||
getAccessToken?: WorkbenchAccess;
|
getAccessToken?: WorkbenchAccess;
|
||||||
geoserverWorkspace?: string;
|
geoserverWorkspace?: string;
|
||||||
@@ -121,8 +120,6 @@ const SOURCE_GROUP_BY_ID: Record<string, string> = {
|
|||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
GEOSERVER_WATER_NETWORK_SOURCE_IDS.map((sourceId) => [sourceId, "geoserver-mvt"])
|
GEOSERVER_WATER_NETWORK_SOURCE_IDS.map((sourceId) => [sourceId, "geoserver-mvt"])
|
||||||
),
|
),
|
||||||
[SIMULATION_SOURCE_IDS.impactArea]: "annotation-source",
|
|
||||||
[SIMULATION_SOURCE_IDS.annotations]: "annotation-source",
|
|
||||||
[SIMULATION_SOURCE_IDS.backendAnalysis]: "annotation-source"
|
[SIMULATION_SOURCE_IDS.backendAnalysis]: "annotation-source"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,7 +139,6 @@ export function useWorkbenchMap({
|
|||||||
onSelectFeature,
|
onSelectFeature,
|
||||||
selectedFeature,
|
selectedFeature,
|
||||||
simulationOverlay,
|
simulationOverlay,
|
||||||
allowDemoSimulation,
|
|
||||||
networkView,
|
networkView,
|
||||||
geoserverWorkspace,
|
geoserverWorkspace,
|
||||||
getAccessToken
|
getAccessToken
|
||||||
@@ -150,11 +146,9 @@ export function useWorkbenchMap({
|
|||||||
const mapRef = useRef<MapLibreMap | null>(null);
|
const mapRef = useRef<MapLibreMap | null>(null);
|
||||||
const impactVisibleRef = useRef(impactVisible);
|
const impactVisibleRef = useRef(impactVisible);
|
||||||
const simulationOverlayRef = useRef(simulationOverlay);
|
const simulationOverlayRef = useRef(simulationOverlay);
|
||||||
const allowDemoSimulationRef = useRef(allowDemoSimulation);
|
|
||||||
const networkViewRef = useRef<NetworkViewParams>(networkView ?? WATER_NETWORK_GLOBAL_VIEW);
|
const networkViewRef = useRef<NetworkViewParams>(networkView ?? WATER_NETWORK_GLOBAL_VIEW);
|
||||||
const fittedNetworkViewRef = useRef<string | null>(null);
|
const fittedNetworkViewRef = useRef<string | null>(null);
|
||||||
simulationOverlayRef.current = simulationOverlay;
|
simulationOverlayRef.current = simulationOverlay;
|
||||||
allowDemoSimulationRef.current = allowDemoSimulation;
|
|
||||||
networkViewRef.current = networkView ?? WATER_NETWORK_GLOBAL_VIEW;
|
networkViewRef.current = networkView ?? WATER_NETWORK_GLOBAL_VIEW;
|
||||||
const [mapReady, setMapReady] = useState(false);
|
const [mapReady, setMapReady] = useState(false);
|
||||||
const [mapError, setMapError] = useState<string | null>(null);
|
const [mapError, setMapError] = useState<string | null>(null);
|
||||||
@@ -315,10 +309,8 @@ export function useWorkbenchMap({
|
|||||||
const source = sources[sourceId];
|
const source = sources[sourceId];
|
||||||
if (source) map.addSource(sourceId, source);
|
if (source) map.addSource(sourceId, source);
|
||||||
});
|
});
|
||||||
map.addSource(SIMULATION_SOURCE_IDS.impactArea, simulationSources.impactArea);
|
|
||||||
map.addSource(SIMULATION_SOURCE_IDS.annotations, simulationSources.annotations);
|
|
||||||
map.addSource(SIMULATION_SOURCE_IDS.backendAnalysis, simulationSources.backendAnalysis);
|
map.addSource(SIMULATION_SOURCE_IDS.backendAnalysis, simulationSources.backendAnalysis);
|
||||||
syncSimulationSourceData(map, simulationOverlayRef.current, allowDemoSimulationRef.current);
|
syncSimulationSourceData(map, simulationOverlayRef.current);
|
||||||
await registerSupplyAssetImages(map, scadaAbortController.signal);
|
await registerSupplyAssetImages(map, scadaAbortController.signal);
|
||||||
if (scadaAbortController.signal.aborted) return;
|
if (scadaAbortController.signal.aborted) return;
|
||||||
const scadaRegistration = await registerScadaImages(map, scadaAbortController.signal);
|
const scadaRegistration = await registerScadaImages(map, scadaAbortController.signal);
|
||||||
@@ -327,16 +319,13 @@ export function useWorkbenchMap({
|
|||||||
scadaRegistration.status === "unavailable"
|
scadaRegistration.status === "unavailable"
|
||||||
? scadaFallbackBusinessLayers
|
? scadaFallbackBusinessLayers
|
||||||
: scadaBusinessLayers;
|
: scadaBusinessLayers;
|
||||||
simulationAnnotationLayers
|
|
||||||
.filter((layer) => layer.id === "simulation-impact-fill")
|
|
||||||
.forEach((layer) => map.addLayer(layer));
|
|
||||||
filterWaterNetworkLayersBySourceIds(
|
filterWaterNetworkLayersBySourceIds(
|
||||||
waterNetworkBusinessLayers,
|
waterNetworkBusinessLayers,
|
||||||
REQUIRED_GEOSERVER_SOURCE_IDS
|
REQUIRED_GEOSERVER_SOURCE_IDS
|
||||||
).forEach((layer) => map.addLayer(layer));
|
).forEach((layer) => map.addLayer(layer));
|
||||||
activeScadaBusinessLayers.forEach((layer) => map.addLayer(layer));
|
activeScadaBusinessLayers.forEach((layer) => map.addLayer(layer));
|
||||||
simulationAnnotationLayers
|
simulationAnnotationLayers
|
||||||
.filter((layer) => layer.type !== "symbol" && layer.id !== "simulation-impact-fill")
|
.filter((layer) => layer.type !== "symbol")
|
||||||
.forEach((layer) => map.addLayer(layer));
|
.forEach((layer) => map.addLayer(layer));
|
||||||
filterWaterNetworkLayersBySourceIds(
|
filterWaterNetworkLayersBySourceIds(
|
||||||
waterNetworkInteractionLayers,
|
waterNetworkInteractionLayers,
|
||||||
@@ -484,8 +473,8 @@ export function useWorkbenchMap({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
if (!map || !mapReady) return;
|
if (!map || !mapReady) return;
|
||||||
syncSimulationSourceData(map, simulationOverlay, allowDemoSimulation);
|
syncSimulationSourceData(map, simulationOverlay);
|
||||||
}, [allowDemoSimulation, mapReady, simulationOverlay]);
|
}, [mapReady, simulationOverlay]);
|
||||||
|
|
||||||
function fitToNetworkBounds() {
|
function fitToNetworkBounds() {
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
@@ -541,23 +530,12 @@ function getScadaStatusMessage(snapshot: Awaited<ReturnType<typeof fetchScadaSna
|
|||||||
|
|
||||||
function syncSimulationSourceData(
|
function syncSimulationSourceData(
|
||||||
map: MapLibreMap,
|
map: MapLibreMap,
|
||||||
backendOverlay: FeatureCollection<Geometry> | null,
|
backendOverlay: FeatureCollection<Geometry> | null
|
||||||
allowDemoSimulation: boolean
|
|
||||||
) {
|
) {
|
||||||
const useBackendOverlay = Boolean(backendOverlay?.features.length);
|
const useBackendOverlay = Boolean(backendOverlay?.features.length);
|
||||||
(map.getSource(SIMULATION_SOURCE_IDS.backendAnalysis) as GeoJSONSource | undefined)?.setData(
|
(map.getSource(SIMULATION_SOURCE_IDS.backendAnalysis) as GeoJSONSource | undefined)?.setData(
|
||||||
useBackendOverlay ? backendOverlay! : EMPTY_SIMULATION_COLLECTION
|
useBackendOverlay ? backendOverlay! : EMPTY_SIMULATION_COLLECTION
|
||||||
);
|
);
|
||||||
(map.getSource(SIMULATION_SOURCE_IDS.impactArea) as GeoJSONSource | undefined)?.setData(
|
|
||||||
useBackendOverlay || !allowDemoSimulation
|
|
||||||
? EMPTY_SIMULATION_COLLECTION
|
|
||||||
: simulationSources.impactArea.data
|
|
||||||
);
|
|
||||||
(map.getSource(SIMULATION_SOURCE_IDS.annotations) as GeoJSONSource | undefined)?.setData(
|
|
||||||
useBackendOverlay || !allowDemoSimulation
|
|
||||||
? EMPTY_SIMULATION_COLLECTION
|
|
||||||
: simulationSources.annotations.data
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncDataDependentGeoServerLayers(
|
function syncDataDependentGeoServerLayers(
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ import {
|
|||||||
import { getResponsivePadding, getResponsiveWorkbenchPadding } from "./map/camera";
|
import { getResponsivePadding, getResponsiveWorkbenchPadding } from "./map/camera";
|
||||||
import { exportMapViewImage } from "./map/export-view";
|
import { exportMapViewImage } from "./map/export-view";
|
||||||
import { setMapLayersVisibility } from "./map/layer-visibility";
|
import { setMapLayersVisibility } from "./map/layer-visibility";
|
||||||
|
import { MAP_STYLE_TOKENS } from "./map/map-colors";
|
||||||
import {
|
import {
|
||||||
BASE_LAYER_OPTIONS,
|
BASE_LAYER_OPTIONS,
|
||||||
INITIAL_LAYER_VISIBILITY,
|
INITIAL_LAYER_VISIBILITY,
|
||||||
@@ -298,9 +299,6 @@ function MapWorkbenchContent({
|
|||||||
const artifactActive = artifactWorkspace.activeArtifact !== null;
|
const artifactActive = artifactWorkspace.activeArtifact !== null;
|
||||||
const activeSpatialOverlay = artifactWorkspace.activeArtifact?.spatialOverlay ?? null;
|
const activeSpatialOverlay = artifactWorkspace.activeArtifact?.spatialOverlay ?? null;
|
||||||
const activeArtifactHasSpatialView = Boolean(activeSpatialOverlay?.features.length);
|
const activeArtifactHasSpatialView = Boolean(activeSpatialOverlay?.features.length);
|
||||||
const allowDemoSimulation =
|
|
||||||
(!getWorkbenchProject(getAccessToken) || getWorkbenchProject(getAccessToken)?.gs_workspace === "tjwater_next")
|
|
||||||
&& (artifactWorkspace.activeArtifact === null || artifactWorkspace.activeArtifact.origin === "demo");
|
|
||||||
const latestAnalysisAvailable = Boolean(
|
const latestAnalysisAvailable = Boolean(
|
||||||
latestAnalysisArtifact?.blocks.some(
|
latestAnalysisArtifact?.blocks.some(
|
||||||
(block) =>
|
(block) =>
|
||||||
@@ -355,11 +353,6 @@ function MapWorkbenchContent({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setImpactVisible(activeArtifactHasSpatialView);
|
setImpactVisible(activeArtifactHasSpatialView);
|
||||||
setLayerVisibility((current) =>
|
|
||||||
current.simulation === activeArtifactHasSpatialView
|
|
||||||
? current
|
|
||||||
: { ...current, simulation: activeArtifactHasSpatialView }
|
|
||||||
);
|
|
||||||
}, [activeArtifactHasSpatialView, artifactWorkspace.activeArtifact?.id]);
|
}, [activeArtifactHasSpatialView, artifactWorkspace.activeArtifact?.id]);
|
||||||
|
|
||||||
function openAgentWorkspaceForViewport() {
|
function openAgentWorkspaceForViewport() {
|
||||||
@@ -458,7 +451,6 @@ function MapWorkbenchContent({
|
|||||||
onSelectFeature: handleSelectFeature,
|
onSelectFeature: handleSelectFeature,
|
||||||
selectedFeature: detailFeature,
|
selectedFeature: detailFeature,
|
||||||
simulationOverlay: activeSpatialOverlay,
|
simulationOverlay: activeSpatialOverlay,
|
||||||
allowDemoSimulation,
|
|
||||||
networkView: projectPresentation.networkView,
|
networkView: projectPresentation.networkView,
|
||||||
geoserverWorkspace: getWorkbenchProject(getAccessToken)?.gs_workspace,
|
geoserverWorkspace: getWorkbenchProject(getAccessToken)?.gs_workspace,
|
||||||
getAccessToken
|
getAccessToken
|
||||||
@@ -690,23 +682,55 @@ function MapWorkbenchContent({
|
|||||||
[activeToolId, mapReady, toggleToolbarTool]
|
[activeToolId, mapReady, toggleToolbarTool]
|
||||||
);
|
);
|
||||||
|
|
||||||
const layerControlItems = useMemo(
|
const overlayLayers = [
|
||||||
() => createLayerControlItems(layerVisibility, availableSourceIds, sourceFeatureCounts),
|
{ scope: "investigation", description: "Agent 调查", surface: presentation },
|
||||||
[availableSourceIds, layerVisibility, sourceFeatureCounts]
|
{ scope: "chat", description: "Agent 对话", surface: chatPresentation },
|
||||||
|
{ scope: "preview", description: "本地预览", surface: previewPresentation }
|
||||||
|
].flatMap(({ scope, description, surface }) => surface.overlay ? [{
|
||||||
|
id: `presentation:${scope}:${surface.overlay.overlay_id}`,
|
||||||
|
title: surface.overlay.title,
|
||||||
|
description: `${description} · ${surface.overlay.mode === "heatmap" ? "热力分布" : "空间范围"}`,
|
||||||
|
featureCount: surface.overlay.data.features.length,
|
||||||
|
color: MAP_STYLE_TOKENS.state.risk as string,
|
||||||
|
visible: surface.overlay.visible,
|
||||||
|
disabled: surface.history.selectedId !== null,
|
||||||
|
onToggle: (visible: boolean) => {
|
||||||
|
if (surface.overlay?.visible !== visible) surface.toggleOverlay();
|
||||||
|
}
|
||||||
|
}] : []);
|
||||||
|
if (activeArtifactHasSpatialView && artifactWorkspace.activeArtifact) {
|
||||||
|
overlayLayers.push({
|
||||||
|
id: `artifact:${artifactWorkspace.activeArtifact.id}`,
|
||||||
|
title: artifactWorkspace.activeArtifact.title,
|
||||||
|
description: "分析成果 · 空间范围",
|
||||||
|
featureCount: activeSpatialOverlay!.features.length,
|
||||||
|
color: MAP_STYLE_TOKENS.state.agent,
|
||||||
|
visible: impactVisible,
|
||||||
|
disabled: false,
|
||||||
|
onToggle: setImpactVisible
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const layerControlItems = createLayerControlItems(
|
||||||
|
layerVisibility, availableSourceIds, sourceFeatureCounts, overlayLayers
|
||||||
);
|
);
|
||||||
const visibleLegendItems = useMemo(
|
const visibleLegendItems = useMemo(
|
||||||
() => createVisibleMapLegendItems(layerControlItems),
|
() => createVisibleMapLegendItems(layerControlItems),
|
||||||
[layerControlItems]
|
[layerControlItems]
|
||||||
);
|
);
|
||||||
function handleToggleLayer(layerControlId: string, visible: boolean) {
|
function handleToggleLayer(layerControlId: string, visible: boolean) {
|
||||||
|
if (layerControlId === "simulation") {
|
||||||
|
setImpactVisible(visible);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const overlay = overlayLayers.find((item) => item.id === layerControlId);
|
||||||
|
if (overlay) {
|
||||||
|
if (!overlay.disabled) overlay.onToggle(visible);
|
||||||
|
return;
|
||||||
|
}
|
||||||
presentationLayerChangesRef.current.delete(layerControlId);
|
presentationLayerChangesRef.current.delete(layerControlId);
|
||||||
const map = mapRef.current;
|
const map = mapRef.current;
|
||||||
|
|
||||||
setLayerVisibility((current) => ({ ...current, [layerControlId]: visible }));
|
setLayerVisibility((current) => ({ ...current, [layerControlId]: visible }));
|
||||||
if (layerControlId === "simulation") {
|
|
||||||
setImpactVisible(visible);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map || !mapReady) {
|
if (!map || !mapReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1079,7 +1103,7 @@ function MapWorkbenchContent({
|
|||||||
measurement.clear();
|
measurement.clear();
|
||||||
drawing.clear();
|
drawing.clear();
|
||||||
geocoding.clear();
|
geocoding.clear();
|
||||||
handleToggleLayer("simulation", false);
|
setImpactVisible(false);
|
||||||
showMapNotice({ tone: "success", message: "已清空所有高亮和覆盖层。" });
|
showMapNotice({ tone: "success", message: "已清空所有高亮和覆盖层。" });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1471,10 +1495,10 @@ function MapWorkbenchContent({
|
|||||||
</div>
|
</div>
|
||||||
{!devPanelOpen ? (
|
{!devPanelOpen ? (
|
||||||
<>
|
<>
|
||||||
<div className="pointer-events-auto absolute right-3 top-10 hidden lg:block">
|
<div className="pointer-events-auto absolute right-3 top-10 z-50 hidden lg:block">
|
||||||
<MapToolbar items={toolbarItems} />
|
<MapToolbar items={toolbarItems} />
|
||||||
</div>
|
</div>
|
||||||
<div className="pointer-events-auto absolute right-16 top-10 hidden lg:block">
|
<div className="pointer-events-auto absolute right-16 top-10 z-50 hidden lg:block">
|
||||||
<ToolbarPanel {...toolbarPanelProps} />
|
<ToolbarPanel {...toolbarPanelProps} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -1570,7 +1594,7 @@ function MapWorkbenchContent({
|
|||||||
|
|
||||||
{mobileSheet === null && !readingWorkspace?.active && artifactWorkspace.surfaceMode !== "focus" ? (
|
{mobileSheet === null && !readingWorkspace?.active && artifactWorkspace.surfaceMode !== "focus" ? (
|
||||||
<div
|
<div
|
||||||
className="absolute left-3 right-3 z-30 transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:hidden"
|
className="absolute left-3 right-3 z-50 transition-[bottom] duration-200 ease-out motion-reduce:transition-none lg:hidden"
|
||||||
style={{
|
style={{
|
||||||
bottom: mobileOverlayLayout.mapControlBottom,
|
bottom: mobileOverlayLayout.mapControlBottom,
|
||||||
"--mobile-tool-available-height": `calc(100dvh - ${mobileOverlayLayout.mapControlBottom}px - 4.5rem)`
|
"--mobile-tool-available-height": `calc(100dvh - ${mobileOverlayLayout.mapControlBottom}px - 4.5rem)`
|
||||||
|
|||||||
@@ -1,54 +1,18 @@
|
|||||||
import type { GeoJSONSourceSpecification, StyleSpecification } from "maplibre-gl";
|
import type { GeoJSONSourceSpecification, StyleSpecification } from "maplibre-gl";
|
||||||
import { annotationPoints, impactAreaPolygon } from "../data/workbench-simulation";
|
|
||||||
import { MAP_STYLE_TOKENS } from "./map-colors";
|
import { MAP_STYLE_TOKENS } from "./map-colors";
|
||||||
|
|
||||||
export const SIMULATION_SOURCE_IDS = {
|
export const SIMULATION_SOURCE_IDS = {
|
||||||
impactArea: "simulation-impact-area",
|
|
||||||
annotations: "simulation-annotations",
|
|
||||||
backendAnalysis: "simulation-backend-analysis"
|
backendAnalysis: "simulation-backend-analysis"
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const EMPTY_FEATURE_COLLECTION = {
|
|
||||||
type: "FeatureCollection" as const,
|
|
||||||
features: []
|
|
||||||
};
|
|
||||||
|
|
||||||
export const simulationSources = {
|
export const simulationSources = {
|
||||||
impactArea: {
|
|
||||||
type: "geojson",
|
|
||||||
data: impactAreaPolygon
|
|
||||||
} satisfies GeoJSONSourceSpecification,
|
|
||||||
annotations: {
|
|
||||||
type: "geojson",
|
|
||||||
data: annotationPoints
|
|
||||||
} satisfies GeoJSONSourceSpecification,
|
|
||||||
backendAnalysis: {
|
backendAnalysis: {
|
||||||
type: "geojson",
|
type: "geojson",
|
||||||
data: EMPTY_FEATURE_COLLECTION
|
data: { type: "FeatureCollection", features: [] }
|
||||||
} satisfies GeoJSONSourceSpecification
|
} satisfies GeoJSONSourceSpecification
|
||||||
};
|
};
|
||||||
|
|
||||||
export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
||||||
{
|
|
||||||
id: "simulation-impact-fill",
|
|
||||||
type: "fill",
|
|
||||||
source: SIMULATION_SOURCE_IDS.impactArea,
|
|
||||||
paint: {
|
|
||||||
"fill-color": MAP_STYLE_TOKENS.state.risk,
|
|
||||||
"fill-opacity": MAP_STYLE_TOKENS.opacity.area
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "simulation-impact-outline",
|
|
||||||
type: "line",
|
|
||||||
source: SIMULATION_SOURCE_IDS.impactArea,
|
|
||||||
paint: {
|
|
||||||
"line-color": MAP_STYLE_TOKENS.state.risk,
|
|
||||||
"line-width": 1.5,
|
|
||||||
"line-dasharray": [2, 2],
|
|
||||||
"line-opacity": 0.72
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "simulation-backend-analysis-fill",
|
id: "simulation-backend-analysis-fill",
|
||||||
type: "fill",
|
type: "fill",
|
||||||
@@ -82,83 +46,6 @@ export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
|||||||
"circle-stroke-width": 2
|
"circle-stroke-width": 2
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "simulation-burst-halo",
|
|
||||||
type: "circle",
|
|
||||||
source: SIMULATION_SOURCE_IDS.annotations,
|
|
||||||
filter: ["==", ["get", "kind"], "burst"],
|
|
||||||
paint: {
|
|
||||||
"circle-color": MAP_STYLE_TOKENS.canvas.casing,
|
|
||||||
"circle-radius": 18,
|
|
||||||
"circle-stroke-color": MAP_STYLE_TOKENS.state.incident,
|
|
||||||
"circle-stroke-width": 3,
|
|
||||||
"circle-opacity": 0.88
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "simulation-burst-point",
|
|
||||||
type: "circle",
|
|
||||||
source: SIMULATION_SOURCE_IDS.annotations,
|
|
||||||
filter: ["==", ["get", "kind"], "burst"],
|
|
||||||
paint: {
|
|
||||||
"circle-color": MAP_STYLE_TOKENS.state.incident,
|
|
||||||
"circle-radius": 7,
|
|
||||||
"circle-stroke-color": MAP_STYLE_TOKENS.canvas.casing,
|
|
||||||
"circle-stroke-width": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "simulation-pipe-label",
|
|
||||||
type: "symbol",
|
|
||||||
source: SIMULATION_SOURCE_IDS.annotations,
|
|
||||||
filter: ["==", ["get", "kind"], "pipe-label"],
|
|
||||||
layout: {
|
|
||||||
"text-field": ["get", "label"],
|
|
||||||
"text-size": 13,
|
|
||||||
"text-font": ["Noto Sans Regular"],
|
|
||||||
"text-allow-overlap": true
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
"text-color": MAP_STYLE_TOKENS.label.inverse,
|
|
||||||
"text-halo-color": MAP_STYLE_TOKENS.state.agent,
|
|
||||||
"text-halo-width": 9
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "simulation-impact-label",
|
|
||||||
type: "symbol",
|
|
||||||
source: SIMULATION_SOURCE_IDS.annotations,
|
|
||||||
filter: ["==", ["get", "kind"], "tooltip"],
|
|
||||||
layout: {
|
|
||||||
"text-field": ["get", "label"],
|
|
||||||
"text-size": 14,
|
|
||||||
"text-font": ["Noto Sans Regular"],
|
|
||||||
"text-offset": [0, 0],
|
|
||||||
"text-allow-overlap": true
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
"text-color": MAP_STYLE_TOKENS.state.risk,
|
|
||||||
"text-halo-color": MAP_STYLE_TOKENS.canvas.casing,
|
|
||||||
"text-halo-width": 4
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "simulation-district-label",
|
|
||||||
type: "symbol",
|
|
||||||
source: SIMULATION_SOURCE_IDS.annotations,
|
|
||||||
filter: ["==", ["get", "kind"], "district"],
|
|
||||||
layout: {
|
|
||||||
"text-field": ["get", "label"],
|
|
||||||
"text-size": 13,
|
|
||||||
"text-font": ["Noto Sans Regular"],
|
|
||||||
"text-allow-overlap": true
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
"text-color": MAP_STYLE_TOKENS.label.secondary,
|
|
||||||
"text-halo-color": MAP_STYLE_TOKENS.canvas.casing,
|
|
||||||
"text-halo-width": 3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: "simulation-backend-analysis-label",
|
id: "simulation-backend-analysis-label",
|
||||||
type: "symbol",
|
type: "symbol",
|
||||||
@@ -176,25 +63,6 @@ export const simulationAnnotationLayers: StyleSpecification["layers"] = [
|
|||||||
"text-halo-color": MAP_STYLE_TOKENS.canvas.casing,
|
"text-halo-color": MAP_STYLE_TOKENS.canvas.casing,
|
||||||
"text-halo-width": 3
|
"text-halo-width": 3
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "simulation-burst-label",
|
|
||||||
type: "symbol",
|
|
||||||
source: SIMULATION_SOURCE_IDS.annotations,
|
|
||||||
filter: ["==", ["get", "kind"], "burst"],
|
|
||||||
layout: {
|
|
||||||
"text-field": "演示爆管位置\nDN600 给水管线\n压力:0.18 MPa\n时间:09:00",
|
|
||||||
"text-size": 12,
|
|
||||||
"text-font": ["Noto Sans Regular"],
|
|
||||||
"text-offset": [5.2, -3.4],
|
|
||||||
"text-anchor": "left",
|
|
||||||
"text-allow-overlap": true
|
|
||||||
},
|
|
||||||
paint: {
|
|
||||||
"text-color": MAP_STYLE_TOKENS.label.primary,
|
|
||||||
"text-halo-color": MAP_STYLE_TOKENS.canvas.casing,
|
|
||||||
"text-halo-width": 5
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -54,8 +54,7 @@ describe("workbench supply layer controls", () => {
|
|||||||
"scada-flow",
|
"scada-flow",
|
||||||
"water_quality_monitoring_points",
|
"water_quality_monitoring_points",
|
||||||
"fx_20200423_pipes",
|
"fx_20200423_pipes",
|
||||||
"fx_20200811_dn100_pipes",
|
"fx_20200811_dn100_pipes"
|
||||||
"simulation"
|
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
expect(items.find((item) => item.id === "fx_20200423_pipes")).toMatchObject({
|
expect(items.find((item) => item.id === "fx_20200423_pipes")).toMatchObject({
|
||||||
@@ -104,12 +103,30 @@ describe("workbench supply layer controls", () => {
|
|||||||
"valves",
|
"valves",
|
||||||
"reservoirs",
|
"reservoirs",
|
||||||
"scada-pressure",
|
"scada-pressure",
|
||||||
"scada-flow",
|
"scada-flow"
|
||||||
"simulation"
|
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defines map-matched symbols for model, source GIS, and analysis layers", () => {
|
it("lists actual presentation overlays with their visibility and removes cleared entries", () => {
|
||||||
|
const overlays = [
|
||||||
|
{ id: "presentation:investigation:area", title: "爆管影响范围", description: "Agent 调查 · 空间范围", featureCount: 1, visible: true },
|
||||||
|
{ id: "presentation:chat:area", title: "投诉点热力", description: "Agent 对话 · 热力分布", featureCount: 8, visible: false }
|
||||||
|
];
|
||||||
|
const items = createLayerControlItems(INITIAL_LAYER_VISIBILITY, undefined, {}, overlays);
|
||||||
|
|
||||||
|
expect(items.filter((item) => item.group === "analysis")).toMatchObject([
|
||||||
|
{ id: overlays[0].id, label: "爆管影响范围", visible: true, featureCount: 1 },
|
||||||
|
{ id: overlays[1].id, label: "投诉点热力", visible: false, featureCount: 8 }
|
||||||
|
]);
|
||||||
|
expect(createVisibleMapLegendItems(items).filter((item) => item.group === "analysis")).toMatchObject([
|
||||||
|
{ label: "爆管影响范围", layerControlId: overlays[0].id, shape: "area" }
|
||||||
|
]);
|
||||||
|
const cleared = createLayerControlItems(INITIAL_LAYER_VISIBILITY);
|
||||||
|
expect(cleared.filter((item) => item.group === "analysis")).toEqual([]);
|
||||||
|
expect(createVisibleMapLegendItems(cleared).filter((item) => item.group === "analysis")).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defines map-matched symbols for model and source GIS layers", () => {
|
||||||
expect(MAP_LEGEND_ITEMS.map((item) => item.id)).toEqual([
|
expect(MAP_LEGEND_ITEMS.map((item) => item.id)).toEqual([
|
||||||
"major-pipe",
|
"major-pipe",
|
||||||
"medium-pipe",
|
"medium-pipe",
|
||||||
@@ -134,8 +151,7 @@ describe("workbench supply layer controls", () => {
|
|||||||
"source-hydrant",
|
"source-hydrant",
|
||||||
"source-connection",
|
"source-connection",
|
||||||
"source-customer-meter",
|
"source-customer-meter",
|
||||||
"source-non-residential-meter",
|
"source-non-residential-meter"
|
||||||
"impact-area"
|
|
||||||
]);
|
]);
|
||||||
expect(MAP_LEGEND_ITEMS.find((item) => item.id === "valve")).toMatchObject({
|
expect(MAP_LEGEND_ITEMS.find((item) => item.id === "valve")).toMatchObject({
|
||||||
imageSrc: "/map/valve.png"
|
imageSrc: "/map/valve.png"
|
||||||
@@ -186,11 +202,6 @@ describe("workbench supply layer controls", () => {
|
|||||||
).toMatchObject({
|
).toMatchObject({
|
||||||
imageSrc: "/map/gis-non-residential-meter.svg"
|
imageSrc: "/map/gis-non-residential-meter.svg"
|
||||||
});
|
});
|
||||||
expect(MAP_LEGEND_ITEMS.find((item) => item.id === "impact-area")).toMatchObject({
|
|
||||||
groupLabel: "分析覆盖",
|
|
||||||
layerControlId: "simulation",
|
|
||||||
shape: "area"
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps the legend synchronized with visible layer controls", () => {
|
it("keeps the legend synchronized with visible layer controls", () => {
|
||||||
@@ -222,8 +233,7 @@ describe("workbench supply layer controls", () => {
|
|||||||
|
|
||||||
const reducedItems = createLayerControlItems({
|
const reducedItems = createLayerControlItems({
|
||||||
...INITIAL_LAYER_VISIBILITY,
|
...INITIAL_LAYER_VISIBILITY,
|
||||||
valves: false,
|
valves: false
|
||||||
simulation: false
|
|
||||||
});
|
});
|
||||||
const reducedLegendIds = createVisibleMapLegendItems(reducedItems).map((item) => item.id);
|
const reducedLegendIds = createVisibleMapLegendItems(reducedItems).map((item) => item.id);
|
||||||
|
|
||||||
|
|||||||
@@ -15,19 +15,6 @@ import {
|
|||||||
import { SCADA_ICON_PATHS, SCADA_LAYER_CONTROL_ITEMS, SCADA_LAYER_GROUPS } from "./scada";
|
import { SCADA_ICON_PATHS, SCADA_LAYER_CONTROL_ITEMS, SCADA_LAYER_GROUPS } from "./scada";
|
||||||
import { SUPPLY_ASSET_ICON_PATHS } from "./supply-icons";
|
import { SUPPLY_ASSET_ICON_PATHS } from "./supply-icons";
|
||||||
|
|
||||||
export const WORKBENCH_LAYER_GROUPS: Record<string, string[]> = {
|
|
||||||
simulation: [
|
|
||||||
"simulation-impact-fill",
|
|
||||||
"simulation-impact-outline",
|
|
||||||
"simulation-burst-halo",
|
|
||||||
"simulation-burst-point",
|
|
||||||
"simulation-pipe-label",
|
|
||||||
"simulation-impact-label",
|
|
||||||
"simulation-district-label",
|
|
||||||
"simulation-burst-label"
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
export const INITIAL_LAYER_VISIBILITY: Record<string, boolean> = {
|
export const INITIAL_LAYER_VISIBILITY: Record<string, boolean> = {
|
||||||
...Object.fromEntries(
|
...Object.fromEntries(
|
||||||
SUPPLY_LAYER_CATALOG.filter((layer) => layer.id !== "scada").map((layer) => [
|
SUPPLY_LAYER_CATALOG.filter((layer) => layer.id !== "scada").map((layer) => [
|
||||||
@@ -35,8 +22,17 @@ export const INITIAL_LAYER_VISIBILITY: Record<string, boolean> = {
|
|||||||
layer.initialVisible
|
layer.initialVisible
|
||||||
])
|
])
|
||||||
),
|
),
|
||||||
...Object.fromEntries(SCADA_LAYER_CONTROL_ITEMS.map((item) => [item.id, true])),
|
...Object.fromEntries(SCADA_LAYER_CONTROL_ITEMS.map((item) => [item.id, true]))
|
||||||
simulation: false
|
};
|
||||||
|
|
||||||
|
export type AnalysisOverlayLayer = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
visible: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
featureCount: number;
|
||||||
|
color?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SOURCE_CONTROL_LABELS = Object.fromEntries(
|
const SOURCE_CONTROL_LABELS = Object.fromEntries(
|
||||||
@@ -421,25 +417,27 @@ export const MAP_LEGEND_ITEMS: MapLegendItem[] = [
|
|||||||
imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceNonResidentialMeter,
|
imageSrc: SUPPLY_ASSET_ICON_PATHS.sourceNonResidentialMeter,
|
||||||
layerControlId: "fx_20200423_non_residential_meters",
|
layerControlId: "fx_20200423_non_residential_meters",
|
||||||
...SOURCE_FACILITY_LEGEND_GROUP
|
...SOURCE_FACILITY_LEGEND_GROUP
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "impact-area",
|
|
||||||
label: "影响范围",
|
|
||||||
description: "事故或分析结果",
|
|
||||||
color: MAP_STYLE_TOKENS.state.incident,
|
|
||||||
shape: "area",
|
|
||||||
layerControlId: "simulation",
|
|
||||||
...ANALYSIS_LEGEND_GROUP
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
export function createVisibleMapLegendItems(
|
export function createVisibleMapLegendItems(
|
||||||
layerItems: readonly Pick<MapLayerControlItem, "id" | "visible">[]
|
layerItems: readonly MapLayerControlItem[]
|
||||||
) {
|
) {
|
||||||
const visibleLayerIds = new Set(layerItems.filter((item) => item.visible).map((item) => item.id));
|
const visibleLayerIds = new Set(layerItems.filter((item) => item.visible).map((item) => item.id));
|
||||||
return MAP_LEGEND_ITEMS.filter((item) =>
|
return [
|
||||||
|
...MAP_LEGEND_ITEMS.filter((item) =>
|
||||||
item.layerControlId ? visibleLayerIds.has(item.layerControlId) : true
|
item.layerControlId ? visibleLayerIds.has(item.layerControlId) : true
|
||||||
);
|
),
|
||||||
|
...layerItems.filter((item) => item.group === "analysis" && item.visible).map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
label: item.label,
|
||||||
|
description: item.description,
|
||||||
|
color: item.visual?.color ?? MAP_STYLE_TOKENS.state.risk,
|
||||||
|
shape: "area" as const,
|
||||||
|
layerControlId: item.id,
|
||||||
|
...ANALYSIS_LEGEND_GROUP
|
||||||
|
}))
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BASE_LAYER_GROUPS = {
|
export const BASE_LAYER_GROUPS = {
|
||||||
@@ -502,7 +500,8 @@ export const BASE_LAYER_OPTIONS: BaseLayerOption[] = [
|
|||||||
export function createLayerControlItems(
|
export function createLayerControlItems(
|
||||||
layerVisibility: Record<string, boolean>,
|
layerVisibility: Record<string, boolean>,
|
||||||
availableSourceIds: readonly WaterNetworkSourceId[] = AVAILABLE_WATER_NETWORK_SOURCE_IDS,
|
availableSourceIds: readonly WaterNetworkSourceId[] = AVAILABLE_WATER_NETWORK_SOURCE_IDS,
|
||||||
featureCounts: Partial<Record<WaterNetworkSourceId, number>> = {}
|
featureCounts: Partial<Record<WaterNetworkSourceId, number>> = {},
|
||||||
|
analysisOverlays: readonly AnalysisOverlayLayer[] = []
|
||||||
): MapLayerControlItem[] {
|
): MapLayerControlItem[] {
|
||||||
const available = new Set<WaterNetworkSourceId>(availableSourceIds);
|
const available = new Set<WaterNetworkSourceId>(availableSourceIds);
|
||||||
const catalogItems = SUPPLY_LAYER_CATALOG.filter(
|
const catalogItems = SUPPLY_LAYER_CATALOG.filter(
|
||||||
@@ -560,16 +559,18 @@ export function createLayerControlItems(
|
|||||||
groupDescription: "源数据快照与专项台账",
|
groupDescription: "源数据快照与专项台账",
|
||||||
groupDefaultOpen: false
|
groupDefaultOpen: false
|
||||||
})),
|
})),
|
||||||
{
|
...analysisOverlays.map((overlay) => ({
|
||||||
id: "simulation",
|
id: overlay.id,
|
||||||
label: "模拟结果",
|
label: overlay.title,
|
||||||
description: "影响范围与事故标注",
|
description: overlay.description,
|
||||||
visual: { kind: "area", color: MAP_STYLE_TOKENS.state.incident },
|
featureCount: overlay.featureCount,
|
||||||
visible: layerVisibility.simulation,
|
visual: { kind: "area" as const, color: overlay.color ?? MAP_STYLE_TOKENS.state.risk },
|
||||||
|
visible: overlay.visible,
|
||||||
|
disabled: overlay.disabled,
|
||||||
group: "analysis",
|
group: "analysis",
|
||||||
groupLabel: "分析覆盖",
|
groupLabel: "分析覆盖",
|
||||||
groupDescription: "随已打开的空间成果显示"
|
groupDescription: "Agent 生成的范围与分析成果"
|
||||||
}
|
}))
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,8 +590,7 @@ export function getWorkbenchLayerIds(
|
|||||||
.map((layer) => layer.id);
|
.map((layer) => layer.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const layerIds = WORKBENCH_LAYER_GROUPS[groupId] ?? [];
|
return [];
|
||||||
return layerIds.filter((layerId) => map.getLayer(layerId));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWaterNetworkSourceId(value: string): value is WaterNetworkSourceId {
|
function isWaterNetworkSourceId(value: string): value is WaterNetworkSourceId {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { investigationFixture, mockInvestigationPresentation, selectInvestigation } from "./support/mock-investigation-presentation";
|
||||||
|
|
||||||
|
test("lists Agent overlays in the layer panel and synchronizes visibility and cleanup", async ({ page }, testInfo) => {
|
||||||
|
test.setTimeout(60_000);
|
||||||
|
const task = investigationFixture(1, "覆盖面图层核验");
|
||||||
|
const mock = await mockInvestigationPresentation(page, [task]);
|
||||||
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||||
|
await selectInvestigation(page, task.title);
|
||||||
|
await expect.poll(() => page.evaluate(() => Boolean(window.__waterNetworkMap?.getLayer("simulation-backend-analysis-fill")))).toBe(true);
|
||||||
|
expect(await page.evaluate(() => Boolean(window.__waterNetworkMap?.getSource("simulation-impact-area")))).toBe(false);
|
||||||
|
const layerTool = page.getByRole("button", { name: /图层:管理地图图层/ });
|
||||||
|
await layerTool.click();
|
||||||
|
const panel = page.getByRole("region", { name: "图层", exact: true });
|
||||||
|
await expect(panel.getByText("模拟结果", { exact: true })).toHaveCount(0);
|
||||||
|
await expect(panel.getByText("分析覆盖", { exact: true })).toHaveCount(0);
|
||||||
|
|
||||||
|
const command = { action: "set_overlay", overlay_id: "affected-area", title: "爆管影响范围", mode: "geometry", evidence_id: "test:area", data: {
|
||||||
|
type: "FeatureCollection", features: [{ type: "Feature", properties: {}, geometry: {
|
||||||
|
type: "Polygon", coordinates: [[[121.48, 31.1], [121.49, 31.1], [121.49, 31.11], [121.48, 31.1]]]
|
||||||
|
} }]
|
||||||
|
} };
|
||||||
|
await mock.emit(task, command);
|
||||||
|
const row = panel.getByRole("button", { name: /^爆管影响范围,/ });
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await expect(row).toHaveAttribute("aria-pressed", "true");
|
||||||
|
const visibility = () => page.evaluate(() => window.__waterNetworkMap?.getLayoutProperty("investigation-presentation-overlay-fill", "visibility"));
|
||||||
|
await expect.poll(visibility).toBe("visible");
|
||||||
|
await row.click();
|
||||||
|
await expect(row).toHaveAttribute("aria-pressed", "false");
|
||||||
|
await expect.poll(visibility).toBe("none");
|
||||||
|
await layerTool.click();
|
||||||
|
const display = page.getByLabel("调查实时展示", { exact: true });
|
||||||
|
await display.getByRole("button", { name: "显示覆盖层", exact: true }).click();
|
||||||
|
await expect.poll(visibility).toBe("visible");
|
||||||
|
await layerTool.click();
|
||||||
|
await expect(row).toHaveAttribute("aria-pressed", "true");
|
||||||
|
await page.screenshot({ path: testInfo.outputPath("agent-overlay-layer-desktop.png"), animations: "disabled" });
|
||||||
|
|
||||||
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
await row.scrollIntoViewIfNeeded();
|
||||||
|
await expect(row).toBeVisible();
|
||||||
|
await row.click();
|
||||||
|
await expect.poll(visibility).toBe("none");
|
||||||
|
await row.click();
|
||||||
|
await expect.poll(visibility).toBe("visible");
|
||||||
|
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(390);
|
||||||
|
await page.screenshot({ path: testInfo.outputPath("agent-overlay-layer-mobile.png"), animations: "disabled" });
|
||||||
|
|
||||||
|
await mock.emit(task, { action: "clear_overlay", overlay_id: "affected-area" });
|
||||||
|
await expect(row).toHaveCount(0);
|
||||||
|
await expect(panel.getByText("分析覆盖", { exact: true })).toHaveCount(0);
|
||||||
|
expect(mock.mutations).toEqual([]);
|
||||||
|
});
|
||||||
@@ -103,7 +103,7 @@ test("shows data-dependent layer controls only when GeoServer reports features",
|
|||||||
const layerIds = globalThis.__waterNetworkMap?.getStyle().layers.map((layer) => layer.id) ?? [];
|
const layerIds = globalThis.__waterNetworkMap?.getStyle().layers.map((layer) => layer.id) ?? [];
|
||||||
return {
|
return {
|
||||||
valveLabel: layerIds.indexOf("workbench-value-label-valves"),
|
valveLabel: layerIds.indexOf("workbench-value-label-valves"),
|
||||||
simulationLabel: layerIds.indexOf("simulation-burst-label")
|
simulationLabel: layerIds.indexOf("simulation-backend-analysis-label")
|
||||||
};
|
};
|
||||||
})).toMatchObject({
|
})).toMatchObject({
|
||||||
valveLabel: expect.any(Number),
|
valveLabel: expect.any(Number),
|
||||||
@@ -113,7 +113,7 @@ test("shows data-dependent layer controls only when GeoServer reports features",
|
|||||||
const layerIds = globalThis.__waterNetworkMap?.getStyle().layers.map((layer) => layer.id) ?? [];
|
const layerIds = globalThis.__waterNetworkMap?.getStyle().layers.map((layer) => layer.id) ?? [];
|
||||||
return [
|
return [
|
||||||
layerIds.indexOf("workbench-value-label-valves"),
|
layerIds.indexOf("workbench-value-label-valves"),
|
||||||
layerIds.indexOf("simulation-burst-label")
|
layerIds.indexOf("simulation-backend-analysis-label")
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
expect(layerOrder[0]).toBeGreaterThanOrEqual(0);
|
expect(layerOrder[0]).toBeGreaterThanOrEqual(0);
|
||||||
|
|||||||
@@ -377,9 +377,8 @@ test("keeps a non-spatial backend result in the analysis workspace without demo
|
|||||||
await expect.poll(() => page.evaluate(() => ({
|
await expect.poll(() => page.evaluate(() => ({
|
||||||
backend: window.__waterNetworkMap
|
backend: window.__waterNetworkMap
|
||||||
?.querySourceFeatures("simulation-backend-analysis").length ?? 0,
|
?.querySourceFeatures("simulation-backend-analysis").length ?? 0,
|
||||||
demo: window.__waterNetworkMap
|
demoSource: Boolean(window.__waterNetworkMap?.getSource("simulation-impact-area"))
|
||||||
?.querySourceFeatures("simulation-impact-area").length ?? 0
|
}))).toEqual({ backend: 0, demoSource: false });
|
||||||
}))).toEqual({ backend: 0, demo: 0 });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("keeps mock conditions while using backend work orders and alerts", async ({ page }) => {
|
test("keeps mock conditions while using backend work orders and alerts", async ({ page }) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user