Skip to content

Commit

Permalink
Don't include the map view's padding during printing (#311) (#326)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Antonia van Eek <a.vaneek@conterra.de>
  • Loading branch information
mbeckem and antoniave committed Jun 17, 2024
1 parent 9b90739 commit 0fd366f
Show file tree
Hide file tree
Showing 9 changed files with 204 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/six-phones-invite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@open-pioneer/printing": patch
---

Don't include the map view's padding by default (see open-pioneer/trails-openlayers-base-packages#311)
2 changes: 2 additions & 0 deletions src/packages/printing/Printing.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import * as PrintingControllerModule from "./PrintingController";

const setFileFormatSpy = vi.fn();
const setTitleSpy = vi.fn();
const setViewPaddingSpy = vi.fn();
const handleMapExportSpy = vi.fn(() => Promise.resolve());
const notifySpy = vi.fn();

/** Mock implementation used by the UI. */
class MockPrintingController {
setTitle = setTitleSpy;
setFileFormat = setFileFormatSpy;
setViewPadding = setViewPaddingSpy;
handleMapExport = handleMapExportSpy;

destroy() {}
Expand Down
22 changes: 17 additions & 5 deletions src/packages/printing/Printing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { PackageIntl } from "@open-pioneer/runtime";
import { useIntl, useService } from "open-pioneer:react-hooks";
import { FC, useEffect, useState } from "react";
import { FileFormatType, PrintingController } from "./PrintingController";
import { PrintingService } from "./index";
import type { ViewPaddingBehavior, PrintingService } from "./index";

const LOG = createLogger("printing");

Expand All @@ -29,6 +29,13 @@ export interface PrintingProps extends CommonComponentProps {
* The id of the map.
*/
mapId: string;

/**
* Whether to respect the map view's padding when printing (default: `"auto"`).
*
* See also {@link ViewPaddingBehavior}.
*/
viewPadding?: ViewPaddingBehavior;
}

/**
Expand All @@ -37,7 +44,7 @@ export interface PrintingProps extends CommonComponentProps {
export const Printing: FC<PrintingProps> = (props) => {
const intl = useIntl();

const { mapId } = props;
const { mapId, viewPadding = "auto" } = props;
const { containerProps } = useCommonComponentProps("printing", props);
const [selectedFileFormat, setSelectedFileFormat] = useState<FileFormatType>("pdf");
const [title, setTitle] = useState<string>("");
Expand All @@ -47,8 +54,7 @@ export const Printing: FC<PrintingProps> = (props) => {
const notifier = useService<NotificationService>("notifier.NotificationService");

const { map } = useMapModel(mapId);

const controller = useController(map, intl, printingService);
const controller = useController(map, intl, printingService, viewPadding);

useEffect(() => {
controller?.setFileFormat(selectedFileFormat);
Expand Down Expand Up @@ -136,7 +142,8 @@ export const Printing: FC<PrintingProps> = (props) => {
function useController(
map: MapModel | undefined,
intl: PackageIntl,
printingService: PrintingService
printingService: PrintingService,
viewPadding: ViewPaddingBehavior
) {
const [controller, setController] = useState<PrintingController | undefined>(undefined);

Expand All @@ -155,5 +162,10 @@ function useController(
setController(undefined);
};
}, [map, intl, printingService]);

useEffect(() => {
controller?.setViewPadding(viewPadding);
}, [controller, viewPadding]);

return controller;
}
10 changes: 8 additions & 2 deletions src/packages/printing/PrintingController.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import OlMap from "ol/Map";
import { PrintingService, PrintResult } from "./index";
import { PrintingService, PrintResult, ViewPaddingBehavior } from "./index";
import { canvasToPng, createBlockUserOverlay } from "./utils";
import { Resource } from "@open-pioneer/core";

Expand All @@ -16,6 +16,7 @@ export class PrintingController {
private i18n: I18n;

private printingService: PrintingService;
private viewPadding: ViewPaddingBehavior | undefined;

private printMap: PrintResult | undefined = undefined;
private overlay: Resource | undefined = undefined;
Expand All @@ -38,6 +39,10 @@ export class PrintingController {
this.fileFormat = format;
}

setViewPadding(padding: ViewPaddingBehavior) {
this.viewPadding = padding;
}

async handleMapExport() {
if (!this.olMap || !this.fileFormat) {
return;
Expand All @@ -46,7 +51,8 @@ export class PrintingController {
try {
this.begin();
this.printMap = await this.printingService.printMap(this.olMap, {
blockUserInteraction: false
blockUserInteraction: false,
viewPadding: this.viewPadding
});
const canvas = this.printMap.getCanvas();
if (canvas) {
Expand Down
48 changes: 43 additions & 5 deletions src/packages/printing/PrintingServiceImpl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,44 @@ afterEach(() => {
});

it("Should successfully print a canvas", async () => {
const { olMap, printToCanvasSpy, printingService } = await setUp();
const { olMap, printingService } = await setUp();
const printResult = await printingService.printMap(olMap);
const canvas = printResult.getCanvas();

printToCanvasSpy.mockImplementation(() => document.createElement("canvas"));
expect(canvas).toBeDefined();
expect(canvas.tagName).toBe("CANVAS");
});

it("should respect the map's padding by default", async () => {
const { olMap, removePaddingSpy, printingService } = await setUp();

// top, right, bottom, left
olMap.getView().padding = [10, 20, 30, 40];

const printResult = await printingService.printMap(olMap);
const canvas = printResult.getCanvas();
expect(canvas).toBeDefined();

expect(removePaddingSpy.mock.lastCall![1]).toMatchInlineSnapshot(`
{
"bottom": 30,
"left": 40,
"right": 20,
"top": 10,
}
`);
});

it("should ignore the map's padding if 'viewPadding' is set to 'ignore'", async () => {
const { olMap, removePaddingSpy, printingService } = await setUp();

// top, right, bottom, left
olMap.getView().padding = [10, 20, 30, 40];

const printResult = await printingService.printMap(olMap, { viewPadding: "ignore" });
const canvas = printResult.getCanvas();
expect(canvas).toBeDefined();
expect(canvas.tagName).toBe("CANVAS");
expect(removePaddingSpy).not.toHaveBeenCalled();
});

it("should create an overlay during print and removes it after print", async () => {
Expand Down Expand Up @@ -154,7 +183,16 @@ async function setUp() {

const printingService = await createService(PrintingServiceImpl, {});
const printToCanvasSpy = vi.spyOn(PrintJob.prototype as any, "printToCanvas");
printToCanvasSpy.mockImplementation(() => document.createElement("canvas"));
printToCanvasSpy.mockImplementation(() => {
const canvas = document.createElement("canvas");
canvas.width = 200;
canvas.height = 200;
return canvas;
});

// Cant use canvas.getContext() in tests
const removePaddingSpy = vi.spyOn(PrintJob.prototype as any, "removePadding");
removePaddingSpy.mockImplementation((canvas: any) => canvas);

return { olMap, printToCanvasSpy, printingService };
return { olMap, printToCanvasSpy, printingService, removePaddingSpy };
}
108 changes: 100 additions & 8 deletions src/packages/printing/PrintingServiceImpl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)
// SPDX-License-Identifier: Apache-2.0
import type { PrintingOptions, PrintingService, PrintResult } from "./index";
import type { PrintingOptions, PrintingService, PrintResult, ViewPaddingBehavior } from "./index";
import OlMap from "ol/Map";
import Draw from "ol/interaction/Draw";
import { StyleLike } from "ol/style/Style";
Expand All @@ -24,17 +24,26 @@ export class PrintingServiceImpl implements PrintingService {
const job = new PrintJob(olMap, {
blockUserInteraction: true,
overlayText: this.defaultOverlayText,
viewPadding: "auto",
...options
});
return await job.printMap();
}
}

interface ViewPadding {
top: number;
right: number;
bottom: number;
left: number;
}

// Exported just for test (mocking)
export class PrintJob {
private olMap: OlMap;
private blockUserInteraction: boolean = false;
private overlayText: string;
private viewPadding: ViewPaddingBehavior;

private running = false;
private drawInformation: { draw: Draw; style: StyleLike | null | undefined }[] | undefined = [];
Expand All @@ -45,6 +54,7 @@ export class PrintJob {
this.olMap = olMap;
this.blockUserInteraction = options.blockUserInteraction;
this.overlayText = options.overlayText;
this.viewPadding = options.viewPadding;
}

async printMap(): Promise<PrintResultImpl> {
Expand All @@ -55,12 +65,15 @@ export class PrintJob {
try {
await this.beginExport();

const canvas = await this.printToCanvas(this.olMap.getViewport());
if (canvas) {
return new PrintResultImpl(canvas);
} else {
let canvas = await this.printToCanvas(this.olMap.getViewport());
if (!canvas) {
throw new Error("Canvas export failed");
}

if (this.viewPadding === "auto") {
canvas = this.removePadding(canvas, this.getViewPadding());
}
return new PrintResultImpl(canvas);
} finally {
// Always remove scale bar
this.reset();
Expand Down Expand Up @@ -98,12 +111,31 @@ export class PrintJob {
}

private async addScaleLine() {
this.scaleLine = new ScaleLine({
const scaleLine = (this.scaleLine = new ScaleLine({
className: "printing-scale-bar ol-scale-bar",
bar: true,
text: true,
minWidth: 125
});
}));

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const scaleLineElement = (scaleLine as any).element as HTMLElement;
if (!scaleLineElement) {
throw new Error("Scale line does not have an element");
}

// Position the scale bar manually.
// The 50px should be plenty to avoid overlapping with open layers attributions on most cases.
// Additionally, take the view padding into account (if behavior is 'auto').
let bottom = 50;
let left = 8;
if (this.viewPadding === "auto") {
const { bottom: paddingBottom, left: paddingLeft } = this.getViewPadding();
bottom = Math.max(paddingBottom + 8, bottom);
left += paddingLeft;
}
scaleLineElement.style.setProperty("--printing-scale-bar-bottom", `${bottom}px`);
scaleLineElement.style.setProperty("--printing-scale-bar-left", `${left}px`);

const renderPromise = createManualPromise<void>();

Expand Down Expand Up @@ -151,7 +183,6 @@ export class PrintJob {
// required when actually printed. This speeds up the initial page load.
const html2canvas = (await import("html2canvas")).default;
const canvas = await html2canvas(element, exportOptions);

return canvas;
}

Expand All @@ -173,6 +204,67 @@ export class PrintJob {
drawInfo.draw.getOverlay().setStyle(drawInfo.style);
});
}

private removePadding(canvas: HTMLCanvasElement, rawPadding: ViewPadding): HTMLCanvasElement {
// The canvas returned by html2canvas is scaled by the device pixel ratio.
// The padding needs to be adjusted (because its in css pixels).
const dpr = window.devicePixelRatio || 1;
const dprPadding = {
top: rawPadding.top * dpr,
right: rawPadding.right * dpr,
bottom: rawPadding.bottom * dpr,
left: rawPadding.left * dpr
};

if (
dprPadding.left === 0 &&
dprPadding.right === 0 &&
dprPadding.top === 0 &&
dprPadding.bottom === 0
) {
return canvas;
}

const { width, height } = canvas;
const newCanvas = document.createElement("canvas");
newCanvas.width = width - dprPadding.left - dprPadding.right;
newCanvas.height = height - dprPadding.top - dprPadding.bottom;

const newCtx = newCanvas.getContext("2d");
if (!newCtx) {
throw new Error("Failed to get a canvas context");
}

newCtx.drawImage(
canvas,
dprPadding.left,
dprPadding.top,
newCanvas.width,
newCanvas.height,
0,
0,
newCanvas.width,
newCanvas.height
);
return newCanvas;
}

private getViewPadding(): ViewPadding {
const map = this.olMap;
// top, right, bottom, left
const rawPadding = (map.getView().padding ?? [0, 0, 0, 0]) as [
number,
number,
number,
number
];
return {
top: rawPadding[0] ?? 0,
right: rawPadding[1] ?? 0,
bottom: rawPadding[2] ?? 0,
left: rawPadding[3] ?? 0
};
}
}

class PrintResultImpl implements PrintResult {
Expand Down
9 changes: 8 additions & 1 deletion src/packages/printing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,14 @@ const printResult = await printingService.printMap(map, {
### Printed elements

The printed map contains all map elements, visible layers, scale-bar and a title. If the user does not enter a title, the map is printed without title.
To prevent custom elements from showing in the printed map, add the classname `printing-hide` to the elements.
To prevent custom elements from showing in the printed map, add the CSS class name `printing-hide` to the elements.

### View padding

The printing widget and the printing service will by default respect the map's current view `padding` (see OpenLayers [View class](https://openlayers.org/en/latest/apidoc/module-ol_View-View.html)).
This means that padded regions (a border on every side) will _not_ be included in the printed result.

To ignore the view padding when printing the map, you can pass `viewPadding: "ignore"` to the printing service or the printing component.

## Known issues

Expand Down
Loading

0 comments on commit 0fd366f

Please sign in to comment.