Performance: fix PDF export for maps with many items
This commit is contained in:
parent
7ae1a6c00f
commit
03d976c567
6 changed files with 131 additions and 57 deletions
|
@ -173,7 +173,7 @@ export default class GeoLocationHandler {
|
||||||
properties[k] = location[k]
|
properties[k] = location[k]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log(location)
|
console.debug("Current location object:", location)
|
||||||
properties["_all"] = JSON.stringify(location)
|
properties["_all"] = JSON.stringify(location)
|
||||||
|
|
||||||
const feature = <Feature>{
|
const feature = <Feature>{
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
mimetype="image/png"
|
mimetype="image/png"
|
||||||
mainText={t.downloadAsPng}
|
mainText={t.downloadAsPng}
|
||||||
helperText={t.downloadAsPngHelper}
|
helperText={t.downloadAsPngHelper}
|
||||||
construct={() => state.mapProperties.exportAsPng(1)}
|
construct={() => state.mapProperties.exportAsPng()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
export let templateName: string
|
export let templateName: string
|
||||||
export let state: ThemeViewState
|
export let state: ThemeViewState
|
||||||
const template: PdfTemplateInfo = SvgToPdf.templates[templateName]
|
const template: PdfTemplateInfo = SvgToPdf.templates[templateName]
|
||||||
console.log("template", template)
|
|
||||||
let mainText: Translation =
|
let mainText: Translation =
|
||||||
typeof template.description === "string"
|
typeof template.description === "string"
|
||||||
? new Translation(template.description)
|
? new Translation(template.description)
|
||||||
|
@ -47,7 +46,6 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const unsub = creator.status.addCallbackAndRunD((s) => {
|
const unsub = creator.status.addCallbackAndRunD((s) => {
|
||||||
console.log("SVG creator status:", s)
|
|
||||||
status?.setData(s)
|
status?.setData(s)
|
||||||
})
|
})
|
||||||
await creator.ExportPdf(Locale.language.data)
|
await creator.ExportPdf(Locale.language.data)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import SvelteUIElement from "../Base/SvelteUIElement"
|
||||||
import MaplibreMap from "./MaplibreMap.svelte"
|
import MaplibreMap from "./MaplibreMap.svelte"
|
||||||
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
import { RasterLayerProperties } from "../../Models/RasterLayerProperties"
|
||||||
import * as htmltoimage from "html-to-image"
|
import * as htmltoimage from "html-to-image"
|
||||||
|
import { draw } from "svelte/transition"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
* The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties`
|
||||||
|
@ -180,20 +181,6 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static setDpi(
|
|
||||||
drawOn: HTMLCanvasElement,
|
|
||||||
ctx: CanvasRenderingContext2D,
|
|
||||||
dpiFactor: number
|
|
||||||
) {
|
|
||||||
drawOn.style.width = drawOn.style.width || drawOn.width + "px"
|
|
||||||
drawOn.style.height = drawOn.style.height || drawOn.height + "px"
|
|
||||||
|
|
||||||
// Resize canvas and scale future draws.
|
|
||||||
drawOn.width = Math.ceil(drawOn.width * dpiFactor)
|
|
||||||
drawOn.height = Math.ceil(drawOn.height * dpiFactor)
|
|
||||||
ctx.scale(dpiFactor, dpiFactor)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prepares an ELI-URL to be compatible with mapbox
|
* Prepares an ELI-URL to be compatible with mapbox
|
||||||
*/
|
*/
|
||||||
|
@ -224,27 +211,55 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
public async exportAsPng(markerScale: number = 1): Promise<Blob> {
|
private static async toBlob(canvas: HTMLCanvasElement): Promise<Blob> {
|
||||||
|
return await new Promise<Blob>((resolve) => canvas.toBlob((blob) => resolve(blob)))
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async createImage(url: string): Promise<HTMLImageElement> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const img = new Image()
|
||||||
|
img.decode = () => resolve(img) as any
|
||||||
|
img.onload = () => resolve(img)
|
||||||
|
img.onerror = reject
|
||||||
|
img.crossOrigin = "anonymous"
|
||||||
|
img.decoding = "async"
|
||||||
|
img.src = url
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
public async exportAsPng(
|
||||||
|
rescaleIcons: number = 1,
|
||||||
|
progress: UIEventSource<{ current: number; total: number }> = undefined
|
||||||
|
): Promise<Blob> {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (!map) {
|
if (!map) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
const drawOn = document.createElement("canvas")
|
const drawOn = document.createElement("canvas", {})
|
||||||
drawOn.width = map.getCanvas().width
|
|
||||||
drawOn.height = map.getCanvas().height
|
|
||||||
|
|
||||||
const ctx = drawOn.getContext("2d")
|
const ctx = drawOn.getContext("2d")
|
||||||
// Set up CSS size.
|
// The width/height has been set in 'mm' on the parent element and converted to pixels by the browser
|
||||||
MapLibreAdaptor.setDpi(drawOn, ctx, markerScale / map.getPixelRatio())
|
const w = map.getContainer().getBoundingClientRect().width
|
||||||
|
const h = map.getContainer().getBoundingClientRect().height
|
||||||
|
|
||||||
|
let dpi = map.getPixelRatio()
|
||||||
|
console.log("Sizes:", {
|
||||||
|
dpi,
|
||||||
|
w,
|
||||||
|
h,
|
||||||
|
origSizeW: drawOn.style.width,
|
||||||
|
origSizeH: drawOn.style.height,
|
||||||
|
})
|
||||||
|
// The 'css'-size stays constant...
|
||||||
|
drawOn.style.width = w + "px"
|
||||||
|
drawOn.style.height = h + "px"
|
||||||
|
|
||||||
|
// ...but the number of pixels is increased
|
||||||
|
drawOn.width = Math.ceil(w * dpi)
|
||||||
|
drawOn.height = Math.ceil(h * dpi)
|
||||||
|
|
||||||
await this.exportBackgroundOnCanvas(ctx)
|
await this.exportBackgroundOnCanvas(ctx)
|
||||||
|
await this.drawMarkers(ctx, rescaleIcons, progress)
|
||||||
// MapLibreAdaptor.setDpi(drawOn, ctx, 1)
|
return await MapLibreAdaptor.toBlob(drawOn)
|
||||||
await this.drawMarkers(markerScale, ctx)
|
|
||||||
ctx.scale(markerScale, markerScale)
|
|
||||||
this._maplibreMap.data?.resize()
|
|
||||||
|
|
||||||
return await new Promise<Blob>((resolve) => drawOn.toBlob((blob) => resolve(blob)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -273,16 +288,26 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
map.resize()
|
map.resize()
|
||||||
}
|
}
|
||||||
|
|
||||||
private async drawMarkers(dpiFactor: number, drawOn: CanvasRenderingContext2D): Promise<void> {
|
/**
|
||||||
|
* Draws the markers of the current map on the specified canvas.
|
||||||
|
* The DPIfactor is used to calculate the correct position, whereas 'rescaleIcons' can be used to make the icons smaller
|
||||||
|
* @param drawOn
|
||||||
|
* @param rescaleIcons
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async drawMarkers(
|
||||||
|
drawOn: CanvasRenderingContext2D,
|
||||||
|
rescaleIcons: number = 1,
|
||||||
|
progress: UIEventSource<{ current: number; total: number }>
|
||||||
|
): Promise<void> {
|
||||||
const map = this._maplibreMap.data
|
const map = this._maplibreMap.data
|
||||||
if (!map) {
|
if (!map) {
|
||||||
|
console.error("There is no map to export from")
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
map.getCanvas().style.display = "none"
|
|
||||||
|
|
||||||
const width = map.getCanvas().width
|
|
||||||
const height = map.getCanvas().height
|
|
||||||
const container = map.getContainer()
|
const container = map.getContainer()
|
||||||
|
|
||||||
function isDisplayed(el: Element) {
|
function isDisplayed(el: Element) {
|
||||||
const r1 = el.getBoundingClientRect()
|
const r1 = el.getBoundingClientRect()
|
||||||
const r2 = container.getBoundingClientRect()
|
const r2 = container.getBoundingClientRect()
|
||||||
|
@ -293,32 +318,50 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
r2.bottom < r1.top
|
r2.bottom < r1.top
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const markers = Array.from(container.getElementsByClassName("marker"))
|
const markers = Array.from(container.getElementsByClassName("marker"))
|
||||||
for (let i = 0; i < markers.length; i++) {
|
for (let i = 0; i < markers.length; i++) {
|
||||||
const marker = markers[i]
|
const marker = <HTMLElement>markers[i]
|
||||||
if (!isDisplayed(marker)) {
|
if (!isDisplayed(marker)) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const markerRect = marker.getBoundingClientRect()
|
|
||||||
const w = markerRect.width
|
|
||||||
const h = markerRect.height
|
|
||||||
console.log("Drawing marker", i, "/", markers.length, marker)
|
|
||||||
const markerImg = await htmltoimage.toCanvas(<HTMLElement>marker, {
|
|
||||||
pixelRatio: dpiFactor,
|
|
||||||
canvasWidth: width * dpiFactor,
|
|
||||||
canvasHeight: height * dpiFactor,
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
const pixelRatio = map.getPixelRatio()
|
||||||
|
let x = marker.getBoundingClientRect().x
|
||||||
|
let y = marker.getBoundingClientRect().y
|
||||||
|
const style = marker.style.transform
|
||||||
|
marker.style.transform = ""
|
||||||
|
const offset = style.match(/translate\(([-0-9]+)%, ?([-0-9]+)%\)/)
|
||||||
|
|
||||||
|
console.log("MARKER", marker)
|
||||||
|
const w = marker.style.width
|
||||||
|
// Force a wider view for icon badges
|
||||||
|
marker.style.width = marker.getBoundingClientRect().width * 4 + "px"
|
||||||
|
const svgSource = await htmltoimage.toSvg(marker)
|
||||||
|
const img = await MapLibreAdaptor.createImage(svgSource)
|
||||||
|
marker.style.width = w
|
||||||
|
if (offset && rescaleIcons !== 1) {
|
||||||
|
const [_, relXStr, relYStr] = offset
|
||||||
|
const relX = Number(relXStr)
|
||||||
|
const relY = Number(relYStr)
|
||||||
|
console.log("Moving icon with", relX, relY, img.width, img.height, x, y)
|
||||||
|
// x += img.width * (relX / 100)
|
||||||
|
y += img.height * (relY / 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
x *= pixelRatio
|
||||||
|
y *= pixelRatio
|
||||||
|
|
||||||
|
if (progress) {
|
||||||
|
progress.setData({ current: i, total: markers.length })
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
drawOn.drawImage(markerImg, markerRect.x, markerRect.y)
|
drawOn.drawImage(img, x, y, img.width * rescaleIcons, img.height * rescaleIcons)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Could not draw image because of", e)
|
console.log("Could not draw image because of", e)
|
||||||
}
|
}
|
||||||
|
marker.style.transform = style
|
||||||
}
|
}
|
||||||
|
|
||||||
map.getCanvas().style.display = "unset"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateStores(isSetup: boolean = false): void {
|
private updateStores(isSetup: boolean = false): void {
|
||||||
|
|
|
@ -6,7 +6,13 @@ import { MapLibreAdaptor } from "../UI/Map/MapLibreAdaptor"
|
||||||
import { AvailableRasterLayers } from "../Models/RasterLayers"
|
import { AvailableRasterLayers } from "../Models/RasterLayers"
|
||||||
|
|
||||||
export interface PngMapCreatorOptions {
|
export interface PngMapCreatorOptions {
|
||||||
|
/**
|
||||||
|
* In mm
|
||||||
|
*/
|
||||||
readonly width: number
|
readonly width: number
|
||||||
|
/**
|
||||||
|
* In mm
|
||||||
|
*/
|
||||||
readonly height: number
|
readonly height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,8 +33,23 @@ export class PngMapCreator {
|
||||||
public async CreatePng(freeComponentId: string, status?: UIEventSource<string>): Promise<Blob> {
|
public async CreatePng(freeComponentId: string, status?: UIEventSource<string>): Promise<Blob> {
|
||||||
const div = document.createElement("div")
|
const div = document.createElement("div")
|
||||||
div.id = "mapdiv-" + PngMapCreator.id
|
div.id = "mapdiv-" + PngMapCreator.id
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We want a certain amount of pixels per mm² for a high print quality
|
||||||
|
* For this, we create a bigger map on the screen with a canvas, which has a pixelratio given
|
||||||
|
*
|
||||||
|
* We know that the default DPI of a canvas is 92, but to print something, we need a bit more
|
||||||
|
* So, instead, we give it PIXELRATIO more mm and let it render.
|
||||||
|
* We then draw this onto the PDF as if it were smaller, so it'll have plenty of quality there.
|
||||||
|
*
|
||||||
|
* However, we also need to compensate for this in the zoom level
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const pixelRatio = 2 // dots per mm
|
||||||
div.style.width = this._options.width + "mm"
|
div.style.width = this._options.width + "mm"
|
||||||
div.style.height = this._options.height + "mm"
|
div.style.height = this._options.height + "mm"
|
||||||
|
|
||||||
PngMapCreator.id++
|
PngMapCreator.id++
|
||||||
try {
|
try {
|
||||||
const layout = this._state.layout
|
const layout = this._state.layout
|
||||||
|
@ -43,18 +64,18 @@ export class PngMapCreator {
|
||||||
const l = settings.location.data
|
const l = settings.location.data
|
||||||
|
|
||||||
document.getElementById(freeComponentId).appendChild(div)
|
document.getElementById(freeComponentId).appendChild(div)
|
||||||
const pixelRatio = 4
|
const newZoom = settings.zoom.data + Math.log2(pixelRatio) - 1
|
||||||
const mapElem = new MlMap({
|
const mapElem = new MlMap({
|
||||||
container: div.id,
|
container: div.id,
|
||||||
style: AvailableRasterLayers.maptilerDefaultLayer.properties.url,
|
style: AvailableRasterLayers.maptilerDefaultLayer.properties.url,
|
||||||
center: [l.lon, l.lat],
|
center: [l.lon, l.lat],
|
||||||
zoom: settings.zoom.data,
|
zoom: newZoom,
|
||||||
pixelRatio,
|
pixelRatio,
|
||||||
})
|
})
|
||||||
|
|
||||||
const map = new UIEventSource<MlMap>(mapElem)
|
const map = new UIEventSource<MlMap>(mapElem)
|
||||||
const mla = new MapLibreAdaptor(map)
|
const mla = new MapLibreAdaptor(map)
|
||||||
mla.zoom.setData(settings.zoom.data)
|
mla.zoom.setData(newZoom)
|
||||||
mla.location.setData(settings.location.data)
|
mla.location.setData(settings.location.data)
|
||||||
mla.rasterLayer.setData(settings.rasterLayer.data)
|
mla.rasterLayer.setData(settings.rasterLayer.data)
|
||||||
mla.allowZooming.setData(false)
|
mla.allowZooming.setData(false)
|
||||||
|
@ -70,9 +91,12 @@ export class PngMapCreator {
|
||||||
console.log("Waiting for the style to be loaded...")
|
console.log("Waiting for the style to be loaded...")
|
||||||
await Utils.waitFor(250)
|
await Utils.waitFor(250)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some extra buffer...
|
// Some extra buffer...
|
||||||
setState("One second pause to make sure all images are loaded...")
|
for (let i = 0; i < 5; i++) {
|
||||||
await Utils.waitFor(1000)
|
setState(5 - i + " seconds pause to make sure all images are loaded...")
|
||||||
|
await Utils.waitFor(1000)
|
||||||
|
}
|
||||||
setState(
|
setState(
|
||||||
"Exporting png (" +
|
"Exporting png (" +
|
||||||
this._options.width +
|
this._options.width +
|
||||||
|
@ -82,7 +106,13 @@ export class PngMapCreator {
|
||||||
pixelRatio +
|
pixelRatio +
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
return await mla.exportAsPng(pixelRatio)
|
const progress = new UIEventSource<{ current: number; total: number }>(undefined)
|
||||||
|
progress.addCallbackD(({ current, total }) => {
|
||||||
|
setState(`Rendering marker ${current}/${total}`)
|
||||||
|
})
|
||||||
|
const png = await mla.exportAsPng(pixelRatio, progress)
|
||||||
|
setState("Offering as download...")
|
||||||
|
return png
|
||||||
} finally {
|
} finally {
|
||||||
div.parentElement.removeChild(div)
|
div.parentElement.removeChild(div)
|
||||||
}
|
}
|
||||||
|
|
|
@ -974,6 +974,9 @@ class SvgToPdfPage {
|
||||||
}
|
}
|
||||||
//*/
|
//*/
|
||||||
svgImage.setAttribute("xlink:href", await SvgToPdfPage.blobToBase64(png))
|
svgImage.setAttribute("xlink:href", await SvgToPdfPage.blobToBase64(png))
|
||||||
|
svgImage.style.width = width + "mm"
|
||||||
|
svgImage.style.height = height + "mm"
|
||||||
|
console.log("Adding map element to PDF", svgImage)
|
||||||
smallestRect.parentElement.insertBefore(svgImage, smallestRect)
|
smallestRect.parentElement.insertBefore(svgImage, smallestRect)
|
||||||
await this.prepareElement(svgImage, [], false)
|
await this.prepareElement(svgImage, [], false)
|
||||||
const smallestRectCss = SvgToPdfInternals.parseCss(smallestRect.getAttribute("style"))
|
const smallestRectCss = SvgToPdfInternals.parseCss(smallestRect.getAttribute("style"))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue