From 905f796baa26b8eedc177ce7f58353331e432535 Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 4 Jun 2023 00:43:32 +0200 Subject: [PATCH] Fix: png correctly outputs all markers now --- Models/MapProperties.ts | 2 +- UI/BigComponents/AllDownloads.ts | 54 -- UI/BigComponents/DownloadPanel.ts | 300 ------ UI/DownloadFlow/DownloadButton.svelte | 85 ++ UI/DownloadFlow/DownloadHelper.ts | 181 ++++ UI/DownloadFlow/DownloadPanel.svelte | 94 ++ UI/ExportPDF.ts | 196 ---- UI/Map/MapLibreAdaptor.ts | 146 +-- UI/ThemeViewGUI.svelte | 6 +- Utils/pngMapCreator.ts | 6 +- Utils/svgToPdf.ts | 118 ++- .../mapcomplete-changes.json | 120 +-- langs/layers/es.json | 2 +- langs/layers/nl.json | 2 +- package-lock.json | 64 +- package.json | 3 +- .../templates/CurrentMapWithHeaderA4.svg | 908 ++++++++++++++++++ public/css/index-tailwind-output.css | 30 +- test.html | 2 +- test.ts | 32 +- 20 files changed, 1509 insertions(+), 842 deletions(-) delete mode 100644 UI/BigComponents/AllDownloads.ts delete mode 100644 UI/BigComponents/DownloadPanel.ts create mode 100644 UI/DownloadFlow/DownloadButton.svelte create mode 100644 UI/DownloadFlow/DownloadHelper.ts create mode 100644 UI/DownloadFlow/DownloadPanel.svelte delete mode 100644 UI/ExportPDF.ts create mode 100644 public/assets/templates/CurrentMapWithHeaderA4.svg diff --git a/Models/MapProperties.ts b/Models/MapProperties.ts index 6a2fde02c..a6b4058bc 100644 --- a/Models/MapProperties.ts +++ b/Models/MapProperties.ts @@ -18,5 +18,5 @@ export interface MapProperties { } export interface ExportableMap { - exportAsPng(): Promise + exportAsPng(dpiFactor: number): Promise } diff --git a/UI/BigComponents/AllDownloads.ts b/UI/BigComponents/AllDownloads.ts deleted file mode 100644 index 175f635f4..000000000 --- a/UI/BigComponents/AllDownloads.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Combine from "../Base/Combine" -import Translations from "../i18n/Translations" -import {UIEventSource} from "../../Logic/UIEventSource" -import Toggle from "../Input/Toggle" -import {SubtleButton} from "../Base/SubtleButton" -import Svg from "../../Svg" -import ExportPDF from "../ExportPDF" -import FilteredLayer from "../../Models/FilteredLayer" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import {BBox} from "../../Logic/BBox" -import Loc from "../../Models/Loc" - -export default class AllDownloads extends SubtleButton { - constructor( - isShown: UIEventSource, - state: { - filteredLayers: UIEventSource - layoutToUse: LayoutConfig - currentBounds: UIEventSource - locationControl: UIEventSource - featureSwitchExportAsPdf: UIEventSource - featureSwitchEnableExport: UIEventSource - } - ) { - const isExporting = new UIEventSource(false, "Pdf-is-exporting") - const generatePdf = () => { - isExporting.setData(true) - new ExportPDF({ - freeDivId: "belowmap", - location: state.locationControl, - layout: state.layoutToUse, - }).isRunning.addCallbackAndRun((isRunning) => isExporting.setData(isRunning)) - } - - const loading = Svg.loading_svg().SetClass("animate-rotate") - - const dloadTrans = Translations.t.general.download - const icon = new Toggle(loading, Svg.floppy_svg(), isExporting) - const text = new Toggle( - dloadTrans.exporting.Clone(), - new Combine([ - dloadTrans.downloadAsPdf.Clone().SetClass("font-bold"), - dloadTrans.downloadAsPdfHelper.Clone(), - ]) - .SetClass("flex flex-col") - .onClick(() => { - generatePdf() - }), - isExporting - ) - - super(icon, text) - } -} diff --git a/UI/BigComponents/DownloadPanel.ts b/UI/BigComponents/DownloadPanel.ts deleted file mode 100644 index 664b87f2a..000000000 --- a/UI/BigComponents/DownloadPanel.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { SubtleButton } from "../Base/SubtleButton" -import Svg from "../../Svg" -import Translations from "../i18n/Translations" -import { Utils } from "../../Utils" -import Combine from "../Base/Combine" -import CheckBoxes from "../Input/Checkboxes" -import { GeoOperations } from "../../Logic/GeoOperations" -import Toggle from "../Input/Toggle" -import Title from "../Base/Title" -import { Store } from "../../Logic/UIEventSource" -import SimpleMetaTagger from "../../Logic/SimpleMetaTagger" -import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" -import { BBox } from "../../Logic/BBox" -import geojson2svg from "geojson2svg" -import LayerConfig from "../../Models/ThemeConfig/LayerConfig" -import { SpecialVisualizationState } from "../SpecialVisualization" -import { Feature, FeatureCollection } from "geojson" -import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore" -import LayerState from "../../Logic/State/LayerState" -import { PriviligedLayerType } from "../../Models/Constants" - -export class DownloadPanel extends Toggle { - constructor(state: SpecialVisualizationState) { - const t = Translations.t.general.download - const name = state.layout.id - - const includeMetaToggle = new CheckBoxes([t.includeMetaData]) - const metaisIncluded = includeMetaToggle.GetValue().map((selected) => selected.length > 0) - - const buttonGeoJson = new SubtleButton( - Svg.floppy_svg(), - new Combine([ - t.downloadGeojson.SetClass("font-bold"), - t.downloadGeoJsonHelper, - ]).SetClass("flex flex-col") - ).OnClickWithLoading(t.exporting, async () => { - const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) - Utils.offerContentsAsDownloadableFile( - JSON.stringify(geojson, null, " "), - `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, - { - mimetype: "application/vnd.geo+json", - } - ) - }) - - const buttonCSV = new SubtleButton( - Svg.floppy_svg(), - new Combine([t.downloadCSV.SetClass("font-bold"), t.downloadCSVHelper]).SetClass( - "flex flex-col" - ) - ).OnClickWithLoading(t.exporting, async () => { - const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) - const csv = GeoOperations.toCSV(geojson.features) - - Utils.offerContentsAsDownloadableFile( - csv, - `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, - { - mimetype: "text/csv", - } - ) - }) - - const buttonSvg = new SubtleButton( - Svg.floppy_svg(), - new Combine([t.downloadAsSvg.SetClass("font-bold"), t.downloadAsSvgHelper]).SetClass( - "flex flex-col" - ) - ).OnClickWithLoading(t.exporting, async () => { - const geojson = DownloadPanel.getCleanGeoJsonPerLayer(state, metaisIncluded.data) - const maindiv = document.getElementById("maindiv") - const layers = state.layout.layers.filter((l) => l.source !== null) - const csv = DownloadPanel.asSvg(geojson, { - layers, - mapExtent: state.mapProperties.bounds.data, - width: maindiv.offsetWidth, - height: maindiv.offsetHeight, - }) - - Utils.offerContentsAsDownloadableFile( - csv, - `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.svg`, - { - mimetype: "image/svg+xml", - } - ) - }) - - const buttonPng = new SubtleButton( - Svg.floppy_svg(), - new Combine([t.downloadAsPng.SetClass("font-bold"), t.downloadAsPngHelper]) - ).OnClickWithLoading(t.exporting, async () => { - const gpsLayer = state.layerState.filteredLayers.get( - "gps_location" - ) - const gpsIsDisplayed = gpsLayer.isDisplayed.data - try { - gpsLayer.isDisplayed.setData(false) - const png = await state.mapProperties.exportAsPng() - Utils.offerContentsAsDownloadableFile( - png, - `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.png`, - { - mimetype: "image/png", - } - ) - } catch (e) { - console.error(e) - } finally { - gpsLayer.isDisplayed.setData(gpsIsDisplayed) - } - }) - - const downloadButtons = new Combine([ - new Title(t.title), - buttonGeoJson, - buttonCSV, - buttonSvg, - buttonPng, - includeMetaToggle, - t.licenseInfo.SetClass("link-underline"), - ]).SetClass("w-full flex flex-col") - - super( - downloadButtons, - t.noDataLoaded, - state.dataIsLoading.map((x) => !x) - ) - } - - /** - * Converts a geojson to an SVG - * - * const feature = { - * "type": "Feature", - * "properties": {}, - * "geometry": { - * "type": "LineString", - * "coordinates": [ - * [-180, 80], - * [180, -80] - * ] - * } - * } - * const perLayer = new Map([["testlayer", [feature]]]) - * DownloadPanel.asSvg(perLayer).replace(/\n/g, "") // => ` ` - */ - public static asSvg( - perLayer: Map, - options?: { - layers?: LayerConfig[] - width?: 1000 | number - height?: 1000 | number - mapExtent?: BBox - unit?: "px" | "mm" | string - } - ) { - options = options ?? {} - const width = options.width ?? 1000 - const height = options.height ?? 1000 - if (width <= 0 || height <= 0) { - throw "Invalid width of height, they should be > 0" - } - const unit = options.unit ?? "px" - const mapExtent = { left: -180, bottom: -90, right: 180, top: 90 } - if (options.mapExtent !== undefined) { - const bbox = options.mapExtent - mapExtent.left = bbox.minLon - mapExtent.right = bbox.maxLon - mapExtent.bottom = bbox.minLat - mapExtent.top = bbox.maxLat - } - console.log("Generateing svg, extent:", { mapExtent, width, height }) - const elements: string[] = [] - - for (const layer of Array.from(perLayer.keys())) { - const features = perLayer.get(layer) - if (features.length === 0) { - continue - } - - const layerDef = options?.layers?.find((l) => l.id === layer) - const rendering = layerDef?.lineRendering[0] - - const converter = geojson2svg({ - viewportSize: { width, height }, - mapExtent, - output: "svg", - attributes: [ - { - property: "style", - type: "static", - value: "fill:none;stroke-width:1", - }, - { - property: "properties.stroke", - type: "dynamic", - key: "stroke", - }, - ], - }) - - 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 - } - - const groupPaths: string[] = converter.convert({ type: "FeatureCollection", features }) - const group = - ` \n` + - groupPaths.map((p) => " " + p).join("\n") + - "\n " - elements.push(group) - } - - const w = width - const h = height - const header = `` - return header + "\n" + elements.join("\n") + "\n" - } - - private static getCleanGeoJson( - state: { - layout: LayoutConfig - mapProperties: { bounds: Store } - perLayer: ReadonlyMap - layerState: LayerState - }, - includeMetaData: boolean - ): FeatureCollection { - const featuresPerLayer = DownloadPanel.getCleanGeoJsonPerLayer(state, includeMetaData) - const features = [].concat(...Array.from(featuresPerLayer.values())) - return { - type: "FeatureCollection", - features, - } - } - - /** - * Returns a new feature of which all the metatags are deleted - */ - private static cleanFeature(f: Feature): Feature { - f = { - type: f.type, - geometry: { ...f.geometry }, - properties: { ...f.properties }, - } - - for (const key in f.properties) { - if (key === "_lon" || key === "_lat") { - continue - } - if (key.startsWith("_")) { - delete f.properties[key] - } - } - const datedKeys = [].concat( - SimpleMetaTagger.metatags - .filter((tagging) => tagging.includesDates) - .map((tagging) => tagging.keys) - ) - for (const key of datedKeys) { - delete f.properties[key] - } - return f - } - - private static getCleanGeoJsonPerLayer( - state: { - layout: LayoutConfig - mapProperties: { bounds: Store } - perLayer: ReadonlyMap - layerState: LayerState - }, - includeMetaData: boolean - ): Map { - const featuresPerLayer = new Map() - const neededLayers = state.layout.layers.filter((l) => l.source !== null).map((l) => l.id) - const bbox = state.mapProperties.bounds.data - - for (const neededLayer of neededLayers) { - const indexedFeatureSource = state.perLayer.get(neededLayer) - let features = indexedFeatureSource.GetFeaturesWithin(bbox) - // The 'indexedFeatureSources' contains _all_ features, they are not filtered yet - const filter = state.layerState.filteredLayers.get(neededLayer) - features = features.filter((f) => - filter.isShown(f.properties, state.layerState.globalFilters.data) - ) - if (!includeMetaData) { - features = features.map((f) => DownloadPanel.cleanFeature(f)) - } - featuresPerLayer.set(neededLayer, features) - } - - return featuresPerLayer - } -} diff --git a/UI/DownloadFlow/DownloadButton.svelte b/UI/DownloadFlow/DownloadButton.svelte new file mode 100644 index 000000000..929e543ae --- /dev/null +++ b/UI/DownloadFlow/DownloadButton.svelte @@ -0,0 +1,85 @@ + + +{#if isError} + +{:else if isExporting} + + + +{:else} + +{/if} diff --git a/UI/DownloadFlow/DownloadHelper.ts b/UI/DownloadFlow/DownloadHelper.ts new file mode 100644 index 000000000..1ef40f80d --- /dev/null +++ b/UI/DownloadFlow/DownloadHelper.ts @@ -0,0 +1,181 @@ +import {SpecialVisualizationState} from "../SpecialVisualization"; +import {Feature, FeatureCollection} from "geojson"; +import {BBox} from "../../Logic/BBox"; +import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; +import {Utils} from "../../Utils"; +import SimpleMetaTagger from "../../Logic/SimpleMetaTagger" +import geojson2svg from "geojson2svg" + +/** + * Exposes the download-functionality + */ +export default class DownloadHelper { + private readonly _state: SpecialVisualizationState; + + constructor(state: SpecialVisualizationState) { + this._state = state; + + } + + /** + * Returns a new feature of which all the metatags are deleted + */ + private static cleanFeature(f: Feature): Feature { + f = { + type: f.type, + geometry: { ...f.geometry }, + properties: { ...f.properties }, + } + + for (const key in f.properties) { + if (key === "_lon" || key === "_lat") { + continue + } + if (key.startsWith("_")) { + delete f.properties[key] + } + } + const datedKeys = [].concat( + SimpleMetaTagger.metatags + .filter((tagging) => tagging.includesDates) + .map((tagging) => tagging.keys) + ) + for (const key of datedKeys) { + delete f.properties[key] + } + return f + } + + public getCleanGeoJson( + includeMetaData: boolean + ): FeatureCollection { + const state = this._state + const featuresPerLayer = this.getCleanGeoJsonPerLayer(includeMetaData) + const features = [].concat(...Array.from(featuresPerLayer.values())) + return { + type: "FeatureCollection", + features, + } + } + + /** + * Converts a geojson to an SVG + * + * const feature = { + * "type": "Feature", + * "properties": {}, + * "geometry": { + * "type": "LineString", + * "coordinates": [ + * [-180, 80], + * [180, -80] + * ] + * } + * } + * const perLayer = new Map([["testlayer", [feature]]]) + * DownloadHelper.asSvg(perLayer).replace(/\n/g, "") // => ` ` + */ + public asSvg( + options?: { + layers?: LayerConfig[] + width?: 1000 | number + height?: 1000 | number + mapExtent?: BBox + unit?: "px" | "mm" | string + } + ) { + const perLayer = this._state.perLayer + options = options ?? {} + const width = options.width ?? 1000 + const height = options.height ?? 1000 + if (width <= 0 || height <= 0) { + throw "Invalid width of height, they should be > 0" + } + const unit = options.unit ?? "px" + const mapExtent = { left: -180, bottom: -90, right: 180, top: 90 } + if (options.mapExtent !== undefined) { + const bbox = options.mapExtent + mapExtent.left = bbox.minLon + mapExtent.right = bbox.maxLon + mapExtent.bottom = bbox.minLat + mapExtent.top = bbox.maxLat + } + console.log("Generateing svg, extent:", { mapExtent, width, height }) + const elements: string[] = [] + + for (const layer of Array.from(perLayer.keys())) { + const features = perLayer.get(layer).features.data + if (features.length === 0) { + continue + } + + const layerDef = options?.layers?.find((l) => l.id === layer) + const rendering = layerDef?.lineRendering[0] + + const converter = geojson2svg({ + viewportSize: { width, height }, + mapExtent, + output: "svg", + attributes: [ + { + property: "style", + type: "static", + value: "fill:none;stroke-width:1", + }, + { + property: "properties.stroke", + type: "dynamic", + key: "stroke", + }, + ], + }) + + 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 + } + + const groupPaths: string[] = converter.convert({ type: "FeatureCollection", features }) + const group = + ` \n` + + groupPaths.map((p) => " " + p).join("\n") + + "\n " + elements.push(group) + } + + const w = width + const h = height + const header = `` + return header + "\n" + elements.join("\n") + "\n" + } + + + + public getCleanGeoJsonPerLayer( + includeMetaData: boolean + ): Map { + const state = this._state + const featuresPerLayer = new Map() + const neededLayers = state.layout.layers.filter((l) => l.source !== null).map((l) => l.id) + const bbox = state.mapProperties.bounds.data + + for (const neededLayer of neededLayers) { + const indexedFeatureSource = state.perLayer.get(neededLayer) + let features = indexedFeatureSource.GetFeaturesWithin(bbox) + // The 'indexedFeatureSources' contains _all_ features, they are not filtered yet + const filter = state.layerState.filteredLayers.get(neededLayer) + features = features.filter((f) => + filter.isShown(f.properties, state.layerState.globalFilters.data) + ) + if (!includeMetaData) { + features = features.map((f) => DownloadHelper.cleanFeature(f)) + } + featuresPerLayer.set(neededLayer, features) + } + + return featuresPerLayer + } + +} diff --git a/UI/DownloadFlow/DownloadPanel.svelte b/UI/DownloadFlow/DownloadPanel.svelte new file mode 100644 index 000000000..c7a4876d8 --- /dev/null +++ b/UI/DownloadFlow/DownloadPanel.svelte @@ -0,0 +1,94 @@ + + + +{#if $isLoading} + +{:else} + +
+

+ +

+ + JSON.stringify(geojson)} + mainText={t.downloadGeojson} + helperText={t.downloadGeoJsonHelper} + {metaIsIncluded}/> + + GeoOperations.toCSV(geojson)} + mainText={t.downloadCSV} + helperText={t.downloadCSVHelper} + {metaIsIncluded}/> + + + + + + + state.mapProperties.exportAsPng(4)} + /> + + state.mapProperties.exportAsPng(4)} + /> + + + +{/if} + diff --git a/UI/ExportPDF.ts b/UI/ExportPDF.ts deleted file mode 100644 index c8fe01c33..000000000 --- a/UI/ExportPDF.ts +++ /dev/null @@ -1,196 +0,0 @@ -import jsPDF from "jspdf" -import { UIEventSource } from "../Logic/UIEventSource" -import Minimap, { MinimapObj } from "./Base/Minimap" -import Loc from "../Models/Loc" -import BaseLayer from "../Models/BaseLayer" -import { FixedUiElement } from "./Base/FixedUiElement" -import Translations from "./i18n/Translations" -import State from "../State" -import Constants from "../Models/Constants" -import LayoutConfig from "../Models/ThemeConfig/LayoutConfig" -import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline" -import ShowDataLayer from "./ShowDataLayer/ShowDataLayer" -import { BBox } from "../Logic/BBox" - -/** - * Creates screenshoter to take png screenshot - * Creates jspdf and downloads it - * - landscape pdf - * - * To add new layout: - * - add new possible layout name in constructor - * - add new layout in "PDFLayout" - * -> in there are more instructions - */ -export default class ExportPDF { - // dimensions of the map in milimeter - public isRunning = new UIEventSource(true) - // A4: 297 * 210mm - private readonly mapW = 297 - private readonly mapH = 210 - private readonly scaling = 2 - private readonly freeDivId: string - private readonly _layout: LayoutConfig - private _screenhotTaken = false - - constructor(options: { - freeDivId: string - location: UIEventSource - background?: UIEventSource - features: FeaturePipeline - layout: LayoutConfig - }) { - this.freeDivId = options.freeDivId - this._layout = options.layout - const self = this - - // We create a minimap at the given location and attach it to the given 'hidden' element - - const l = options.location.data - const loc = { - lat: l.lat, - lon: l.lon, - zoom: l.zoom + 1, - } - - const minimap = Minimap.createMiniMap({ - location: new UIEventSource(loc), // We remove the link between the old and the new UI-event source as moving the map while the export is running fucks up the screenshot - background: options.background, - allowMoving: false, - onFullyLoaded: (_) => - window.setTimeout(() => { - if (self._screenhotTaken) { - return - } - try { - self.CreatePdf(minimap) - .then(() => self.cleanup()) - .catch(() => self.cleanup()) - } catch (e) { - console.error(e) - self.cleanup() - } - }, 500), - }) - - minimap.SetStyle( - `width: ${this.mapW * this.scaling}mm; height: ${this.mapH * this.scaling}mm;` - ) - minimap.AttachTo(options.freeDivId) - - // Next: we prepare the features. Only fully contained features are shown - minimap.leafletMap.addCallbackAndRunD((leaflet) => { - const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.2)) - options.features.GetTilesPerLayerWithin(bounds, (tile) => { - if (tile.layer.layerDef.minzoom > l.zoom) { - return - } - if (tile.layer.layerDef.id.startsWith("note_import")) { - // Don't export notes to import - return - } - new ShowDataLayer({ - features: tile, - leafletMap: minimap.leafletMap, - layerToShow: tile.layer.layerDef, - doShowLayer: tile.layer.isDisplayed, - state: undefined, - }) - }) - }) - - State.state.AddAllOverlaysToMap(minimap.leafletMap) - } - - private cleanup() { - new FixedUiElement("Screenshot taken!").AttachTo(this.freeDivId) - this._screenhotTaken = true - } - - private async CreatePdf(minimap: MinimapObj) { - console.log("PDF creation started") - const t = Translations.t.general.pdf - const layout = this._layout - - let doc = new jsPDF("landscape") - - const image = await minimap.TakeScreenshot() - // @ts-ignore - doc.addImage(image, "PNG", 0, 0, this.mapW, this.mapH) - - doc.setDrawColor(255, 255, 255) - doc.setFillColor(255, 255, 255) - doc.roundedRect(12, 10, 145, 25, 5, 5, "FD") - - doc.setFontSize(20) - doc.textWithLink(layout.title.txt, 40, 18.5, { - maxWidth: 125, - url: window.location.href, - }) - doc.setFontSize(10) - doc.text(t.generatedWith.txt, 40, 23, { - maxWidth: 125, - }) - const backgroundLayer: BaseLayer = State.state.backgroundLayer.data - const attribution = new FixedUiElement( - backgroundLayer.layer().getAttribution() ?? backgroundLayer.name - ).ConstructElement().textContent - doc.textWithLink(t.attr.txt, 40, 26.5, { - maxWidth: 125, - url: "https://www.openstreetmap.org/copyright", - }) - - doc.text( - t.attrBackground.Subs({ - background: attribution, - }).txt, - 40, - 30 - ) - - let date = new Date().toISOString().substr(0, 16) - - doc.setFontSize(7) - doc.text( - t.versionInfo.Subs({ - version: Constants.vNumber, - date: date, - }).txt, - 40, - 34, - { - maxWidth: 125, - } - ) - - // Add the logo of the layout - let img = document.createElement("img") - const imgSource = layout.icon - const imgType = imgSource.substring(imgSource.lastIndexOf(".") + 1) - img.src = imgSource - if (imgType.toLowerCase() === "svg") { - new FixedUiElement("").AttachTo(this.freeDivId) - - // This is an svg image, we use the canvas to convert it to a png - const canvas = document.createElement("canvas") - const ctx = canvas.getContext("2d") - canvas.width = 500 - canvas.height = 500 - img.style.width = "100%" - img.style.height = "100%" - ctx.drawImage(img, 0, 0, 500, 500) - const base64img = canvas.toDataURL("image/png") - doc.addImage(base64img, "png", 15, 12, 20, 20) - } else { - try { - doc.addImage(img, imgType, 15, 12, 20, 20) - } catch (e) { - console.error(e) - } - } - - doc.save(`MapComplete_${layout.title.txt}_${date}.pdf`) - - this.isRunning.setData(false) - } -} diff --git a/UI/Map/MapLibreAdaptor.ts b/UI/Map/MapLibreAdaptor.ts index 094931a64..e241f0b18 100644 --- a/UI/Map/MapLibreAdaptor.ts +++ b/UI/Map/MapLibreAdaptor.ts @@ -7,8 +7,8 @@ import {BBox} from "../../Logic/BBox" import {ExportableMap, MapProperties} from "../../Models/MapProperties" import SvelteUIElement from "../Base/SvelteUIElement" import MaplibreMap from "./MaplibreMap.svelte" -import html2canvas from "html2canvas" import {RasterLayerProperties} from "../../Models/RasterLayerProperties" +import * as htmltoimage from 'html-to-image'; /** * The 'MapLibreAdaptor' bridges 'MapLibre' with the various properties of the `MapProperties` @@ -50,7 +50,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { this._maplibreMap = maplibreMap this.location = state?.location ?? new UIEventSource({lon: 0, lat: 0}) - if(this.location.data){ + if (this.location.data) { // The MapLibre adaptor updates the element in the location and then pings them // Often, code setting this up doesn't expect the object they pass in to be changed, so we create a copy this.location.setData({...this.location.data}) @@ -199,85 +199,95 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap { return url } - async exportAsPng(): Promise { + private 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) + console.log("Resizing canvas with setDPI:", drawOn.width, drawOn.height, drawOn.style.width, drawOn.style.height) + } + + public async exportAsPng(dpiFactor: number): Promise { const map = this._maplibreMap.data if (!map) { return undefined } + const drawOn = document.createElement("canvas") + drawOn.width = map.getCanvas().width + drawOn.height = map.getCanvas().height - function setDPI(canvas, dpi) { - // Set up CSS size. - canvas.style.width = canvas.style.width || canvas.width + "px" - canvas.style.height = canvas.style.height || canvas.height + "px" + console.log("Canvas size:", drawOn.width, drawOn.height) + const ctx = drawOn.getContext("2d") + // Set up CSS size. + MapLibreAdaptor.setDpi(drawOn, ctx, dpiFactor) - // Resize canvas and scale future draws. - const scaleFactor = dpi / 96 - canvas.width = Math.ceil(canvas.width * scaleFactor) - canvas.height = Math.ceil(canvas.height * scaleFactor) - const ctx = canvas.getContext("2d") - ctx?.scale(scaleFactor, scaleFactor) - } + await this.exportBackgroundOnCanvas(drawOn, ctx, dpiFactor) + + drawOn.toBlob(blob => { + Utils.offerContentsAsDownloadableFile(blob, "bg.png") + }) + console.log("Getting markers") + // MapLibreAdaptor.setDpi(drawOn, ctx, 1) + const markers = await this.drawMarkers(dpiFactor) + console.log("Drawing markers (" + markers.width + "*" + markers.height + ") onto drawOn (" + drawOn.width + "*" + drawOn.height + ")") + ctx.scale(1/dpiFactor,1/dpiFactor ) + ctx.drawImage(markers, 0, 0, drawOn.width, drawOn.height) + ctx.scale(dpiFactor, dpiFactor) + markers.toBlob(blob => { + Utils.offerContentsAsDownloadableFile(blob, "markers.json") + }) + this._maplibreMap.data?.resize() + return await new Promise(resolve => drawOn.toBlob(blob => resolve(blob))) + } + + /** + * Exports the background map and lines to PNG. + * Markers are _not_ rendered + */ + private async exportBackgroundOnCanvas(drawOn: HTMLCanvasElement, ctx: CanvasRenderingContext2D, dpiFactor: number = 1): Promise { + const map = this._maplibreMap.data + // We draw the maplibre-map onto the canvas. This does not export markers + // Inspiration by https://github.com/mapbox/mapbox-gl-js/issues/2766 // Total hack - see https://stackoverflow.com/questions/42483449/mapbox-gl-js-export-map-to-png-or-pdf - - const drawOn = document.createElement("canvas") - drawOn.width = document.documentElement.clientWidth - drawOn.height = document.documentElement.clientHeight - - setDPI(drawOn, 4 * 96) - - const destinationCtx = drawOn.getContext("2d") - { - // First, we draw the maplibre-map onto the canvas. This does not export markers - // Inspiration by https://github.com/mapbox/mapbox-gl-js/issues/2766 - - const promise = new Promise((resolve) => { - map.once("render", () => { - destinationCtx.drawImage(map.getCanvas(), 0, 0) - resolve() - }) + const promise = new Promise((resolve) => { + map.once("render", () => { + ctx.drawImage(map.getCanvas(), 0, 0) + resolve() }) + }) - while (!map.isStyleLoaded()) { - console.log("Waiting to fully load the style...") - await Utils.waitFor(100) - } - map.triggerRepaint() - await promise - // Reset the canvas width and height - map.resize() + while (!map.isStyleLoaded()) { + console.log("Waiting to fully load the style...") + await Utils.waitFor(100) } - { - // now, we draw the markers on top of the map + map.triggerRepaint() + await promise + map.resize() + } - /* We use html2canvas for this, but disable the map canvas object itself: - * it cannot deal with this canvas object. - * - * We also have to patch up a few more objects - * */ - const container = map.getCanvasContainer() - const origHeight = container.style.height - const origStyle = map.getCanvas().style.display - try { - map.getCanvas().style.display = "none" - if (!container.style.height) { - container.style.height = document.documentElement.clientHeight + "px" - } - - await html2canvas(map.getCanvasContainer(), { - backgroundColor: "#00000000", - canvas: drawOn, - }) - } catch (e) { - console.error(e) - } finally { - map.getCanvas().style.display = origStyle - container.style.height = origHeight - } + private async drawMarkers(dpiFactor: number): Promise { + const map = this._maplibreMap.data + if (!map) { + return undefined } - - // At last, we return the actual blob - return new Promise((resolve) => drawOn.toBlob((data) => resolve(data))) + const width = map.getCanvas().clientWidth + const height = map.getCanvas().clientHeight + console.log("Canvas size markers:", map.getCanvas().width, map.getCanvas().height, "canvasClientRect:", width, height) + map.getCanvas().style.display = "none" + const img = await htmltoimage.toCanvas(map.getCanvasContainer(), { + pixelRatio: dpiFactor, + canvasWidth: width, + canvasHeight: height, + width: width, + height: height, + }) + map.getCanvas().style.display = "unset" + return img } private updateStores(isSetup: boolean = false): void { diff --git a/UI/ThemeViewGUI.svelte b/UI/ThemeViewGUI.svelte index cda7dafc6..7c1408282 100644 --- a/UI/ThemeViewGUI.svelte +++ b/UI/ThemeViewGUI.svelte @@ -28,7 +28,7 @@ import LoginToggle from "./Base/LoginToggle.svelte"; import LoginButton from "./Base/LoginButton.svelte"; import CopyrightPanel from "./BigComponents/CopyrightPanel"; - import {DownloadPanel} from "./BigComponents/DownloadPanel"; + import DownloadPanel from "./DownloadFlow/DownloadPanel.svelte"; import ModalRight from "./Base/ModalRight.svelte"; import {Utils} from "../Utils"; import Hotkeys from "./Base/Hotkeys"; @@ -42,10 +42,10 @@ import {ShareScreen} from "./BigComponents/ShareScreen"; import ThemeIntroPanel from "./BigComponents/ThemeIntroPanel.svelte"; import type {RasterLayerPolygon} from "../Models/RasterLayers"; + import {AvailableRasterLayers} from "../Models/RasterLayers"; import RasterLayerOverview from "./Map/RasterLayerOverview.svelte"; import IfHidden from "./Base/IfHidden.svelte"; import {onDestroy} from "svelte"; - import {AvailableRasterLayers} from "../Models/RasterLayers"; export let state: ThemeViewState; let layout = state.layout; @@ -244,7 +244,7 @@
- new DownloadPanel(state)}/> +
diff --git a/Utils/pngMapCreator.ts b/Utils/pngMapCreator.ts index 9abcb2a89..eb94f5348 100644 --- a/Utils/pngMapCreator.ts +++ b/Utils/pngMapCreator.ts @@ -35,9 +35,10 @@ export class PngMapCreator { const map = this._state.map new SvelteUIElement(MaplibreMap, { map }) .SetStyle( - "width: " + this._options.width + "mm; height: " + this._options.height + "mm" + "width: " + this._options.width + "mm; height: " + this._options.height + "mm; border: 2px solid red;" ) .AttachTo("extradiv") + map.data.resize() setState("Waiting for the data") await this._state.dataIsLoading.AsPromise((loading) => !loading) setState("Waiting for styles to be fully loaded") @@ -48,6 +49,7 @@ export class PngMapCreator { await Utils.waitFor(1000) setState("Exporting png") console.log("Loading for", this._state.layout.id, "is done") - return this._state.mapProperties.exportAsPng() + console.log("Map export: starting actual export, target size is", this._options.width,"mm * ",this._options.height+"mm") + return this._state.mapProperties.exportAsPng(4) } } diff --git a/Utils/svgToPdf.ts b/Utils/svgToPdf.ts index c73b96d26..44bbb9b04 100644 --- a/Utils/svgToPdf.ts +++ b/Utils/svgToPdf.ts @@ -1,18 +1,18 @@ -import jsPDF, { Matrix } from "jspdf" -import { Translation, TypedTranslation } from "../UI/i18n/Translation" -import { PngMapCreator } from "./pngMapCreator" -import { AllKnownLayouts } from "../Customizations/AllKnownLayouts" +import jsPDF, {Matrix} from "jspdf" +import {Translation, TypedTranslation} from "../UI/i18n/Translation" +import {PngMapCreator} from "./pngMapCreator" +import {AllKnownLayouts} from "../Customizations/AllKnownLayouts" import "../assets/fonts/Ubuntu-M-normal.js" import "../assets/fonts/Ubuntu-L-normal.js" import "../assets/fonts/UbuntuMono-B-bold.js" -import { makeAbsolute, parseSVG } from "svg-path-parser" +import {makeAbsolute, parseSVG} from "svg-path-parser" import Translations from "../UI/i18n/Translations" -import { Utils } from "../Utils" +import {Utils} from "../Utils" import Constants from "../Models/Constants" import Hash from "../Logic/Web/Hash" import ThemeViewState from "../Models/ThemeViewState" -import { Store, UIEventSource } from "../Logic/UIEventSource" -import { FixedUiElement } from "../UI/Base/FixedUiElement" +import {Store, UIEventSource} from "../Logic/UIEventSource" +import {FixedUiElement} from "../UI/Base/FixedUiElement" class SvgToPdfInternals { private static readonly dummyDoc: jsPDF = new jsPDF() @@ -131,7 +131,7 @@ class SvgToPdfInternals { const x = SvgToPdfInternals.attrNumber(mapSpec, "x") const y = SvgToPdfInternals.attrNumber(mapSpec, "y") - return runningM.applyToPoint({ x, y }) + return runningM.applyToPoint({x, y}) } private static attr( @@ -409,7 +409,7 @@ class SvgToPdfInternals { const base64img = canvas.toDataURL("image/png") this.addMatrix(this.doc.Matrix(width / svgWidth, 0, 0, height / svgHeight, 0, 0)) - const p = this.currentMatrixInverted.applyToPoint({ x, y }) + const p = this.currentMatrixInverted.applyToPoint({x, y}) this.doc.addImage( base64img, "png", @@ -443,24 +443,24 @@ class SvgToPdfInternals { for (const c of parsed) { if (c.code === "C" || c.code === "c") { - const command = { op: "c", c: [c.x1, c.y1, c.x2, c.y2, c.x, c.y] } + const command = {op: "c", c: [c.x1, c.y1, c.x2, c.y2, c.x, c.y]} this.doc.path([command]) continue } if (c.code === "H") { - const command = { op: "l", c: [c.x, c.y] } + const command = {op: "l", c: [c.x, c.y]} this.doc.path([command]) continue } if (c.code === "V") { - const command = { op: "l", c: [c.x, c.y] } + const command = {op: "l", c: [c.x, c.y]} this.doc.path([command]) continue } - this.doc.path([{ op: c.code.toLowerCase(), c: [c.x, c.y] }]) + this.doc.path([{op: c.code.toLowerCase(), c: [c.x, c.y]}]) } //"fill:#ffffff;stroke:#000000;stroke-width:0.8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:20" @@ -499,7 +499,8 @@ export interface SvgToPdfOptions { disableMaps?: false | true textSubstitutions?: Record beforePage?: (i: number) => void - overrideLocation?: { lat: number; lon: number } + overrideLocation?: { lat: number; lon: number }, + disableDataLoading?: boolean | false } class SvgToPdfPage { @@ -556,6 +557,11 @@ class SvgToPdfPage { return translations } + /** + * Does some preparatory work, most importantly gathering the map specifications into parameter `mapTextSpecs` and substituting translations + * @param element + * @param mapTextSpecs + */ public async prepareElement( element: SVGSVGElement | Element, mapTextSpecs: SVGTSpanElement[] @@ -612,14 +618,14 @@ class SvgToPdfPage { // Always fetch the remote data - it's cached anyway this.layerTranslations[language] = await Utils.downloadJsonCached( "https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/langs/layers/" + - language + - ".json", + language + + ".json", 24 * 60 * 60 * 1000 ) const shared_questions = await Utils.downloadJsonCached( "https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/langs/shared-questions/" + - language + - ".json", + language + + ".json", 24 * 60 * 60 * 1000 ) this.layerTranslations[language]["shared-questions"] = shared_questions["shared_questions"] @@ -636,7 +642,7 @@ class SvgToPdfPage { } for (const mapSpec of mapSpecs) { - await this.prepareMap(mapSpec) + await this.prepareMap(mapSpec,! this.options?.disableDataLoading) } } @@ -692,7 +698,7 @@ class SvgToPdfPage { } if (typeof t === "string") { - t = new TypedTranslation({ "*": t }) + t = new TypedTranslation({"*": t}) } if (t instanceof TypedTranslation) { if (strict && (t.translations[language] ?? t.translations["*"]) === undefined) { @@ -744,9 +750,15 @@ class SvgToPdfPage { }) } - private async prepareMap(mapSpec: SVGTSpanElement): Promise { + /** + * Replaces a mapSpec with the appropriate map + * @param mapSpec + * @private + */ + + private async prepareMap(mapSpec: SVGTSpanElement, loadData: boolean = true): Promise { // Upper left point of the tspan - const { x, y } = SvgToPdfInternals.GetActualXY(mapSpec) + const {x, y} = SvgToPdfInternals.GetActualXY(mapSpec) let textElement: Element = mapSpec // We recurse up to get the actual, full specification @@ -791,6 +803,7 @@ class SvgToPdfPage { const svgImage = document.createElement("image") svgImage.setAttribute("x", smallestRect.getAttribute("x")) svgImage.setAttribute("y", smallestRect.getAttribute("y")) + // width and height are in mm const width = SvgToPdfInternals.attrNumber(smallestRect, "width") const height = SvgToPdfInternals.attrNumber(smallestRect, "height") svgImage.setAttribute("width", "" + width) @@ -837,7 +850,7 @@ class SvgToPdfPage { for (const filteredLayer of fl) { if (params["layer-" + filteredLayer.layerDef.id] !== undefined) { filteredLayer.isDisplayed.setData( - params["layer-" + filteredLayer.layerDef.id].trim().toLowerCase() !== "false" + loadData && params["layer-" + filteredLayer.layerDef.id].trim().toLowerCase() !== "false" ) } else if (params["layers"] === "none") { filteredLayer.isDisplayed.setData(false) @@ -850,20 +863,24 @@ class SvgToPdfPage { if (paramsKey.startsWith("layer-")) { const layerName = paramsKey.substring("layer-".length) const key = params[paramsKey].toLowerCase().trim() - const isDisplayed = key === "true" || key === "force" + const isDisplayed = loadData && (key === "true" || key === "force") const layer = fl.find((l) => l.layerDef.id === layerName) - console.log( - "Setting ", - layer?.layerDef?.id, - " to visibility", - isDisplayed, - "(minzoom:", - layer?.layerDef?.minzoomVisible, - layer?.layerDef?.minzoom, - ")" - ) - layer.isDisplayed.setData(isDisplayed) - if (key === "force") { + if (!loadData) { + console.log("Not loading map data as 'loadData' is falsed, this is probably a test run") + } else { + console.log( + "Setting ", + layer?.layerDef?.id, + " to visibility", + isDisplayed, + "(minzoom:", + layer?.layerDef?.minzoomVisible, + layer?.layerDef?.minzoom, + ")" + ) + } + layer.isDisplayed.setData(loadData && isDisplayed) + if (key === "force" && loadData) { layer.layerDef.minzoom = 0 layer.layerDef.minzoomVisible = 0 layer.isDisplayed.addCallback((isDisplayed) => { @@ -877,8 +894,8 @@ class SvgToPdfPage { } console.log("Creating a map width ", width, height, params.scalingFactor) const pngCreator = new PngMapCreator(state, { - width: width * 4, - height: height * 4, + width: 4 * width, + height: 4 * height, }) const png = await pngCreator.CreatePng(this._state) svgImage.setAttribute("xlink:href", await SvgToPdfPage.blobToBase64(png)) @@ -899,8 +916,8 @@ class SvgToPdfPage { export class SvgToPdf { public static readonly templates: Record< - "flyer_a4" | "poster_a3" | "poster_a2", - { pages: string[]; description: string | Translation } + "flyer_a4" | "poster_a3" | "poster_a2" | "current_view_a4", + { pages: string[]; description: string | Translation; isPublic: boolean } > = { flyer_a4: { pages: [ @@ -908,15 +925,23 @@ export class SvgToPdf { "./assets/templates/MapComplete-flyer.back.svg", ], description: Translations.t.flyer.description, + isPublic: false }, poster_a3: { pages: ["./assets/templates/MapComplete-poster-a3.svg"], description: "A basic A3 poster (similar to the flyer)", + isPublic: false }, poster_a2: { pages: ["./assets/templates/MapComplete-poster-a2.svg"], description: "A basic A2 poster (similar to the flyer); scaled up from the A3 poster", + isPublic: false }, + current_view_a4: { + pages:["./assets/templates/CurrentMapWithHeaderA4.svg"], + description: "Export a PDF (A4, portrait) of the current view", + isPublic: true + } } public readonly status: Store public readonly _status: UIEventSource @@ -947,12 +972,9 @@ export class SvgToPdf { const height = SvgToPdfInternals.attrNumber(firstPage, "height") const mode = width > height ? "landscape" : "portrait" - await this.Prepare() + await this.Prepare(language) console.log("Global prepare done") - for (const page of this._pages) { - await page.Prepare() - await page.PrepareLanguage(language) - } + this._status.setData("Maps are rendered, building pdf") new FixedUiElement("").AttachTo("extradiv") @@ -1002,11 +1024,11 @@ export class SvgToPdf { /** * Prepares all the minimaps - * @constructor */ - public async Prepare(): Promise { + public async Prepare(language1: string): Promise { for (const page of this._pages) { await page.Prepare() + await page.PrepareLanguage(language1) } return this } diff --git a/assets/themes/mapcomplete-changes/mapcomplete-changes.json b/assets/themes/mapcomplete-changes/mapcomplete-changes.json index 9ef3714f8..2bd225a5b 100644 --- a/assets/themes/mapcomplete-changes/mapcomplete-changes.json +++ b/assets/themes/mapcomplete-changes/mapcomplete-changes.json @@ -1,22 +1,13 @@ { "id": "mapcomplete-changes", "title": { - "en": "Changes made with MapComplete", - "ca": "Canvis fets amb MapComplete", - "cs": "Změny provedené pomocí MapComplete", - "de": "Änderungen mit MapComplete" + "en": "Changes made with MapComplete" }, "shortDescription": { - "en": "Shows changes made by MapComplete", - "ca": "Mostra els canvis fets per MapComplete", - "cs": "Zobrazuje změny provedené nástrojem MapComplete", - "de": "Zeigt Änderungen, die mit MapComplete vorgenommen wurden" + "en": "Shows changes made by MapComplete" }, "description": { - "en": "This maps shows all the changes made with MapComplete", - "ca": "Aquest mapa mostra tots els canvis fets amb MapComplete", - "cs": "Tyto mapy zobrazují všechny změny provedené pomocí MapComplete", - "de": "Diese Karte zeigt alle mit MapComplete vorgenommenen Änderungen" + "en": "This maps shows all the changes made with MapComplete" }, "icon": "./assets/svg/logo.svg", "hideFromOverview": true, @@ -29,10 +20,7 @@ { "id": "mapcomplete-changes", "name": { - "en": "Changeset centers", - "ca": "Centre del conjunt de canvis", - "cs": "Centra změn", - "de": "Zentrum der Änderungssätze" + "en": "Changeset centers" }, "minzoom": 0, "source": { @@ -43,62 +31,41 @@ }, "title": { "render": { - "en": "Changeset for {theme}", - "ca": "Conjunt de canvis per a {theme}", - "cs": "Sada změn pro {theme}", - "de": "Änderungssatz für {theme}" + "en": "Changeset for {theme}" } }, "description": { - "en": "Shows all MapComplete changes", - "ca": "Mostra tots els canvis de MapComplete", - "cs": "Zobrazí všechny změny MapComplete", - "de": "Zeigt alle MapComplete-Änderungen" + "en": "Shows all MapComplete changes" }, "tagRenderings": [ { "id": "show_changeset_id", "render": { - "en": "Changeset {id}", - "ca": "Conjunt de canvi {id}", - "cs": "Sada změn je {id}", - "de": "Änderungssatz {id}" + "en": "Changeset {id}" } }, { "id": "contributor", "question": { - "en": "What contributor did make this change?", - "ca": "Quin col·laborador va fer aquest canvi?", - "cs": "Který přispěvatel tuto změnu provedl?", - "de": "Wer hat diese Änderung vorgenommen?" + "en": "What contributor did make this change?" }, "freeform": { "key": "user" }, "render": { - "en": "Change made by {user}", - "ca": "Canvi fet per {user}", - "cs": "Změna provedená {user}", - "de": "Änderung vorgenommen von {user}" + "en": "Change made by {user}" } }, { "id": "theme-id", "question": { - "en": "What theme was used to make this change?", - "ca": "Quin tema es va utilitzar per fer aquest canvi?", - "cs": "Jaké téma bylo použito k provedení této změny?", - "de": "Welches Thema wurde für diese Änderung verwendet?" + "en": "What theme was used to make this change?" }, "freeform": { "key": "theme" }, "render": { - "en": "Change with theme {theme}", - "ca": "Canvi amb el tema {theme}", - "cs": "Změna pomocí tématu {theme}", - "de": "Geändert mit Thema {theme}" + "en": "Change with theme {theme}" } }, { @@ -107,31 +74,19 @@ "key": "locale" }, "question": { - "en": "What locale (language) was this change made in?", - "ca": "Amb quina configuració regional (idioma) s'ha fet aquest canvi?", - "cs": "V jakém prostředí (jazyce) byla tato změna provedena?", - "de": "In welchem Gebietsschema (Sprache) wurde diese Änderung vorgenommen?" + "en": "What locale (language) was this change made in?" }, "render": { - "en": "User locale is {locale}", - "ca": "La configuració regional de l'usuari és {locale}", - "cs": "Uživatelské prostředí je {locale}", - "de": "Benutzergebietsschema ist {locale}" + "en": "User locale is {locale}" } }, { "id": "host", "render": { - "en": "Change with with {host}", - "ca": "Canvi amb {host}", - "cs": "Změna u {host}", - "de": "Geändert über {host}" + "en": "Change with with {host}" }, "question": { - "en": "What host (website) was this change made with?", - "ca": "Amb quin amfitrió (lloc web) es va fer aquest canvi?", - "cs": "U jakého hostitele (webové stránky) byla tato změna provedena?", - "de": "Über welchen Host (Webseite) wurde diese Änderung vorgenommen?" + "en": "What host (website) was this change made with?" }, "freeform": { "key": "host" @@ -476,10 +431,7 @@ } ], "question": { - "en": "Themename contains {search}", - "ca": "El nom del tema conté {search}", - "cs": "Themename obsahuje {search}", - "de": "Themename enthält {search}" + "en": "Themename contains {search}" } } ] @@ -495,10 +447,7 @@ } ], "question": { - "en": "Made by contributor {search}", - "ca": "Fet pel col·laborador {search}", - "cs": "Vytvořil přispěvatel {search}", - "de": "Erstellt von {search}" + "en": "Made by contributor {search}" } } ] @@ -514,10 +463,7 @@ } ], "question": { - "en": "Not made by contributor {search}", - "ca": "No fet pel col·laborador {search}", - "cs": "Ne vytvořeno přispěvatelem {search}", - "de": "Nicht erstellt von {search}" + "en": "Not made by contributor {search}" } } ] @@ -534,10 +480,7 @@ } ], "question": { - "en": "Made before {search}", - "ca": "Fet abans de {search}", - "cs": "Vytvořeno před {search}", - "de": "Erstellt vor {search}" + "en": "Made before {search}" } } ] @@ -554,10 +497,7 @@ } ], "question": { - "en": "Made after {search}", - "ca": "Fet després de {search}", - "cs": "Vytvořeno po {search}", - "de": "Erstellt nach {search}" + "en": "Made after {search}" } } ] @@ -573,10 +513,7 @@ } ], "question": { - "en": "User language (iso-code) {search}", - "ca": "Idioma de l'usuari (codi iso) {search}", - "cs": "Jazyk uživatele (iso-kód) {search}", - "de": "Benutzersprache (ISO-Code) {search}" + "en": "User language (iso-code) {search}" } } ] @@ -592,10 +529,7 @@ } ], "question": { - "en": "Made with host {search}", - "ca": "Fet amb l'amfitrió {search}", - "cs": "Vyrobeno u hostitele {search}", - "de": "Erstellt mit host {search}" + "en": "Made with host {search}" } } ] @@ -606,10 +540,7 @@ { "osmTags": "add-image>0", "question": { - "en": "Changeset added at least one image", - "ca": "El conjunt de canvis ha afegit almenys una imatge", - "cs": "Sada změn přidala alespoň jeden obrázek", - "de": "Im Änderungssatz wurde mindestens ein Bild hinzugefügt" + "en": "Changeset added at least one image" } } ] @@ -624,10 +555,7 @@ { "id": "link_to_more", "render": { - "en": "More statistics can be found here", - "ca": "Es poden trobar més estadístiques aquí", - "cs": "Další statistiky lze nalézt zde", - "de": "Weitere Statistiken hier" + "en": "More statistics can be found here" } }, { diff --git a/langs/layers/es.json b/langs/layers/es.json index cdaef0c91..7f6498a20 100644 --- a/langs/layers/es.json +++ b/langs/layers/es.json @@ -4490,4 +4490,4 @@ } } } -} +} \ No newline at end of file diff --git a/langs/layers/nl.json b/langs/layers/nl.json index a81a9ee42..aaf6927f8 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -8838,4 +8838,4 @@ } } } -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5e7029b8c..e2834882a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,18 +19,19 @@ "@turf/distance": "^6.5.0", "@turf/length": "^6.5.0", "@turf/turf": "^6.5.0", - "@types/html2canvas": "^1.0.0", + "@types/dom-to-image": "^2.6.4", "@types/showdown": "^2.0.0", "chart.js": "^3.8.0", "country-language": "^0.1.7", "csv-parse": "^5.1.0", "doctest-ts-improved": "^0.8.8", + "dom-to-image": "^2.6.0", "email-validator": "^2.0.4", "escape-html": "^1.0.3", "fake-dom": "^1.0.4", "geojson2svg": "^1.3.3", + "html-to-image": "^1.11.11", "html-to-markdown": "^1.0.0", - "html2canvas": "^1.4.1", "i18next-client": "^1.11.4", "idb-keyval": "^6.0.3", "jest-mock": "^29.4.1", @@ -3645,6 +3646,11 @@ "@types/chai": "*" } }, + "node_modules/@types/dom-to-image": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.4.tgz", + "integrity": "sha512-UddUdGF1qulrSDulkz3K2Ypq527MR6ixlgAzqLbxSiQ0icx0XDlIV+h4+edmjq/1dqn0KgN0xGSe1kI9t+vGuw==" + }, "node_modules/@types/estree": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", @@ -3655,15 +3661,6 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" }, - "node_modules/@types/html2canvas": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz", - "integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==", - "deprecated": "This is a stub types definition. html2canvas provides its own type definitions, so you do not need this installed.", - "dependencies": { - "html2canvas": "*" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -4243,6 +4240,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "optional": true, "engines": { "node": ">= 0.6.0" } @@ -4882,6 +4880,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -5458,6 +5457,11 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dom-to-image": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz", + "integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==" + }, "node_modules/dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", @@ -6476,6 +6480,11 @@ "node": ">=12" } }, + "node_modules/html-to-image": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", + "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==" + }, "node_modules/html-to-markdown": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/html-to-markdown/-/html-to-markdown-1.0.0.tgz", @@ -6485,6 +6494,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "optional": true, "dependencies": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -10163,6 +10173,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "optional": true, "dependencies": { "utrie": "^1.0.2" } @@ -11492,6 +11503,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "optional": true, "dependencies": { "base64-arraybuffer": "^1.0.2" } @@ -14756,6 +14768,11 @@ "@types/chai": "*" } }, + "@types/dom-to-image": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.4.tgz", + "integrity": "sha512-UddUdGF1qulrSDulkz3K2Ypq527MR6ixlgAzqLbxSiQ0icx0XDlIV+h4+edmjq/1dqn0KgN0xGSe1kI9t+vGuw==" + }, "@types/estree": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.0.tgz", @@ -14766,14 +14783,6 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" }, - "@types/html2canvas": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/html2canvas/-/html2canvas-1.0.0.tgz", - "integrity": "sha512-BJpVf+FIN9UERmzhbtUgpXj6XBZpG67FMgBLLoj9HZKd9XifcCpSV+UnFcwTZfEyun4U/KmCrrVOG7829L589w==", - "requires": { - "html2canvas": "*" - } - }, "@types/istanbul-lib-coverage": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", @@ -15258,7 +15267,8 @@ "base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", - "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==" + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "optional": true }, "base64-js": { "version": "1.5.1", @@ -15716,6 +15726,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "optional": true, "requires": { "utrie": "^1.0.2" } @@ -16131,6 +16142,11 @@ } } }, + "dom-to-image": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz", + "integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA==" + }, "dom-walk": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", @@ -16934,6 +16950,11 @@ "whatwg-encoding": "^2.0.0" } }, + "html-to-image": { + "version": "1.11.11", + "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.11.tgz", + "integrity": "sha512-9gux8QhvjRO/erSnDPv28noDZcPZmYE7e1vFsBLKLlRlKDSqNJYebj6Qz1TGd5lsRV+X+xYyjCKjuZdABinWjA==" + }, "html-to-markdown": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/html-to-markdown/-/html-to-markdown-1.0.0.tgz", @@ -16943,6 +16964,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "optional": true, "requires": { "css-line-break": "^2.1.0", "text-segmentation": "^1.0.3" @@ -19660,6 +19682,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "optional": true, "requires": { "utrie": "^1.0.2" } @@ -20744,6 +20767,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "optional": true, "requires": { "base64-arraybuffer": "^1.0.2" } diff --git a/package.json b/package.json index b8bbf42c4..857080eb0 100644 --- a/package.json +++ b/package.json @@ -72,7 +72,6 @@ "@turf/distance": "^6.5.0", "@turf/length": "^6.5.0", "@turf/turf": "^6.5.0", - "@types/html2canvas": "^1.0.0", "@types/showdown": "^2.0.0", "chart.js": "^3.8.0", "country-language": "^0.1.7", @@ -82,8 +81,8 @@ "escape-html": "^1.0.3", "fake-dom": "^1.0.4", "geojson2svg": "^1.3.3", + "html-to-image": "^1.11.11", "html-to-markdown": "^1.0.0", - "html2canvas": "^1.4.1", "i18next-client": "^1.11.4", "idb-keyval": "^6.0.3", "jest-mock": "^29.4.1", diff --git a/public/assets/templates/CurrentMapWithHeaderA4.svg b/public/assets/templates/CurrentMapWithHeaderA4.svg new file mode 100644 index 000000000..b4af661f9 --- /dev/null +++ b/public/assets/templates/CurrentMapWithHeaderA4.svg @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $map(theme:aed,z:14,lat:51.2098,lon:3.2284) + $flyer.toerisme_vlaanderen + + $map(theme:toerisme_vlaanderen,lat:50.8552,lon:4.3156, z:10,layers:none, +layer-charging_station_ebikes:force) + + $map(theme:cyclofix,z:14,lat:51.05016,lon:3.717842,layers:none,layer-bike_repair_station:true,layer-drinking_water:true,layer-bike_cafe:true, layer-charging_station_ebikes:false,layer-bicycle_tube_vending_machine: true) + + + $map(theme:artwork,z:15,lat:51.2098,lon:3.2284,background:AGIV) + + $map(theme:cyclestreets,z:15,lat:51.02702,lon:4.48029, scaling:3) + + + + + + + + + + $map(theme:benches,z:14,lat:51.2098,lon:3.2284, layers:none, layer-bench:force) + $flyer.aerial + $flyer.examples + + + + + + + + + + + + + + + + + + + + $flyer.lines_too + + $map(theme:onwheels,z:19,lat:50.86622,lon:4.35012,layer-governments:false,layer-parking:false,layer-toilet:false,layer-cafe_pub:false,layer-food:false,scaling:1.5) + $flyer.onwheels + + + + + + + + + + $flyer.cyclofix + $version + + + + + + + + + + + + + + + $flyer.title + + + diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 7ac3e10f0..8a0cb8c34 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -825,10 +825,6 @@ video { margin-right: 1rem; } -.mb-4 { - margin-bottom: 1rem; -} - .mr-2 { margin-right: 0.5rem; } @@ -865,6 +861,10 @@ video { margin-right: 0.25rem; } +.mb-4 { + margin-bottom: 1rem; +} + .ml-4 { margin-left: 1rem; } @@ -921,10 +921,6 @@ video { margin-left: -1.5rem; } -.mr-3 { - margin-right: 0.75rem; -} - .-ml-12 { margin-left: -3rem; } @@ -933,6 +929,10 @@ video { margin-top: -3rem; } +.mr-3 { + margin-right: 0.75rem; +} + .box-border { box-sizing: border-box; } @@ -1273,6 +1273,10 @@ video { align-content: flex-start; } +.items-start { + align-items: flex-start; +} + .items-end { align-items: flex-end; } @@ -1513,16 +1517,6 @@ video { background-color: rgb(224 231 255 / var(--tw-bg-opacity)); } -.bg-red-500 { - --tw-bg-opacity: 1; - background-color: rgb(239 68 68 / var(--tw-bg-opacity)); -} - -.bg-red-200 { - --tw-bg-opacity: 1; - background-color: rgb(254 202 202 / var(--tw-bg-opacity)); -} - .bg-red-600 { --tw-bg-opacity: 1; background-color: rgb(220 38 38 / var(--tw-bg-opacity)); diff --git a/test.html b/test.html index 2a5d2478c..aece23274 100644 --- a/test.html +++ b/test.html @@ -17,7 +17,7 @@ -
'maindiv' not attached
+
'maindiv' not attached
'extradiv' not attached
diff --git a/test.ts b/test.ts index 5d697e8a7..e9b6b815b 100644 --- a/test.ts +++ b/test.ts @@ -4,12 +4,8 @@ import ThemeViewState from "./Models/ThemeViewState" import Combine from "./UI/Base/Combine" import SpecialVisualizations from "./UI/SpecialVisualizations" import {VariableUiElement} from "./UI/Base/VariableUIElement" -import SvelteUIElement from "./UI/Base/SvelteUIElement" import {SvgToPdf} from "./Utils/svgToPdf" import {Utils} from "./Utils" -import DeleteWizard from "./UI/Popup/DeleteFlow/DeleteWizard.svelte"; -import DeleteConfig from "./Models/ThemeConfig/DeleteConfig"; -import {UIEventSource} from "./Logic/UIEventSource"; function testspecial() { const layout = new LayoutConfig(theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data) @@ -32,33 +28,7 @@ async function testPdf() { await pdf.ConvertSvg("nl") } - -function testDelete() { - const layout = new LayoutConfig(theme, true) // qp.data === "" ? : new AllKnownLayoutsLazy().get(qp.data) - const state = new ThemeViewState(layout) - const tags = new UIEventSource({"amenity": "public_bookcase"}) - new SvelteUIElement(DeleteWizard, { - state, - tags, - layer: layout.layers.find(l => l.id === "public_bookcase"), - featureId: "node/10944136609", - deleteConfig: new DeleteConfig({ - nonDeleteMappings: [ - { - if: {"and": ["disused:amenity=public_bookcase", "amenity="]}, - then: { - en: "The bookcase still exists but is not maintained anymore" - } - } - ] - }, "test") - }).AttachTo("maindiv") - -} - -testDelete() - -// testPdf().then((_) => console.log("All done")) +testPdf().then((_) => console.log("All done")) /*/ testspecial() //*/