import maplibregl, { type Map as MapLibreMap, type StyleSpecification } from "maplibre-gl"; const MAX_EXPORT_DIMENSION = 4096; const EXPORT_IDLE_TIMEOUT_MS = 6000; export type ExportMapViewOptions = { scale?: number; targetLongEdge?: number; filename?: string; }; export async function exportMapViewImage(map: MapLibreMap, { scale, targetLongEdge, filename }: ExportMapViewOptions = {}) { const canvas = map.getCanvas(); const sourceWidth = canvas.clientWidth || canvas.width; const sourceHeight = canvas.clientHeight || canvas.height; if (!sourceWidth || !sourceHeight) { throw new Error("地图画布尺寸不可用。"); } const requestedScale = targetLongEdge ? targetLongEdge / Math.max(sourceWidth, sourceHeight) : scale ?? 1; const exportScale = Math.max(1, Math.min(requestedScale, MAX_EXPORT_DIMENSION / sourceWidth, MAX_EXPORT_DIMENSION / sourceHeight)); const exportWidth = Math.round(sourceWidth * exportScale); const exportHeight = Math.round(sourceHeight * exportScale); const container = createExportContainer(exportWidth, exportHeight); const style = cloneMapStyle(map.getStyle()); const bounds = map.getBounds(); document.body.appendChild(container); const exportMap = new maplibregl.Map({ container, style, center: map.getCenter(), zoom: map.getZoom(), pitch: map.getPitch(), bearing: map.getBearing(), preserveDrawingBuffer: true, interactive: false, attributionControl: false, fadeDuration: 0 }); try { await onceMapEvent(exportMap, "load"); exportMap.fitBounds(bounds, { padding: 0, duration: 0 }); await waitForMapIdle(exportMap); const link = document.createElement("a"); link.download = filename ?? getDefaultExportFilename(getExportLabel(targetLongEdge, exportScale)); link.href = exportMap.getCanvas().toDataURL("image/png"); link.click(); return { width: exportWidth, height: exportHeight, scale: exportScale }; } finally { exportMap.remove(); container.remove(); } } function createExportContainer(width: number, height: number) { const container = document.createElement("div"); container.style.position = "fixed"; container.style.left = "-10000px"; container.style.top = "0"; container.style.width = `${width}px`; container.style.height = `${height}px`; container.style.pointerEvents = "none"; container.style.opacity = "0"; return container; } function cloneMapStyle(style: StyleSpecification) { if (typeof structuredClone === "function") { return structuredClone(style); } return JSON.parse(JSON.stringify(style)) as StyleSpecification; } function onceMapEvent(map: MapLibreMap, eventName: "load" | "idle") { return new Promise((resolve, reject) => { map.once(eventName, () => resolve()); map.once("error", (event) => reject(event.error ?? new Error("地图导出渲染失败。"))); }); } function waitForMapIdle(map: MapLibreMap) { return Promise.race([ onceMapEvent(map, "idle"), new Promise((resolve) => { window.setTimeout(resolve, EXPORT_IDLE_TIMEOUT_MS); }) ]); } function getExportLabel(targetLongEdge: number | undefined, scale: number) { if (targetLongEdge === 3840) { return "4k"; } return `${scale.toFixed(1)}x`; } function getDefaultExportFilename(label: string) { const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, "-"); return `drainage-network-view-${label}-${timestamp}.png`; }