fix(sensor-placement): refine drawing export and editor density
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
@@ -10,9 +16,7 @@ import {
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
FormControlLabel,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Stack,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
@@ -21,10 +25,7 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
import OlMap from "ol/Map";
|
||||
import View from "ol/View";
|
||||
import LayerGroup from "ol/layer/Group";
|
||||
import TileLayer from "ol/layer/Tile";
|
||||
import VectorTileLayer from "ol/layer/VectorTile";
|
||||
import type TileSource from "ol/source/Tile";
|
||||
import type VectorTileSource from "ol/source/VectorTile";
|
||||
import type { StyleLike } from "ol/style/Style";
|
||||
import {
|
||||
@@ -61,8 +62,6 @@ const DRAWING_MAP_SIZE: [number, number] = [
|
||||
A3_LANDSCAPE_HEIGHT - 650,
|
||||
];
|
||||
|
||||
type DrawingMode = "linework" | "basemap";
|
||||
|
||||
type DrawingPipeLayer = {
|
||||
getSource?: () => VectorTileSource | null;
|
||||
getExtent?: () => number[] | undefined;
|
||||
@@ -110,52 +109,10 @@ export const fitMapToFullNetwork = (
|
||||
interface DrawingMapSession {
|
||||
map: OlMap;
|
||||
extent: [number, number, number, number];
|
||||
attribution: string | null;
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
const cloneVisibleBaseLayers = (
|
||||
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 createDrawingMapSession = (mainMap: OlMap): DrawingMapSession => {
|
||||
const mainPipeLayer = getDrawingPipeLayer(mainMap);
|
||||
const source = mainPipeLayer?.getSource?.();
|
||||
const extent = mainPipeLayer?.getExtent?.();
|
||||
@@ -196,18 +153,13 @@ const createDrawingMapSession = (
|
||||
});
|
||||
pipeLayer.set("value", "pipes");
|
||||
pipeLayer.setExtent(networkExtent);
|
||||
const basemap =
|
||||
mode === "basemap"
|
||||
? cloneVisibleBaseLayers(mainMap)
|
||||
: { layers: [], attribution: null };
|
||||
const layers = [...basemap.layers, pipeLayer];
|
||||
drawingMap = new OlMap({
|
||||
target,
|
||||
pixelRatio: 1,
|
||||
view: new View({
|
||||
projection: mainMap.getView().getProjection(),
|
||||
}),
|
||||
layers,
|
||||
layers: [pipeLayer],
|
||||
controls: [],
|
||||
interactions: [],
|
||||
});
|
||||
@@ -218,7 +170,6 @@ const createDrawingMapSession = (
|
||||
return {
|
||||
map: drawingMap,
|
||||
extent: networkExtent,
|
||||
attribution: basemap.attribution,
|
||||
dispose: () => {
|
||||
if (disposed) return;
|
||||
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 = (
|
||||
map: OlMap,
|
||||
extent: [number, number, number, number],
|
||||
@@ -383,6 +281,27 @@ const escapeHtml = (value: string) =>
|
||||
})[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> = ({
|
||||
open,
|
||||
onClose,
|
||||
@@ -395,25 +314,40 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||
const [networkData, setNetworkData] = useState<NetworkDrawingData | 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 previewUrlRef = useRef<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
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(
|
||||
() => `${scheme.scheme_name}${dirty ? "_未保存草稿" : ""}_布置图.png`,
|
||||
[dirty, scheme.scheme_name],
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (previewUrlRef.current) {
|
||||
URL.revokeObjectURL(previewUrlRef.current);
|
||||
previewUrlRef.current = null;
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
let cancelled = false;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setNetworkData(null);
|
||||
setMapCanvas(null);
|
||||
setMapAttribution(null);
|
||||
replacePreviewUrl(null);
|
||||
if (!map) {
|
||||
setError("主地图尚未初始化,请稍后重试");
|
||||
setLoading(false);
|
||||
@@ -421,7 +355,7 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||
}
|
||||
let drawingSession: DrawingMapSession;
|
||||
try {
|
||||
drawingSession = createDrawingMapSession(map, drawingMode);
|
||||
drawingSession = createDrawingMapSession(map);
|
||||
} catch (reason) {
|
||||
setError(
|
||||
reason instanceof Error ? reason.message : "无法适配完整管网范围",
|
||||
@@ -430,24 +364,15 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||
return;
|
||||
}
|
||||
waitForDrawingMapRender(drawingSession.map)
|
||||
.then(() => {
|
||||
const data = readNetworkDataFromMap(
|
||||
.then(() =>
|
||||
readNetworkDataFromMap(
|
||||
drawingSession.map,
|
||||
drawingSession.extent,
|
||||
);
|
||||
const captured =
|
||||
drawingMode === "basemap"
|
||||
? captureMapCanvas(drawingSession.map)
|
||||
: null;
|
||||
return { data, captured };
|
||||
})
|
||||
.then(({ data, captured }) => {
|
||||
),
|
||||
)
|
||||
.then((data) => {
|
||||
if (!cancelled) {
|
||||
setNetworkData(data);
|
||||
setMapCanvas(captured);
|
||||
setMapAttribution(
|
||||
drawingMode === "basemap" ? drawingSession.attribution : null,
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((reason) => {
|
||||
@@ -465,12 +390,11 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||
cancelled = true;
|
||||
drawingSession.dispose();
|
||||
};
|
||||
}, [drawingMode, map, network, open]);
|
||||
}, [map, network, open, replacePreviewUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open || !networkData) return;
|
||||
let cancelled = false;
|
||||
let currentUrl: string | null = null;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
renderEngineeringDrawing({
|
||||
@@ -478,19 +402,13 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||
rows,
|
||||
network,
|
||||
networkData,
|
||||
mapCanvas,
|
||||
mapAttribution,
|
||||
dirty,
|
||||
width: 1600,
|
||||
})
|
||||
.then(canvasToPngBlob)
|
||||
.then((blob) => {
|
||||
if (cancelled) return;
|
||||
currentUrl = URL.createObjectURL(blob);
|
||||
setPreviewUrl((previous) => {
|
||||
if (previous) URL.revokeObjectURL(previous);
|
||||
return currentUrl;
|
||||
});
|
||||
replacePreviewUrl(URL.createObjectURL(blob));
|
||||
})
|
||||
.catch((reason) => {
|
||||
if (!cancelled)
|
||||
@@ -501,17 +419,15 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (currentUrl) URL.revokeObjectURL(currentUrl);
|
||||
};
|
||||
}, [
|
||||
dirty,
|
||||
mapAttribution,
|
||||
mapCanvas,
|
||||
network,
|
||||
networkData,
|
||||
open,
|
||||
rows,
|
||||
scheme,
|
||||
replacePreviewUrl,
|
||||
]);
|
||||
|
||||
const createFullResolutionBlob = async () => {
|
||||
@@ -521,8 +437,6 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||
rows,
|
||||
network,
|
||||
networkData,
|
||||
mapCanvas,
|
||||
mapAttribution,
|
||||
dirty,
|
||||
width: A3_LANDSCAPE_WIDTH,
|
||||
});
|
||||
@@ -600,42 +514,53 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||
<DialogTitle id="scheme-drawing-title" sx={{ pb: 1 }}>
|
||||
工程布置图预览
|
||||
</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
|
||||
data-testid="drawing-format-summary"
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
gap: 2,
|
||||
columnGap: 2,
|
||||
rowGap: 0.75,
|
||||
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",
|
||||
}}
|
||||
>
|
||||
<RadioGroup
|
||||
row
|
||||
value={drawingMode}
|
||||
onChange={(event) =>
|
||||
setDrawingMode(event.target.value as DrawingMode)
|
||||
}
|
||||
aria-label="工程图底图模式"
|
||||
>
|
||||
<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 variant="body2" sx={{ lineHeight: 1.5 }}>
|
||||
<Box component="span" sx={{ color: "text.secondary", mr: 1 }}>
|
||||
输出样式
|
||||
</Box>
|
||||
<Box component="span" sx={{ color: "text.primary", fontWeight: 700 }}>
|
||||
简化管网线稿
|
||||
</Box>
|
||||
</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>
|
||||
{error && (
|
||||
<Alert severity="warning" sx={{ mb: 1.5 }}>
|
||||
@@ -652,11 +577,9 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||
}}
|
||||
>
|
||||
{previewUrl && (
|
||||
<Box
|
||||
component="img"
|
||||
<DrawingPreview
|
||||
src={previewUrl}
|
||||
alt={`${scheme.scheme_name} 压力监测点布置图预览`}
|
||||
sx={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||
label={`${scheme.scheme_name} 压力监测点布置图预览`}
|
||||
/>
|
||||
)}
|
||||
{loading && (
|
||||
@@ -673,6 +596,7 @@ const SchemeDrawingDialog: React.FC<SchemeDrawingDialogProps> = ({
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, py: 2 }}>
|
||||
<Button onClick={onClose}>关闭</Button>
|
||||
|
||||
@@ -31,7 +31,20 @@ jest.mock("@components/olmap/core/MapComponent", () => ({
|
||||
}));
|
||||
|
||||
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,
|
||||
}));
|
||||
|
||||
@@ -173,4 +186,25 @@ describe("SchemeEditor notifications", () => {
|
||||
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}
|
||||
localeText={zhCN.components.MuiDataGrid.defaultProps.localeText}
|
||||
getRowId={(row) => row.node_id}
|
||||
rowHeight={48}
|
||||
columnHeaderHeight={44}
|
||||
density="compact"
|
||||
initialState={{ density: "compact" }}
|
||||
disableRowSelectionOnClick={false}
|
||||
rowSelectionModel={selectedNodeId ? [selectedNodeId] : []}
|
||||
onRowSelectionModelChange={(selection: GridRowSelectionModel) =>
|
||||
|
||||
@@ -75,6 +75,7 @@ import {
|
||||
renderEngineeringDrawing,
|
||||
} from "./engineeringDrawing";
|
||||
import SchemeDrawingDialog, {
|
||||
DrawingPreview,
|
||||
fitMapToFullNetwork,
|
||||
} from "./SchemeDrawingDialog";
|
||||
import type { SensorPlacementScheme, SensorPointRow } from "./types";
|
||||
@@ -182,14 +183,36 @@ describe("engineering drawing map framing", () => {
|
||||
);
|
||||
|
||||
await waitFor(() => expect(getAllLayers).toHaveBeenCalled());
|
||||
expect(screen.getByLabelText("简化管网线稿")).toBeChecked();
|
||||
expect(screen.getByLabelText("地图底图 + 管网")).toBeInTheDocument();
|
||||
expect(screen.getByText(/简化管网线稿/)).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(setCenter).not.toHaveBeenCalled();
|
||||
expect(setResolution).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", () => {
|
||||
const extent = [13500000, 3600000, 13600000, 3700000];
|
||||
const cancelAnimations = jest.fn();
|
||||
@@ -276,6 +299,7 @@ describe("engineering drawing", () => {
|
||||
const [startX, startY] = context.moveTo.mock.calls[0];
|
||||
const [endX, endY] = context.lineTo.mock.calls[0];
|
||||
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 () => {
|
||||
@@ -303,39 +327,4 @@ describe("engineering drawing", () => {
|
||||
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[];
|
||||
network: string;
|
||||
networkData: NetworkDrawingData;
|
||||
mapCanvas?: HTMLCanvasElement | null;
|
||||
mapAttribution?: string | null;
|
||||
dirty: boolean;
|
||||
width?: number;
|
||||
}
|
||||
@@ -123,8 +121,6 @@ export const renderEngineeringDrawing = async ({
|
||||
rows,
|
||||
network,
|
||||
networkData,
|
||||
mapCanvas,
|
||||
mapAttribution,
|
||||
dirty,
|
||||
width = A3_LANDSCAPE_WIDTH,
|
||||
}: DrawingOptions): Promise<HTMLCanvasElement> => {
|
||||
@@ -161,15 +157,6 @@ export const renderEngineeringDrawing = async ({
|
||||
);
|
||||
context.fillStyle = "#fbfcfd";
|
||||
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
|
||||
.map(parseNode)
|
||||
@@ -184,7 +171,6 @@ export const renderEngineeringDrawing = async ({
|
||||
mapBox.y + mapBox.height - ((y - minY) / (maxY - minY)) * mapBox.height,
|
||||
});
|
||||
|
||||
if (!mapCanvas) {
|
||||
context.strokeStyle = "#45677a";
|
||||
context.lineWidth = px(5);
|
||||
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.fill();
|
||||
}
|
||||
}
|
||||
|
||||
rows.forEach((row) => {
|
||||
const point = toCanvas(row.map_x, row.map_y);
|
||||
@@ -263,20 +248,6 @@ export const renderEngineeringDrawing = async ({
|
||||
context.strokeStyle = "#111827";
|
||||
context.lineWidth = px(4);
|
||||
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) {
|
||||
const ratio = index / 4;
|
||||
|
||||
Reference in New Issue
Block a user