More work on the flyers

This commit is contained in:
pietervdvn 2022-09-14 12:18:51 +02:00
parent 921132b478
commit 27ccce70c0
16 changed files with 687 additions and 308 deletions

View file

@ -22,7 +22,7 @@ export default class LayoutConfig {
public readonly startLat: number
public readonly startLon: number
public widenFactor: number
public readonly defaultBackgroundId?: string
public defaultBackgroundId?: string
public layers: LayerConfig[]
public tileLayerSources: TilesourceConfig[]
public readonly clustering?: {
@ -46,7 +46,7 @@ export default class LayoutConfig {
public readonly customCss?: string
public readonly overpassUrl: string[]
public readonly overpassTimeout: number
public overpassTimeout: number
public readonly overpassMaxZoom: number
public readonly osmApiTileSize: number
public readonly official: boolean

View file

@ -5,7 +5,6 @@ import Minimap, { MinimapObj } from "../Base/Minimap"
import BaseLayer from "../../Models/BaseLayer"
import Combine from "../Base/Combine"
import Svg from "../../Svg"
import State from "../../State"
import {GeoOperations} from "../../Logic/GeoOperations"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
@ -16,11 +15,13 @@ import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import BaseUIElement from "../BaseUIElement"
import Toggle from "./Toggle"
import * as matchpoint from "../../assets/layers/matchpoint/matchpoint.json"
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
import FilteredLayer from "../../Models/FilteredLayer";
import {ElementStorage} from "../../Logic/ElementStorage";
export default class LocationInput
extends BaseUIElement
implements ReadonlyInputElement<Loc>, MinimapObj
{
implements ReadonlyInputElement<Loc>, MinimapObj {
private static readonly matchLayer = new LayerConfig(
matchpoint,
"LocationInput.matchpoint",
@ -47,6 +48,13 @@ export default class LocationInput
private readonly map: BaseUIElement & MinimapObj
private readonly clickLocation: UIEventSource<Loc>
private readonly _minZoom: number
private readonly _state: {
readonly filteredLayers: Store<FilteredLayer[]>;
readonly backgroundLayer: UIEventSource<BaseLayer>;
readonly layoutToUse: LayoutConfig;
readonly selectedElement: UIEventSource<any>;
readonly allElements: ElementStorage
}
constructor(options: {
minZoom?: number
@ -57,6 +65,13 @@ export default class LocationInput
requiresSnapping?: boolean
centerLocation: UIEventSource<Loc>
bounds?: UIEventSource<BBox>
state: {
readonly filteredLayers: Store<FilteredLayer[]>;
readonly backgroundLayer: UIEventSource<BaseLayer>;
readonly layoutToUse: LayoutConfig;
readonly selectedElement: UIEventSource<any>;
readonly allElements: ElementStorage
}
}) {
super()
this._snapTo = options.snapTo?.map((features) =>
@ -67,13 +82,14 @@ export default class LocationInput
this._snappedPointTags = options.snappedPointTags
this._bounds = options.bounds
this._minZoom = options.minZoom
this._state = options.state
if (this._snapTo === undefined) {
this._value = this._centerLocation
} else {
const self = this
if (self._snappedPointTags !== undefined) {
const layout = State.state.layoutToUse
const layout = this._state.layoutToUse
let matchingLayer = LocationInput.matchLayer
for (const layer of layout.layers) {
@ -149,14 +165,14 @@ export default class LocationInput
}
})
}
this.mapBackground = options.mapBackground ?? State.state?.backgroundLayer
this.mapBackground = options.mapBackground ?? this._state?.backgroundLayer
this.SetClass("block h-full")
this.clickLocation = new UIEventSource<Loc>(undefined)
this.map = Minimap.createMiniMap({
location: this._centerLocation,
background: this.mapBackground,
attribution: this.mapBackground !== State.state?.backgroundLayer,
attribution: this.mapBackground !== this._state?.backgroundLayer,
lastClickLocation: this.clickLocation,
bounds: this._bounds,
addLayerControl: true,
@ -204,7 +220,7 @@ export default class LocationInput
features: StaticFeatureSource.fromDateless(this._snapTo),
zoomToFeatures: false,
leafletMap: this.map.leafletMap,
layers: State.state.filteredLayers,
layers: this._state.filteredLayers,
})
// Show the central point
const matchPoint = this._snappedPoint.map((loc) => {
@ -220,8 +236,8 @@ export default class LocationInput
zoomToFeatures: false,
leafletMap: this.map.leafletMap,
layerToShow: this._matching_layer,
state: State.state,
selectedElement: State.state.selectedElement,
state: this._state,
selectedElement: this._state.selectedElement,
})
}
this.mapBackground.map(
@ -267,7 +283,10 @@ export default class LocationInput
}
}
TakeScreenshot(): Promise<string> {
return this.map.TakeScreenshot()
TakeScreenshot(format: "image"): Promise<string>;
TakeScreenshot(format: "blob"): Promise<Blob>;
TakeScreenshot(format: "image" | "blob"): Promise<string | Blob>;
TakeScreenshot(format: "image" | "blob"): Promise<string | Blob> {
return this.map.TakeScreenshot(format)
}
}

View file

@ -75,6 +75,7 @@ export default class ConfirmLocationOfPoint extends Combine {
snappedPointTags: tags,
maxSnapDistance: preset.preciseInput.maxSnapDistance,
bounds: mapBounds,
state: <any> state
})
preciseInput.installBounds(preset.boundsFactor ?? 0.25, true)
preciseInput

View file

@ -145,6 +145,7 @@ export default class MoveWizard extends Toggle {
minZoom: reason.minZoom,
centerLocation: loc,
mapBackground: new UIEventSource<BaseLayer>(preferredBackground), // We detach the layer
state: <any> state
})
if (reason.lockBounds) {

View file

@ -6,7 +6,6 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature
import { GeoOperations } from "../../Logic/GeoOperations"
import { Tiles } from "../../Models/TileRange"
import * as clusterstyle from "../../assets/layers/cluster_style/cluster_style.json"
import State from "../../State"
export default class ShowTileInfo {
public static readonly styling = new LayerConfig(clusterstyle, "ShowTileInfo", true)
@ -16,7 +15,7 @@ export default class ShowTileInfo {
leafletMap: UIEventSource<any>
layer?: LayerConfig
doShowLayer?: UIEventSource<boolean>
}) {
}, state) {
const source = options.source
const metaFeature: Store<{ feature; freshness: Date }[]> = source.features.map(
(features) => {
@ -56,7 +55,7 @@ export default class ShowTileInfo {
features: new StaticFeatureSource(metaFeature),
leafletMap: options.leafletMap,
doShowLayer: options.doShowLayer,
state: State.state,
state
})
}
}

View file

@ -823,7 +823,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
} else if (xhr.status === 509 || xhr.status === 429) {
reject("rate limited")
} else {
reject(xhr.statusText)
reject("Could not download "+url+" due to "+xhr.statusText)
}
}
xhr.open("GET", url)

View file

@ -6,20 +6,19 @@ import ShowDataLayer from "../UI/ShowDataLayer/ShowDataLayer";
import {BBox} from "../Logic/BBox";
import Minimap from "../UI/Base/Minimap";
import AvailableBaseLayers from "../Logic/Actors/AvailableBaseLayers";
import AvailableBaseLayersImplementation from "../Logic/Actors/AvailableBaseLayersImplementation";
import {Utils} from "../Utils";
import {FixedUiElement} from "../UI/Base/FixedUiElement";
export interface PngMapCreatorOptions{
readonly divId: string; readonly width: number; readonly height: number; readonly scaling?: 1 | number,
readonly dummyMode?: boolean
}
export class PngMapCreator {
private readonly _state: FeaturePipelineState;
private readonly _options: {
readonly divId: string; readonly width: number; readonly height: number; readonly scaling?: 1 | number
};
private readonly _state: FeaturePipelineState | undefined;
private readonly _options: PngMapCreatorOptions;
constructor(state: FeaturePipelineState, options: {
readonly divId: string
readonly width: number,
readonly height: number,
readonly scaling?: 1 | number
}) {
constructor(state: FeaturePipelineState | undefined, options: PngMapCreatorOptions) {
this._state = state;
this._options = {...options, scaling: options.scaling ?? 1};
}
@ -62,13 +61,19 @@ export class PngMapCreator {
// Lets first init the minimap and wait for all background tiles to load
const minimap = await this.createAndLoadMinimap()
const state = this._state
const freediv = this._options.divId
const dummyMode = this._options.dummyMode ?? false
console.log("Dummy mode is", dummyMode)
return new Promise<string | Blob>(resolve => {
// Next: we prepare the features. Only fully contained features are shown
minimap.leafletMap.addCallbackAndRunD(async (leaflet) => {
const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.1).pad(-state.layoutToUse.widenFactor))
// Ping the featurepipeline to download what is needed
if (dummyMode) {
console.warn("Dummy mode is active - not loading map layers")
} else {
const bounds = BBox.fromLeafletBounds(leaflet.getBounds().pad(0.1).pad(-state.layoutToUse.widenFactor))
state.currentBounds.setData(bounds)
if (state.featurePipeline.runningQuery.data) {
// A query is running!
// Let's wait for it to complete
@ -77,9 +82,6 @@ export class PngMapCreator {
console.log("Query has completeted!")
}
window.setTimeout(() => {
state.featurePipeline.GetTilesPerLayerWithin(bounds, (tile) => {
if (tile.layer.layerDef.minzoom > state.locationControl.data.zoom) {
@ -97,9 +99,14 @@ export class PngMapCreator {
state: undefined,
})
})
minimap.TakeScreenshot(format).then(result => resolve(result))
}, 2500)
await Utils.waitFor(2500)
}
minimap.TakeScreenshot(format).then(result => {
new FixedUiElement("Done!").AttachTo(freediv)
return resolve(result);
})
})
state.AddAllOverlaysToMap(minimap.leafletMap)
})
}

View file

@ -4,6 +4,11 @@ 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 "../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';
class SvgToPdfInternals {
private readonly doc: jsPDF;
@ -179,7 +184,7 @@ 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
}
@ -315,6 +320,38 @@ class SvgToPdfInternals {
}
}
private drawPath(element: SVGPathElement): void {
const path = element.getAttribute("d")
const parsed: { code: string, x: number, y: number, x2?, y2?, x1?, y1? }[] = parseSVG(path)
makeAbsolute(parsed)
for (const c of parsed) {
if (c.code === "C" || c.code === "c") {
const command = {op: "c", c: [c.x1, c.y1, c.x2, c.y2, c.x, c.y]}
this.doc.path([command])
continue
}
this.doc.path([{op: c.code.toLowerCase(), c: [c.x, c.y]}])
}
const css = SvgToPdfInternals.css(element)
this.doc.setDrawColor(css["color"])
this.doc.setFillColor(css["fill"])
if (css["stroke-width"]) {
this.doc.setLineWidth(Number(css["stroke-width"]))
}
if (css["stroke-linejoin"] !== undefined) {
this.doc.setLineJoin(css["stroke-linejoin"])
}
if (css["fill-rule"] === "evenodd") {
this.doc.fillEvenOdd()
} else {
this.doc.fill()
}
}
public handleElement(element: SVGSVGElement | Element): void {
const isTransformed = this.setTransform(element)
if (element.tagName === "tspan") {
@ -331,6 +368,10 @@ class SvgToPdfInternals {
this.drawImage(element)
}
if (element.tagName === "path") {
this.drawPath(<any>element)
}
if (element.tagName === "g" || element.tagName === "text") {
for (let child of Array.from(element.children)) {
@ -371,6 +412,13 @@ class SvgToPdfInternals {
}
}
export interface SvgToPdfOptions {
getFreeDiv: () => string,
disableMaps?: false | true
textSubstitutions?: Record<string, string>, beforePage?: (i: number) => void
}
export class SvgToPdf {
private images: Record<string, HTMLImageElement> = {}
@ -379,18 +427,20 @@ export class SvgToPdf {
private readonly _textSubstitutions: Record<string, string>;
private readonly _beforePage: ((i: number) => void) | undefined;
public readonly _usedTranslations: Set<string> = new Set<string>()
private readonly _freeDivId: string | undefined;
private readonly _freeDivId: () => string;
private readonly _currentState = new UIEventSource<string>("Initing")
public readonly currentState: Store<string>
private readonly _disableMaps: boolean ;
constructor(pages: string[], options?: {
freeDivId?: string,
textSubstitutions?: Record<string, string>, beforePage?: (i: number) => void
}) {
constructor(pages: string[], options?:SvgToPdfOptions) {
this.currentState = this._currentState
this._textSubstitutions = options?.textSubstitutions ?? {};
this._beforePage = options?.beforePage;
this._freeDivId = options?.freeDivId
this._freeDivId = options?.getFreeDiv
this._disableMaps = options.disableMaps ?? false
const parser = new DOMParser();
for (const page of pages) {
const xmlDoc = parser.parseFromString(page, "text/xml");
const xmlDoc = parser.parseFromString(page, "image/svg+xml");
const svgRoot = xmlDoc.getElementsByTagName("svg")[0];
this._svgRoots.push(svgRoot)
}
@ -423,6 +473,7 @@ 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()
@ -466,19 +517,11 @@ export class SvgToPdf {
private _isPrepared = false;
public async Prepare() {
if (this._isPrepared) {
return
}
this._isPrepared = true;
const mapSpecs: SVGTSpanElement[] = []
for (const svgRoot of this._svgRoots) {
for (let child of Array.from(svgRoot.children)) {
await this.prepareElement(<any>child, mapSpecs)
}
private setState(message: string) {
this._currentState.setData(message)
}
for (const mapSpec of mapSpecs) {
private async prepareMap(mapSpec: SVGTSpanElement,): Promise<void> {
// Upper left point of the tspan
const {x, y} = SvgToPdfInternals.GetActualXY(mapSpec)
@ -493,6 +536,8 @@ export class SvgToPdf {
throw "Invalid mapspec:" + spec
}
const params = SvgToPdfInternals.parseCss(match[1], ",")
const ctx = `Preparing map (theme ${params["theme"]})`
this.setState(ctx + "...")
let smallestRect: SVGRectElement = undefined
let smallestSurface: number = undefined;
@ -533,12 +578,11 @@ export class SvgToPdf {
throw "Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
}
layout.widenFactor = 0
layout.overpassTimeout = 180
layout.defaultBackgroundId = params["background"] ?? layout.defaultBackgroundId
const zoom = Number(params["zoom"] ?? params["z"] ?? 14);
for (const l of layout.layers) {
l.minzoom = zoom
}
const state = new FeaturePipelineState(layout)
state.backgroundLayer.addCallbackAndRunD(l => console.log("baselayer is", l.id))
state.locationControl.setData({
zoom,
lat: Number(params["lat"] ?? 51.05016),
@ -557,35 +601,68 @@ export class SvgToPdf {
for (const paramsKey in params) {
if (paramsKey.startsWith("layer-")) {
const layerName = paramsKey.substring("layer-".length)
const isDisplayed = params[paramsKey].toLowerCase().trim() === "true";
console.log("Setting display status of ", layerName, "to", isDisplayed)
state.filteredLayers.data.find(l => l.layerDef.id === layerName).isDisplayed.setData(
const key = params[paramsKey].toLowerCase().trim()
const isDisplayed = key === "true" || key === "force";
const layer = state.filteredLayers.data.find(l => l.layerDef.id === layerName)
layer.isDisplayed.setData(
isDisplayed
)
if (key === "force") {
layer.layerDef.minzoom = zoom
}
}
}
this.setState(ctx + ": loading map data...")
const pngCreator = new PngMapCreator(
state,
{
width,
height,
scaling: Number(params["scaling"] ?? 1.5),
divId: this._freeDivId
divId: this._freeDivId(),
dummyMode : this._disableMaps
}
)
this.setState(ctx + ": rendering png")
const png = await pngCreator.CreatePng("image")
svgImage.setAttribute('xlink:href', png)
smallestRect.parentElement.insertBefore(svgImage, smallestRect)
await this.prepareElement(svgImage, [])
smallestRect.setAttribute("style", "fill:#ff00ff00;fill-opacity:0;stroke:#000000;stroke-width:0.202542;stroke-linecap:round;stroke-opacity:1")
const smallestRectCss = SvgToPdfInternals.parseCss(smallestRect.getAttribute("style"))
smallestRectCss["fill-opacity"] = "0"
smallestRect.setAttribute("style", Object.keys(smallestRectCss).map(k => k + ":" + smallestRectCss[k]).join(";"))
textElement.parentElement.removeChild(textElement)
}
public async Prepare() {
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)) {
await this.prepareElement(<any>child, mapSpecs)
}
}
const self = this;
await Promise.all(mapSpecs.map(ms => self.prepareMap(ms)))
}
public async ConvertSvg(saveAs: string): Promise<void> {
await this.Prepare()
const ctx = "Rendering PDF"
this.setState(ctx + "...")
const firstPage = this._svgRoots[0]
const width = SvgToPdfInternals.attrNumber(firstPage, "width")
const height = SvgToPdfInternals.attrNumber(firstPage, "height")
@ -598,6 +675,7 @@ export class SvgToPdf {
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)) {
@ -608,7 +686,9 @@ export class SvgToPdf {
}
}
})
this.setState("Serving PDF...")
await doc.save(saveAs);
this.setState("Done")
}

View file

@ -21,7 +21,7 @@
"render": "{export_as_geojson()}"
},
"wikipedia": {
"description": "Shows a wikipedia box with the corresponding wikipedia article",
"description": "Shows a wikipedia box with the corresponding wikipedia article; the wikidata-item link can be changed by a contributor",
"render": "{wikipedia():max-height:25rem}",
"question": {
"en": "What is the corresponding Wikidata entity?",

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View file

@ -26,15 +26,15 @@
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="1.2400294"
inkscape:cx="712.48308"
inkscape:cy="537.08402"
inkscape:zoom="1.2563786"
inkscape:cx="450.10316"
inkscape:cy="393.98951"
inkscape:window-width="1920"
inkscape:window-height="1007"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer2"
inkscape:current-layer="layer1"
inkscape:snap-global="false">
<sodipodi:guide
position="98.990966,200.95191"
@ -230,9 +230,9 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:0.75;font-family:sans-serif;white-space:pre;shape-inside:url(#rect45500);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="27.585938"
y="594.75243"
id="tspan37960"><tspan
id="tspan39535"><tspan
style="font-size:20px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37958">$flyer.frontParagraph</tspan></tspan></text>
id="tspan39533">$flyer.frontParagraph</tspan></tspan></text>
<text
xml:space="preserve"
transform="scale(0.26458333)"
@ -256,9 +256,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(#rect61742);fill:#d4f1f4;fill-opacity:1;stroke:none"><tspan
x="11.105469"
y="116.27391"
id="tspan37964"><tspan
id="tspan39539"><tspan
style="font-weight:bold;font-size:26.6667px;-inkscape-font-specification:'sans-serif, Bold'"
id="tspan37962">$flyer.tagline</tspan></tspan></text>
id="tspan39537">$flyer.tagline</tspan></tspan></text>
<text
xml:space="preserve"
transform="scale(0.26458333)"
@ -271,7 +271,7 @@
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:32px;line-height:1.25;font-family:sans-serif;-inkscape-font-specification:'sans-serif, Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;white-space:pre;shape-inside:url(#rect135032);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="8.7285156"
y="34.819131"
id="tspan37966">$flyer.title</tspan></text>
id="tspan39541">$flyer.title</tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,97.529993,-16.582379)"
@ -279,9 +279,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(#rect61742-2);display:inline;fill:#d4f1f4;fill-opacity:1;stroke:none"><tspan
x="11.105469"
y="116.27391"
id="tspan37970"><tspan
id="tspan39545"><tspan
style="font-weight:bold;font-size:32px;-inkscape-font-specification:'sans-serif, Bold';fill:#000000"
id="tspan37968">$flyer.whatIsOsm</tspan></tspan></text>
id="tspan39543">$flyer.whatIsOsm</tspan></tspan></text>
<image
width="55.363865"
height="21.384291"
@ -298,9 +298,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(#rect20620);fill:#d4f1f4;fill-opacity:1;stroke:none"><tspan
x="11.105469"
y="116.27391"
id="tspan37974"><tspan
id="tspan39549"><tspan
style="font-weight:bold;font-size:26.6667px;-inkscape-font-specification:'sans-serif, Bold'"
id="tspan37972">$flyer.mapcomplete.title</tspan></tspan></text>
id="tspan39547">$flyer.mapcomplete.title</tspan></tspan></text>
<text
xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,93.634029,-58.617677)"
@ -308,63 +308,69 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:0.75;font-family:sans-serif;white-space:pre;shape-inside:url(#rect21926);fill:#000000;fill-opacity:1;stroke:none"><tspan
x="27.585938"
y="594.75243"
id="tspan37978"><tspan
id="tspan39553"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37976">$flyer.mapcomplete.intro
id="tspan39551">$flyer.mapcomplete.intro
</tspan></tspan><tspan
x="27.585938"
y="624.75243"
id="tspan37982"><tspan
id="tspan39557"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37980">
id="tspan39555">
</tspan></tspan><tspan
x="27.585938"
y="654.75243"
id="tspan37986"><tspan
id="tspan39561"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37984">$list(flyer.mapcomplete.li)
id="tspan39559">$list(flyer.mapcomplete.li)
</tspan></tspan><tspan
x="27.585938"
y="684.75243"
id="tspan37990"><tspan
id="tspan39565"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37988">
id="tspan39563">
</tspan></tspan><tspan
x="27.585938"
y="714.75243"
id="tspan37994"><tspan
id="tspan39569"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37992">
id="tspan39567">
</tspan></tspan><tspan
x="27.585938"
y="744.75243"
id="tspan37998"><tspan
id="tspan39573"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37996">
id="tspan39571">
</tspan></tspan><tspan
x="27.585938"
y="774.75243"
id="tspan38002"><tspan
id="tspan39577"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38000">
id="tspan39575">
</tspan></tspan><tspan
x="27.585938"
y="804.75243"
id="tspan38006"><tspan
id="tspan39581"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38004">
id="tspan39579">
</tspan></tspan><tspan
x="27.585938"
y="834.75243"
id="tspan38010"><tspan
id="tspan39585"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38008">
id="tspan39583">
</tspan></tspan><tspan
x="27.585938"
y="864.75243"
id="tspan38014"><tspan
id="tspan39589"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38012">$flyer.mapcomplete.customize</tspan></tspan></text>
id="tspan39587">
</tspan></tspan><tspan
x="27.585938"
y="894.75243"
id="tspan39593"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan39591">$flyer.mapcomplete.customize</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"
@ -423,17 +429,17 @@
style="font-style:normal;font-weight:normal;font-size:40px;line-height:0.75;font-family:sans-serif;white-space:pre;shape-inside:url(#rect169564);display:inline;fill:#000000;fill-opacity:1;stroke:none"><tspan
x="27.585938"
y="594.75243"
id="tspan38020"><tspan
id="tspan39599"><tspan
style="font-weight:bold;font-size:24px;-inkscape-font-specification:'sans-serif, Bold'"
id="tspan38016">$flyer.callToAction</tspan><tspan
id="tspan39595">$flyer.callToAction</tspan><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38018">
id="tspan39597">
</tspan></tspan><tspan
x="27.585938"
y="624.75243"
id="tspan38024"><tspan
id="tspan39603"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38022"> </tspan></tspan></text>
id="tspan39601"> </tspan></tspan></text>
<image
width="73.774147"
height="103.17501"
@ -11777,8 +11783,8 @@ eTmE1QAAAABJRU5ErkJggg==
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;white-space:pre;shape-inside:url(#rect171375);display:inline;fill:#000000;fill-opacity:1;stroke:none"><tspan
x="381.5332"
y="126.50438"
id="tspan38028"><tspan
id="tspan39607"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38026">$flyer.osm</tspan></tspan></text>
id="tspan39605">$flyer.osm</tspan></tspan></text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View file

@ -40,13 +40,12 @@
"reload": "Reload the data"
},
"flyer": {
"aerial": "This map uses a different background, namely aerial imagery by Agentschap Informatie Vlaanderen",
"callToAction": "Test it on mapcomplete.osm.be",
"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 ",
"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.",
"license": {
"text": "The webversion is free to use, both for viewing and adding data.\nAdding data requires a free account on OpenStreetMap.org.\n\n 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.\n\nMapComplete is fully Open Source (GPL-licenses).\n\nData on OpenStreetMap is under the ODbL-license, which means all data can be reused for all purposes, as long as attribution is given and all (improvements to) the data are republished under the same license.\nSee osm.org/copyright for more details",
"title": "License and pricing"
},
"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.",
"li0": "Communicate where POI are",
"li1": "Add new points and update information on existing points",
@ -56,11 +55,13 @@
"li5": "See aerial imagery and map backgrounds",
"li6": "Can be placed in other websites as iFrame",
"li7": "Embedded within the OpenStreetMap-ecosystem, which has many tools available",
"li8": "Fully open source (GPL) and free to use",
"title": "What is MapComplete?"
},
"osm": "OpenStreetMap is an online map which can be edited and reused by anyone for any purpose - just like Wikipedia.\n\nIt is the biggest geospatial database in the world and is reused by thousands of applications and websites.",
"osm": "OpenStreetMap is an online map which can be edited and reused by anyone for any purpose as long as attribution is given and the data is kept open.\n\nIt is the biggest geospatial database in the world and is reused by thousands of applications and websites.",
"tagline": "Collect geodata easily with OpenStreetMap",
"title": "MapComplete.osm.be",
"toerisme_vlaanderen": "For joint project with Toerism Flanders, 'Pin your point' was created. Over 160 contributors added a few thousand benches and picnictables and spotted 100 charging station for bicycles.",
"whatIsOsm": "What is OpenStreetMap?"
},
"general": {

View file

@ -40,13 +40,12 @@
"reload": "Herlaad de data"
},
"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",
"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 ",
"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.",
"license": {
"text": "De webversie is gratis te gebruiken, zowel voor het bekijken als voor het toevoegen van data.\nVoor het toevoegen van data is een gratis account op OpenStreetMap.org vereist.\n\nWil 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.\n\nMapComplete is volledig OpenSource (GPL-licentie).\n\nData op OpenStreetMap valt onder de ODbL-licentie. Data mag herbruikt worden voor alle doeleinden, mits bronvermelding en het openhouden van (verbeteringen aan) de data.\nZie osm.org/copyright voor alle details.",
"title": "Licentie and kostprijs"
},
"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.",
"intro": "MapComplete is een website met {mapCount} interactieve kaarten. Op iedere kaart kunnen gebruikers data zien en updaten.",
"li0": "Communiceer waar interessepunten zijn",
"li1": "Voeg nieuwe punten toe en update informatie van reeds bestaande punten",
@ -56,11 +55,13 @@
"li5": "Wissel tussen kaart- en luchtfoto's als achtergrond",
"li6": "Eenvoudig te embedden in een website als iFrame",
"li7": "Deel van het OpenStreetMap-ecosysteem waarbinnen honderden andere tools bestaan",
"li8": "Volledig Open-Source (GPL) en gratis te gebruiken",
"title": "Wat is MapComplete?"
},
"osm": "OpenStreetMap is een online kaart die door iedereen aangepast en herbruikt mag worden - net zoals Wikipedia.\n\nHet is de grootste geodatabank ter wereld en wordt herbruikt door miljoenen websites en applicaties.",
"tagline": "Verzamel geodata eenvoudig met OpenStreetMap",
"osm": "OpenStreetMap is een online kaart die door iedereen aangepast en herbruikt mag worden - mits bronvermelding en het openhouden van de data.\n\nHet is de grootste geodatabank ter wereld en wordt herbruikt door miljoenen websites en applicaties.",
"tagline": "Eenvoudig geodata verzamelen met OpenStreetMap",
"title": "MapComplete.osm.be",
"toerisme_vlaanderen": "In samenwerking met Toerisme Vlaanderen werd 'Pin Je Punt' gecre<72>erd. Op enkele maanden tijd werden duizenden zitbanken en picnictafels en meer dan honderd oplaadpunten voor elektrische fietsen toegevoegd aan de kaart door meer dan 160 bijdragers.",
"whatIsOsm": "Wat is OpenStreetMap?"
},
"general": {

11
package-lock.json generated
View file

@ -39,6 +39,7 @@
"osmtogeojson": "^3.0.0-beta.4",
"papaparse": "^5.3.1",
"prompt-sync": "^4.2.0",
"svg-path-parser": "^1.1.0",
"tailwindcss": "^3.1.8",
"togpx": "^0.5.4",
"wikibase-sdk": "^7.14.0",
@ -14430,6 +14431,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svg-path-parser": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/svg-path-parser/-/svg-path-parser-1.1.0.tgz",
"integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A=="
},
"node_modules/svg-pathdata": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-5.0.5.tgz",
@ -28146,6 +28152,11 @@
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="
},
"svg-path-parser": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/svg-path-parser/-/svg-path-parser-1.1.0.tgz",
"integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A=="
},
"svg-pathdata": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-5.0.5.tgz",

View file

@ -99,6 +99,7 @@
"osmtogeojson": "^3.0.0-beta.4",
"papaparse": "^5.3.1",
"prompt-sync": "^4.2.0",
"svg-path-parser": "^1.1.0",
"tailwindcss": "^3.1.8",
"togpx": "^0.5.4",
"wikibase-sdk": "^7.14.0",

49
test.ts
View file

@ -1,36 +1,55 @@
import "./assets/templates/Ubuntu-M-normal.js"
import "./assets/templates/Ubuntu-L-normal.js"
import "./assets/templates/UbuntuMono-B-bold.js"
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
import MinimapImplementation from "./UI/Base/MinimapImplementation";
import {Utils} from "./Utils";
import FeaturePipelineState from "./Logic/State/FeaturePipelineState";
import Locale from "./UI/i18n/Locale";
import {SvgToPdf} from "./Utils/svgToPdf";
MinimapImplementation.initialize()
import {Utils} from "./Utils";
import {SvgToPdf, SvgToPdfOptions} from "./Utils/svgToPdf";
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
import Locale from "./UI/i18n/Locale";
let i = 0
function createElement(): string {
const div = document.createElement("div")
div.id = "freediv-" + (i++)
document.getElementById("extradiv").append(div)
return div.id
}
async function main() {
const svg = await Utils.download(window.location.protocol + "//" + window.location.host + "/assets/templates/MapComplete-flyer.svg")
const svgBack = await Utils.download(window.location.protocol + "//" + window.location.host + "/assets/templates/MapComplete-flyer.back.svg")
Locale.language.setData("en")
/*
await new SvgToPdf([svg], {
state,
const options = <SvgToPdfOptions>{
getFreeDiv: createElement,
textSubstitutions: {
mapCount: "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length
},
disableMaps: false
}
}).ConvertSvg("flyer_en.pdf")
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 svg = await Utils.download(window.location.protocol + "//" + window.location.host + "/assets/templates/MapComplete-flyer.svg")
//*/
Locale.language.setData("en")
const svgToPdf = new SvgToPdf([svgBack], {
freeDivId: "extradiv",
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")