diff --git a/src/components/olmap/MonitoringPlaceOptimization/SchemeDrawingDialog.tsx b/src/components/olmap/MonitoringPlaceOptimization/SchemeDrawingDialog.tsx index e88ce3a..34779ff 100644 --- a/src/components/olmap/MonitoringPlaceOptimization/SchemeDrawingDialog.tsx +++ b/src/components/olmap/MonitoringPlaceOptimization/SchemeDrawingDialog.tsx @@ -33,6 +33,7 @@ import { lineStringFromFlatCoordinates, } from "@components/olmap/core/tileFeatureIndex"; import { + A3_LANDSCAPE_HEIGHT, A3_LANDSCAPE_WIDTH, canvasToPngBlob, getPaddedDrawingExtent, @@ -55,7 +56,10 @@ interface SchemeDrawingDialogProps { } const DRAWING_MAP_RENDER_TIMEOUT_MS = 10_000; -const DRAWING_MAP_SIZE: [number, number] = [1600, 981]; +const DRAWING_MAP_SIZE: [number, number] = [ + A3_LANDSCAPE_WIDTH - 300, + A3_LANDSCAPE_HEIGHT - 650, +]; type DrawingMode = "linework" | "basemap"; @@ -106,17 +110,26 @@ export const fitMapToFullNetwork = ( interface DrawingMapSession { map: OlMap; extent: [number, number, number, number]; + attribution: string | null; dispose: () => void; } -const cloneVisibleBaseLayers = (mainMap: OlMap): TileLayer[] => { +const cloneVisibleBaseLayers = ( + mainMap: OlMap, +): { layers: TileLayer[]; attribution: string | null } => { const cloned: TileLayer[] = []; + const attributions = new Set(); 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; @@ -133,7 +146,10 @@ const cloneVisibleBaseLayers = (mainMap: OlMap): TileLayer[] => { ); }; mainMap.getLayers().getArray().forEach((layer) => collect(layer)); - return cloned; + return { + layers: cloned, + attribution: attributions.size ? [...attributions].join(" · ") : null, + }; }; const createDrawingMapSession = ( @@ -180,12 +196,14 @@ const createDrawingMapSession = ( }); pipeLayer.set("value", "pipes"); pipeLayer.setExtent(networkExtent); - const layers = + const basemap = mode === "basemap" - ? [...cloneVisibleBaseLayers(mainMap), pipeLayer] - : [pipeLayer]; + ? cloneVisibleBaseLayers(mainMap) + : { layers: [], attribution: null }; + const layers = [...basemap.layers, pipeLayer]; drawingMap = new OlMap({ target, + pixelRatio: 1, view: new View({ projection: mainMap.getView().getProjection(), }), @@ -200,6 +218,7 @@ const createDrawingMapSession = ( return { map: drawingMap, extent: networkExtent, + attribution: basemap.attribution, dispose: () => { if (disposed) return; disposed = true; @@ -378,6 +397,7 @@ const SchemeDrawingDialog: React.FC = ({ ); const [drawingMode, setDrawingMode] = useState("linework"); const [mapCanvas, setMapCanvas] = useState(null); + const [mapAttribution, setMapAttribution] = useState(null); const [previewUrl, setPreviewUrl] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -393,6 +413,7 @@ const SchemeDrawingDialog: React.FC = ({ setError(null); setNetworkData(null); setMapCanvas(null); + setMapAttribution(null); if (!map) { setError("主地图尚未初始化,请稍后重试"); setLoading(false); @@ -424,6 +445,9 @@ const SchemeDrawingDialog: React.FC = ({ if (!cancelled) { setNetworkData(data); setMapCanvas(captured); + setMapAttribution( + drawingMode === "basemap" ? drawingSession.attribution : null, + ); } }) .catch((reason) => { @@ -455,6 +479,7 @@ const SchemeDrawingDialog: React.FC = ({ network, networkData, mapCanvas, + mapAttribution, dirty, width: 1600, }) @@ -478,7 +503,16 @@ const SchemeDrawingDialog: React.FC = ({ cancelled = true; if (currentUrl) URL.revokeObjectURL(currentUrl); }; - }, [dirty, mapCanvas, network, networkData, open, rows, scheme]); + }, [ + dirty, + mapAttribution, + mapCanvas, + network, + networkData, + open, + rows, + scheme, + ]); const createFullResolutionBlob = async () => { if (!networkData) throw new Error("管网数据尚未加载"); @@ -488,6 +522,7 @@ const SchemeDrawingDialog: React.FC = ({ network, networkData, mapCanvas, + mapAttribution, dirty, width: A3_LANDSCAPE_WIDTH, }); diff --git a/src/components/olmap/MonitoringPlaceOptimization/engineeringDrawing.test.ts b/src/components/olmap/MonitoringPlaceOptimization/engineeringDrawing.test.ts index a0243b1..c25a3da 100644 --- a/src/components/olmap/MonitoringPlaceOptimization/engineeringDrawing.test.ts +++ b/src/components/olmap/MonitoringPlaceOptimization/engineeringDrawing.test.ts @@ -320,6 +320,7 @@ describe("engineering drawing", () => { extent: [0, 0, 1000, 1000], }, mapCanvas, + mapAttribution: "© Mapbox © OpenStreetMap", dirty: false, width: 1600, }); @@ -331,5 +332,10 @@ describe("engineering drawing", () => { expect.any(Number), expect.any(Number), ); + expect(context.fillText).toHaveBeenCalledWith( + "底图来源:© Mapbox © OpenStreetMap", + expect.any(Number), + expect.any(Number), + ); }); }); diff --git a/src/components/olmap/MonitoringPlaceOptimization/engineeringDrawing.ts b/src/components/olmap/MonitoringPlaceOptimization/engineeringDrawing.ts index d1d6fcd..3950f22 100644 --- a/src/components/olmap/MonitoringPlaceOptimization/engineeringDrawing.ts +++ b/src/components/olmap/MonitoringPlaceOptimization/engineeringDrawing.ts @@ -13,6 +13,7 @@ interface DrawingOptions { network: string; networkData: NetworkDrawingData; mapCanvas?: HTMLCanvasElement | null; + mapAttribution?: string | null; dirty: boolean; width?: number; } @@ -123,6 +124,7 @@ export const renderEngineeringDrawing = async ({ network, networkData, mapCanvas, + mapAttribution, dirty, width = A3_LANDSCAPE_WIDTH, }: DrawingOptions): Promise => { @@ -261,6 +263,20 @@ 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; diff --git a/src/components/olmap/core/Controls/BaseLayers.test.ts b/src/components/olmap/core/Controls/BaseLayers.test.ts index 5008031..5db2f20 100644 --- a/src/components/olmap/core/Controls/BaseLayers.test.ts +++ b/src/components/olmap/core/Controls/BaseLayers.test.ts @@ -16,24 +16,38 @@ jest.mock("ol/layer/Tile.js", () => ({ __esModule: true, default: class MockTileLayer { private readonly source: unknown; + private readonly properties = new Map(); constructor(options: any) { this.source = options.source; } getSource() { return this.source; } + set(key: string, value: unknown) { + this.properties.set(key, value); + } + get(key: string) { + return this.properties.get(key); + } }, })); jest.mock("ol/layer/Group", () => ({ __esModule: true, default: class MockGroup { private readonly layers: unknown[]; + private readonly properties = new Map(); constructor(options: any) { this.layers = options.layers; } getLayers() { return { getArray: () => this.layers }; } + set(key: string, value: unknown) { + this.properties.set(key, value); + } + get(key: string) { + return this.properties.get(key); + } }, })); @@ -70,6 +84,7 @@ describe("base layer resources", () => { expect(getLeafSources(entry.layer)).toEqual( getLeafSources(compare[index].layer), ); + expect(entry.layer.get("exportAttribution")).toBe(entry.attribution); }); }); }); diff --git a/src/components/olmap/core/Controls/BaseLayers.tsx b/src/components/olmap/core/Controls/BaseLayers.tsx index ebb001b..f74f140 100644 --- a/src/components/olmap/core/Controls/BaseLayers.tsx +++ b/src/components/olmap/core/Controls/BaseLayers.tsx @@ -18,16 +18,42 @@ import { markMapResourcePersistent } from "../mapLifecycle"; const INITIAL_LAYER = "mapbox-light"; const BASE_LAYER_METADATA = [ - { id: "mapbox-light", name: "默认地图", img: mapboxLight.src }, - { id: "mapbox-satellite", name: "卫星地图", img: mapboxSatellite.src }, + { + id: "mapbox-light", + name: "默认地图", + img: mapboxLight.src, + attribution: "© Mapbox © OpenStreetMap", + }, + { + id: "mapbox-satellite", + name: "卫星地图", + img: mapboxSatellite.src, + attribution: "© Mapbox", + }, { id: "mapbox-satellite-streets", name: "卫星街道地图", img: mapboxSatelliteStreet.src, + attribution: "© Mapbox © OpenStreetMap", + }, + { + id: "mapbox-streets", + name: "街道地图", + img: mapboxStreets.src, + attribution: "© Mapbox © OpenStreetMap", + }, + { + id: "tianditu-vector", + name: "天地图矢量", + img: mapboxOutdoors.src, + attribution: "© 天地图", + }, + { + id: "tianditu-image", + name: "天地图影像", + img: mapboxSatellite.src, + attribution: "© 天地图", }, - { id: "mapbox-streets", name: "街道地图", img: mapboxStreets.src }, - { id: "tianditu-vector", name: "天地图矢量", img: mapboxOutdoors.src }, - { id: "tianditu-image", name: "天地图影像", img: mapboxSatellite.src }, ] as const; const createTileSource = (url: string, attributions: string) => @@ -123,10 +149,11 @@ export const createBaseLayerEntries = (sources: BaseLayerSources) => { ], }), }, - ].map((entry) => ({ - ...entry, - layer: markMapResourcePersistent(entry.layer), - })); + ].map((entry) => { + const layer = markMapResourcePersistent(entry.layer); + layer.set("exportAttribution", entry.attribution); + return { ...entry, layer }; + }); }; const BaseLayers: React.FC = () => {