370 lines
9.2 KiB
TypeScript
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");
|
|
});
|