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 maxAge = this._maxAgeSeconds
|
||||||
const timeDiff = Date.now() - date
|
const timeDiff = Date.now() - date
|
||||||
if (timeDiff >= maxAge) {
|
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)
|
await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, undefined)
|
||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
|
|
|
@ -65,14 +65,14 @@ export default class GeoJsonSource implements FeatureSource {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.LoadJSONFrom(url, eventsource, layer)
|
this.LoadJSONFrom(url, eventsource, layer)
|
||||||
.then((fs) => console.log("Loaded",fs.length, "features from", url))
|
.then((fs) => console.debug("Loaded",fs.length, "features from", url))
|
||||||
.catch((err) => console.error("Could not load ", url, "due to", err))
|
.catch((err) => console.warn("Could not load ", url, "due to", err))
|
||||||
return true // data is loaded, we can safely unregister
|
return true // data is loaded, we can safely unregister
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.LoadJSONFrom(url, eventsource, layer)
|
this.LoadJSONFrom(url, eventsource, layer)
|
||||||
.then((fs) => console.log("Loaded",fs.length, "features from", url))
|
.then((fs) => console.debug("Loaded",fs.length, "features from", url))
|
||||||
.catch((err) => console.error("Could not load ", url, "due to", err))
|
.catch((err) => console.warn("Could not load ", url, "due to", err))
|
||||||
}
|
}
|
||||||
this.features = eventsource
|
this.features = eventsource
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ export class AvailableRasterLayers {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
properties: {
|
properties: {
|
||||||
name: "MapTiler",
|
name: "MapTiler",
|
||||||
url: null,
|
url: "https://api.maptiler.com/maps/15cc8f61-0353-4be6-b8da-13daea5f7432/style.json?key=GvoVAJgu46I5rZapJuAy",
|
||||||
category: "osmbasedmap",
|
category: "osmbasedmap",
|
||||||
id: "maptiler",
|
id: "maptiler",
|
||||||
attribution: {
|
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()
|
this.fullNodeDatabase = new FullNodeDatabaseSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -181,9 +181,10 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
}
|
}
|
||||||
currentViewIndex++
|
currentViewIndex++
|
||||||
return <Feature[]>[bbox.asGeoJson({
|
return <Feature[]>[bbox.asGeoJson({
|
||||||
zoom: this.mapProperties.zoom.data,
|
zoom: this.mapProperties.zoom.data,
|
||||||
...this.mapProperties.location.data,
|
...this.mapProperties.location.data,
|
||||||
id: "current_view" }
|
id: "current_view"
|
||||||
|
}
|
||||||
)];
|
)];
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -240,39 +241,6 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.featureProperties,
|
this.featureProperties,
|
||||||
fs.layer.layerDef.maxAgeOfCache
|
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) => {
|
this.floors = this.featuresInView.features.stabilized(500).map((features) => {
|
||||||
|
@ -317,6 +285,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
|
||||||
this.changes
|
this.changes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
this.showNormalDataOn(this.map)
|
||||||
this.initActors()
|
this.initActors()
|
||||||
this.addLastClick(lastClick)
|
this.addLastClick(lastClick)
|
||||||
this.drawSpecialLayers()
|
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
|
* 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 DownloadHelper from "./DownloadHelper";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import type {PriviligedLayerType} from "../../Models/Constants";
|
import type {PriviligedLayerType} from "../../Models/Constants";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
|
||||||
export let state: SpecialVisualizationState
|
export let state: SpecialVisualizationState
|
||||||
|
|
||||||
export let extension: string
|
export let extension: string
|
||||||
export let mimetype: 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 mainText: Translation
|
||||||
export let helperText: Translation
|
export let helperText: Translation
|
||||||
export let metaIsIncluded: boolean
|
export let metaIsIncluded: boolean
|
||||||
|
@ -26,51 +27,63 @@
|
||||||
let isExporting = false
|
let isExporting = false
|
||||||
let isError = false
|
let isError = false
|
||||||
|
|
||||||
|
let status: UIEventSource<string> = new UIEventSource<string>(undefined)
|
||||||
|
|
||||||
async function clicked() {
|
async function clicked() {
|
||||||
isExporting = true
|
isExporting = true
|
||||||
const gpsLayer = state.layerState.filteredLayers.get(
|
const gpsLayer = state.layerState.filteredLayers.get(
|
||||||
<PriviligedLayerType>"gps_location"
|
<PriviligedLayerType>"gps_location"
|
||||||
)
|
)
|
||||||
state.lastClickObject.features.setData([])
|
state.lastClickObject.features.setData([])
|
||||||
|
|
||||||
const gpsIsDisplayed = gpsLayer.isDisplayed.data
|
const gpsIsDisplayed = gpsLayer.isDisplayed.data
|
||||||
try {
|
try {
|
||||||
gpsLayer.isDisplayed.setData(false)
|
gpsLayer.isDisplayed.setData(false)
|
||||||
const geojson: FeatureCollection = downloadHelper.getCleanGeoJson(metaIsIncluded)
|
const geojson: FeatureCollection = downloadHelper.getCleanGeoJson(metaIsIncluded)
|
||||||
const name = state.layout.id
|
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
|
let data: Blob | string
|
||||||
if (typeof promise === "string") {
|
if (typeof promise === "string") {
|
||||||
data = promise
|
data = promise
|
||||||
} else if (typeof promise["then"] === "function") {
|
} else if (typeof promise["then"] === "function") {
|
||||||
data = await <Promise<Blob | string>> promise
|
data = await <Promise<Blob | string>>promise
|
||||||
} else {
|
} else {
|
||||||
data = <Blob>promise
|
data = <Blob>promise
|
||||||
}
|
}
|
||||||
|
if (!data) {
|
||||||
|
return
|
||||||
|
}
|
||||||
console.log("Got data", data)
|
console.log("Got data", data)
|
||||||
Utils.offerContentsAsDownloadableFile(
|
Utils.offerContentsAsDownloadableFile(
|
||||||
data,
|
data,
|
||||||
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.${extension}`,
|
title,
|
||||||
{
|
{
|
||||||
mimetype,
|
mimetype,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
isError = true
|
isError = true
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
isExporting = false
|
||||||
|
gpsLayer.isDisplayed.setData(gpsIsDisplayed)
|
||||||
}
|
}
|
||||||
gpsLayer.isDisplayed.setData(gpsIsDisplayed)
|
|
||||||
|
|
||||||
isExporting = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if isError}
|
{#if isError}
|
||||||
<Tr cls="alert" t={Translations.t.error}/>
|
<Tr cls="alert" t={Translations.t.general.error}/>
|
||||||
{:else if isExporting}
|
{:else if isExporting}
|
||||||
<Loading>
|
<Loading>
|
||||||
<Tr t={t.exporting}/>
|
{#if $status}
|
||||||
|
{$status}
|
||||||
|
{:else}
|
||||||
|
<Tr t={t.exporting}/>
|
||||||
|
{/if}
|
||||||
</Loading>
|
</Loading>
|
||||||
{:else}
|
{:else}
|
||||||
<button class="flex w-full" on:click={clicked}>
|
<button class="flex w-full" on:click={clicked}>
|
||||||
|
|
|
@ -1,14 +1,21 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|
||||||
import type {SpecialVisualizationState} from "../SpecialVisualization";
|
|
||||||
import Loading from "../Base/Loading.svelte";
|
import Loading from "../Base/Loading.svelte";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import Tr from "../Base/Tr.svelte";
|
import Tr from "../Base/Tr.svelte";
|
||||||
import DownloadHelper from "./DownloadHelper";
|
import DownloadHelper from "./DownloadHelper";
|
||||||
import DownloadButton from "./DownloadButton.svelte";
|
import DownloadButton from "./DownloadButton.svelte";
|
||||||
import {GeoOperations} from "../../Logic/GeoOperations";
|
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
|
let isLoading = state.dataIsLoading
|
||||||
|
|
||||||
const t = Translations.t.general.download
|
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>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,7 +116,7 @@
|
||||||
extension="pdf"
|
extension="pdf"
|
||||||
mainText={t.downloadAsPdf}
|
mainText={t.downloadAsPdf}
|
||||||
helperText={t.downloadAsPdfHelper}
|
helperText={t.downloadAsPdfHelper}
|
||||||
construct={_ => state.mapProperties.exportAsPng(4)}
|
construct={constructPdf}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -199,7 +199,7 @@ export class MapLibreAdaptor implements MapProperties, ExportableMap {
|
||||||
return url
|
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.width = drawOn.style.width || drawOn.width + "px"
|
||||||
drawOn.style.height = drawOn.style.height || drawOn.height + "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)
|
console.log("Canvas size:", drawOn.width, drawOn.height)
|
||||||
const ctx = drawOn.getContext("2d")
|
const ctx = drawOn.getContext("2d")
|
||||||
// Set up CSS size.
|
// 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")
|
console.log("Getting markers")
|
||||||
// MapLibreAdaptor.setDpi(drawOn, ctx, 1)
|
// MapLibreAdaptor.setDpi(drawOn, ctx, 1)
|
||||||
const markers = await this.drawMarkers(dpiFactor)
|
const markers = await this.drawMarkers(dpiFactor)
|
||||||
console.log("Drawing markers (" + markers.width + "*" + markers.height + ") onto drawOn (" + drawOn.width + "*" + drawOn.height + ")")
|
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.drawImage(markers, 0, 0, drawOn.width, drawOn.height)
|
||||||
ctx.scale(dpiFactor, dpiFactor)
|
ctx.scale(dpiFactor, dpiFactor)
|
||||||
markers.toBlob(blob => {
|
|
||||||
Utils.offerContentsAsDownloadableFile(blob, "markers.json")
|
|
||||||
})
|
|
||||||
this._maplibreMap.data?.resize()
|
this._maplibreMap.data?.resize()
|
||||||
return await new Promise<Blob>(resolve => drawOn.toBlob(blob => resolve(blob)))
|
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.
|
* Exports the background map and lines to PNG.
|
||||||
* Markers are _not_ rendered
|
* 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
|
const map = this._maplibreMap.data
|
||||||
// We draw the maplibre-map onto the canvas. This does not export markers
|
// We draw the maplibre-map onto the canvas. This does not export markers
|
||||||
// Inspiration by https://github.com/mapbox/mapbox-gl-js/issues/2766
|
// Inspiration by https://github.com/mapbox/mapbox-gl-js/issues/2766
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
import { Map } from "@onsvisual/svelte-maps";
|
import { Map } from "@onsvisual/svelte-maps";
|
||||||
import type { Map as MaplibreMap } from "maplibre-gl";
|
import type { Map as MaplibreMap } from "maplibre-gl";
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
|
import {AvailableRasterLayers} from "../../Models/RasterLayers";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,7 +25,7 @@
|
||||||
$map.resize();
|
$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>
|
</script>
|
||||||
<main>
|
<main>
|
||||||
<Map bind:center={center}
|
<Map bind:center={center}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import ThemeViewState from "../Models/ThemeViewState"
|
import ThemeViewState from "../Models/ThemeViewState"
|
||||||
import SvelteUIElement from "../UI/Base/SvelteUIElement"
|
import {Utils} from "../Utils"
|
||||||
import MaplibreMap from "../UI/Map/MaplibreMap.svelte"
|
import {UIEventSource} from "../Logic/UIEventSource"
|
||||||
import { Utils } from "../Utils"
|
import {Map as MlMap} from "maplibre-gl"
|
||||||
import { UIEventSource } from "../Logic/UIEventSource"
|
import {MapLibreAdaptor} from "../UI/Map/MapLibreAdaptor";
|
||||||
|
import {AvailableRasterLayers} from "../Models/RasterLayers";
|
||||||
|
|
||||||
export interface PngMapCreatorOptions {
|
export interface PngMapCreatorOptions {
|
||||||
readonly width: number
|
readonly width: number
|
||||||
|
@ -23,33 +24,56 @@ export class PngMapCreator {
|
||||||
* Creates a base64-encoded PNG image
|
* Creates a base64-encoded PNG image
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
public async CreatePng(status: UIEventSource<string>): Promise<Blob> {
|
public async CreatePng(freeComponentId: string, status?: UIEventSource<string>): Promise<Blob> {
|
||||||
const div = document.createElement("div")
|
const div = document.createElement("div")
|
||||||
div.id = "mapdiv-" + PngMapCreator.id
|
div.id = "mapdiv-" + PngMapCreator.id
|
||||||
|
div.style.width = this._options.width + "mm"
|
||||||
|
div.style.height = this._options.height + "mm"
|
||||||
|
|
||||||
PngMapCreator.id++
|
PngMapCreator.id++
|
||||||
const layout = this._state.layout
|
const layout = this._state.layout
|
||||||
|
|
||||||
function setState(msg: string) {
|
function setState(msg: string) {
|
||||||
status.setData(layout.id + ": " + msg)
|
status?.setData(layout.id + ": " + msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
setState("Initializing map")
|
setState("Initializing map")
|
||||||
const map = this._state.map
|
|
||||||
new SvelteUIElement(MaplibreMap, { map })
|
const settings = this._state.mapProperties
|
||||||
.SetStyle(
|
const l = settings.location.data
|
||||||
"width: " + this._options.width + "mm; height: " + this._options.height + "mm; border: 2px solid red;"
|
|
||||||
)
|
document.getElementById(freeComponentId).appendChild(div)
|
||||||
.AttachTo("extradiv")
|
const mapElem = new MlMap({
|
||||||
map.data.resize()
|
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")
|
setState("Waiting for the data")
|
||||||
await this._state.dataIsLoading.AsPromise((loading) => !loading)
|
await this._state.dataIsLoading.AsPromise((loading) => !loading)
|
||||||
setState("Waiting for styles to be fully loaded")
|
setState("Waiting for styles to be fully loaded")
|
||||||
while (!map?.data?.isStyleLoaded()) {
|
while (!map?.data?.isStyleLoaded()) {
|
||||||
|
console.log("Waiting for the style to be loaded...")
|
||||||
await Utils.waitFor(250)
|
await Utils.waitFor(250)
|
||||||
}
|
}
|
||||||
// Some extra buffer...
|
// Some extra buffer...
|
||||||
await Utils.waitFor(1000)
|
await Utils.waitFor(1000)
|
||||||
setState("Exporting png")
|
setState("Exporting png")
|
||||||
console.log("Loading for", this._state.layout.id, "is done")
|
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")
|
const png = await mla.exportAsPng(6)
|
||||||
return this._state.mapProperties.exportAsPng(4)
|
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 Hash from "../Logic/Web/Hash"
|
||||||
import ThemeViewState from "../Models/ThemeViewState"
|
import ThemeViewState from "../Models/ThemeViewState"
|
||||||
import {Store, UIEventSource} from "../Logic/UIEventSource"
|
import {Store, UIEventSource} from "../Logic/UIEventSource"
|
||||||
import {FixedUiElement} from "../UI/Base/FixedUiElement"
|
|
||||||
|
|
||||||
class SvgToPdfInternals {
|
class SvgToPdfInternals {
|
||||||
private static readonly dummyDoc: jsPDF = new jsPDF()
|
private static readonly dummyDoc: jsPDF = new jsPDF()
|
||||||
|
@ -260,15 +259,29 @@ class SvgToPdfInternals {
|
||||||
const ry = SvgToPdfInternals.attrNumber(element, "ry", false) ?? 0
|
const ry = SvgToPdfInternals.attrNumber(element, "ry", false) ?? 0
|
||||||
const rx = SvgToPdfInternals.attrNumber(element, "rx", false) ?? 0
|
const rx = SvgToPdfInternals.attrNumber(element, "rx", false) ?? 0
|
||||||
const css = SvgToPdfInternals.css(element)
|
const css = SvgToPdfInternals.css(element)
|
||||||
|
this.doc.saveGraphicsState()
|
||||||
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
|
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")
|
this.doc.roundedRect(x, y, width, height, rx, ry, "F")
|
||||||
}
|
}
|
||||||
if (css["stroke"] && css["stroke"] !== "none") {
|
if (css["stroke"] && css["stroke"] !== "none") {
|
||||||
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
|
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
|
||||||
this.doc.setDrawColor(css["stroke"] ?? "black")
|
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.roundedRect(x, y, width, height, rx, ry, "S")
|
||||||
}
|
}
|
||||||
|
this.doc.restoreGraphicsState()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -290,13 +303,27 @@ class SvgToPdfInternals {
|
||||||
}
|
}
|
||||||
|
|
||||||
private drawTspan(tspan: Element) {
|
private drawTspan(tspan: Element) {
|
||||||
if (tspan.textContent == "") {
|
const txt = tspan.textContent
|
||||||
|
if (txt == "") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const x = SvgToPdfInternals.attrNumber(tspan, "x")
|
const x = SvgToPdfInternals.attrNumber(tspan, "x")
|
||||||
const y = SvgToPdfInternals.attrNumber(tspan, "y")
|
const y = SvgToPdfInternals.attrNumber(tspan, "y")
|
||||||
|
|
||||||
const css = SvgToPdfInternals.css(tspan)
|
const css = SvgToPdfInternals.css(tspan)
|
||||||
|
const imageMatch = txt.match(/\$img\(([^)])+\)/)
|
||||||
|
if (imageMatch) {
|
||||||
|
const [key, width, height] = imageMatch[1].split(",").map(s => s.trim())
|
||||||
|
const url = key.startsWith("http") ? key : this.extractTranslation("${" + key + "}")
|
||||||
|
const img = this._images[url]
|
||||||
|
console.log("Drawing an injected image", {key, url, img: img.src})
|
||||||
|
this.doc.addImage(
|
||||||
|
img.src
|
||||||
|
, x, y, Number(width), Number(height)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let maxWidth: number = undefined
|
let maxWidth: number = undefined
|
||||||
if (css["shape-inside"]) {
|
if (css["shape-inside"]) {
|
||||||
const matched = css["shape-inside"].match(/url\(#([a-zA-Z0-9-]+)\)/)
|
const matched = css["shape-inside"].match(/url\(#([a-zA-Z0-9-]+)\)/)
|
||||||
|
@ -322,7 +349,6 @@ class SvgToPdfInternals {
|
||||||
this.doc.setTextColor("black")
|
this.doc.setTextColor("black")
|
||||||
}
|
}
|
||||||
let fontsize = parseFloat(css["font-size"])
|
let fontsize = parseFloat(css["font-size"])
|
||||||
|
|
||||||
this.doc.setFontSize(fontsize * 2.5)
|
this.doc.setFontSize(fontsize * 2.5)
|
||||||
|
|
||||||
let textTemplate = tspan.textContent.split(" ")
|
let textTemplate = tspan.textContent.split(" ")
|
||||||
|
@ -339,7 +365,13 @@ class SvgToPdfInternals {
|
||||||
addSpace = false
|
addSpace = false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if (text.startsWith(`$\{`)) {
|
||||||
|
if (addSpace) {
|
||||||
|
result += " "
|
||||||
|
}
|
||||||
|
result += this.extractTranslation(text)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if (!text.startsWith("$")) {
|
if (!text.startsWith("$")) {
|
||||||
if (addSpace) {
|
if (addSpace) {
|
||||||
result += " "
|
result += " "
|
||||||
|
@ -496,11 +528,16 @@ class SvgToPdfInternals {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SvgToPdfOptions {
|
export interface SvgToPdfOptions {
|
||||||
|
freeComponentId: string,
|
||||||
disableMaps?: false | true
|
disableMaps?: false | true
|
||||||
textSubstitutions?: Record<string, string>
|
textSubstitutions?: Record<string, string>
|
||||||
beforePage?: (i: number) => void
|
beforePage?: (i: number) => void
|
||||||
overrideLocation?: { lat: number; lon: number },
|
overrideLocation?: { lat: number; lon: number },
|
||||||
disableDataLoading?: boolean | false
|
disableDataLoading?: boolean | false,
|
||||||
|
/**
|
||||||
|
* Override all the maps to generate with this map
|
||||||
|
*/
|
||||||
|
state?: ThemeViewState
|
||||||
}
|
}
|
||||||
|
|
||||||
class SvgToPdfPage {
|
class SvgToPdfPage {
|
||||||
|
@ -569,7 +606,6 @@ class SvgToPdfPage {
|
||||||
if (element.tagName === "rect") {
|
if (element.tagName === "rect") {
|
||||||
this.rects[element.id] = <SVGRectElement>element
|
this.rects[element.id] = <SVGRectElement>element
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.tagName === "image") {
|
if (element.tagName === "image") {
|
||||||
await this.loadImage(element)
|
await this.loadImage(element)
|
||||||
}
|
}
|
||||||
|
@ -577,6 +613,14 @@ class SvgToPdfPage {
|
||||||
if (element.tagName === "tspan" && element.childElementCount == 0) {
|
if (element.tagName === "tspan" && element.childElementCount == 0) {
|
||||||
const specialValues = element.textContent.split(" ").filter((t) => t.startsWith("$"))
|
const specialValues = element.textContent.split(" ").filter((t) => t.startsWith("$"))
|
||||||
for (let specialValue of specialValues) {
|
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(
|
const importMatch = element.textContent.match(
|
||||||
/\$import ([a-zA-Z-_0-9.? ]+) as ([a-zA-Z0-9]+)/
|
/\$import ([a-zA-Z-_0-9.? ]+) as ([a-zA-Z0-9]+)/
|
||||||
)
|
)
|
||||||
|
@ -596,6 +640,7 @@ class SvgToPdfPage {
|
||||||
this.options.textSubstitutions
|
this.options.textSubstitutions
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.textContent.startsWith("$map(")) {
|
if (element.textContent.startsWith("$map(")) {
|
||||||
mapTextSpecs.push(<any>element)
|
mapTextSpecs.push(<any>element)
|
||||||
}
|
}
|
||||||
|
@ -642,7 +687,12 @@ class SvgToPdfPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const mapSpec of mapSpecs) {
|
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
|
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? ]+)(.*)/)
|
const pathPart = text.match(/\$(([_a-zA-Z0-9? ]+\.)+[_a-zA-Z0-9? ]+)(.*)/)
|
||||||
if (pathPart === null) {
|
if (pathPart === null) {
|
||||||
return text
|
return text
|
||||||
|
@ -715,8 +769,8 @@ class SvgToPdfPage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadImage(element: Element): Promise<void> {
|
private loadImage(element: Element | string): Promise<void> {
|
||||||
const xlink = element.getAttribute("xlink:href")
|
const xlink = typeof element === "string" ? element : element.getAttribute("xlink:href")
|
||||||
let img = document.createElement("img")
|
let img = document.createElement("img")
|
||||||
|
|
||||||
if (xlink.startsWith("data:image/svg+xml;")) {
|
if (xlink.startsWith("data:image/svg+xml;")) {
|
||||||
|
@ -752,11 +806,11 @@ class SvgToPdfPage {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replaces a mapSpec with the appropriate map
|
* Replaces a mapSpec with the appropriate map
|
||||||
* @param mapSpec
|
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
|
private async prepareMap(mapSpec: SVGTSpanElement, loadData: boolean): Promise<void> {
|
||||||
private async prepareMap(mapSpec: SVGTSpanElement, loadData: boolean = true): Promise<void> {
|
if (this.options.disableMaps) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// Upper left point of the tspan
|
// Upper left point of the tspan
|
||||||
const {x, y} = SvgToPdfInternals.GetActualXY(mapSpec)
|
const {x, y} = SvgToPdfInternals.GetActualXY(mapSpec)
|
||||||
|
|
||||||
|
@ -766,11 +820,6 @@ class SvgToPdfPage {
|
||||||
textElement = textElement.parentElement
|
textElement = textElement.parentElement
|
||||||
}
|
}
|
||||||
const spec = textElement.textContent
|
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 smallestRect: SVGRectElement = undefined
|
||||||
let smallestSurface: number = undefined
|
let smallestSurface: number = undefined
|
||||||
|
@ -783,6 +832,7 @@ class SvgToPdfPage {
|
||||||
const h = SvgToPdfInternals.attrNumber(rect, "height")
|
const h = SvgToPdfInternals.attrNumber(rect, "height")
|
||||||
const inBounds = rx <= x && x <= rx + w && ry <= y && y <= ry + h
|
const inBounds = rx <= x && x <= rx + w && ry <= y && y <= ry + h
|
||||||
if (!inBounds) {
|
if (!inBounds) {
|
||||||
|
console.log("Not in bounds: rectangle", id)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const surface = w * h
|
const surface = w * h
|
||||||
|
@ -809,95 +859,111 @@ class SvgToPdfPage {
|
||||||
svgImage.setAttribute("width", "" + width)
|
svgImage.setAttribute("width", "" + width)
|
||||||
svgImage.setAttribute("height", "" + height)
|
svgImage.setAttribute("height", "" + height)
|
||||||
|
|
||||||
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
|
let png: Blob
|
||||||
if (layout === undefined) {
|
if (this.options.state !== undefined) {
|
||||||
console.error("Could not show map with parameters", params)
|
png = await (new PngMapCreator(this.options.state, {
|
||||||
throw (
|
width, height,
|
||||||
"Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
|
}).CreatePng(this.options.freeComponentId))
|
||||||
)
|
} else {
|
||||||
}
|
const match = spec.match(/\$map\(([^)]*)\)$/)
|
||||||
layout.widenFactor = 0
|
if (match === null) {
|
||||||
layout.overpassTimeout = 600
|
throw "Invalid mapspec:" + spec
|
||||||
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 params = SvgToPdfInternals.parseCss(match[1], ",")
|
||||||
const zoom = Number(params["zoom"] ?? params["z"] ?? 14)
|
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
|
||||||
|
if (layout === undefined) {
|
||||||
Hash.hash.setData(undefined)
|
console.error("Could not show map with parameters", params)
|
||||||
// QueryParameters.ClearAll()
|
throw (
|
||||||
const state = new ThemeViewState(layout)
|
"Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
|
||||||
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)
|
layout.widenFactor = 0
|
||||||
} else if (filteredLayer.layerDef.id.startsWith("note_import")) {
|
layout.overpassTimeout = 600
|
||||||
filteredLayer.isDisplayed.setData(false)
|
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))
|
svgImage.setAttribute("xlink:href", await SvgToPdfPage.blobToBase64(png))
|
||||||
smallestRect.parentElement.insertBefore(svgImage, smallestRect)
|
smallestRect.parentElement.insertBefore(svgImage, smallestRect)
|
||||||
await this.prepareElement(svgImage, [])
|
await this.prepareElement(svgImage, [])
|
||||||
|
@ -916,7 +982,7 @@ class SvgToPdfPage {
|
||||||
|
|
||||||
export class SvgToPdf {
|
export class SvgToPdf {
|
||||||
public static readonly templates: Record<
|
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 }
|
{ pages: string[]; description: string | Translation; isPublic: boolean }
|
||||||
> = {
|
> = {
|
||||||
flyer_a4: {
|
flyer_a4: {
|
||||||
|
@ -938,8 +1004,13 @@ export class SvgToPdf {
|
||||||
isPublic: false
|
isPublic: false
|
||||||
},
|
},
|
||||||
current_view_a4: {
|
current_view_a4: {
|
||||||
pages:["./assets/templates/CurrentMapWithHeaderA4.svg"],
|
pages: ["./assets/templates/CurrentMapWithHeaderA4.svg"],
|
||||||
description: "Export a PDF (A4, portrait) of the current view",
|
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
|
isPublic: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -965,7 +1036,10 @@ export class SvgToPdf {
|
||||||
this._pages = pages.map((page) => new SvgToPdfPage(page, state, options))
|
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...")
|
console.log("Building svg...")
|
||||||
const firstPage = this._pages[0]._svgRoot
|
const firstPage = this._pages[0]._svgRoot
|
||||||
const width = SvgToPdfInternals.attrNumber(firstPage, "width")
|
const width = SvgToPdfInternals.attrNumber(firstPage, "width")
|
||||||
|
@ -975,13 +1049,12 @@ export class SvgToPdf {
|
||||||
await this.Prepare(language)
|
await this.Prepare(language)
|
||||||
console.log("Global prepare done")
|
console.log("Global prepare done")
|
||||||
|
|
||||||
|
|
||||||
this._status.setData("Maps are rendered, building pdf")
|
this._status.setData("Maps are rendered, building pdf")
|
||||||
new FixedUiElement("").AttachTo("extradiv")
|
|
||||||
console.log("Pages are prepared")
|
console.log("Pages are prepared")
|
||||||
|
|
||||||
const doc = new jsPDF(mode, undefined, [width, height])
|
const doc = new jsPDF(mode, undefined, [width, height])
|
||||||
doc.advancedAPI((advancedApi) => {
|
doc.advancedAPI((advancedApi) => {
|
||||||
|
const canvas = advancedApi.canvas
|
||||||
for (let i = 0; i < this._pages.length; i++) {
|
for (let i = 0; i < this._pages.length; i++) {
|
||||||
console.log("Rendering page", i)
|
console.log("Rendering page", i)
|
||||||
if (i > 0) {
|
if (i > 0) {
|
||||||
|
@ -1022,28 +1095,6 @@ export class SvgToPdf {
|
||||||
return allTranslations
|
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) {
|
getTranslation(translationKey: string, language: string, strict: boolean = false) {
|
||||||
for (const page of this._pages) {
|
for (const page of this._pages) {
|
||||||
const tr = page.extractTranslation(translationKey, language, strict)
|
const tr = page.extractTranslation(translationKey, language, strict)
|
||||||
|
@ -1057,4 +1108,15 @@ export class SvgToPdf {
|
||||||
}
|
}
|
||||||
return undefined
|
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))
|
SvgToPdf.templates["flyer_a4"].pages.map((url) => Utils.download(url))
|
||||||
)
|
)
|
||||||
console.log("Building svg")
|
console.log("Building svg")
|
||||||
const pdf = new SvgToPdf("Test", svgs, {})
|
const pdf = new SvgToPdf("Test", svgs, {
|
||||||
|
freeComponentId:"extradiv"
|
||||||
|
})
|
||||||
new VariableUiElement(pdf.status).AttachTo("maindiv")
|
new VariableUiElement(pdf.status).AttachTo("maindiv")
|
||||||
await pdf.ConvertSvg("nl")
|
await pdf.ExportPdf("nl")
|
||||||
}
|
}
|
||||||
|
|
||||||
testPdf().then((_) => console.log("All done"))
|
testPdf().then((_) => console.log("All done"))
|
||||||
|
|
|
@ -38,7 +38,6 @@
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
||||||
<span class="absolute" id="belowmap" style="z-index: -1; visibility: hidden">Below</span>
|
|
||||||
<div class="h-full" id="maindiv">
|
<div class="h-full" id="maindiv">
|
||||||
<div id="default-main h-full">
|
<div id="default-main h-full">
|
||||||
<div class="w-full h-screen flex flex-col items-center justify-between p-8">
|
<div class="w-full h-screen flex flex-col items-center justify-between p-8">
|
||||||
|
@ -64,6 +63,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="belowmap" style="z-index: -1">Below</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue