Near-finished flyer

This commit is contained in:
pietervdvn 2022-09-16 02:08:28 +02:00
parent de4e394088
commit ba0046120f
6 changed files with 1386 additions and 11563 deletions

View file

@ -1,17 +1,16 @@
import jsPDF, {Matrix} from "jspdf";
import Translations from "../UI/i18n/Translations";
import {Translation, TypedTranslation} from "../UI/i18n/Translation";
import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
import {PngMapCreator} from "./pngMapCreator";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import {Store, UIEventSource} from "../Logic/UIEventSource";
import {Store} from "../Logic/UIEventSource";
import "../assets/templates/Ubuntu-M-normal.js"
import "../assets/templates/Ubuntu-L-normal.js"
import "../assets/templates/UbuntuMono-B-bold.js"
import {parseSVG, makeAbsolute} from 'svg-path-parser';
import {And} from "../Logic/Tags/And";
import {Tag} from "../Logic/Tags/Tag";
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
import {makeAbsolute, parseSVG} from 'svg-path-parser';
import Translations from "../UI/i18n/Translations";
import {Utils} from "../Utils";
import Locale from "../UI/i18n/Locale";
class SvgToPdfInternals {
private readonly doc: jsPDF;
@ -24,13 +23,17 @@ class SvgToPdfInternals {
private currentMatrixInverted: Matrix;
private readonly _images: Record<string, HTMLImageElement>;
private readonly _layerTranslations: Record<string, Record<string, any>>;
private readonly _rects: Record<string, SVGRectElement>;
private readonly _importedTranslations: Record<string, any>;
constructor(advancedApi: jsPDF, textSubstitutions: Record<string, string>, images: Record<string, HTMLImageElement>, rects: Record<string, SVGRectElement>) {
constructor(advancedApi: jsPDF, textSubstitutions: Record<string, string>, images: Record<string, HTMLImageElement>, rects: Record<string, SVGRectElement>, importedTranslations: Record<string, any>, layerTranslations: Record<string, Record<string, any>>) {
this._layerTranslations = layerTranslations;
this.textSubstitutions = textSubstitutions;
this.doc = advancedApi;
this._images = images;
this._rects = rects;
this._importedTranslations = importedTranslations;
this.currentMatrix = this.doc.unitMatrix;
this.currentMatrixInverted = this.doc.unitMatrix;
}
@ -119,26 +122,46 @@ class SvgToPdfInternals {
return r
};
private drawRect(element: Element) {
private drawRect(element: SVGRectElement) {
const x = Number(element.getAttribute("x"))
const y = Number(element.getAttribute("y"))
const width = Number(element.getAttribute("width"))
const height = Number(element.getAttribute("height"))
const style = element.getAttribute("style")
const css = SvgToPdfInternals.parseCss(style)
if (css["fill-opacity"] !== "0") {
const ry = SvgToPdfInternals.attrNumber(element, "ry", false) ?? 0
const rx = SvgToPdfInternals.attrNumber(element, "rx", false) ?? 0
const css = SvgToPdfInternals.css(element)
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
this.doc.setFillColor(css["fill"] ?? "black")
this.doc.rect(x, y, width, height, "F")
this.doc.roundedRect(x, y, width, height, rx, ry, "F")
}
if (css["stroke"]) {
if (css["stroke"] && css["stroke"] !== "none") {
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
this.doc.setDrawColor(css["stroke"] ?? "black")
this.doc.rect(x, y, width, height, "S")
this.doc.roundedRect(x, y, width, height, rx, ry, "S")
}
return
}
private drawCircle(element: SVGCircleElement) {
const x = Number(element.getAttribute("cx"))
const y = Number(element.getAttribute("cy"))
const r = Number(element.getAttribute("r"))
const css = SvgToPdfInternals.css(element)
if (css["fill-opacity"] !== "0" && css["fill"] !== "none") {
this.doc.setFillColor(css["fill"] ?? "black")
this.doc.circle(x, y, r, "F")
}
if (css["stroke"] && css["stroke"] !== "none") {
this.doc.setLineWidth(Number(css["stroke-width"] ?? 1))
this.doc.setDrawColor(css["stroke"] ?? "black")
this.doc.circle(x, y, r, "S")
}
return
}
private static attr(element: Element, name: string, recurseup: boolean = true): string | undefined {
if (element === null || element === undefined) {
return undefined
@ -187,20 +210,35 @@ class SvgToPdfInternals {
}
private extractTranslation(text: string) {
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) {
return text
}
const path = pathPart[1].split(".")
const rest = pathPart[3] ?? ""
let t: any = Translations.t
const path = pathPart[1].split(".")
if (this._importedTranslations[path[0]]) {
path.splice(0, 1, ...this._importedTranslations[path[0]].split("."))
}
const rest = pathPart[3] ?? ""
if (path[0] === "layer") {
t = this._layerTranslations[Locale.language.data]
if (t === undefined) {
console.error("No layerTranslation available for language " + Locale.language.data)
return text
}
path.splice(0, 1)
}
for (const crumb of path) {
t = t[crumb]
if (t === undefined) {
console.error("No value found to substitute " + text)
console.error("No value found to substitute " + text, "the path is", path)
return undefined
}
}
if (typeof t === "string") {
t = new TypedTranslation({"*": t})
}
if (t instanceof TypedTranslation) {
return (<TypedTranslation<any>>t).Subs(this.textSubstitutions).txt + rest
} else {
@ -324,6 +362,8 @@ class SvgToPdfInternals {
}
private drawPath(element: SVGPathElement): void {
if (element.id === "rect25327")
console.log("Drawing", element.id, element)
const path = element.getAttribute("d")
const parsed: { code: string, x: number, y: number, x2?, y2?, x1?, y1? }[] = parseSVG(path)
makeAbsolute(parsed)
@ -335,28 +375,56 @@ class SvgToPdfInternals {
continue
}
this.doc.path([{op: c.code.toLowerCase(), c: [c.x, c.y]}])
if (c.code === "H") {
const command = {op: "l", c: [c.x, c.y]}
this.doc.path([command])
continue
}
if (c.code === "V") {
const command = {op: "l", c: [c.x, c.y]}
this.doc.path([command])
continue
}
this.doc.path([{op: c.code.toLowerCase(), c: [c.x, c.y]}])
}
//"fill:#ffffff;stroke:#000000;stroke-width:0.8;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:20"
const css = SvgToPdfInternals.css(element)
if (css["color"] && css["color"].toLowerCase() !== "none") {
this.doc.setDrawColor(css["color"])
this.doc.setFillColor(css["fill"])
}
if (css["stroke-width"]) {
this.doc.setLineWidth(Number(css["stroke-width"]))
this.doc.setLineWidth(parseFloat(css["stroke-width"]))
}
if (css["stroke-linejoin"] !== undefined) {
this.doc.setLineJoin(css["stroke-linejoin"])
}
let doFill = false
if (css["fill-rule"] === "evenodd") {
this.doc.fillEvenOdd()
} else if (css["fill"] && css["fill"] !== "none") {
this.doc.setFillColor(css["fill"])
doFill = true
}
if (css["stroke"] && css["stroke"] !== "none") {
this.doc.setDrawColor(css["stroke"])
if (doFill) {
this.doc.fillStroke()
} else {
this.doc.stroke()
}
} else if (doFill) {
this.doc.fill()
}
}
public handleElement(element: SVGSVGElement | Element): void {
const isTransformed = this.setTransform(element)
try {
if (element.tagName === "tspan") {
if (element.childElementCount == 0) {
this.drawTspan(element)
@ -383,9 +451,16 @@ class SvgToPdfInternals {
}
if (element.tagName === "rect") {
this.drawRect(element)
this.drawRect(<any>element)
}
if (element.tagName === "circle") {
this.drawCircle(<any>element)
}
} catch (e) {
console.error("Could not handle element", element, "due to", e)
}
if (isTransformed) {
this.undoTransform()
}
@ -418,35 +493,27 @@ class SvgToPdfInternals {
export interface SvgToPdfOptions {
getFreeDiv: () => string,
disableMaps?: false | true
textSubstitutions?: Record<string, string>, beforePage?: (i: number) => void
textSubstitutions?: Record<string, string>,
beforePage?: (i: number) => void,
}
export class SvgToPdf {
export class SvgToPdfPage {
private images: Record<string, HTMLImageElement> = {}
private rects: Record<string, SVGRectElement> = {}
private readonly _svgRoots: SVGSVGElement[] = [];
private readonly _textSubstitutions: Record<string, string>;
private readonly _beforePage: ((i: number) => void) | undefined;
public readonly _svgRoot: SVGSVGElement;
public readonly _usedTranslations: Set<string> = new Set<string>()
private readonly _freeDivId: () => string;
private readonly _currentState = new UIEventSource<string>("Initing")
public readonly currentState: Store<string>
private readonly _disableMaps: boolean ;
private readonly importedTranslations: Record<string, string> = {}
private readonly layerTranslations: Record<string, Record<string, any>> = {}
private readonly options: SvgToPdfOptions
constructor(pages: string[], options?:SvgToPdfOptions) {
this.currentState = this._currentState
this._textSubstitutions = options?.textSubstitutions ?? {};
this._beforePage = options?.beforePage;
this._freeDivId = options?.getFreeDiv
this._disableMaps = options.disableMaps ?? false
constructor(page: string, options?: SvgToPdfOptions) {
this.options = options ?? (<SvgToPdfOptions>{})
const parser = new DOMParser();
for (const page of pages) {
const xmlDoc = parser.parseFromString(page, "image/svg+xml");
const svgRoot = xmlDoc.getElementsByTagName("svg")[0];
this._svgRoots.push(svgRoot)
}
this._svgRoot = xmlDoc.getElementsByTagName("svg")[0];
}
@ -476,7 +543,6 @@ export class SvgToPdf {
}
this.images[xlink] = img
this.setState("Preparing: loading image " + Object.keys(this.images).length + ": " + img.src.substring(0, 30))
return new Promise((resolve) => {
img.onload = _ => {
resolve()
@ -502,6 +568,17 @@ export class SvgToPdf {
if (translationMatch !== null) {
this._usedTranslations.add(translationMatch[1])
}
const importMatch = element.textContent.match(/\$import ([a-zA-Z-_0-9.? ]+) as ([a-zA-Z0-9]+)/)
if (importMatch !== null) {
const [, pathRaw, as] = importMatch
this.importedTranslations[as] = pathRaw
}
const setPropertyMatch = element.textContent.match(/\$set\(([a-zA-Z-_0-9.?:]+),(.+)\)/)
if (setPropertyMatch) {
this.options.textSubstitutions[setPropertyMatch[1].trim()] = setPropertyMatch[2].trim()
console.log("Setting a property:", setPropertyMatch, this.options.textSubstitutions)
}
if (element.textContent.startsWith("$map(")) {
mapTextSpecs.push(<any>element)
@ -520,10 +597,6 @@ export class SvgToPdf {
private _isPrepared = false;
private setState(message: string) {
this._currentState.setData(message)
}
private async prepareMap(mapSpec: SVGTSpanElement,): Promise<void> {
// Upper left point of the tspan
const {x, y} = SvgToPdfInternals.GetActualXY(mapSpec)
@ -540,7 +613,6 @@ export class SvgToPdf {
}
const params = SvgToPdfInternals.parseCss(match[1], ",")
const ctx = `Preparing map (theme ${params["theme"]})`
this.setState(ctx + "...")
let smallestRect: SVGRectElement = undefined
let smallestSurface: number = undefined;
@ -589,7 +661,7 @@ export class SvgToPdf {
const key = params[paramsKey].toLowerCase().trim()
const layer = layout.layers.find(l => l.id === layerName)
if (key === "force") {
console.log("Forcing minzoom of layer",layer.id)
console.log("Forcing minzoom of layer", layer.id)
layer.minzoom = 0
layer.minzoomVisible = 0
}
@ -619,7 +691,7 @@ export class SvgToPdf {
const key = params[paramsKey].toLowerCase().trim()
const isDisplayed = key === "true" || key === "force";
const layer = state.filteredLayers.data.find(l => l.layerDef.id === layerName)
console.log("Setting ", layer?.layerDef?.id," to visibility", isDisplayed, "(minzoom:", layer?.layerDef?.minzoomVisible, layer?.layerDef?.minzoom,")")
console.log("Setting ", layer?.layerDef?.id, " to visibility", isDisplayed, "(minzoom:", layer?.layerDef?.minzoomVisible, layer?.layerDef?.minzoom, ")")
layer.isDisplayed.setData(
isDisplayed
)
@ -630,18 +702,16 @@ export class SvgToPdf {
}
}
this.setState(ctx + ": loading map data...")
const pngCreator = new PngMapCreator(
state,
{
width,
height,
scaling: Number(params["scaling"] ?? 1.5),
divId: this._freeDivId(),
dummyMode : this._disableMaps
divId: this.options.getFreeDiv(),
dummyMode: this.options.disableMaps
}
)
this.setState(ctx + ": rendering png")
const png = await pngCreator.CreatePng("image")
svgImage.setAttribute('xlink:href', png)
@ -657,18 +727,20 @@ export class SvgToPdf {
textElement.parentElement.removeChild(textElement)
}
public async Prepare() {
public async Prepare(language: string) {
// Always fetch the remote data - it's cached anyway
this.layerTranslations[language] = await Utils.downloadJsonCached("https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/langs/layers/" + language + ".json", 24 * 60 * 60 * 1000)
const shared_questions = await Utils.downloadJsonCached("https://raw.githubusercontent.com/pietervdvn/MapComplete/develop/langs/shared-questions/" + language + ".json", 24 * 60 * 60 * 1000)
this.layerTranslations[language]["shared-questions"] = shared_questions["shared_questions"]
if (this._isPrepared) {
return
}
this._isPrepared = true;
this.setState("Preparing...")
const mapSpecs: SVGTSpanElement[] = []
for (const svgRoot of this._svgRoots) {
for (let child of Array.from(svgRoot.children)) {
for (let child of Array.from(this._svgRoot.children)) {
await this.prepareElement(<any>child, mapSpecs)
}
}
const self = this;
await Promise.all(mapSpecs.map(ms => self.prepareMap(ms)))
@ -676,36 +748,58 @@ export class SvgToPdf {
}
public async ConvertSvg(saveAs: string): Promise<void> {
await this.Prepare()
const ctx = "Rendering PDF"
this.setState(ctx + "...")
const firstPage = this._svgRoots[0]
public drawPage(advancedApi: jsPDF, i: number, language: string): void {
if(!this._isPrepared){
throw "Run 'Prepare()' first!"
}
if (this.options.beforePage) {
this.options.beforePage(i)
}
const internal = new SvgToPdfInternals(advancedApi, this.options.textSubstitutions, this.images, this.rects, this.importedTranslations, this.layerTranslations);
for (let child of Array.from(this._svgRoot.children)) {
internal.handleElement(<any>child)
}
}
}
export class SvgToPdf {
private readonly _pages: SvgToPdfPage[]
constructor(pages: string[], options?: SvgToPdfOptions) {
this._pages = pages.map(page => new SvgToPdfPage(page, options))
}
public async ConvertSvg(saveAs: string, language: string): Promise<void> {
const firstPage = this._pages[0]._svgRoot
const width = SvgToPdfInternals.attrNumber(firstPage, "width")
const height = SvgToPdfInternals.attrNumber(firstPage, "height")
const mode = width > height ? "landscape" : "portrait"
const doc = new jsPDF(mode)
const beforePage = this._beforePage ?? (_ => {
});
const svgRoots = this._svgRoots;
doc.advancedAPI(advancedApi => {
const internal = new SvgToPdfInternals(advancedApi, this._textSubstitutions, this.images, this.rects);
for (let i = 0; i < this._svgRoots.length; i++) {
this.setState(ctx + ": page " + i + "/" + this._svgRoots.length)
beforePage(i)
const svgRoot = svgRoots[i];
for (let child of Array.from(svgRoot.children)) {
internal.handleElement(<any>child)
for (const page of this._pages) {
await page.Prepare(language)
}
Locale.language.setData(language)
const doc = new jsPDF(mode)
doc.advancedAPI(advancedApi => {
for (let i = 0; i < this._pages.length; i++) {
if (i > 0) {
advancedApi.addPage()
const mediabox : {bottomLeftX: number, bottomLeftY: number, topRightX: number, topRightY: number} = advancedApi.getCurrentPageInfo().pageContext.mediaBox
const targetWidth = 297
const targetHeight = 210
const sx = mediabox.topRightX / targetWidth
const sy = mediabox.topRightY / targetHeight
advancedApi.setCurrentTransformationMatrix(advancedApi.Matrix(sx, 0, 0, -sy, 0, mediabox.topRightY))
}
this._pages[i].drawPage(advancedApi, i, language)
}
})
this.setState("Serving PDF...")
await doc.save(saveAs);
this.setState("Done")
}

View file

@ -26,9 +26,9 @@
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="1.0169528"
inkscape:cx="389.89027"
inkscape:cy="403.16522"
inkscape:zoom="1.0430996"
inkscape:cx="836.4494"
inkscape:cy="155.30636"
inkscape:window-width="1920"
inkscape:window-height="1007"
inkscape:window-x="0"
@ -347,8 +347,8 @@
<rect
x="28.759502"
y="661.40118"
width="417.32852"
height="149.75414"
width="265.23891"
height="144.38572"
id="rect21432-8" />
<rect
x="28.759502"
@ -386,6 +386,12 @@
width="349.20448"
height="136.45527"
id="rect3512" />
<rect
x="28.759503"
y="661.40117"
width="266.88498"
height="79.149891"
id="rect5917" />
</defs>
<g
inkscape:groupmode="layer"
@ -423,9 +429,14 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect21432-8);display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264848;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"><tspan
x="28.759766"
y="697.7954"
id="tspan1852"><tspan
id="tspan8486"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1850">$map(theme:aed,z:14,lat:51.2098,lon:3.2284)</tspan></tspan></text>
id="tspan8484">$map(theme:aed,z:14,lat:</tspan></tspan><tspan
x="28.759766"
y="747.7954"
id="tspan8490"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan8488">51.2098,lon:3.2284)</tspan></tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,1.3325782,-88.396258)"
@ -433,9 +444,9 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect13433);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="28.759766"
y="697.7954"
id="tspan1856"><tspan
id="tspan8494"><tspan
style="font-size:16px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1854">$flyer.toerisme_vlaanderen</tspan></tspan></text>
id="tspan8492">$flyer.toerisme_vlaanderen</tspan></tspan></text>
<rect
style="fill:#deadff;fill-opacity:1;stroke:#000000;stroke-width:0.0270132;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11121"
@ -450,19 +461,19 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect17153);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="28.759766"
y="697.7954"
id="tspan1860"><tspan
id="tspan8498"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1858">$map(theme:toerisme_vlaanderen,layers:none</tspan></tspan><tspan
id="tspan8496">$map(theme:toerisme_vlaanderen,layers:none</tspan></tspan><tspan
x="28.759766"
y="747.7954"
id="tspan1864"><tspan
id="tspan8502"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1862">,layer-charging_station_ebikes:force,lat:</tspan></tspan><tspan
id="tspan8500">,layer-charging_station_ebikes:force,lat:</tspan></tspan><tspan
x="28.759766"
y="797.7954"
id="tspan1868"><tspan
id="tspan8506"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1866">51.02403,lon:5.1, z:10)</tspan></tspan></text>
id="tspan8504">51.02403,lon:5.1, z:10)</tspan></tspan></text>
<rect
style="fill:#cccccc;fill-opacity:1;stroke:#000000;stroke-width:0.0178908;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11459"
@ -477,31 +488,31 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect21432);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="28.759766"
y="697.7954"
id="tspan1874"><tspan
id="tspan8512"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1870">$map(theme:cyclofix,z:14,lat:</tspan><tspan
id="tspan8508">$map(theme:cyclofix,z:14,lat:</tspan><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1872">51.05016,lon:</tspan></tspan><tspan
id="tspan8510">51.05016,lon:</tspan></tspan><tspan
x="28.759766"
y="747.7954"
id="tspan1878"><tspan
id="tspan8516"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1876">3.717842,layers:none,layer-</tspan></tspan><tspan
id="tspan8514">3.717842,layers:none,layer-</tspan></tspan><tspan
x="28.759766"
y="797.7954"
id="tspan1882"><tspan
id="tspan8520"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1880">bike_repair_station:true,layer-</tspan></tspan><tspan
id="tspan8518">bike_repair_station:true,layer-</tspan></tspan><tspan
x="28.759766"
y="847.7954"
id="tspan1886"><tspan
id="tspan8524"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1884">drinking_water:true,layer-bike_cafe:true,layer-</tspan></tspan><tspan
id="tspan8522">drinking_water:true,layer-bike_cafe:true,layer-</tspan></tspan><tspan
x="28.759766"
y="897.7954"
id="tspan1890"><tspan
id="tspan8528"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1888">bicycle_tube_vending_machine: true)</tspan></tspan></text>
id="tspan8526">bicycle_tube_vending_machine: true)</tspan></tspan></text>
<rect
style="fill:#733034;fill-opacity:1;stroke:#000000;stroke-width:0.0661458;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11543"
@ -518,19 +529,19 @@
y="79.200089" />
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,218.0015,-40.954244)"
transform="matrix(0.26458333,0,0,0.26458333,210.26838,-40.304813)"
id="text20455"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;white-space:pre;shape-inside:url(#rect20457);display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264848;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"><tspan
x="28.759766"
y="697.7954"
id="tspan1894"><tspan
id="tspan8532"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1892">$map(theme:artwork,z:15,lat:51.2098,lon:</tspan></tspan><tspan
id="tspan8530">$map(theme:artwork,z:15,lat:51.2098,lon:</tspan></tspan><tspan
x="28.759766"
y="747.7954"
id="tspan1898"><tspan
id="tspan8536"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1896">3.2284,background:AGIV)</tspan></tspan></text>
id="tspan8534">3.2284,background:AGIV)</tspan></tspan></text>
<rect
style="fill:#cccccc;fill-opacity:1;stroke:#000000;stroke-width:0.0700743;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect11647"
@ -545,14 +556,14 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect3239);display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264848;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"><tspan
x="28.759766"
y="697.7954"
id="tspan1902"><tspan
id="tspan8540"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1900">$map(theme:cyclestreets,z:12,lat:51.2098,lon:</tspan></tspan><tspan
id="tspan8538">$map(theme:cyclestreets,z:12,lat:51.2098,lon:</tspan></tspan><tspan
x="28.759766"
y="747.7954"
id="tspan1906"><tspan
id="tspan8544"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1904">3.2284)</tspan></tspan></text>
id="tspan8542">3.2284)</tspan></tspan></text>
<g
id="g1367"
transform="matrix(1,0,0.20502864,-1,-20.554711,213.09746)">
@ -585,14 +596,14 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect890);display:inline;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.264848;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"><tspan
x="28.759766"
y="697.7954"
id="tspan1910"><tspan
id="tspan8548"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1908">$map(theme:benches,z:14,lat:51.2098,lon:</tspan></tspan><tspan
id="tspan8546">$map(theme:benches,z:14,lat:51.2098,lon:</tspan></tspan><tspan
x="28.759766"
y="747.7954"
id="tspan1914"><tspan
id="tspan8552"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1912">3.2284, layers:none, layer-bench:force)</tspan></tspan></text>
id="tspan8550">3.2284, layers:none, layer-bench:force)</tspan></tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,205.99418,0.58092297)"
@ -600,9 +611,9 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect6468);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="28.759766"
y="697.7954"
id="tspan1918"><tspan
id="tspan8556"><tspan
style="font-size:16px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1916">$flyer.aerial</tspan></tspan></text>
id="tspan8554">$flyer.aerial</tspan></tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,-1.7998979,-153.42245)"
@ -610,9 +621,9 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect3512);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="28.759766"
y="697.7954"
id="tspan1922"><tspan
id="tspan8560"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan1920">$flyer.examples</tspan></tspan></text>
id="tspan8558">$flyer.examples</tspan></tspan></text>
<g
id="path15616"
transform="matrix(-1,0,0,1,497.66957,-0.86523396)">
@ -645,6 +656,44 @@
id="image24447"
x="272.42087"
y="32.116085" />
<g
id="g291542"
transform="matrix(0.99886181,-0.04769808,0.15709718,-1.0086413,188.12191,121.48089)"
style="display:inline;fill:#100000;fill-opacity:1;stroke-width:1;stroke-miterlimit:20;stroke-dasharray:none">
<path
style="color:#000000;fill:#100000;fill-opacity:1;stroke-width:1;stroke-miterlimit:20;stroke-dasharray:none"
d="m 15.528974,83.478843 c -5.003614,5.887381 -4.440301,9.920838 -2.578536,12.442602 0,0 0.304205,-0.15572 0.335844,-0.18682 -1.100777,-1.491001 -1.750416,-3.51647 -0.937001,-6.208183 0.508852,-1.683868 1.590263,-3.628467 3.491964,-5.866057 z"
id="path291532"
sodipodi:nodetypes="cccscc" />
<g
id="g291540"
transform="matrix(1.3416022,-0.58611319,0.61075156,1.1012623,-53.459605,-14.204129)"
style="fill:#100000;fill-opacity:1;stroke-width:0.738128;stroke-miterlimit:20;stroke-dasharray:none">
<g
id="g291538"
style="fill:#100000;fill-opacity:1;stroke-width:0.738128;stroke-miterlimit:20;stroke-dasharray:none">
<path
style="color:#000000;fill:#100000;fill-opacity:1;fill-rule:evenodd;stroke-width:0.738128;stroke-linejoin:round;stroke-miterlimit:20;stroke-dasharray:none"
d="m 5.9062068,93.870487 3.351582,-0.495843 -2.2319045,2.549032 C 7.1413596,95.074353 6.6865692,94.246503 5.9062068,93.870487 Z"
id="path291534" />
<path
style="color:#000000;fill:#100000;fill-opacity:1;fill-rule:evenodd;stroke-width:0.738128;stroke-linejoin:round;stroke-miterlimit:20;stroke-dasharray:none"
d="M 9.2441406,93.285156 5.8925781,93.78125 c -0.088278,0.01373 -0.1059275,0.133203 -0.025391,0.171875 0.7457822,0.359354 1.1784769,1.149068 1.0683594,1.958984 -0.011715,0.0889 0.098674,0.139328 0.1582031,0.07227 l 2.2324219,-2.550781 c 0.053217,-0.06373 2.437e-4,-0.159586 -0.082031,-0.148438 z"
id="path291536"
sodipodi:nodetypes="ccccccsc" />
</g>
</g>
</g>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,208.79128,-170.1103)"
id="text5915"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect5917);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="28.759766"
y="697.7954"
id="tspan8564"><tspan
style="font-size:16px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan8562">$flyer.lines_too</tspan></tspan></text>
</g>
<g
inkscape:label="Layer 1"
@ -679,9 +728,9 @@
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="8.7285156"
y="42.098132"
id="tspan1926"><tspan
id="tspan8568"><tspan
style="font-weight:bold;font-size:34.6667px;-inkscape-font-specification:'sans-serif, Bold'"
id="tspan1924">$flyer.title</tspan></tspan></text>
id="tspan8566">$flyer.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"

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 48 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.6 MiB

View file

@ -42,8 +42,21 @@
"flyer": {
"aerial": "This map uses a different background, namely aerial imagery by Agentschap Informatie Vlaanderen",
"callToAction": "Test it on mapcomplete.osm.be",
"editing": {
"intro": "The user is greeted by a map with points. Upon selecting a point, the information about the point is shown. A simplified example of what this looks like for a nature reserve is shown below.",
"title": "What does the interface look like?"
},
"examples": "There are many thematic maps available of which a few are printed here, namely the maps with AED's, artwork, cyclestreets, benches and cyclepumps.\n\nThere are many more thematic maps online: about healthcare, indoor navigation, wheelchair accessibility, waste facilities, public bookcases, pedestrian crossings with a rainbow-painting,... Discover them all on mapcomplete.osm.be ",
"fakeui": {
"add_images": "Add images with a few clicks",
"attributes": "Shows attributes in a friendly way",
"edit": "Wrong or outdated info? The edit button is right there",
"question": "If an attribute is not yet known, MapComplete shows a question",
"see_images": "Shows images from previous contributors, Wikipedia, Mapillary, ... ",
"wikipedia": "Linked Wikipedia articles are shown"
},
"frontParagraph": "MapComplete is an easy to use web application to collect geodata in OpenStreetMap, enabling collecting and managing relevant data in an open, crowdsourced and reusable way.\n\nNew categories and attributes can be added upon request.",
"lines_too": "Lines and polygons are shown too. Attributes and images can be added and updated as well.",
"mapcomplete": {
"customize": "MapComplete can tailored to your needs, with new map layers, new functionalities or styled to your organisation styleguide. We also have experience with starting campaigns to crowdsource geodata.\nContact pietervdvn@posteo.net for a quote.",
"intro": "MapComplete is a website which has {mapCount} interactive maps. Every single map allows to add or update information.",

View file

@ -42,7 +42,19 @@
"flyer": {
"aerial": "Deze kaart gebruikt luchtfoto's van het Agentschap Informatie Vlaanderen als achtergrond.\nOok het GRB is beschikbaar als achtergrondlaag.",
"callToAction": "Probeer het uit op mapcomplete.osm.be",
"editing": {
"intro": "De gebruiker krijgt eerst een kaart met interesspunten te zien. Wanneer er op een punt geklikt wordt, wordt een interface met informatie geopend. Een (versimpeld) voorbeeld voor een natuurgebied wordt hieronder getoond:",
"title": "Hoe ziet de interface eruit?"
},
"examples": "Er zijn vele thematische kaarten beschikbaar. Enkele voorbeelden zijn hier geprint, zoals de kaart met AEDs, kunstwerken, fietsstraten, banken en fietspompen.\n\nOnline zijn er nog kaarten met diverse thema's, zoals gezondheidszorg, binnenruimtes, rolstoeltoegankelijkheid, afvalcontainers, boekenruilkasten, regenboog-zebrapaden,... Ontdek ze allemaal mapcomplete.osm.be ",
"fakeui": {
"add_images": "Voeg foto's met een paar klikken toe",
"attributes": "Attributen worden getoond op begrijpbare wijze",
"edit": "Foute of verouderde gegevens? Aanpassen kan hier",
"question": "Is een attribuut nog niet gekend? MapComplete toont een vraag",
"see_images": "Toont afbeeldingen van eerdere bijdragers, Wikipedia, Mapillary, ...",
"wikipedia": "Gelinkte Wikipedia-artikels worden getoond"
},
"frontParagraph": "MapComplete is een web-applicatie om OpenStreetMap-data te tonen en aan te passen op basis van thematische kaarten. Het maakt het mogelijk om open geodata te crowdsourcen en te managen op een makkelijke manier.\n\nNieuwe categorie<69>n en attributen kunnen op vraag worden toegevoegd.",
"mapcomplete": {
"customize": "Wil je een versie op maat? Wil je een versie in jullie huisstijl?\nWil je een nieuwe kaartlaag of functionaliteit? Wil je een crowdsourcing-campagne opzetten?\nNeem contact op met pietervdvn@posteo.net voor een offerte.",

43
test.ts
View file

@ -41,47 +41,12 @@ async function main() {
textSubstitutions: {
mapCount: "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length
},
disableMaps: false
disableMaps: true
}
Locale.language.setData("nl")
const back = new SvgToPdf([svgBack], options)
const front = await new SvgToPdf([svg], options)
await back.ConvertSvg("Flyer-back-nl.pdf")
await front.ConvertSvg("Flyer-front-nl.pdf")
Locale.language.setData("en")
await back.ConvertSvg("Flyer-back-en.pdf")
await front.ConvertSvg("Flyer-front-en.pdf")
const front = await new SvgToPdf([svg, svgBack], options)
await front.ConvertSvg("Flyer-nl.pdf", "nl")
await front.ConvertSvg("Flyer-en.pdf", "en")
/*
const svg = await Utils.download(window.location.protocol + "//" + window.location.host + "/assets/templates/MapComplete-flyer.svg")
Locale.language.setData("en")
const svgToPdf = new SvgToPdf([svgBack], {
getFreeDiv: createElement,
textSubstitutions: {
mapCount: "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length
}
})
new VariableUiElement(svgToPdf.currentState).AttachTo("maindiv")
await svgToPdf.Prepare()
console.log("Used translations", svgToPdf._usedTranslations)
await svgToPdf.ConvertSvg("flyer_nl.pdf")
/*
Locale.language.setData("en")
await new SvgToPdf([svgBack], {
textSubstitutions: {
mapCount: "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length
}
}).ConvertSvg("flyer_en.pdf")
Locale.language.setData("nl")
await new SvgToPdf([svgBack], {
textSubstitutions: {
mapCount: "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length
}
}).ConvertSvg("flyer_nl.pdf")*/
}
main().then(() => console.log("Done!"))