forked from MapComplete/MapComplete
Fix: decent PDF-export
This commit is contained in:
parent
905f796baa
commit
de20b00b8f
15 changed files with 619 additions and 1396 deletions
|
@ -84,7 +84,7 @@ export default class TileLocalStorage<T> {
|
|||
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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 <Feature[]>[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<MlMap>) {
|
||||
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
|
||||
*/
|
||||
|
|
|
@ -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<boolean>
|
||||
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<boolean>
|
||||
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: <SvgToPdfOptions>{
|
||||
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<boolean>
|
||||
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<boolean>
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -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<void>
|
||||
export let construct: (geojsonCleaned: FeatureCollection, title: string) => (Blob | string) | Promise<void>
|
||||
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<string> = new UIEventSource<string>(undefined)
|
||||
|
||||
async function clicked() {
|
||||
isExporting = true
|
||||
const gpsLayer = state.layerState.filteredLayers.get(
|
||||
<PriviligedLayerType>"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<Blob | string>> promise
|
||||
data = await <Promise<Blob | string>>promise
|
||||
} else {
|
||||
data = <Blob>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
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
{#if isError}
|
||||
<Tr cls="alert" t={Translations.t.error}/>
|
||||
<Tr cls="alert" t={Translations.t.general.error}/>
|
||||
{:else if isExporting}
|
||||
<Loading>
|
||||
<Tr t={t.exporting}/>
|
||||
{#if $status}
|
||||
{$status}
|
||||
{:else}
|
||||
<Tr t={t.exporting}/>
|
||||
{/if}
|
||||
</Loading>
|
||||
{:else}
|
||||
<button class="flex w-full" on:click={clicked}>
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
<script lang="ts">
|
||||
|
||||
import type {SpecialVisualizationState} from "../SpecialVisualization";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import DownloadHelper from "./DownloadHelper";
|
||||
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";
|
||||
|
||||
export let state: SpecialVisualizationState
|
||||
export let state: ThemeViewState
|
||||
let isLoading = state.dataIsLoading
|
||||
|
||||
const t = Translations.t.general.download
|
||||
|
@ -30,6 +37,30 @@
|
|||
})
|
||||
}
|
||||
|
||||
async function constructPdf(_, title: string, status: UIEventSource<string>) {
|
||||
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: <Record<string, string>> {
|
||||
"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
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
|
@ -85,7 +116,7 @@
|
|||
extension="pdf"
|
||||
mainText={t.downloadAsPdf}
|
||||
helperText={t.downloadAsPdfHelper}
|
||||
construct={_ => state.mapProperties.exportAsPng(4)}
|
||||
construct={constructPdf}
|
||||
/>
|
||||
|
||||
|
||||
|
|
|
@ -199,7 +199,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
return url
|
||||
}
|
||||
|
||||
private static setDpi(drawOn: HTMLCanvasElement, ctx: CanvasRenderingContext2D, dpiFactor: number) {
|
||||
public static setDpi(drawOn: HTMLCanvasElement, ctx: CanvasRenderingContext2D, dpiFactor: number) {
|
||||
drawOn.style.width = drawOn.style.width || drawOn.width + "px"
|
||||
drawOn.style.height = drawOn.style.height || drawOn.height + "px"
|
||||
|
||||
|
@ -223,23 +223,16 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
console.log("Canvas size:", drawOn.width, drawOn.height)
|
||||
const ctx = drawOn.getContext("2d")
|
||||
// Set up CSS size.
|
||||
MapLibreAdaptor.setDpi(drawOn, ctx, dpiFactor)
|
||||
MapLibreAdaptor.setDpi(drawOn, ctx, dpiFactor / map.getPixelRatio())
|
||||
|
||||
await this.exportBackgroundOnCanvas(drawOn, ctx, dpiFactor)
|
||||
await this.exportBackgroundOnCanvas(ctx)
|
||||
|
||||
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<Blob>(resolve => drawOn.toBlob(blob => resolve(blob)))
|
||||
}
|
||||
|
@ -248,7 +241,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
|||
* Exports the background map and lines to PNG.
|
||||
* Markers are _not_ rendered
|
||||
*/
|
||||
private async exportBackgroundOnCanvas(drawOn: HTMLCanvasElement, ctx: CanvasRenderingContext2D, dpiFactor: number = 1): Promise<void> {
|
||||
private async exportBackgroundOnCanvas(ctx: CanvasRenderingContext2D): Promise<void> {
|
||||
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
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import { Map } from "@onsvisual/svelte-maps";
|
||||
import type { Map as MaplibreMap } from "maplibre-gl";
|
||||
import type { Writable } from "svelte/store";
|
||||
import {AvailableRasterLayers} from "../../Models/RasterLayers";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -24,7 +25,7 @@
|
|||
$map.resize();
|
||||
});
|
||||
});
|
||||
const styleUrl = "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy";
|
||||
const styleUrl = AvailableRasterLayers.maplibre.properties.url;
|
||||
</script>
|
||||
<main>
|
||||
<Map bind:center={center}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import ThemeViewState from "../Models/ThemeViewState"
|
||||
import SvelteUIElement from "../UI/Base/SvelteUIElement"
|
||||
import MaplibreMap from "../UI/Map/MaplibreMap.svelte"
|
||||
import { Utils } from "../Utils"
|
||||
import { UIEventSource } from "../Logic/UIEventSource"
|
||||
import {Utils} from "../Utils"
|
||||
import {UIEventSource} from "../Logic/UIEventSource"
|
||||
import {Map as MlMap} from "maplibre-gl"
|
||||
import {MapLibreAdaptor} from "../UI/Map/MapLibreAdaptor";
|
||||
import {AvailableRasterLayers} from "../Models/RasterLayers";
|
||||
|
||||
export interface PngMapCreatorOptions {
|
||||
readonly width: number
|
||||
|
@ -23,33 +24,56 @@ export class PngMapCreator {
|
|||
* Creates a base64-encoded PNG image
|
||||
* @constructor
|
||||
*/
|
||||
public async CreatePng(status: UIEventSource<string>): Promise<Blob> {
|
||||
public async CreatePng(freeComponentId: string, status?: UIEventSource<string>): Promise<Blob> {
|
||||
const div = document.createElement("div")
|
||||
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
|
||||
|
||||
function setState(msg: string) {
|
||||
status.setData(layout.id + ": " + msg)
|
||||
status?.setData(layout.id + ": " + msg)
|
||||
}
|
||||
|
||||
setState("Initializing map")
|
||||
const map = this._state.map
|
||||
new SvelteUIElement(MaplibreMap, { map })
|
||||
.SetStyle(
|
||||
"width: " + this._options.width + "mm; height: " + this._options.height + "mm; border: 2px solid red;"
|
||||
)
|
||||
.AttachTo("extradiv")
|
||||
map.data.resize()
|
||||
|
||||
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<MlMap>(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")
|
||||
console.log("Map export: starting actual export, target size is", this._options.width,"mm * ",this._options.height+"mm")
|
||||
return this._state.mapProperties.exportAsPng(4)
|
||||
const png = await mla.exportAsPng(6)
|
||||
div.parentElement.removeChild(div)
|
||||
Utils.offerContentsAsDownloadableFile(png, "test.png")
|
||||
return png
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ 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"
|
||||
|
||||
class SvgToPdfInternals {
|
||||
private static readonly dummyDoc: jsPDF = new jsPDF()
|
||||
|
@ -260,15 +259,29 @@ class SvgToPdfInternals {
|
|||
const ry = SvgToPdfInternals.attrNumber(element, "ry", false) ?? 0
|
||||
const rx = SvgToPdfInternals.attrNumber(element, "rx", false) ?? 0
|
||||
const css = SvgToPdfInternals.css(element)
|
||||
this.doc.saveGraphicsState()
|
||||
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
|
||||
this.doc.setFillColor(css["fill"] ?? "black")
|
||||
let color = css["fill"] ?? "black"
|
||||
let opacity = 1
|
||||
if (css["fill-opacity"]) {
|
||||
opacity = Number(css["fill-opacity"])
|
||||
this.doc.setGState(this.doc.GState({opacity: opacity}))
|
||||
}
|
||||
console.log("Fill color is:", color, opacity)
|
||||
|
||||
this.doc.setFillColor(color)
|
||||
this.doc.roundedRect(x, y, width, height, rx, ry, "F")
|
||||
}
|
||||
if (css["stroke"] && css["stroke"] !== "none") {
|
||||
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
|
||||
this.doc.setDrawColor(css["stroke"] ?? "black")
|
||||
if (css["opacity"]) {
|
||||
const opacity = Number(css["opacity"])
|
||||
this.doc.setGState(this.doc.GState({"stroke-opacity": opacity}))
|
||||
}
|
||||
this.doc.roundedRect(x, y, width, height, rx, ry, "S")
|
||||
}
|
||||
this.doc.restoreGraphicsState()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -290,13 +303,27 @@ class SvgToPdfInternals {
|
|||
}
|
||||
|
||||
private drawTspan(tspan: Element) {
|
||||
if (tspan.textContent == "") {
|
||||
const txt = tspan.textContent
|
||||
if (txt == "") {
|
||||
return
|
||||
}
|
||||
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
|
||||
if (css["shape-inside"]) {
|
||||
const matched = css["shape-inside"].match(/url\(#([a-zA-Z0-9-]+)\)/)
|
||||
|
@ -322,7 +349,6 @@ class SvgToPdfInternals {
|
|||
this.doc.setTextColor("black")
|
||||
}
|
||||
let fontsize = parseFloat(css["font-size"])
|
||||
|
||||
this.doc.setFontSize(fontsize * 2.5)
|
||||
|
||||
let textTemplate = tspan.textContent.split(" ")
|
||||
|
@ -339,7 +365,13 @@ class SvgToPdfInternals {
|
|||
addSpace = false
|
||||
continue
|
||||
}
|
||||
|
||||
if (text.startsWith(`$\{`)) {
|
||||
if (addSpace) {
|
||||
result += " "
|
||||
}
|
||||
result += this.extractTranslation(text)
|
||||
continue
|
||||
}
|
||||
if (!text.startsWith("$")) {
|
||||
if (addSpace) {
|
||||
result += " "
|
||||
|
@ -496,11 +528,16 @@ class SvgToPdfInternals {
|
|||
}
|
||||
|
||||
export interface SvgToPdfOptions {
|
||||
freeComponentId: string,
|
||||
disableMaps?: false | true
|
||||
textSubstitutions?: Record<string, string>
|
||||
beforePage?: (i: number) => void
|
||||
overrideLocation?: { lat: number; lon: number },
|
||||
disableDataLoading?: boolean | false
|
||||
disableDataLoading?: boolean | false,
|
||||
/**
|
||||
* Override all the maps to generate with this map
|
||||
*/
|
||||
state?: ThemeViewState
|
||||
}
|
||||
|
||||
class SvgToPdfPage {
|
||||
|
@ -569,7 +606,6 @@ class SvgToPdfPage {
|
|||
if (element.tagName === "rect") {
|
||||
this.rects[element.id] = <SVGRectElement>element
|
||||
}
|
||||
|
||||
if (element.tagName === "image") {
|
||||
await this.loadImage(element)
|
||||
}
|
||||
|
@ -577,6 +613,14 @@ 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]+)/
|
||||
)
|
||||
|
@ -596,6 +640,7 @@ class SvgToPdfPage {
|
|||
this.options.textSubstitutions
|
||||
)
|
||||
}
|
||||
|
||||
if (element.textContent.startsWith("$map(")) {
|
||||
mapTextSpecs.push(<any>element)
|
||||
}
|
||||
|
@ -642,7 +687,12 @@ class SvgToPdfPage {
|
|||
}
|
||||
|
||||
for (const mapSpec of mapSpecs) {
|
||||
await this.prepareMap(mapSpec,! this.options?.disableDataLoading)
|
||||
try{
|
||||
|
||||
await this.prepareMap(mapSpec, !this.options?.disableDataLoading)
|
||||
}catch(e){
|
||||
console.error("Couldn't prepare a map:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -671,6 +721,10 @@ class SvgToPdfPage {
|
|||
Constants.vNumber
|
||||
)
|
||||
}
|
||||
if (text.startsWith("${") && text.endsWith("}")) {
|
||||
const key = text.substring(2, text.length - 1)
|
||||
return this.options.textSubstitutions[key]
|
||||
}
|
||||
const pathPart = text.match(/\$(([_a-zA-Z0-9? ]+\.)+[_a-zA-Z0-9? ]+)(.*)/)
|
||||
if (pathPart === null) {
|
||||
return text
|
||||
|
@ -715,8 +769,8 @@ class SvgToPdfPage {
|
|||
}
|
||||
}
|
||||
|
||||
private loadImage(element: Element): Promise<void> {
|
||||
const xlink = element.getAttribute("xlink:href")
|
||||
private loadImage(element: Element | string): Promise<void> {
|
||||
const xlink = typeof element === "string" ? element : element.getAttribute("xlink:href")
|
||||
let img = document.createElement("img")
|
||||
|
||||
if (xlink.startsWith("data:image/svg+xml;")) {
|
||||
|
@ -752,11 +806,11 @@ class SvgToPdfPage {
|
|||
|
||||
/**
|
||||
* Replaces a mapSpec with the appropriate map
|
||||
* @param mapSpec
|
||||
* @private
|
||||
*/
|
||||
|
||||
private async prepareMap(mapSpec: SVGTSpanElement, loadData: boolean = true): Promise<void> {
|
||||
private async prepareMap(mapSpec: SVGTSpanElement, loadData: boolean): Promise<void> {
|
||||
if (this.options.disableMaps) {
|
||||
return
|
||||
}
|
||||
// Upper left point of the tspan
|
||||
const {x, y} = SvgToPdfInternals.GetActualXY(mapSpec)
|
||||
|
||||
|
@ -766,11 +820,6 @@ class SvgToPdfPage {
|
|||
textElement = textElement.parentElement
|
||||
}
|
||||
const spec = textElement.textContent
|
||||
const match = spec.match(/\$map\(([^)]+)\)$/)
|
||||
if (match === null) {
|
||||
throw "Invalid mapspec:" + spec
|
||||
}
|
||||
const params = SvgToPdfInternals.parseCss(match[1], ",")
|
||||
|
||||
let smallestRect: SVGRectElement = undefined
|
||||
let smallestSurface: number = undefined
|
||||
|
@ -783,6 +832,7 @@ class SvgToPdfPage {
|
|||
const h = SvgToPdfInternals.attrNumber(rect, "height")
|
||||
const inBounds = rx <= x && x <= rx + w && ry <= y && y <= ry + h
|
||||
if (!inBounds) {
|
||||
console.log("Not in bounds: rectangle", id)
|
||||
continue
|
||||
}
|
||||
const surface = w * h
|
||||
|
@ -809,95 +859,111 @@ class SvgToPdfPage {
|
|||
svgImage.setAttribute("width", "" + width)
|
||||
svgImage.setAttribute("height", "" + height)
|
||||
|
||||
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
|
||||
if (layout === undefined) {
|
||||
console.error("Could not show map with parameters", params)
|
||||
throw (
|
||||
"Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
|
||||
)
|
||||
}
|
||||
layout.widenFactor = 0
|
||||
layout.overpassTimeout = 600
|
||||
layout.defaultBackgroundId = params["background"] ?? layout.defaultBackgroundId
|
||||
for (const paramsKey in params) {
|
||||
if (paramsKey.startsWith("layer-")) {
|
||||
const layerName = paramsKey.substring("layer-".length)
|
||||
const key = params[paramsKey].toLowerCase().trim()
|
||||
const layer = layout.layers.find((l) => l.id === layerName)
|
||||
if (layer === undefined) {
|
||||
throw "No layer found for " + paramsKey
|
||||
}
|
||||
if (key === "force") {
|
||||
layer.minzoom = 0
|
||||
layer.minzoomVisible = 0
|
||||
}
|
||||
let png: Blob
|
||||
if (this.options.state !== undefined) {
|
||||
png = await (new PngMapCreator(this.options.state, {
|
||||
width, height,
|
||||
}).CreatePng(this.options.freeComponentId))
|
||||
} else {
|
||||
const match = spec.match(/\$map\(([^)]*)\)$/)
|
||||
if (match === null) {
|
||||
throw "Invalid mapspec:" + spec
|
||||
}
|
||||
}
|
||||
const zoom = Number(params["zoom"] ?? params["z"] ?? 14)
|
||||
|
||||
Hash.hash.setData(undefined)
|
||||
// QueryParameters.ClearAll()
|
||||
const state = new ThemeViewState(layout)
|
||||
state.mapProperties.location.setData({
|
||||
lat: this.options?.overrideLocation?.lat ?? Number(params["lat"] ?? 51.05016),
|
||||
lon: this.options?.overrideLocation?.lon ?? Number(params["lon"] ?? 3.717842),
|
||||
})
|
||||
state.mapProperties.zoom.setData(zoom)
|
||||
|
||||
console.log("Params are", params, params["layers"] === "none")
|
||||
|
||||
const fl = Array.from(state.layerState.filteredLayers.values())
|
||||
for (const filteredLayer of fl) {
|
||||
if (params["layer-" + filteredLayer.layerDef.id] !== undefined) {
|
||||
filteredLayer.isDisplayed.setData(
|
||||
loadData && params["layer-" + filteredLayer.layerDef.id].trim().toLowerCase() !== "false"
|
||||
const params = SvgToPdfInternals.parseCss(match[1], ",")
|
||||
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
|
||||
if (layout === undefined) {
|
||||
console.error("Could not show map with parameters", params)
|
||||
throw (
|
||||
"Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
|
||||
)
|
||||
} else if (params["layers"] === "none") {
|
||||
filteredLayer.isDisplayed.setData(false)
|
||||
} else if (filteredLayer.layerDef.id.startsWith("note_import")) {
|
||||
filteredLayer.isDisplayed.setData(false)
|
||||
}
|
||||
layout.widenFactor = 0
|
||||
layout.overpassTimeout = 600
|
||||
layout.defaultBackgroundId = params["background"] ?? layout.defaultBackgroundId
|
||||
for (const paramsKey in params) {
|
||||
if (paramsKey.startsWith("layer-")) {
|
||||
const layerName = paramsKey.substring("layer-".length)
|
||||
const key = params[paramsKey].toLowerCase().trim()
|
||||
const layer = layout.layers.find((l) => l.id === layerName)
|
||||
if (layer === undefined) {
|
||||
throw "No layer found for " + paramsKey
|
||||
}
|
||||
if (key === "force") {
|
||||
layer.minzoom = 0
|
||||
layer.minzoomVisible = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
const zoom = Number(params["zoom"] ?? params["z"] ?? 14)
|
||||
|
||||
Hash.hash.setData(undefined)
|
||||
// QueryParameters.ClearAll()
|
||||
const state = new ThemeViewState(layout)
|
||||
state.mapProperties.location.setData({
|
||||
lat: this.options?.overrideLocation?.lat ?? Number(params["lat"] ?? 51.05016),
|
||||
lon: this.options?.overrideLocation?.lon ?? Number(params["lon"] ?? 3.717842),
|
||||
})
|
||||
state.mapProperties.zoom.setData(zoom)
|
||||
|
||||
console.log("Params are", params, params["layers"] === "none")
|
||||
|
||||
const fl = Array.from(state.layerState.filteredLayers.values())
|
||||
for (const filteredLayer of fl) {
|
||||
if (params["layer-" + filteredLayer.layerDef.id] !== undefined) {
|
||||
filteredLayer.isDisplayed.setData(
|
||||
loadData && params["layer-" + filteredLayer.layerDef.id].trim().toLowerCase() !== "false"
|
||||
)
|
||||
} else if (params["layers"] === "none") {
|
||||
filteredLayer.isDisplayed.setData(false)
|
||||
} else if (filteredLayer.layerDef.id.startsWith("note_import")) {
|
||||
filteredLayer.isDisplayed.setData(false)
|
||||
}
|
||||
}
|
||||
|
||||
for (const paramsKey in params) {
|
||||
if (paramsKey.startsWith("layer-")) {
|
||||
const layerName = paramsKey.substring("layer-".length)
|
||||
const key = params[paramsKey].toLowerCase().trim()
|
||||
const isDisplayed = loadData && (key === "true" || key === "force")
|
||||
const layer = fl.find((l) => l.layerDef.id === layerName)
|
||||
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) => {
|
||||
if (!isDisplayed) {
|
||||
console.warn("Forcing layer " + paramsKey + " as true")
|
||||
layer.isDisplayed.setData(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("Creating a map width ", width, height, params.scalingFactor)
|
||||
const pngCreator = new PngMapCreator(state, {
|
||||
width: 4 * width,
|
||||
height: 4 * height,
|
||||
})
|
||||
png = await pngCreator.CreatePng(this.options.freeComponentId, this._state)
|
||||
if(!png){
|
||||
throw "PngCreator did not output anything..."
|
||||
}
|
||||
}
|
||||
|
||||
for (const paramsKey in params) {
|
||||
if (paramsKey.startsWith("layer-")) {
|
||||
const layerName = paramsKey.substring("layer-".length)
|
||||
const key = params[paramsKey].toLowerCase().trim()
|
||||
const isDisplayed = loadData && (key === "true" || key === "force")
|
||||
const layer = fl.find((l) => l.layerDef.id === layerName)
|
||||
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) => {
|
||||
if (!isDisplayed) {
|
||||
console.warn("Forcing layer " + paramsKey + " as true")
|
||||
layer.isDisplayed.setData(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("Creating a map width ", width, height, params.scalingFactor)
|
||||
const pngCreator = new PngMapCreator(state, {
|
||||
width: 4 * width,
|
||||
height: 4 * height,
|
||||
})
|
||||
const png = await pngCreator.CreatePng(this._state)
|
||||
svgImage.setAttribute("xlink:href", await SvgToPdfPage.blobToBase64(png))
|
||||
smallestRect.parentElement.insertBefore(svgImage, smallestRect)
|
||||
await this.prepareElement(svgImage, [])
|
||||
|
@ -916,7 +982,7 @@ class SvgToPdfPage {
|
|||
|
||||
export class SvgToPdf {
|
||||
public static readonly templates: Record<
|
||||
"flyer_a4" | "poster_a3" | "poster_a2" | "current_view_a4",
|
||||
"flyer_a4" | "poster_a3" | "poster_a2" | "current_view_a4" | "current_view_a3",
|
||||
{ pages: string[]; description: string | Translation; isPublic: boolean }
|
||||
> = {
|
||||
flyer_a4: {
|
||||
|
@ -938,8 +1004,13 @@ export class SvgToPdf {
|
|||
isPublic: false
|
||||
},
|
||||
current_view_a4: {
|
||||
pages:["./assets/templates/CurrentMapWithHeaderA4.svg"],
|
||||
description: "Export a PDF (A4, portrait) of the current view",
|
||||
pages: ["./assets/templates/CurrentMapWithHeaderA4.svg"],
|
||||
description: "Export a PDF (A4, landscape) of the current view",
|
||||
isPublic: true
|
||||
},
|
||||
current_view_a3: {
|
||||
pages: ["./assets/templates/CurrentMapWithHeaderA3.svg"],
|
||||
description: "Export a PDF (A3, portrait) of the current view",
|
||||
isPublic: true
|
||||
}
|
||||
}
|
||||
|
@ -965,7 +1036,10 @@ export class SvgToPdf {
|
|||
this._pages = pages.map((page) => new SvgToPdfPage(page, state, options))
|
||||
}
|
||||
|
||||
public async ConvertSvg(language: string): Promise<void> {
|
||||
/**
|
||||
* Construct the PDF (including the maps to create), offers them to the user to downlaod.
|
||||
*/
|
||||
public async ExportPdf(language: string): Promise<void> {
|
||||
console.log("Building svg...")
|
||||
const firstPage = this._pages[0]._svgRoot
|
||||
const width = SvgToPdfInternals.attrNumber(firstPage, "width")
|
||||
|
@ -975,13 +1049,12 @@ export class SvgToPdf {
|
|||
await this.Prepare(language)
|
||||
console.log("Global prepare done")
|
||||
|
||||
|
||||
this._status.setData("Maps are rendered, building pdf")
|
||||
new FixedUiElement("").AttachTo("extradiv")
|
||||
console.log("Pages are prepared")
|
||||
|
||||
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) {
|
||||
|
@ -1022,28 +1095,6 @@ export class SvgToPdf {
|
|||
return allTranslations
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares all the minimaps
|
||||
*/
|
||||
public async Prepare(language1: string): Promise<SvgToPdf> {
|
||||
for (const page of this._pages) {
|
||||
await page.Prepare()
|
||||
await page.PrepareLanguage(language1)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
public async PrepareLanguages(languages: string[]): Promise<boolean> {
|
||||
for (const page of this._pages) {
|
||||
// Load all languages at once.
|
||||
// We don't parallelize the pages, as they'll probably reload the same languages anyway (and they are cached)
|
||||
await Promise.all(
|
||||
languages.map(async (language) => await page.PrepareLanguage(language))
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
getTranslation(translationKey: string, language: string, strict: boolean = false) {
|
||||
for (const page of this._pages) {
|
||||
const tr = page.extractTranslation(translationKey, language, strict)
|
||||
|
@ -1057,4 +1108,15 @@ export class SvgToPdf {
|
|||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares all the minimaps
|
||||
*/
|
||||
private async Prepare(language1: string): Promise<SvgToPdf> {
|
||||
for (const page of this._pages) {
|
||||
await page.Prepare()
|
||||
await page.PrepareLanguage(language1)
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
|
189
public/assets/templates/CurrentMapWithHeaderA3.svg
Normal file
189
public/assets/templates/CurrentMapWithHeaderA3.svg
Normal file
|
@ -0,0 +1,189 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="297mm"
|
||||
height="420mm"
|
||||
viewBox="0 0 297 420"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)"
|
||||
sodipodi:docname="CurrentMapWithHeaderA3.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs33">
|
||||
<rect
|
||||
x="41.54771"
|
||||
y="103.43336"
|
||||
width="118.76163"
|
||||
height="107.62454"
|
||||
id="rect19815" />
|
||||
<rect
|
||||
x="730.99915"
|
||||
y="857.75903"
|
||||
width="646.31287"
|
||||
height="26.69614"
|
||||
id="rect10143" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffe1d9"
|
||||
bordercolor="#111111"
|
||||
borderopacity="1"
|
||||
inkscape:pageshadow="0"
|
||||
inkscape:pageopacity="1"
|
||||
inkscape:pagecheckerboard="1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:zoom="0.48119622"
|
||||
inkscape:cx="338.73915"
|
||||
inkscape:cy="566.29705"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="995"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer2"
|
||||
inkscape:snap-global="false" />
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="bg"
|
||||
style="display:inline">
|
||||
<rect
|
||||
style="fill:none;stroke:#000000;stroke-width:1.34605;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect27895"
|
||||
width="290.34955"
|
||||
height="403.45847"
|
||||
x="3.7768779"
|
||||
y="6.4455185"
|
||||
ry="0" />
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:0.456196;stroke:#000000;stroke-width:0.581828;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect28206"
|
||||
width="203.38158"
|
||||
height="35.362419"
|
||||
x="6.3702731"
|
||||
y="9.6101332"
|
||||
ry="3.858089"
|
||||
rx="4.3605742" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="scale(0.26458333)"
|
||||
id="text4911"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect4913);fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="scale(0.26458333)"
|
||||
id="text10253"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect10255);fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,14.472331,73.799994)"
|
||||
id="text56705"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:0.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect56707);display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1"><tspan
|
||||
x="0"
|
||||
y="0"
|
||||
id="tspan891"><tspan
|
||||
style="font-size:13.3333px;-inkscape-font-specification:'sans-serif, Normal'"
|
||||
id="tspan889">$map(current)</tspan></tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,12.08115,27.672609)"
|
||||
id="text3510"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:0;font-family:sans-serif;white-space:pre;shape-inside:url(#rect3512);fill:#000000;fill-opacity:1;stroke:#000000;stroke-opacity:1"><tspan
|
||||
x="0"
|
||||
y="0"
|
||||
id="tspan895"><tspan
|
||||
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
|
||||
id="tspan893">$general.pdf.attr
|
||||
</tspan></tspan><tspan
|
||||
x="0"
|
||||
y="16.799999"
|
||||
id="tspan899"><tspan
|
||||
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
|
||||
id="tspan897">$general.pdf.attrBackground
|
||||
</tspan></tspan><tspan
|
||||
x="0"
|
||||
y="35.692733"
|
||||
id="tspan905"><tspan
|
||||
style="font-size:16px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
|
||||
id="tspan901">$general.pdf.generatedWith</tspan><tspan
|
||||
style="font-size:18.6667px;line-height:1.05;-inkscape-font-specification:'sans-serif, Normal'"
|
||||
id="tspan903">
|
||||
</tspan></tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="scale(0.26458333)"
|
||||
id="text19136"
|
||||
style="font-size:16px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:center;white-space:pre;shape-inside:url(#rect19138);fill:#000000;stroke:#000000;stroke-width:0.377953;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,-308.35032,184.63585)"
|
||||
id="text10141"
|
||||
style="font-size:16px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';text-align:end;white-space:pre;shape-inside:url(#rect10143);fill:#000000;fill-opacity:0.914749;stroke:#ff0000;stroke-width:3.77953;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none"><tspan
|
||||
x="1182.4844"
|
||||
y="871.91602"
|
||||
id="tspan909"><tspan
|
||||
style="fill-opacity:1;stroke:none"
|
||||
id="tspan907">$general.pdf.versionInfo</tspan></tspan></text>
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
style="display:inline">
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="scale(0.26458333)"
|
||||
id="text62796"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect62798);fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
||||
x="102.80793"
|
||||
y="16.415634"
|
||||
id="text8611-8"><tspan
|
||||
sodipodi:role="line"
|
||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:7.05556px;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583"
|
||||
x="102.80793"
|
||||
y="16.415634"
|
||||
id="tspan8613-8" /></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="scale(0.26458333)"
|
||||
id="text81704"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect81706);fill:#000000;fill-opacity:1;stroke:none" />
|
||||
<text
|
||||
xml:space="preserve"
|
||||
transform="matrix(0.26458333,0,0,0.26458333,11.738978,20.267151)"
|
||||
id="text135030"
|
||||
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect135032);fill:#000000;fill-opacity:1;stroke:none"><tspan
|
||||
x="0"
|
||||
y="0"
|
||||
id="tspan913"><tspan
|
||||
style="font-weight:bold;font-size:34.6667px;-inkscape-font-specification:'sans-serif, Bold'"
|
||||
id="tspan911">${title}</tspan></tspan></text>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:1.25;font-family:sans-serif;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
|
||||
x="105.86118"
|
||||
y="116.25558"
|
||||
id="text53309"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan53307"
|
||||
style="stroke-width:0.264583"
|
||||
x="105.86118"
|
||||
y="116.25558" /><tspan
|
||||
sodipodi:role="line"
|
||||
style="stroke-width:0.264583"
|
||||
id="tspan53311"
|
||||
x="105.86118"
|
||||
y="129.4847" /></text>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.1 KiB |
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 8.1 KiB |
6
test.ts
6
test.ts
|
@ -23,9 +23,11 @@ async function testPdf() {
|
|||
SvgToPdf.templates["flyer_a4"].pages.map((url) => Utils.download(url))
|
||||
)
|
||||
console.log("Building svg")
|
||||
const pdf = new SvgToPdf("Test", svgs, {})
|
||||
const pdf = new SvgToPdf("Test", svgs, {
|
||||
freeComponentId:"extradiv"
|
||||
})
|
||||
new VariableUiElement(pdf.status).AttachTo("maindiv")
|
||||
await pdf.ConvertSvg("nl")
|
||||
await pdf.ExportPdf("nl")
|
||||
}
|
||||
|
||||
testPdf().then((_) => console.log("All done"))
|
||||
|
|
|
@ -38,7 +38,6 @@
|
|||
<body>
|
||||
|
||||
|
||||
<span class="absolute" id="belowmap" style="z-index: -1; visibility: hidden">Below</span>
|
||||
<div class="h-full" id="maindiv">
|
||||
<div id="default-main h-full">
|
||||
<div class="w-full h-screen flex flex-col items-center justify-between p-8">
|
||||
|
@ -64,6 +63,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="belowmap" style="z-index: -1">Below</div>
|
||||
|
||||
<script>
|
||||
|
||||
|
|
Loading…
Reference in a new issue