Files
TJWaterFrontend_Refine/src/components/olmap/MonitoringPlaceOptimization/engineeringDrawing.ts
T
jiang 65ed1fd212
Build Push and Deploy / docker-image (push) Failing after 23s
Build Push and Deploy / deploy-fallback-log (push) Successful in 1s
fix(sensor-placement): refine drawing export and editor density
2026-07-30 21:39:27 +08:00

370 lines
9.2 KiB
TypeScript

import type {
NetworkDrawingData,
SensorPlacementScheme,
SensorPointRow,
} from "./types";
export const A3_LANDSCAPE_WIDTH = 4961;
export const A3_LANDSCAPE_HEIGHT = 3508;
interface DrawingOptions {
scheme: SensorPlacementScheme;
rows: SensorPointRow[];
network: string;
networkData: NetworkDrawingData;
dirty: boolean;
width?: number;
}
interface ParsedNode {
id: string;
x: number;
y: number;
}
type DrawingExtent = [number, number, number, number];
export const DRAWING_CONTENT_PADDING_RATIO = 0.04;
const toDrawingExtent = (extent: number[]): DrawingExtent => {
if (
extent.length !== 4 ||
!extent.every(Number.isFinite) ||
extent[0] >= extent[2] ||
extent[1] >= extent[3]
) {
throw new Error("地图范围不可用");
}
return [extent[0], extent[1], extent[2], extent[3]];
};
const parseNode = (value: string): ParsedNode | null => {
const [id, , x, y] = value.split(":");
const parsedX = Number(x);
const parsedY = Number(y);
if (!id || !Number.isFinite(parsedX) || !Number.isFinite(parsedY))
return null;
return { id, x: parsedX, y: parsedY };
};
const fitExtentToAspectRatio = (
extent: DrawingExtent,
targetAspectRatio: number,
): DrawingExtent => {
const [minX, minY, maxX, maxY] = extent;
const centerX = (minX + maxX) / 2;
const centerY = (minY + maxY) / 2;
let spanX = maxX - minX;
let spanY = maxY - minY;
if (spanX / spanY > targetAspectRatio) {
spanY = spanX / targetAspectRatio;
} else {
spanX = spanY * targetAspectRatio;
}
return [
centerX - spanX / 2,
centerY - spanY / 2,
centerX + spanX / 2,
centerY + spanY / 2,
];
};
export const getPaddedDrawingExtent = (
extent: number[],
targetAspectRatio: number,
): DrawingExtent => {
const fitted = fitExtentToAspectRatio(
toDrawingExtent(extent),
targetAspectRatio,
);
const [minX, minY, maxX, maxY] = fitted;
const horizontalPadding =
((maxX - minX) * DRAWING_CONTENT_PADDING_RATIO) /
(1 - DRAWING_CONTENT_PADDING_RATIO * 2);
const verticalPadding =
((maxY - minY) * DRAWING_CONTENT_PADDING_RATIO) /
(1 - DRAWING_CONTENT_PADDING_RATIO * 2);
return [
minX - horizontalPadding,
minY - verticalPadding,
maxX + horizontalPadding,
maxY + verticalPadding,
];
};
const drawText = (
context: CanvasRenderingContext2D,
value: string,
x: number,
y: number,
size: number,
weight = 400,
align: CanvasTextAlign = "left",
outlineColor?: string,
outlineWidth = 0,
) => {
context.font = `${weight} ${size}px -apple-system, "PingFang SC", "Noto Sans SC", sans-serif`;
context.textAlign = align;
context.textBaseline = "middle";
if (outlineColor && outlineWidth > 0) {
context.strokeStyle = outlineColor;
context.lineWidth = outlineWidth;
context.lineJoin = "round";
context.strokeText(value, x, y);
}
context.fillText(value, x, y);
};
export const renderEngineeringDrawing = async ({
scheme,
rows,
network,
networkData,
dirty,
width = A3_LANDSCAPE_WIDTH,
}: DrawingOptions): Promise<HTMLCanvasElement> => {
if (!rows.length) throw new Error("方案没有可出图的监测点");
const height = Math.round((width * A3_LANDSCAPE_HEIGHT) / A3_LANDSCAPE_WIDTH);
const scale = width / A3_LANDSCAPE_WIDTH;
const px = (value: number) => value * scale;
const canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
const context = canvas.getContext("2d");
if (!context) throw new Error("无法创建工程图画布");
context.fillStyle = "#ffffff";
context.fillRect(0, 0, width, height);
context.strokeStyle = "#111827";
context.lineWidth = px(8);
context.strokeRect(px(70), px(70), width - px(140), height - px(140));
const mapBox = {
x: px(150),
y: px(150),
width: width - px(300),
height: height - px(650),
};
context.save();
context.beginPath();
context.rect(mapBox.x, mapBox.y, mapBox.width, mapBox.height);
context.clip();
const extent = getPaddedDrawingExtent(
networkData.extent,
mapBox.width / mapBox.height,
);
context.fillStyle = "#fbfcfd";
context.fillRect(mapBox.x, mapBox.y, mapBox.width, mapBox.height);
const nodes = networkData.nodes
.map(parseNode)
.filter((node): node is ParsedNode => Boolean(node));
const nodesById = new globalThis.Map(
nodes.map((node) => [node.id, node] as const),
);
const [minX, minY, maxX, maxY] = extent;
const toCanvas = (x: number, y: number) => ({
x: mapBox.x + ((x - minX) / (maxX - minX)) * mapBox.width,
y:
mapBox.y + mapBox.height - ((y - minY) / (maxY - minY)) * mapBox.height,
});
context.strokeStyle = "#45677a";
context.lineWidth = px(5);
for (const link of networkData.links) {
const [, , startId, endId] = link.split(":");
const start = nodesById.get(startId);
const end = nodesById.get(endId);
if (!start || !end) continue;
if (
Math.max(start.x, end.x) < minX ||
Math.min(start.x, end.x) > maxX ||
Math.max(start.y, end.y) < minY ||
Math.min(start.y, end.y) > maxY
) {
continue;
}
const startPoint = toCanvas(start.x, start.y);
const endPoint = toCanvas(end.x, end.y);
context.beginPath();
context.moveTo(startPoint.x, startPoint.y);
context.lineTo(endPoint.x, endPoint.y);
context.stroke();
}
context.fillStyle = "#668697";
for (const node of nodes) {
if (node.x < minX || node.x > maxX || node.y < minY || node.y > maxY) {
continue;
}
const point = toCanvas(node.x, node.y);
context.beginPath();
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);
context.fillStyle = "#ffffff";
context.beginPath();
context.arc(point.x, point.y, px(36), 0, Math.PI * 2);
context.fill();
context.strokeStyle = "rgba(255,255,255,0.96)";
context.lineWidth = px(14);
context.stroke();
context.strokeStyle = "#c9252d";
context.lineWidth = px(5);
context.stroke();
context.fillStyle = "#a51f28";
drawText(
context,
String(row.sequence),
point.x,
point.y,
px(32),
700,
"center",
);
context.fillStyle = "#273746";
drawText(
context,
row.node_id,
point.x,
point.y + px(62),
px(25),
650,
"center",
"rgba(255,255,255,0.98)",
px(9),
);
});
context.restore();
context.strokeStyle = "#111827";
context.lineWidth = px(4);
context.strokeRect(mapBox.x, mapBox.y, mapBox.width, mapBox.height);
for (let index = 0; index <= 4; index += 1) {
const ratio = index / 4;
const x = mapBox.x + ratio * mapBox.width;
const y = mapBox.y + mapBox.height - ratio * mapBox.height;
context.fillStyle = "#334155";
drawText(
context,
(minX + ratio * (maxX - minX)).toFixed(0),
x,
mapBox.y + mapBox.height + px(32),
px(20),
500,
"center",
);
drawText(
context,
(minY + ratio * (maxY - minY)).toFixed(0),
mapBox.x - px(18),
y,
px(20),
500,
"right",
);
}
const titleTop = height - px(420);
context.strokeStyle = "#111827";
context.lineWidth = px(4);
context.strokeRect(px(150), titleTop, width - px(300), px(270));
context.beginPath();
context.moveTo(width - px(1700), titleTop);
context.lineTo(width - px(1700), titleTop + px(270));
context.stroke();
context.fillStyle = "#111827";
drawText(
context,
`${scheme.scheme_name} 压力监测点布置图`,
px(220),
titleTop + px(70),
px(48),
700,
);
drawText(
context,
`项目:${network} 监测点:${rows.length} 个 地图:EPSG:3857 经纬度:WGS84`,
px(220),
titleTop + px(145),
px(26),
500,
);
drawText(
context,
dirty ? "状态:未保存草稿" : "状态:当前方案",
px(220),
titleTop + px(215),
px(26),
dirty ? 700 : 500,
);
const infoX = width - px(1620);
drawText(
context,
`创建人:${scheme.username}`,
infoX,
titleTop + px(55),
px(24),
500,
);
drawText(
context,
`创建时间:${new Date(scheme.create_time).toLocaleString("zh-CN")}`,
infoX,
titleTop + px(115),
px(24),
500,
);
drawText(
context,
`制图时间:${new Date().toLocaleString("zh-CN")}`,
infoX,
titleTop + px(175),
px(24),
500,
);
drawText(
context,
"图幅:A3 横向 300 DPI",
infoX,
titleTop + px(235),
px(24),
500,
);
context.strokeStyle = "#111827";
context.lineWidth = px(5);
const northX = width - px(260);
const northY = px(270);
context.beginPath();
context.moveTo(northX, northY - px(70));
context.lineTo(northX - px(32), northY + px(35));
context.lineTo(northX, northY + px(15));
context.lineTo(northX + px(32), northY + px(35));
context.closePath();
context.stroke();
context.fillStyle = "#111827";
drawText(context, "N", northX, northY - px(110), px(30), 700, "center");
return canvas;
};
export const canvasToPngBlob = (canvas: HTMLCanvasElement) =>
new Promise<Blob>((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) resolve(blob);
else reject(new Error("PNG 生成失败"));
}, "image/png");
});