- I am working on a React project where I use the
react-map-gland@deck.gl/reactlibraries to display a map along with DeckGL layers on the screen. - I have added PDF printing functionality to the project, utilizing the
jsPDFlibrary for this purpose. - The printing functionality includes options such as page size and orientation.
- However, there is a problem: the PDF output only captures the visible portion of the map that is currently displayed on the screen. For example, if I select an A3 page size in landscape orientation, the screenshot includes only the visible map area, while the rest of the page is filled with whitespace.
- Below is my code for the map and print components.
Map component:
import * as React from "react";
import ReactMap, { MapRef } from "react-map-gl";
import DeckGL from "@deck.gl/react/typed";
import useEffect from "react";
import { useDispatch } from "react-redux";
import { setMapRef, setDeckRef } from "@/store/features/map/mapSlice";
export default function Map(props: MapProps) {
const mapRef = useRef<MapRef | null>(null);
const deckRef = useRef(null);
useEffect(() => {
if (mapRef.current) {
dispatch(setMapRef(mapRef.current)); // Pass the `current` value only
}
if (deckRef.current) {
dispatch(setDeckRef(deckRef.current)); // Pass the `current` value only
}
}, [mapRef.current, deckRef.current, dispatch]);
const layers = [...]
return (
<DeckGL
ref={deckRef}
useDevicePixels={true}
glOptions={{ preserveDrawingBuffer: true }}
layers={layers}
>
<ReactMap
ref={mapRef}
mapboxAccessToken={import.meta.env.VITE_MAPBOX_ACCESS_TOKEN}
mapLib={import("mapbox-gl")}
reuseMaps
mapStyle={getMapboxMapStyle(theme.palette.mode === "dark", props.style)}
projection={{ name: "mercator" }}
onLoad={onRender}
preserveDrawingBuffer={true}
/>
</DeckGL>
);
}
PrintMap.tsx component:
import { Button, Paper, PaperProps, Stack, Typography, FormControl, InputLabel, Select, MenuItem } from "@mui/material";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { jsPDF } from 'jspdf';
import { selectMapRef, selectDeckRef } from "@/store/features/map/selectors/selectDrawnShape";
import { useSelector } from "react-redux";
import { RootState } from "@/store";
export type PrintMapProps = {
/**
* Callback when user cancels the dialog
*/
onCancel: () => void;
/**
* Additional styles
*/
sx?: PaperProps["sx"];
};
export default function PrintMap(props: PrintMapProps) {
const { t } = useTranslation();
const pdfTitle = useSelector(
(state: RootState) => state.cycles.currentCycle?.id,
);
const mapRef = useSelector(selectMapRef) as any;
const deckRef = useSelector(selectDeckRef) as any;
console.log("deckRef", deckRef)
const [printOptions, setPrintOptions] = useState({
pageSize: 'A4',
orientation: 'landscape',
});
const handlePrintMap = async () => {
const deck = (deckRef as any)?.deck;
try {
if (!deckRef || !mapRef || !mapRef.getMap()) {
console.error("Map or DeckGL not ready");
return;
}
const deckCanvas = deck.canvas;
if (!document.body.contains(deckCanvas)) {
console.error("DeckGL canvas is not attached to the document");
return;
}
const mapCanvas = mapRef.getMap().getCanvas();
const screenWidth = window.innerWidth;
const screenHeight = window.innerHeight;
const dpi = 96; // Pixels per inch (standard screen DPI)
const mmToPx = (mm: number) => (mm / 25.4) * dpi; // Convert mm to pixels
const pageSize = printOptions.pageSize;
const isLandscape = printOptions.orientation === 'landscape';
let paperWidthMM = 0;
let paperHeightMM = 0;
if (pageSize === 'A3') {
paperWidthMM = isLandscape ? 420 : 297;
paperHeightMM = isLandscape ? 297 : 420;
} else if (pageSize === 'A4') {
paperWidthMM = isLandscape ? 297 : 210;
paperHeightMM = isLandscape ? 210 : 297;
} else if (pageSize === 'A5') {
paperWidthMM = isLandscape ? 210 : 148;
paperHeightMM = isLandscape ? 148 : 210;
} else if (pageSize === 'A6') {
paperWidthMM = isLandscape ? 148 : 105;
paperHeightMM = isLandscape ? 105 : 148;
} else { // Default to A4
paperWidthMM = isLandscape ? 297 : 210;
paperHeightMM = isLandscape ? 210 : 297;
}
const customWidth = mmToPx(paperWidthMM);
const customHeight = mmToPx(paperHeightMM);
const startX = Math.max(0, (screenWidth - customWidth) / 2);
const startY = Math.max(0, (screenHeight - customHeight) / 2);
const combinedCanvas = document.createElement('canvas');
combinedCanvas.width = customWidth;
combinedCanvas.height = customHeight;
const ctx = combinedCanvas.getContext('2d');
if (ctx) {
// Draw the specific region of mapCanvas
ctx.drawImage(
mapCanvas, // Source canvas
startX, // Source X
startY, // Source Y
customWidth, // Source width
customHeight, // Source height
0, // Destination X
0, // Destination Y
customWidth, // Destination width
customHeight // Destination height
);
ctx.drawImage(
deckCanvas, // Source canvas
startX, // Source X
startY, // Source Y
customWidth, // Source width
customHeight, // Source height
0, // Destination X
0, // Destination Y
customWidth, // Destination width
customHeight // Destination height
);
} else {
console.error("Failed to get 2D context");
return;
}
const combinedImage = combinedCanvas.toDataURL('image/png');
console.log("combinedImage", combinedImage);
const pdf = new jsPDF({
orientation: printOptions.orientation as 'landscape' | 'portrait',
unit: 'mm',
format: [paperWidthMM, paperHeightMM],
});
pdf.addImage(combinedImage, 'PNG', 0, 0, paperWidthMM, paperHeightMM, undefined, 'NONE');
pdf.save(`${pdfTitle}`);
} catch (error) {
console.error("Error while printing:", error);
}
};
return (
<Paper
elevation={3}
sx={{
p: 5,
pb: 3,
pt: 3,
width: "28.5rem",
borderRadius: 3,
...(props.sx ?? {}),
}}
>
<Stack direction="column" gap={2} width="100%">
<Typography variant="h1">{t("Print map")}</Typography>
<Typography variant="body1">
{t("Set printing options as per your requirement")}
</Typography>
<Stack gap={2}>
<FormControl fullWidth>
<InputLabel>Page Size</InputLabel>
<Select
value={printOptions.pageSize}
label="Page Size"
onChange={(e) => setPrintOptions({ ...printOptions, pageSize: e.target.value })}
>
<MenuItem value="Letter">Letter</MenuItem>
<MenuItem value="A3">A3</MenuItem>
<MenuItem value="A4">A4</MenuItem>
<MenuItem value="A5">A5</MenuItem>
<MenuItem value="A6">A6</MenuItem>
</Select>
</FormControl>
<FormControl fullWidth>
<InputLabel>Orientation</InputLabel>
<Select
value={printOptions.orientation}
label="Orientation"
onChange={(e) => setPrintOptions({ ...printOptions, orientation: e.target.value })}
>
<MenuItem value="portrait">Portrait</MenuItem>
<MenuItem value="landscape">Landscape</MenuItem>
</Select>
</FormControl>
</Stack>
<Stack gap={1}>
<Button
variant="contained"
color="primary"
fullWidth
disableRipple
disableElevation
onClick={() => { handlePrintMap(), props.onCancel() }}
>
{t("Print")}
</Button>
<Button
variant="text"
color="secondary"
fullWidth
disableRipple
disableElevation
onClick={() => props.onCancel()}
>
{t("Cancel")}
</Button>
</Stack>
</Stack>
</Paper>
);
}
Package.jsonfile:
[
"react-map-gl": "7.1.6",
"@deck.gl/core": "8.9.35",
"jspdf": "^2.5.2",
]
- The screenshot shows the issue where the map captured in the PDF only includes the currently visible area, leaving blank spaces where the offscreen parts of the map should be. This highlights the challenge of capturing a complete map view in scenarios like A3 landscape printing.(https://i.sstatic.net/kE3aKV5b.png)
Additional info:
- I conducted some research and found suggestions to set preserveDrawingBuffer={true} in both DeckGL and ReactMap. However, this approach did not resolve the issue.
- I also tried other potential solutions, but none of them worked, and now I’m stuck on how to fix this problem.
- The remaining functionality works correctly without any issues.
- The printing functionality works as expected for A5, A6, and letter page sizes in both landscape and portrait orientations. However, it does not work as expected for larger page sizes like A2, A3, and A4.