Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't include the map view's padding during printing (#311) #326

Merged
merged 3 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading