From de20b00b8f8af6796cbab060262d85a5ab171c5d Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Sun, 4 Jun 2023 22:52:13 +0200 Subject: [PATCH] Fix: decent PDF-export --- .../FeatureSource/Actors/TileLocalStorage.ts | 2 +- Logic/FeatureSource/Sources/GeoJsonSource.ts | 8 +- Models/RasterLayers.ts | 2 +- Models/ThemeViewState.ts | 79 +- UI/BigComponents/PdfExportGui.ts | 378 -------- UI/DownloadFlow/DownloadButton.svelte | 31 +- UI/DownloadFlow/DownloadPanel.svelte | 37 +- UI/Map/MapLibreAdaptor.ts | 15 +- UI/Map/MaplibreMap.svelte | 3 +- Utils/pngMapCreator.ts | 54 +- Utils/svgToPdf.ts | 324 ++++--- .../templates/CurrentMapWithHeaderA3.svg | 189 ++++ .../templates/CurrentMapWithHeaderA4.svg | 885 ++---------------- test.ts | 6 +- theme.html | 2 +- 15 files changed, 619 insertions(+), 1396 deletions(-) delete mode 100644 UI/BigComponents/PdfExportGui.ts create mode 100644 public/assets/templates/CurrentMapWithHeaderA3.svg diff --git a/Logic/FeatureSource/Actors/TileLocalStorage.ts b/Logic/FeatureSource/Actors/TileLocalStorage.ts index b1f7cc6f6..97feb53d3 100644 --- a/Logic/FeatureSource/Actors/TileLocalStorage.ts +++ b/Logic/FeatureSource/Actors/TileLocalStorage.ts @@ -84,7 +84,7 @@ export default class TileLocalStorage { const maxAge = this._maxAgeSeconds const timeDiff = Date.now() - date if (timeDiff >= maxAge) { - console.log("Dropping cache for", this._layername, tileIndex, "out of date") + console.debug("Dropping cache for", this._layername, tileIndex, "out of date") await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, undefined) return undefined diff --git a/Logic/FeatureSource/Sources/GeoJsonSource.ts b/Logic/FeatureSource/Sources/GeoJsonSource.ts index 1131774aa..02c76c4ed 100644 --- a/Logic/FeatureSource/Sources/GeoJsonSource.ts +++ b/Logic/FeatureSource/Sources/GeoJsonSource.ts @@ -65,14 +65,14 @@ export default class GeoJsonSource implements FeatureSource { return } this.LoadJSONFrom(url, eventsource, layer) - .then((fs) => console.log("Loaded",fs.length, "features from", url)) - .catch((err) => console.error("Could not load ", url, "due to", err)) + .then((fs) => console.debug("Loaded",fs.length, "features from", url)) + .catch((err) => console.warn("Could not load ", url, "due to", err)) return true // data is loaded, we can safely unregister }) } else { this.LoadJSONFrom(url, eventsource, layer) - .then((fs) => console.log("Loaded",fs.length, "features from", url)) - .catch((err) => console.error("Could not load ", url, "due to", err)) + .then((fs) => console.debug("Loaded",fs.length, "features from", url)) + .catch((err) => console.warn("Could not load ", url, "due to", err)) } this.features = eventsource } diff --git a/Models/RasterLayers.ts b/Models/RasterLayers.ts index 5435c3779..e549c6a13 100644 --- a/Models/RasterLayers.ts +++ b/Models/RasterLayers.ts @@ -41,7 +41,7 @@ export class AvailableRasterLayers { type: "Feature", properties: { name: "MapTiler", - url: null, + url: "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy", category: "osmbasedmap", id: "maptiler", attribution: { diff --git a/Models/ThemeViewState.ts b/Models/ThemeViewState.ts index 4340a8fea..2ef87de87 100644 --- a/Models/ThemeViewState.ts +++ b/Models/ThemeViewState.ts @@ -159,7 +159,7 @@ export default class ThemeViewState implements SpecialVisualizationState { */ - if(this.layout.layers.some(l => l._needsFullNodeDatabase)){ + if (this.layout.layers.some(l => l._needsFullNodeDatabase)) { this.fullNodeDatabase = new FullNodeDatabaseSource() } @@ -181,9 +181,10 @@ export default class ThemeViewState implements SpecialVisualizationState { } currentViewIndex++ return [bbox.asGeoJson({ - zoom: this.mapProperties.zoom.data, - ...this.mapProperties.location.data, - id: "current_view" } + zoom: this.mapProperties.zoom.data, + ...this.mapProperties.location.data, + id: "current_view" + } )]; } ) @@ -240,39 +241,6 @@ export default class ThemeViewState implements SpecialVisualizationState { this.featureProperties, fs.layer.layerDef.maxAgeOfCache ) - - const doShowLayer = this.mapProperties.zoom.map( - (z) => - (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), - [fs.layer.isDisplayed] - ) - - if ( - !doShowLayer.data && - (this.featureSwitches.featureSwitchFilter.data === false || !fs.layer.layerDef.name) - ) { - /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined) - * - * This means that we don't have to filter it, nor do we have to display it - * */ - return - } - - const filtered = new FilteringFeatureSource( - fs.layer, - fs, - (id) => this.featureProperties.getStore(id), - this.layerState.globalFilters - ) - - new ShowDataLayer(this.map, { - layer: fs.layer.layerDef, - features: filtered, - doShowLayer, - selectedElement: this.selectedElement, - selectedLayer: this.selectedLayer, - fetchStore: (id) => this.featureProperties.getStore(id), - }) }) this.floors = this.featuresInView.features.stabilized(500).map((features) => { @@ -317,6 +285,7 @@ export default class ThemeViewState implements SpecialVisualizationState { this.changes ) + this.showNormalDataOn(this.map) this.initActors() this.addLastClick(lastClick) this.drawSpecialLayers() @@ -327,6 +296,42 @@ export default class ThemeViewState implements SpecialVisualizationState { } } + public showNormalDataOn(map: Store) { + this.perLayer.forEach((fs) => { + const doShowLayer = this.mapProperties.zoom.map( + (z) => + (fs.layer.isDisplayed?.data ?? true) && z >= (fs.layer.layerDef?.minzoom ?? 0), + [fs.layer.isDisplayed] + ) + + if ( + !doShowLayer.data && + (this.featureSwitches.featureSwitchFilter.data === false || !fs.layer.layerDef.name) + ) { + /* This layer is hidden and there is no way to enable it (filterview is disabled or this layer doesn't show up in the filter view as the name is not defined) + * + * This means that we don't have to filter it, nor do we have to display it + * */ + return + } + const filtered = new FilteringFeatureSource( + fs.layer, + fs, + (id) => this.featureProperties.getStore(id), + this.layerState.globalFilters + ) + + new ShowDataLayer(map, { + layer: fs.layer.layerDef, + features: filtered, + doShowLayer, + selectedElement: this.selectedElement, + selectedLayer: this.selectedLayer, + fetchStore: (id) => this.featureProperties.getStore(id), + }) + }) + } + /** * Various small methods that need to be called */ diff --git a/UI/BigComponents/PdfExportGui.ts b/UI/BigComponents/PdfExportGui.ts deleted file mode 100644 index 95cc929b0..000000000 --- a/UI/BigComponents/PdfExportGui.ts +++ /dev/null @@ -1,378 +0,0 @@ -import Combine from "../Base/Combine" -import {FlowPanelFactory, FlowStep} from "../ImportFlow/FlowStep" -import {ImmutableStore, Store, UIEventSource} from "../../Logic/UIEventSource" -import {InputElement} from "../Input/InputElement" -import {SvgToPdf, SvgToPdfOptions} from "../../Utils/svgToPdf" -import {FixedInputElement} from "../Input/FixedInputElement" -import {FixedUiElement} from "../Base/FixedUiElement" -import FileSelectorButton from "../Input/FileSelectorButton" -import InputElementMap from "../Input/InputElementMap" -import {RadioButton} from "../Input/RadioButton" -import {Utils} from "../../Utils" -import {VariableUiElement} from "../Base/VariableUIElement" -import Loading from "../Base/Loading" -import BaseUIElement from "../BaseUIElement" -import Img from "../Base/Img" -import Title from "../Base/Title" -import {CheckBox} from "../Input/Checkboxes" -import Toggle from "../Input/Toggle" -import List from "../Base/List" -import LeftIndex from "../Base/LeftIndex" -import Constants from "../../Models/Constants" -import Toggleable from "../Base/Toggleable" -import Lazy from "../Base/Lazy" -import LinkToWeblate from "../Base/LinkToWeblate" -import Link from "../Base/Link" -import {AllLanguagesSelector} from "../Popup/AllLanguagesSelector" - -class SelectTemplate extends Combine implements FlowStep<{ title: string; pages: string[] }> { - readonly IsValid: Store - readonly Value: Store<{ title: string; pages: string[] }> - - constructor() { - const elements: InputElement<{ templateName: string; pages: string[] }>[] = [] - for (const templateName in SvgToPdf.templates) { - const template = SvgToPdf.templates[templateName] - elements.push( - new FixedInputElement( - new Combine([ - new FixedUiElement(templateName).SetClass("font-bold pr-2"), - template.description, - ]), - new UIEventSource({ templateName, pages: template.pages }) - ) - ) - } - - const file = new FileSelectorButton( - new FixedUiElement("Select an svg image which acts as template"), - { - acceptType: "image/svg+xml", - allowMultiple: true, - } - ) - const fileMapped = new InputElementMap< - FileList, - { templateName: string; pages: string[]; fromFile: true } - >( - file, - (x0, x1) => x0 === x1, - (filelist) => { - if (filelist === undefined) { - return undefined - } - const pages = [] - let templateName: string = undefined - for (const file of Array.from(filelist)) { - if (templateName == undefined) { - templateName = file.name.substring(file.name.lastIndexOf("/") + 1) - templateName = templateName.substring(0, templateName.lastIndexOf(".")) - } - pages.push(file.text()) - } - - return { - templateName, - pages, - fromFile: true, - } - }, - (_) => undefined - ) - elements.push(fileMapped) - const radio = new RadioButton(elements, { selectFirstAsDefault: true }) - - const loaded: Store<{ success: { title: string; pages: string[] } } | { error: any }> = - radio.GetValue().bind((template) => { - if (template === undefined) { - return undefined - } - if (template["fromFile"]) { - return UIEventSource.FromPromiseWithErr( - Promise.all(template.pages).then((pages) => ({ - title: template.templateName, - pages, - })) - ) - } - const urls = template.pages.map((p) => SelectTemplate.ToUrl(p)) - const dloadAll: Promise<{ title: string; pages: string[] }> = Promise.all( - urls.map((url) => Utils.download(url)) - ).then((pages) => ({ - pages, - title: template.templateName, - })) - - return UIEventSource.FromPromiseWithErr(dloadAll) - }) - const preview = new VariableUiElement( - loaded.map((pages) => { - if (pages === undefined) { - return new Loading() - } - if (pages["error"] !== undefined) { - return new FixedUiElement("Loading preview failed: " + pages["err"]).SetClass( - "alert" - ) - } - const svgs = pages["success"].pages - if (svgs.length === 0) { - return new FixedUiElement("No pages loaded...").SetClass("alert") - } - const els: BaseUIElement[] = [] - for (const pageSrc of svgs) { - const el = new Img(pageSrc, true).SetClass("w-96 m-2 border-black border-2") - els.push(el) - } - return new Combine(els).SetClass("flex border border-subtle rounded-xl") - }) - ) - - super([new Title("Select template"), radio, new Title("Preview"), preview]) - this.Value = loaded.map((l) => (l === undefined ? undefined : l["success"])) - this.IsValid = this.Value.map((v) => v !== undefined) - } - - public static ToUrl(spec: string) { - if (spec.startsWith("http")) { - return spec - } - let path = window.location.pathname - path = path.substring(0, path.lastIndexOf("/")) - return window.location.protocol + "//" + window.location.host + path + "/" + spec - } -} - -class SelectPdfOptions - extends Combine - implements FlowStep<{ title: string; pages: string[]; options: SvgToPdfOptions }> -{ - readonly IsValid: Store - readonly Value: Store<{ title: string; pages: string[]; options: SvgToPdfOptions }> - - constructor(title: string, pages: string[], getFreeDiv: () => string) { - const dummy = new CheckBox("Don't add data to the map (to quickly preview the PDF)", false) - const overrideMapLocation = new CheckBox( - "Override map location: use a selected location instead of the location set in the template", - false - ) - const locationInput = Minimap.createMiniMap().SetClass("block w-full") - const searchField = undefined // new SearchAndGo({ leafletMap: locationInput.leafletMap }) - const selectLocation = new Combine([ - new Toggle( - new Combine([new Title("Select override location"), searchField]).SetClass("flex"), - undefined, - overrideMapLocation.GetValue() - ), - new Toggle( - locationInput.SetStyle("height: 20rem"), - undefined, - overrideMapLocation.GetValue() - ).SetStyle("height: 20rem"), - ]) - .SetClass("block") - .SetStyle("height: 25rem") - super([new Title("Select options"), dummy, overrideMapLocation, selectLocation]) - this.Value = dummy.GetValue().map( - (disableMaps) => { - return { - pages, - title, - options: { - disableMaps, - getFreeDiv, - overrideLocation: overrideMapLocation.GetValue().data - ? locationInput.location.data - : undefined, - }, - } - }, - [overrideMapLocation.GetValue(), locationInput.location] - ) - this.IsValid = new ImmutableStore(true) - } -} - -class PreparePdf extends Combine implements FlowStep<{ svgToPdf: SvgToPdf; languages: string[] }> { - readonly IsValid: Store - readonly Value: Store<{ svgToPdf: SvgToPdf; languages: string[] }> - - constructor(title: string, pages: string[], options: SvgToPdfOptions) { - const svgToPdf = new SvgToPdf(title, pages, options) - const languageSelector = new AllLanguagesSelector() - const isPrepared = UIEventSource.FromPromiseWithErr(svgToPdf.Prepare()) - - super([ - new Title("Select languages..."), - languageSelector, - new Toggle( - new Loading("Preparing maps..."), - undefined, - isPrepared.map((p) => p === undefined) - ), - ]) - this.Value = isPrepared.map( - (isPrepped) => { - if (isPrepped === undefined) { - return undefined - } - if (isPrepped["success"] !== undefined) { - const svgToPdf = isPrepped["success"] - const langs = languageSelector.GetValue().data - console.log("Languages are", langs) - if (langs.length === 0) { - return undefined - } - return { svgToPdf, languages: langs } - } - return undefined - }, - [languageSelector.GetValue()] - ) - this.IsValid = this.Value.map((v) => v !== undefined) - } -} - -class InspectStrings - extends Toggle - implements FlowStep<{ svgToPdf: SvgToPdf; languages: string[] }> -{ - readonly IsValid: Store - readonly Value: Store<{ svgToPdf: SvgToPdf; languages: string[] }> - - constructor(svgToPdf: SvgToPdf, languages: string[]) { - const didLoadLanguages = UIEventSource.FromPromiseWithErr( - svgToPdf.PrepareLanguages(languages) - ).map((l) => l !== undefined && l["success"] !== undefined) - - super( - new Combine([ - new Title("Inspect translation strings"), - ...languages.map( - (l) => new Lazy(() => InspectStrings.createOverviewPanel(svgToPdf, l)) - ), - ]), - new Loading(), - didLoadLanguages - ) - this.Value = new ImmutableStore({ svgToPdf, languages }) - this.IsValid = didLoadLanguages - } - - private static createOverviewPanel(svgToPdf: SvgToPdf, language: string): BaseUIElement { - const elements: BaseUIElement[] = [] - let foundTranslations = 0 - const allKeys = Array.from(svgToPdf.translationKeys()) - for (const translationKey of allKeys) { - let spec = translationKey - if (translationKey.startsWith("layer.")) { - spec = "layers:" + translationKey.substring(6) - } else { - spec = "core:" + translationKey - } - const translated = svgToPdf.getTranslation("$" + translationKey, language, true) - if (translated) { - foundTranslations++ - } - const linkToWeblate = new Link( - spec, - LinkToWeblate.hrefToWeblate(language, spec), - true - ).SetClass("font-bold link-underline") - elements.push( - new Combine([ - linkToWeblate, - " ", - translated ?? new FixedUiElement("No translation found!").SetClass("alert"), - ]) - ) - } - - return new Toggleable( - new Title("Translations for " + language), - new Combine([ - `${foundTranslations}/${allKeys.length} of translations are found (${Math.floor( - (100 * foundTranslations) / allKeys.length - )}%)`, - "The following keys are used:", - new List(elements), - ]), - { closeOnClick: false, height: "15rem" } - ) - } -} - -class SavePdf extends Combine { - constructor(svgToPdf: SvgToPdf, languages: string[]) { - super([ - new Title("Generating your pdfs..."), - new List( - languages.map( - (lng) => - new Toggle( - lng + " is done!", - new Loading("Creating pdf for " + lng), - UIEventSource.FromPromiseWithErr( - svgToPdf.ConvertSvg(lng).then(() => true) - ).map((x) => x !== undefined && x["success"] === true) - ) - ) - ), - ]) - } -} - -export class PdfExportGui extends LeftIndex { - constructor(freeDivId: string) { - let i = 0 - const createDiv = (): string => { - const div = document.createElement("div") - div.id = "freediv-" + i++ - document.getElementById(freeDivId).append(div) - return div.id - } - - Constants.defaultOverpassUrls.splice(0, 1) - const { flow, furthestStep, titles } = FlowPanelFactory.start( - new Title("Select template"), - new SelectTemplate() - ) - .then( - new Title("Select options"), - ({ title, pages }) => new SelectPdfOptions(title, pages, createDiv) - ) - .then( - "Generate maps...", - ({ title, pages, options }) => new PreparePdf(title, pages, options) - ) - .then( - "Inspect translations", - ({ svgToPdf, languages }) => new InspectStrings(svgToPdf, languages) - ) - .finish("Generating...", ({ svgToPdf, languages }) => new SavePdf(svgToPdf, languages)) - - const toc = new List( - titles.map( - (title, i) => - new VariableUiElement( - furthestStep.map((currentStep) => { - if (i > currentStep) { - return new Combine([title]).SetClass("subtle") - } - if (i == currentStep) { - return new Combine([title]).SetClass("font-bold") - } - if (i < currentStep) { - return title - } - }) - ) - ), - true - ) - - const leftContents: BaseUIElement[] = [toc].map((el) => el?.SetClass("pl-4")) - - super(leftContents, flow) - } -} diff --git a/UI/DownloadFlow/DownloadButton.svelte b/UI/DownloadFlow/DownloadButton.svelte index 929e543ae..73cda4f29 100644 --- a/UI/DownloadFlow/DownloadButton.svelte +++ b/UI/DownloadFlow/DownloadButton.svelte @@ -10,12 +10,13 @@ import DownloadHelper from "./DownloadHelper"; import {Utils} from "../../Utils"; import type {PriviligedLayerType} from "../../Models/Constants"; + import {UIEventSource} from "../../Logic/UIEventSource"; export let state: SpecialVisualizationState export let extension: string export let mimetype: string - export let construct: (geojsonCleaned: FeatureCollection) => (Blob | string) | Promise + export let construct: (geojsonCleaned: FeatureCollection, title: string) => (Blob | string) | Promise export let mainText: Translation export let helperText: Translation export let metaIsIncluded: boolean @@ -26,51 +27,63 @@ let isExporting = false let isError = false + let status: UIEventSource = new UIEventSource(undefined) + async function clicked() { isExporting = true const gpsLayer = state.layerState.filteredLayers.get( "gps_location" ) state.lastClickObject.features.setData([]) - + const gpsIsDisplayed = gpsLayer.isDisplayed.data try { gpsLayer.isDisplayed.setData(false) const geojson: FeatureCollection = downloadHelper.getCleanGeoJson(metaIsIncluded) const name = state.layout.id - const promise = construct(geojson) + const title = `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.${extension}` + const promise = construct(geojson, title) let data: Blob | string if (typeof promise === "string") { data = promise } else if (typeof promise["then"] === "function") { - data = await > promise + data = await >promise } else { data = promise } + if (!data) { + return + } console.log("Got data", data) Utils.offerContentsAsDownloadableFile( data, - `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.${extension}`, + title, { mimetype, } ) } catch (e) { isError = true + console.error(e) + } finally { + isExporting = false + gpsLayer.isDisplayed.setData(gpsIsDisplayed) } - gpsLayer.isDisplayed.setData(gpsIsDisplayed) - isExporting = false } {#if isError} - + {:else if isExporting} - + {#if $status} + {$status} + {:else} + + {/if} {:else}