fix(sensor-placement): refine drawing export and editor density
Build Push and Deploy / docker-image (push) Failing after 23s
Build Push and Deploy / deploy-fallback-log (push) Successful in 1s

This commit is contained in:
2026-07-30 21:39:27 +08:00
parent 81cc5dcae1
commit 65ed1fd212
5 changed files with 230 additions and 314 deletions
@@ -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;