fix(sensor-placement): refine drawing export and editor density
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
@@ -10,9 +16,7 @@ import {
|
|||||||
DialogActions,
|
DialogActions,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
FormControlLabel,
|
Stack,
|
||||||
Radio,
|
|
||||||
RadioGroup,
|
|
||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
@@ -21,10 +25,7 @@ import {
|
|||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
import OlMap from "ol/Map";
|
import OlMap from "ol/Map";
|
||||||
import View from "ol/View";
|
import View from "ol/View";
|
||||||
import LayerGroup from "ol/layer/Group";
|
|
||||||
import TileLayer from "ol/layer/Tile";
|
|
||||||
import VectorTileLayer from "ol/layer/VectorTile";
|
import VectorTileLayer from "ol/layer/VectorTile";
|
||||||
import type TileSource from "ol/source/Tile";
|
|
||||||
import type VectorTileSource from "ol/source/VectorTile";
|
import type VectorTileSource from "ol/source/VectorTile";
|
||||||
import type { StyleLike } from "ol/style/Style";
|
import type { StyleLike } from "ol/style/Style";
|
||||||
import {
|
import {
|
||||||
@@ -61,8 +62,6 @@ const DRAWING_MAP_SIZE: [number, number] = [
|
|||||||
A3_LANDSCAPE_HEIGHT - 650,
|
A3_LANDSCAPE_HEIGHT - 650,
|
||||||
];
|
];
|
||||||
|
|
||||||
type DrawingMode = "linework" | "basemap";
|
|
||||||
|
|
||||||
type DrawingPipeLayer = {
|
type DrawingPipeLayer = {
|
||||||
getSource?: () => VectorTileSource | null;
|
getSource?: () => VectorTileSource | null;
|
||||||
getExtent?: () => number[] | undefined;
|
getExtent?: () => number[] | undefined;
|
||||||
@@ -110,52 +109,10 @@ export const fitMapToFullNetwork = (
|
|||||||
interface DrawingMapSession {
|
interface DrawingMapSession {
|
||||||
map: OlMap;
|
map: OlMap;
|
||||||
extent: [number, number, number, number];
|
extent: [number, number, number, number];
|
||||||
attribution: string | null;
|
|
||||||
dispose: () => void;
|
dispose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cloneVisibleBaseLayers = (
|
const createDrawingMapSession = (mainMap: OlMap): DrawingMapSession => {
|
||||||
mainMap: OlMap,
|
|
||||||
): { layers: TileLayer<TileSource>[]; attribution: string | null } => {
|
|
||||||
const cloned: TileLayer<TileSource>[] = [];
|
|
||||||
const attributions = new Set<string>();
|
|
||||||
const collect = (layer: unknown, parentVisible = true) => {
|
|
||||||
const candidate = layer as {
|
|
||||||
getVisible?: () => boolean;
|
|
||||||
get?: (key: string) => unknown;
|
|
||||||
getLayers?: () => { getArray: () => unknown[] };
|
|
||||||
};
|
|
||||||
if (!parentVisible || candidate.getVisible?.() === false) return;
|
|
||||||
const attribution = candidate.get?.("exportAttribution");
|
|
||||||
if (typeof attribution === "string" && attribution) {
|
|
||||||
attributions.add(attribution);
|
|
||||||
}
|
|
||||||
if (layer instanceof LayerGroup) {
|
|
||||||
candidate.getLayers?.().getArray().forEach((child) => collect(child));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!(layer instanceof TileLayer)) return;
|
|
||||||
const source = layer.getSource();
|
|
||||||
if (!source) return;
|
|
||||||
cloned.push(
|
|
||||||
new TileLayer({
|
|
||||||
source,
|
|
||||||
opacity: layer.getOpacity(),
|
|
||||||
visible: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
mainMap.getLayers().getArray().forEach((layer) => collect(layer));
|
|
||||||
return {
|
|
||||||
layers: cloned,
|
|
||||||
attribution: attributions.size ? [...attributions].join(" · ") : null,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const createDrawingMapSession = (
|
|
||||||
mainMap: OlMap,
|
|
||||||
mode: DrawingMode,
|
|
||||||
): DrawingMapSession => {
|
|
||||||
const mainPipeLayer = getDrawingPipeLayer(mainMap);
|
const mainPipeLayer = getDrawingPipeLayer(mainMap);
|
||||||
const source = mainPipeLayer?.getSource?.();
|
const source = mainPipeLayer?.getSource?.();
|
||||||
const extent = mainPipeLayer?.getExtent?.();
|
const extent = mainPipeLayer?.getExtent?.();
|
||||||
@@ -196,18 +153,13 @@ const createDrawingMapSession = (
|
|||||||
});
|
});
|
||||||
pipeLayer.set("value", "pipes");
|
pipeLayer.set("value", "pipes");
|
||||||
pipeLayer.setExtent(networkExtent);
|
pipeLayer.setExtent(networkExtent);
|
||||||
const basemap =
|
|
||||||
mode === "basemap"
|
|
||||||
? cloneVisibleBaseLayers(mainMap)
|
|
||||||
: { layers: [], attribution: null };
|
|
||||||
const layers = [...basemap.layers, pipeLayer];
|
|
||||||
drawingMap = new OlMap({
|
drawingMap = new OlMap({
|
||||||
target,
|
target,
|
||||||
pixelRatio: 1,
|
pixelRatio: 1,
|
||||||
view: new View({
|
view: new View({
|
||||||
projection: mainMap.getView().getProjection(),
|
projection: mainMap.getView().getProjection(),
|
||||||
}),
|
}),
|
||||||
layers,
|
layers: [pipeLayer],
|
||||||
controls: [],
|
controls: [],
|
||||||
interactions: [],
|
interactions: [],
|
||||||
});
|
});
|
||||||
@@ -218,7 +170,6 @@ const createDrawingMapSession = (
|
|||||||
return {
|
return {
|
||||||
map: drawingMap,
|
map: drawingMap,
|
||||||
extent: networkExtent,
|
extent: networkExtent,
|
||||||
attribution: basemap.attribution,
|
|
||||||
dispose: () => {
|
dispose: () => {
|
||||||
if (disposed) return;
|
if (disposed) return;
|
||||||
disposed = true;
|
disposed = true;
|
||||||
@@ -263,59 +214,6 @@ const waitForDrawingMapRender = (map: OlMap) =>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const captureMapCanvas = (map: OlMap): HTMLCanvasElement => {
|
|
||||||
const size = map.getSize();
|
|
||||||
if (!size) throw new Error("工程图地图尺寸不可用");
|
|
||||||
const output = document.createElement("canvas");
|
|
||||||
output.width = size[0];
|
|
||||||
output.height = size[1];
|
|
||||||
const context = output.getContext("2d");
|
|
||||||
if (!context) throw new Error("无法创建地图底图画布");
|
|
||||||
context.fillStyle = "#ffffff";
|
|
||||||
context.fillRect(0, 0, output.width, output.height);
|
|
||||||
|
|
||||||
const canvases = map
|
|
||||||
.getViewport()
|
|
||||||
.querySelectorAll<HTMLCanvasElement>(".ol-layer canvas, canvas.ol-layer");
|
|
||||||
canvases.forEach((canvas) => {
|
|
||||||
if (!canvas.width || !canvas.height) return;
|
|
||||||
const opacityText =
|
|
||||||
canvas.parentElement?.style.opacity || canvas.style.opacity || "1";
|
|
||||||
const opacity = Number(opacityText);
|
|
||||||
context.globalAlpha = Number.isFinite(opacity) ? opacity : 1;
|
|
||||||
|
|
||||||
const matrix = canvas.style.transform
|
|
||||||
.match(/^matrix\(([^)]+)\)$/)?.[1]
|
|
||||||
.split(",")
|
|
||||||
.map(Number);
|
|
||||||
if (matrix?.length === 6 && matrix.every(Number.isFinite)) {
|
|
||||||
context.setTransform(
|
|
||||||
matrix[0],
|
|
||||||
matrix[1],
|
|
||||||
matrix[2],
|
|
||||||
matrix[3],
|
|
||||||
matrix[4],
|
|
||||||
matrix[5],
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const width = Number.parseFloat(canvas.style.width) || canvas.width;
|
|
||||||
const height = Number.parseFloat(canvas.style.height) || canvas.height;
|
|
||||||
context.setTransform(
|
|
||||||
width / canvas.width,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
height / canvas.height,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
context.drawImage(canvas, 0, 0);
|
|
||||||
});
|
|
||||||
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
||||||
context.globalAlpha = 1;
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
||||||
const readNetworkDataFromMap = (
|
const readNetworkDataFromMap = (
|
||||||
map: OlMap,
|
map: OlMap,
|
||||||
extent: [number, number, number, number],
|
extent: [number, number, number, number],
|
||||||
@@ -383,6 +281,27 @@ const escapeHtml = (value: string) =>
|
|||||||
})[character] ?? character,
|
})[character] ?? character,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const DrawingPreview = ({
|
||||||
|
src,
|
||||||
|
label,
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
label: string;
|
||||||
|
}) => (
|
||||||
|
<Box
|
||||||
|
role="img"
|
||||||
|
aria-label={label}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundImage: `url("${src}")`,
|
||||||
|
backgroundPosition: "center",
|
||||||
|
backgroundRepeat: "no-repeat",
|
||||||
|
backgroundSize: "contain",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -395,25 +314,40 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
|||||||
const [networkData, setNetworkData] = useState<NetworkDrawingData | null>(
|
const [networkData, setNetworkData] = useState<NetworkDrawingData | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [drawingMode, setDrawingMode] = useState<DrawingMode>("linework");
|
|
||||||
const [mapCanvas, setMapCanvas] = useState<HTMLCanvasElement | null>(null);
|
|
||||||
const [mapAttribution, setMapAttribution] = useState<string | null>(null);
|
|
||||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||||
|
const previewUrlRef = useRef<string | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const replacePreviewUrl = useCallback((nextUrl: string | null) => {
|
||||||
|
const previousUrl = previewUrlRef.current;
|
||||||
|
previewUrlRef.current = nextUrl;
|
||||||
|
setPreviewUrl(nextUrl);
|
||||||
|
if (previousUrl && previousUrl !== nextUrl) {
|
||||||
|
URL.revokeObjectURL(previousUrl);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
const filename = useMemo(
|
const filename = useMemo(
|
||||||
() => `${scheme.scheme_name}${dirty ? "_未保存草稿" : ""}_布置图.png`,
|
() => `${scheme.scheme_name}${dirty ? "_未保存草稿" : ""}_布置图.png`,
|
||||||
[dirty, scheme.scheme_name],
|
[dirty, scheme.scheme_name],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (previewUrlRef.current) {
|
||||||
|
URL.revokeObjectURL(previewUrlRef.current);
|
||||||
|
previewUrlRef.current = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return;
|
if (!open) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setNetworkData(null);
|
setNetworkData(null);
|
||||||
setMapCanvas(null);
|
replacePreviewUrl(null);
|
||||||
setMapAttribution(null);
|
|
||||||
if (!map) {
|
if (!map) {
|
||||||
setError("主地图尚未初始化,请稍后重试");
|
setError("主地图尚未初始化,请稍后重试");
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -421,7 +355,7 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
|||||||
}
|
}
|
||||||
let drawingSession: DrawingMapSession;
|
let drawingSession: DrawingMapSession;
|
||||||
try {
|
try {
|
||||||
drawingSession = createDrawingMapSession(map, drawingMode);
|
drawingSession = createDrawingMapSession(map);
|
||||||
} catch (reason) {
|
} catch (reason) {
|
||||||
setError(
|
setError(
|
||||||
reason instanceof Error ? reason.message : "无法适配完整管网范围",
|
reason instanceof Error ? reason.message : "无法适配完整管网范围",
|
||||||
@@ -430,24 +364,15 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
waitForDrawingMapRender(drawingSession.map)
|
waitForDrawingMapRender(drawingSession.map)
|
||||||
.then(() => {
|
.then(() =>
|
||||||
const data = readNetworkDataFromMap(
|
readNetworkDataFromMap(
|
||||||
drawingSession.map,
|
drawingSession.map,
|
||||||
drawingSession.extent,
|
drawingSession.extent,
|
||||||
);
|
),
|
||||||
const captured =
|
)
|
||||||
drawingMode === "basemap"
|
.then((data) => {
|
||||||
? captureMapCanvas(drawingSession.map)
|
|
||||||
: null;
|
|
||||||
return { data, captured };
|
|
||||||
})
|
|
||||||
.then(({ data, captured }) => {
|
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setNetworkData(data);
|
setNetworkData(data);
|
||||||
setMapCanvas(captured);
|
|
||||||
setMapAttribution(
|
|
||||||
drawingMode === "basemap" ? drawingSession.attribution : null,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
@@ -465,12 +390,11 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
|||||||
cancelled = true;
|
cancelled = true;
|
||||||
drawingSession.dispose();
|
drawingSession.dispose();
|
||||||
};
|
};
|
||||||
}, [drawingMode, map, network, open]);
|
}, [map, network, open, replacePreviewUrl]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open || !networkData) return;
|
if (!open || !networkData) return;
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
let currentUrl: string | null = null;
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
renderEngineeringDrawing({
|
renderEngineeringDrawing({
|
||||||
@@ -478,19 +402,13 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
|||||||
rows,
|
rows,
|
||||||
network,
|
network,
|
||||||
networkData,
|
networkData,
|
||||||
mapCanvas,
|
|
||||||
mapAttribution,
|
|
||||||
dirty,
|
dirty,
|
||||||
width: 1600,
|
width: 1600,
|
||||||
})
|
})
|
||||||
.then(canvasToPngBlob)
|
.then(canvasToPngBlob)
|
||||||
.then((blob) => {
|
.then((blob) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
currentUrl = URL.createObjectURL(blob);
|
replacePreviewUrl(URL.createObjectURL(blob));
|
||||||
setPreviewUrl((previous) => {
|
|
||||||
if (previous) URL.revokeObjectURL(previous);
|
|
||||||
return currentUrl;
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
if (!cancelled)
|
if (!cancelled)
|
||||||
@@ -501,17 +419,15 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
|||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
if (currentUrl) URL.revokeObjectURL(currentUrl);
|
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
dirty,
|
dirty,
|
||||||
mapAttribution,
|
|
||||||
mapCanvas,
|
|
||||||
network,
|
network,
|
||||||
networkData,
|
networkData,
|
||||||
open,
|
open,
|
||||||
rows,
|
rows,
|
||||||
scheme,
|
scheme,
|
||||||
|
replacePreviewUrl,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const createFullResolutionBlob = async () => {
|
const createFullResolutionBlob = async () => {
|
||||||
@@ -521,8 +437,6 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
|||||||
rows,
|
rows,
|
||||||
network,
|
network,
|
||||||
networkData,
|
networkData,
|
||||||
mapCanvas,
|
|
||||||
mapAttribution,
|
|
||||||
dirty,
|
dirty,
|
||||||
width: A3_LANDSCAPE_WIDTH,
|
width: A3_LANDSCAPE_WIDTH,
|
||||||
});
|
});
|
||||||
@@ -600,42 +514,53 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
|||||||
<DialogTitle id="scheme-drawing-title" sx={{ pb: 1 }}>
|
<DialogTitle id="scheme-drawing-title" sx={{ pb: 1 }}>
|
||||||
工程布置图预览
|
工程布置图预览
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogContent sx={{ bgcolor: "#eef2f5", pt: 1.5 }}>
|
<DialogContent sx={{ bgcolor: "#eef2f5", p: 0 }}>
|
||||||
|
<Box data-testid="drawing-content-spacing" sx={{ px: 3, pt: 1.5, pb: 2 }}>
|
||||||
<Box
|
<Box
|
||||||
|
data-testid="drawing-format-summary"
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
gap: 2,
|
columnGap: 2,
|
||||||
|
rowGap: 0.75,
|
||||||
mb: 1.5,
|
mb: 1.5,
|
||||||
|
px: 1.5,
|
||||||
|
py: 1,
|
||||||
|
minHeight: 44,
|
||||||
|
bgcolor: "rgba(37, 125, 212, 0.06)",
|
||||||
|
border: "1px solid rgba(37, 125, 212, 0.14)",
|
||||||
|
borderRadius: 2,
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<RadioGroup
|
<Typography variant="body2" sx={{ lineHeight: 1.5 }}>
|
||||||
row
|
<Box component="span" sx={{ color: "text.secondary", mr: 1 }}>
|
||||||
value={drawingMode}
|
输出样式
|
||||||
onChange={(event) =>
|
</Box>
|
||||||
setDrawingMode(event.target.value as DrawingMode)
|
<Box component="span" sx={{ color: "text.primary", fontWeight: 700 }}>
|
||||||
}
|
简化管网线稿
|
||||||
aria-label="工程图底图模式"
|
</Box>
|
||||||
>
|
|
||||||
<FormControlLabel
|
|
||||||
value="linework"
|
|
||||||
control={<Radio size="small" />}
|
|
||||||
label="简化管网线稿"
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
value="basemap"
|
|
||||||
control={<Radio size="small" />}
|
|
||||||
label="地图底图 + 管网"
|
|
||||||
/>
|
|
||||||
</RadioGroup>
|
|
||||||
<Typography
|
|
||||||
variant="caption"
|
|
||||||
sx={{ color: "text.secondary", fontVariantNumeric: "tabular-nums" }}
|
|
||||||
>
|
|
||||||
A3 横向 · 300 DPI · 4961 × 3508
|
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
spacing={1}
|
||||||
|
divider={
|
||||||
|
<Box
|
||||||
|
aria-hidden="true"
|
||||||
|
sx={{ width: 1, height: 14, bgcolor: "rgba(15, 23, 42, 0.14)" }}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
color: "text.secondary",
|
||||||
|
fontVariantNumeric: "tabular-nums",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="caption">A3 横向</Typography>
|
||||||
|
<Typography variant="caption">300 DPI</Typography>
|
||||||
|
<Typography variant="caption">4961 × 3508</Typography>
|
||||||
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
{error && (
|
{error && (
|
||||||
<Alert severity="warning" sx={{ mb: 1.5 }}>
|
<Alert severity="warning" sx={{ mb: 1.5 }}>
|
||||||
@@ -652,11 +577,9 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{previewUrl && (
|
{previewUrl && (
|
||||||
<Box
|
<DrawingPreview
|
||||||
component="img"
|
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
alt={`${scheme.scheme_name} 压力监测点布置图预览`}
|
label={`${scheme.scheme_name} 压力监测点布置图预览`}
|
||||||
sx={{ width: "100%", height: "100%", objectFit: "contain" }}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{loading && (
|
{loading && (
|
||||||
@@ -673,6 +596,7 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
|||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions sx={{ px: 3, py: 2 }}>
|
<DialogActions sx={{ px: 3, py: 2 }}>
|
||||||
<Button onClick={onClose}>关闭</Button>
|
<Button onClick={onClose}>关闭</Button>
|
||||||
|
|||||||
@@ -31,7 +31,20 @@ jest.mock("@components/olmap/core/MapComponent", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock("@mui/x-data-grid", () => ({
|
jest.mock("@mui/x-data-grid", () => ({
|
||||||
DataGrid: () => <div data-testid="scheme-grid" />,
|
DataGrid: (props: {
|
||||||
|
density?: string;
|
||||||
|
initialState?: { density?: string };
|
||||||
|
rowHeight?: number;
|
||||||
|
columnHeaderHeight?: number;
|
||||||
|
}) => (
|
||||||
|
<div
|
||||||
|
data-testid="scheme-grid"
|
||||||
|
data-density={props.density ?? "uncontrolled"}
|
||||||
|
data-initial-density={props.initialState?.density ?? ""}
|
||||||
|
data-row-height={props.rowHeight ?? "automatic"}
|
||||||
|
data-column-header-height={props.columnHeaderHeight ?? "automatic"}
|
||||||
|
/>
|
||||||
|
),
|
||||||
GridToolbar: () => null,
|
GridToolbar: () => null,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -173,4 +186,25 @@ describe("SchemeEditor notifications", () => {
|
|||||||
message: "请先点击要替换的监测点",
|
message: "请先点击要替换的监测点",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("lets the Data Grid density selector control row sizing", () => {
|
||||||
|
render(<SchemeEditor scheme={scheme} network="fengyang" />);
|
||||||
|
|
||||||
|
expect(screen.getByTestId("scheme-grid")).toHaveAttribute(
|
||||||
|
"data-density",
|
||||||
|
"uncontrolled",
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("scheme-grid")).toHaveAttribute(
|
||||||
|
"data-initial-density",
|
||||||
|
"compact",
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("scheme-grid")).toHaveAttribute(
|
||||||
|
"data-row-height",
|
||||||
|
"automatic",
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId("scheme-grid")).toHaveAttribute(
|
||||||
|
"data-column-header-height",
|
||||||
|
"automatic",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -748,9 +748,7 @@ const SchemeEditor: React.FC<SchemeEditorProps> = ({
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
localeText={zhCN.components.MuiDataGrid.defaultProps.localeText}
|
localeText={zhCN.components.MuiDataGrid.defaultProps.localeText}
|
||||||
getRowId={(row) => row.node_id}
|
getRowId={(row) => row.node_id}
|
||||||
rowHeight={48}
|
initialState={{ density: "compact" }}
|
||||||
columnHeaderHeight={44}
|
|
||||||
density="compact"
|
|
||||||
disableRowSelectionOnClick={false}
|
disableRowSelectionOnClick={false}
|
||||||
rowSelectionModel={selectedNodeId ? [selectedNodeId] : []}
|
rowSelectionModel={selectedNodeId ? [selectedNodeId] : []}
|
||||||
onRowSelectionModelChange={(selection: GridRowSelectionModel) =>
|
onRowSelectionModelChange={(selection: GridRowSelectionModel) =>
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ import {
|
|||||||
renderEngineeringDrawing,
|
renderEngineeringDrawing,
|
||||||
} from "./engineeringDrawing";
|
} from "./engineeringDrawing";
|
||||||
import SchemeDrawingDialog, {
|
import SchemeDrawingDialog, {
|
||||||
|
DrawingPreview,
|
||||||
fitMapToFullNetwork,
|
fitMapToFullNetwork,
|
||||||
} from "./SchemeDrawingDialog";
|
} from "./SchemeDrawingDialog";
|
||||||
import type { SensorPlacementScheme, SensorPointRow } from "./types";
|
import type { SensorPlacementScheme, SensorPointRow } from "./types";
|
||||||
@@ -182,14 +183,36 @@ describe("engineering drawing map framing", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => expect(getAllLayers).toHaveBeenCalled());
|
await waitFor(() => expect(getAllLayers).toHaveBeenCalled());
|
||||||
expect(screen.getByLabelText("简化管网线稿")).toBeChecked();
|
expect(screen.getByText(/简化管网线稿/)).toBeInTheDocument();
|
||||||
expect(screen.getByLabelText("地图底图 + 管网")).toBeInTheDocument();
|
expect(screen.queryByText("地图底图 + 管网")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByTestId("HubOutlinedIcon")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("drawing-content-spacing")).toHaveStyle({
|
||||||
|
paddingTop: "12px",
|
||||||
|
});
|
||||||
|
expect(screen.getByTestId("drawing-format-summary")).toHaveStyle({
|
||||||
|
marginBottom: "12px",
|
||||||
|
});
|
||||||
expect(fit).not.toHaveBeenCalled();
|
expect(fit).not.toHaveBeenCalled();
|
||||||
expect(setCenter).not.toHaveBeenCalled();
|
expect(setCenter).not.toHaveBeenCalled();
|
||||||
expect(setResolution).not.toHaveBeenCalled();
|
expect(setResolution).not.toHaveBeenCalled();
|
||||||
expect(setRotation).not.toHaveBeenCalled();
|
expect(setRotation).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not use a native image fallback for the generated preview", () => {
|
||||||
|
render(
|
||||||
|
createElement(DrawingPreview, {
|
||||||
|
src: "blob:engineering-drawing-preview",
|
||||||
|
label: "测试方案压力监测点布置图预览",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const preview = screen.getByRole("img", {
|
||||||
|
name: "测试方案压力监测点布置图预览",
|
||||||
|
});
|
||||||
|
expect(preview.tagName).toBe("DIV");
|
||||||
|
expect(preview).not.toHaveAttribute("src");
|
||||||
|
});
|
||||||
|
|
||||||
it("fits the OpenLayers map to the complete pipe layer extent", () => {
|
it("fits the OpenLayers map to the complete pipe layer extent", () => {
|
||||||
const extent = [13500000, 3600000, 13600000, 3700000];
|
const extent = [13500000, 3600000, 13600000, 3700000];
|
||||||
const cancelAnimations = jest.fn();
|
const cancelAnimations = jest.fn();
|
||||||
@@ -276,6 +299,7 @@ describe("engineering drawing", () => {
|
|||||||
const [startX, startY] = context.moveTo.mock.calls[0];
|
const [startX, startY] = context.moveTo.mock.calls[0];
|
||||||
const [endX, endY] = context.lineTo.mock.calls[0];
|
const [endX, endY] = context.lineTo.mock.calls[0];
|
||||||
expect(Math.abs(endX - startX)).toBeCloseTo(Math.abs(endY - startY), 6);
|
expect(Math.abs(endX - startX)).toBeCloseTo(Math.abs(endY - startY), 6);
|
||||||
|
expect(context.drawImage).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps visible space on the left and right of the complete network", async () => {
|
it("keeps visible space on the left and right of the complete network", async () => {
|
||||||
@@ -303,39 +327,4 @@ describe("engineering drawing", () => {
|
|||||||
expect(endX).toBeLessThan(1500);
|
expect(endX).toBeLessThan(1500);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the captured OpenLayers map for the basemap drawing mode", async () => {
|
|
||||||
const context = createContext();
|
|
||||||
jest
|
|
||||||
.spyOn(HTMLCanvasElement.prototype, "getContext")
|
|
||||||
.mockReturnValue(context as unknown as CanvasRenderingContext2D);
|
|
||||||
const mapCanvas = document.createElement("canvas");
|
|
||||||
|
|
||||||
await renderEngineeringDrawing({
|
|
||||||
scheme,
|
|
||||||
rows,
|
|
||||||
network: "test",
|
|
||||||
networkData: {
|
|
||||||
nodes: ["A:junction:0:0", "B:junction:1000:1000"],
|
|
||||||
links: ["P1:pipe:A:B"],
|
|
||||||
extent: [0, 0, 1000, 1000],
|
|
||||||
},
|
|
||||||
mapCanvas,
|
|
||||||
mapAttribution: "© Mapbox © OpenStreetMap",
|
|
||||||
dirty: false,
|
|
||||||
width: 1600,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(context.drawImage).toHaveBeenCalledWith(
|
|
||||||
mapCanvas,
|
|
||||||
expect.any(Number),
|
|
||||||
expect.any(Number),
|
|
||||||
expect.any(Number),
|
|
||||||
expect.any(Number),
|
|
||||||
);
|
|
||||||
expect(context.fillText).toHaveBeenCalledWith(
|
|
||||||
"底图来源:© Mapbox © OpenStreetMap",
|
|
||||||
expect.any(Number),
|
|
||||||
expect.any(Number),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ interface DrawingOptions {
|
|||||||
rows: SensorPointRow[];
|
rows: SensorPointRow[];
|
||||||
network: string;
|
network: string;
|
||||||
networkData: NetworkDrawingData;
|
networkData: NetworkDrawingData;
|
||||||
mapCanvas?: HTMLCanvasElement | null;
|
|
||||||
mapAttribution?: string | null;
|
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
}
|
}
|
||||||
@@ -123,8 +121,6 @@ export const renderEngineeringDrawing = async ({
|
|||||||
rows,
|
rows,
|
||||||
network,
|
network,
|
||||||
networkData,
|
networkData,
|
||||||
mapCanvas,
|
|
||||||
mapAttribution,
|
|
||||||
dirty,
|
dirty,
|
||||||
width = A3_LANDSCAPE_WIDTH,
|
width = A3_LANDSCAPE_WIDTH,
|
||||||
}: DrawingOptions): Promise<HTMLCanvasElement> => {
|
}: DrawingOptions): Promise<HTMLCanvasElement> => {
|
||||||
@@ -161,15 +157,6 @@ export const renderEngineeringDrawing = async ({
|
|||||||
);
|
);
|
||||||
context.fillStyle = "#fbfcfd";
|
context.fillStyle = "#fbfcfd";
|
||||||
context.fillRect(mapBox.x, mapBox.y, mapBox.width, mapBox.height);
|
context.fillRect(mapBox.x, mapBox.y, mapBox.width, mapBox.height);
|
||||||
if (mapCanvas) {
|
|
||||||
context.drawImage(
|
|
||||||
mapCanvas,
|
|
||||||
mapBox.x,
|
|
||||||
mapBox.y,
|
|
||||||
mapBox.width,
|
|
||||||
mapBox.height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes = networkData.nodes
|
const nodes = networkData.nodes
|
||||||
.map(parseNode)
|
.map(parseNode)
|
||||||
@@ -184,7 +171,6 @@ export const renderEngineeringDrawing = async ({
|
|||||||
mapBox.y + mapBox.height - ((y - minY) / (maxY - minY)) * mapBox.height,
|
mapBox.y + mapBox.height - ((y - minY) / (maxY - minY)) * mapBox.height,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!mapCanvas) {
|
|
||||||
context.strokeStyle = "#45677a";
|
context.strokeStyle = "#45677a";
|
||||||
context.lineWidth = px(5);
|
context.lineWidth = px(5);
|
||||||
for (const link of networkData.links) {
|
for (const link of networkData.links) {
|
||||||
@@ -218,7 +204,6 @@ export const renderEngineeringDrawing = async ({
|
|||||||
context.arc(point.x, point.y, px(4), 0, Math.PI * 2);
|
context.arc(point.x, point.y, px(4), 0, Math.PI * 2);
|
||||||
context.fill();
|
context.fill();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
rows.forEach((row) => {
|
rows.forEach((row) => {
|
||||||
const point = toCanvas(row.map_x, row.map_y);
|
const point = toCanvas(row.map_x, row.map_y);
|
||||||
@@ -263,20 +248,6 @@ export const renderEngineeringDrawing = async ({
|
|||||||
context.strokeStyle = "#111827";
|
context.strokeStyle = "#111827";
|
||||||
context.lineWidth = px(4);
|
context.lineWidth = px(4);
|
||||||
context.strokeRect(mapBox.x, mapBox.y, mapBox.width, mapBox.height);
|
context.strokeRect(mapBox.x, mapBox.y, mapBox.width, mapBox.height);
|
||||||
if (mapCanvas && mapAttribution) {
|
|
||||||
context.fillStyle = "#334155";
|
|
||||||
drawText(
|
|
||||||
context,
|
|
||||||
`底图来源:${mapAttribution}`,
|
|
||||||
mapBox.x + mapBox.width - px(16),
|
|
||||||
mapBox.y + mapBox.height - px(18),
|
|
||||||
px(18),
|
|
||||||
500,
|
|
||||||
"right",
|
|
||||||
"rgba(255,255,255,0.96)",
|
|
||||||
px(6),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let index = 0; index <= 4; index += 1) {
|
for (let index = 0; index <= 4; index += 1) {
|
||||||
const ratio = index / 4;
|
const ratio = index / 4;
|
||||||
|
|||||||
Reference in New Issue
Block a user