diff --git a/UI/DownloadFlow/DownloadHelper.ts b/UI/DownloadFlow/DownloadHelper.ts index 1ef40f80d..8af469c15 100644 --- a/UI/DownloadFlow/DownloadHelper.ts +++ b/UI/DownloadFlow/DownloadHelper.ts @@ -23,8 +23,8 @@ export default class DownloadHelper { private static cleanFeature(f: Feature): Feature { f = { type: f.type, - geometry: { ...f.geometry }, - properties: { ...f.properties }, + geometry: {...f.geometry}, + properties: {...f.properties}, } for (const key in f.properties) { @@ -48,8 +48,7 @@ export default class DownloadHelper { public getCleanGeoJson( includeMetaData: boolean - ): FeatureCollection { - const state = this._state + ): string | FeatureCollection { const featuresPerLayer = this.getCleanGeoJsonPerLayer(includeMetaData) const features = [].concat(...Array.from(featuresPerLayer.values())) return { @@ -92,7 +91,7 @@ export default class DownloadHelper { throw "Invalid width of height, they should be > 0" } const unit = options.unit ?? "px" - const mapExtent = { left: -180, bottom: -90, right: 180, top: 90 } + const mapExtent = {left: -180, bottom: -90, right: 180, top: 90} if (options.mapExtent !== undefined) { const bbox = options.mapExtent mapExtent.left = bbox.minLon @@ -100,7 +99,7 @@ export default class DownloadHelper { mapExtent.bottom = bbox.minLat mapExtent.top = bbox.maxLat } - console.log("Generateing svg, extent:", { mapExtent, width, height }) + console.log("Generateing svg, extent:", {mapExtent, width, height}) const elements: string[] = [] for (const layer of Array.from(perLayer.keys())) { @@ -113,7 +112,7 @@ export default class DownloadHelper { const rendering = layerDef?.lineRendering[0] const converter = geojson2svg({ - viewportSize: { width, height }, + viewportSize: {width, height}, mapExtent, output: "svg", attributes: [ @@ -133,11 +132,10 @@ export default class DownloadHelper { for (const feature of features) { const stroke = rendering?.color?.GetRenderValue(feature.properties)?.txt ?? "#ff0000" - const color = Utils.colorAsHex(Utils.color(stroke)) - feature.properties.stroke = color + feature.properties.stroke = Utils.colorAsHex(Utils.color(stroke)) } - const groupPaths: string[] = converter.convert({ type: "FeatureCollection", features }) + const groupPaths: string[] = converter.convert({type: "FeatureCollection", features}) const group = ` \n` + groupPaths.map((p) => " " + p).join("\n") + @@ -151,8 +149,6 @@ export default class DownloadHelper { return header + "\n" + elements.join("\n") + "\n" } - - public getCleanGeoJsonPerLayer( includeMetaData: boolean ): Map { diff --git a/UI/DownloadFlow/DownloadPanel.svelte b/UI/DownloadFlow/DownloadPanel.svelte index 569439ffa..22f609aec 100644 --- a/UI/DownloadFlow/DownloadPanel.svelte +++ b/UI/DownloadFlow/DownloadPanel.svelte @@ -7,13 +7,8 @@ import DownloadButton from "./DownloadButton.svelte"; import {GeoOperations} from "../../Logic/GeoOperations"; import {SvgToPdf} from "../../Utils/svgToPdf"; - import Locale from "../i18n/Locale"; import ThemeViewState from "../../Models/ThemeViewState"; - import {Utils} from "../../Utils"; - import Constants from "../../Models/Constants"; - import {Translation} from "../i18n/Translation"; - import {AvailableRasterLayers} from "../../Models/RasterLayers"; - import {UIEventSource} from "../../Logic/UIEventSource"; + import DownloadPdf from "./DownloadPdf.svelte"; export let state: ThemeViewState let isLoading = state.dataIsLoading @@ -37,29 +32,6 @@ }) } - async function constructPdf(_, title: string, status: UIEventSource) { - const templateUrls = SvgToPdf.templates["current_view_a3"].pages - const templates: string[] = await Promise.all(templateUrls.map(url => Utils.download(url))) - console.log("Templates are", templates) - const bg = state.mapProperties.rasterLayer.data ?? AvailableRasterLayers.maplibre - const creator = new SvgToPdf(title, templates, { - state, - freeComponentId: "belowmap", - textSubstitutions: > { - "layout.title": state.layout.title, - title: state.layout.title, - layoutImg: state.layout.icon, - version: Constants.vNumber, - date: new Date().toISOString().substring(0,16), - background: new Translation(bg.properties.name).txt - } - }) - - const unsub = creator.status.addCallbackAndRunD(s => status?.setData(s)) - await creator.ExportPdf(Locale.language.data) - unsub() - return undefined - } @@ -111,13 +83,14 @@ construct={_ => state.mapProperties.exportAsPng(4)} /> - + +
+ {#each Object.keys(SvgToPdf.templates) as key} + {#if SvgToPdf.templates[key].isPublic} + + {/if} + {/each} +
diff --git a/UI/DownloadFlow/DownloadPdf.svelte b/UI/DownloadFlow/DownloadPdf.svelte new file mode 100644 index 000000000..596bffa4b --- /dev/null +++ b/UI/DownloadFlow/DownloadPdf.svelte @@ -0,0 +1,57 @@ + + + + diff --git a/Utils/pngMapCreator.ts b/Utils/pngMapCreator.ts index 7627b6bbc..d1f5d724a 100644 --- a/Utils/pngMapCreator.ts +++ b/Utils/pngMapCreator.ts @@ -29,51 +29,55 @@ export class PngMapCreator { div.id = "mapdiv-" + PngMapCreator.id div.style.width = this._options.width + "mm" div.style.height = this._options.height + "mm" - PngMapCreator.id++ - const layout = this._state.layout + try { + const layout = this._state.layout - function setState(msg: string) { - status?.setData(layout.id + ": " + msg) + function setState(msg: string) { + status?.setData(layout.id + ": " + msg) + } + + setState("Initializing map") + + const settings = this._state.mapProperties + const l = settings.location.data + + document.getElementById(freeComponentId).appendChild(div) + const mapElem = new MlMap({ + container: div.id, + style: AvailableRasterLayers.maplibre.properties.url, + center: [l.lon, l.lat], + zoom: settings.zoom.data, + pixelRatio: 6 + }); + + const map = new UIEventSource(mapElem) + const mla = new MapLibreAdaptor(map) + mla.zoom.setData(settings.zoom.data) + mla.location.setData(settings.location.data) + mla.rasterLayer.setData(settings.rasterLayer.data) + mla.allowZooming.setData(false) + mla.allowMoving.setData(false) + + + this._state?.showNormalDataOn(map) + console.log("Creating a map with size", this._options.width, this._options.height) + + setState("Waiting for the data") + await this._state.dataIsLoading.AsPromise((loading) => !loading) + setState("Waiting for styles to be fully loaded") + while (!map?.data?.isStyleLoaded()) { + console.log("Waiting for the style to be loaded...") + await Utils.waitFor(250) + } + // Some extra buffer... + await Utils.waitFor(1000) + setState("Exporting png") + console.log("Loading for", this._state.layout.id, "is done") + const png = await mla.exportAsPng(6) + return png + } finally { + div.parentElement.removeChild(div) } - - setState("Initializing map") - - const settings = this._state.mapProperties - const l = settings.location.data - - document.getElementById(freeComponentId).appendChild(div) - const mapElem = new MlMap({ - container: div.id, - style: AvailableRasterLayers.maplibre.properties.url, - center: [l.lon, l.lat], - zoom: settings.zoom.data, - pixelRatio: 6 - }); - - const map = new UIEventSource(mapElem) - const mla = new MapLibreAdaptor(map) - mla.zoom.setData(settings.zoom.data) - mla.location.setData(settings.location.data) - mla.rasterLayer.setData(settings.rasterLayer.data) - - this._state?.showNormalDataOn(map) - console.log("Creating a map with size", this._options.width, this._options.height) - - setState("Waiting for the data") - await this._state.dataIsLoading.AsPromise((loading) => !loading) - setState("Waiting for styles to be fully loaded") - while (!map?.data?.isStyleLoaded()) { - console.log("Waiting for the style to be loaded...") - await Utils.waitFor(250) - } - // Some extra buffer... - await Utils.waitFor(1000) - setState("Exporting png") - console.log("Loading for", this._state.layout.id, "is done") - const png = await mla.exportAsPng(6) - div.parentElement.removeChild(div) - Utils.offerContentsAsDownloadableFile(png, "test.png") - return png } } diff --git a/Utils/svgToPdf.ts b/Utils/svgToPdf.ts index 24ad1b25a..53ff4de8c 100644 --- a/Utils/svgToPdf.ts +++ b/Utils/svgToPdf.ts @@ -302,6 +302,7 @@ class SvgToPdfInternals { return } + private drawTspan(tspan: Element) { const txt = tspan.textContent if (txt == "") { @@ -310,27 +311,18 @@ class SvgToPdfInternals { const x = SvgToPdfInternals.attrNumber(tspan, "x") const y = SvgToPdfInternals.attrNumber(tspan, "y") - const css = SvgToPdfInternals.css(tspan) - const imageMatch = txt.match(/\$img\(([^)])+\)/) - if (imageMatch) { - const [key, width, height] = imageMatch[1].split(",").map(s => s.trim()) - const url = key.startsWith("http") ? key : this.extractTranslation("${" + key + "}") - const img = this._images[url] - console.log("Drawing an injected image", {key, url, img: img.src}) - this.doc.addImage( - img.src - , x, y, Number(width), Number(height) - ) - return - } - let maxWidth: number = undefined + let maxHeight: number = undefined + const css = SvgToPdfInternals.css(tspan) + if (css["shape-inside"]) { const matched = css["shape-inside"].match(/url\(#([a-zA-Z0-9-]+)\)/) if (matched !== null) { const rectId = matched[1] const rect = this._rects[rectId] maxWidth = SvgToPdfInternals.attrNumber(rect, "width", false) + maxHeight = SvgToPdfInternals.attrNumber(rect, "height", false) + } } @@ -537,7 +529,7 @@ export interface SvgToPdfOptions { /** * Override all the maps to generate with this map */ - state?: ThemeViewState + state?: ThemeViewState, } class SvgToPdfPage { @@ -553,11 +545,10 @@ class SvgToPdfPage { */ private readonly _state: UIEventSource private _isPrepared = false - private state: UIEventSource - constructor(page: string, state: UIEventSource, options?: SvgToPdfOptions) { + constructor(page: string, state: UIEventSource, options: SvgToPdfOptions) { this._state = state - this.options = options ?? {} + this.options = options const parser = new DOMParser() const xmlDoc = parser.parseFromString(page, "image/svg+xml") this._svgRoot = xmlDoc.getElementsByTagName("svg")[0] @@ -613,13 +604,6 @@ class SvgToPdfPage { if (element.tagName === "tspan" && element.childElementCount == 0) { const specialValues = element.textContent.split(" ").filter((t) => t.startsWith("$")) for (let specialValue of specialValues) { - const imageMatch = element.textContent.match(/\$img\(([^)])+\)/) - if (imageMatch) { - const key = imageMatch[1] - const url = key.startsWith("http") ? key : this.extractTranslation("${" + key + "}", `en`, false) - await this.loadImage(url) - continue - } const importMatch = element.textContent.match( /\$import ([a-zA-Z-_0-9.? ]+) as ([a-zA-Z0-9]+)/ @@ -687,10 +671,10 @@ class SvgToPdfPage { } for (const mapSpec of mapSpecs) { - try{ + try { - await this.prepareMap(mapSpec, !this.options?.disableDataLoading) - }catch(e){ + await this.prepareMap(mapSpec, !this.options?.disableDataLoading) + } catch (e) { console.error("Couldn't prepare a map:", e) } } @@ -706,7 +690,7 @@ class SvgToPdfPage { } const self = this const internal = new SvgToPdfInternals(advancedApi, this.images, this.rects, (key) => - self.extractTranslation(key, language) + self.extractTranslation(key, language) ) for (let child of Array.from(this._svgRoot.children)) { internal.handleElement(child) @@ -863,7 +847,7 @@ class SvgToPdfPage { if (this.options.state !== undefined) { png = await (new PngMapCreator(this.options.state, { width, height, - }).CreatePng(this.options.freeComponentId)) + }).CreatePng(this.options.freeComponentId, this._state)) } else { const match = spec.match(/\$map\(([^)]*)\)$/) if (match === null) { @@ -959,7 +943,7 @@ class SvgToPdfPage { height: 4 * height, }) png = await pngCreator.CreatePng(this.options.freeComponentId, this._state) - if(!png){ + if (!png) { throw "PngCreator did not output anything..." } } @@ -1005,12 +989,13 @@ export class SvgToPdf { }, current_view_a4: { pages: ["./assets/templates/CurrentMapWithHeaderA4.svg"], - description: "Export a PDF (A4, landscape) of the current view", + description: Translations.t.general.download.pdf.current_view_a4, + isPublic: true }, current_view_a3: { pages: ["./assets/templates/CurrentMapWithHeaderA3.svg"], - description: "Export a PDF (A3, portrait) of the current view", + description: Translations.t.general.download.pdf.current_view_a3, isPublic: true } } @@ -1019,16 +1004,13 @@ export class SvgToPdf { private readonly _title: string private readonly _pages: SvgToPdfPage[] - constructor(title: string, pages: string[], options?: SvgToPdfOptions) { + constructor(title: string, pages: string[], options) { this._title = title - options = options ?? {} options.textSubstitutions = options.textSubstitutions ?? {} - const mapCount = - "" + + options.textSubstitutions["mapCount"] = "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter( (th) => !th.hideFromOverview ).length - options.textSubstitutions["mapCount"] = mapCount const state = new UIEventSource("Initializing...") this.status = state @@ -1054,7 +1036,6 @@ export class SvgToPdf { const doc = new jsPDF(mode, undefined, [width, height]) doc.advancedAPI((advancedApi) => { - const canvas = advancedApi.canvas for (let i = 0; i < this._pages.length; i++) { console.log("Rendering page", i) if (i > 0) { diff --git a/langs/en.json b/langs/en.json index babaac10d..7864007bc 100644 --- a/langs/en.json +++ b/langs/en.json @@ -183,6 +183,10 @@ "includeMetaData": "Include metadata (last editor, calculated values, …)", "licenseInfo": "

Copyright notice

The provided data is available under ODbL. Reusing it is gratis for any purpose, but
  • the attribution © OpenStreetMap contributors must be shown
  • Any change must be published under the same license
Please read the full copyright notice for details.", "noDataLoaded": "No data is loaded yet. Download will be available soon", + "pdf": { + "current_view_a3": "Export a PDF (A3, portrait) of the current view", + "current_view_a4": "Export a PDF (A4, landscape) of the current view" + }, "title": "Download", "uploadGpx": "Upload your track to OpenStreetMap" }, diff --git a/public/assets/templates/CurrentMapWithHeaderA4.svg b/public/assets/templates/CurrentMapWithHeaderA4.svg index b177ec537..cb093c1f7 100644 --- a/public/assets/templates/CurrentMapWithHeaderA4.svg +++ b/public/assets/templates/CurrentMapWithHeaderA4.svg @@ -15,6 +15,12 @@ xmlns:svg="http://www.w3.org/2000/svg"> + $map(current) + id="tspan2016">$map(current) $general.pdf.attr + id="tspan2020">$general.pdf.attr $general.pdf.attrBackground + id="tspan2024">$general.pdf.attrBackground $general.pdf.generatedWith$general.pdf.generatedWith + id="tspan2030"> $general.pdf.versionInfo + id="tspan2034">$general.pdf.versionInfo + $img(layouticon)
${title} + id="tspan2042">${title}
- +
Loading MapComplete, hang on...
- +

MapComplete is an easy to use map viewer and map editor @@ -56,14 +56,14 @@ Made with OpenStreetMap

- + - +
-
Below
+
Below