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

View file

@ -1,26 +1,27 @@
import { ReadonlyInputElement } from "./InputElement" import {ReadonlyInputElement} from "./InputElement"
import Loc from "../../Models/Loc" import Loc from "../../Models/Loc"
import { Store, UIEventSource } from "../../Logic/UIEventSource" import {Store, UIEventSource} from "../../Logic/UIEventSource"
import Minimap, { MinimapObj } from "../Base/Minimap" import Minimap, {MinimapObj} from "../Base/Minimap"
import BaseLayer from "../../Models/BaseLayer" import BaseLayer from "../../Models/BaseLayer"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Svg from "../../Svg" import Svg from "../../Svg"
import State from "../../State" import {GeoOperations} from "../../Logic/GeoOperations"
import { GeoOperations } from "../../Logic/GeoOperations"
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
import { BBox } from "../../Logic/BBox" import {BBox} from "../../Logic/BBox"
import { FixedUiElement } from "../Base/FixedUiElement" import {FixedUiElement} from "../Base/FixedUiElement"
import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import Toggle from "./Toggle" import Toggle from "./Toggle"
import * as matchpoint from "../../assets/layers/matchpoint/matchpoint.json" 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 export default class LocationInput
extends BaseUIElement extends BaseUIElement
implements ReadonlyInputElement<Loc>, MinimapObj implements ReadonlyInputElement<Loc>, MinimapObj {
{
private static readonly matchLayer = new LayerConfig( private static readonly matchLayer = new LayerConfig(
matchpoint, matchpoint,
"LocationInput.matchpoint", "LocationInput.matchpoint",
@ -47,6 +48,13 @@ export default class LocationInput
private readonly map: BaseUIElement & MinimapObj private readonly map: BaseUIElement & MinimapObj
private readonly clickLocation: UIEventSource<Loc> private readonly clickLocation: UIEventSource<Loc>
private readonly _minZoom: number 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: { constructor(options: {
minZoom?: number minZoom?: number
@ -57,6 +65,13 @@ export default class LocationInput
requiresSnapping?: boolean requiresSnapping?: boolean
centerLocation: UIEventSource<Loc> centerLocation: UIEventSource<Loc>
bounds?: UIEventSource<BBox> bounds?: UIEventSource<BBox>
state: {
readonly filteredLayers: Store<FilteredLayer[]>;
readonly backgroundLayer: UIEventSource<BaseLayer>;
readonly layoutToUse: LayoutConfig;
readonly selectedElement: UIEventSource<any>;
readonly allElements: ElementStorage
}
}) { }) {
super() super()
this._snapTo = options.snapTo?.map((features) => this._snapTo = options.snapTo?.map((features) =>
@ -67,13 +82,14 @@ export default class LocationInput
this._snappedPointTags = options.snappedPointTags this._snappedPointTags = options.snappedPointTags
this._bounds = options.bounds this._bounds = options.bounds
this._minZoom = options.minZoom this._minZoom = options.minZoom
this._state = options.state
if (this._snapTo === undefined) { if (this._snapTo === undefined) {
this._value = this._centerLocation this._value = this._centerLocation
} else { } else {
const self = this const self = this
if (self._snappedPointTags !== undefined) { if (self._snappedPointTags !== undefined) {
const layout = State.state.layoutToUse const layout = this._state.layoutToUse
let matchingLayer = LocationInput.matchLayer let matchingLayer = LocationInput.matchLayer
for (const layer of layout.layers) { for (const layer of layout.layers) {
@ -129,7 +145,7 @@ export default class LocationInput
return { return {
type: "Feature", type: "Feature",
properties: options.snappedPointTags ?? min.properties, properties: options.snappedPointTags ?? min.properties,
geometry: { type: "Point", coordinates: [loc.lon, loc.lat] }, geometry: {type: "Point", coordinates: [loc.lon, loc.lat]},
} }
} }
} }
@ -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.SetClass("block h-full")
this.clickLocation = new UIEventSource<Loc>(undefined) this.clickLocation = new UIEventSource<Loc>(undefined)
this.map = Minimap.createMiniMap({ this.map = Minimap.createMiniMap({
location: this._centerLocation, location: this._centerLocation,
background: this.mapBackground, background: this.mapBackground,
attribution: this.mapBackground !== State.state?.backgroundLayer, attribution: this.mapBackground !== this._state?.backgroundLayer,
lastClickLocation: this.clickLocation, lastClickLocation: this.clickLocation,
bounds: this._bounds, bounds: this._bounds,
addLayerControl: true, addLayerControl: true,
@ -181,7 +197,7 @@ export default class LocationInput
try { try {
const self = this const self = this
const hasMoved = new UIEventSource(false) const hasMoved = new UIEventSource(false)
const startLocation = { ...this._centerLocation.data } const startLocation = {...this._centerLocation.data}
this._centerLocation.addCallbackD((newLocation) => { this._centerLocation.addCallbackD((newLocation) => {
const f = 100000 const f = 100000
console.log(newLocation.lon, startLocation.lon) console.log(newLocation.lon, startLocation.lon)
@ -204,14 +220,14 @@ export default class LocationInput
features: StaticFeatureSource.fromDateless(this._snapTo), features: StaticFeatureSource.fromDateless(this._snapTo),
zoomToFeatures: false, zoomToFeatures: false,
leafletMap: this.map.leafletMap, leafletMap: this.map.leafletMap,
layers: State.state.filteredLayers, layers: this._state.filteredLayers,
}) })
// Show the central point // Show the central point
const matchPoint = this._snappedPoint.map((loc) => { const matchPoint = this._snappedPoint.map((loc) => {
if (loc === undefined) { if (loc === undefined) {
return [] return []
} }
return [{ feature: loc }] return [{feature: loc}]
}) })
console.log("Constructing the match layer", matchPoint) console.log("Constructing the match layer", matchPoint)
@ -220,8 +236,8 @@ export default class LocationInput
zoomToFeatures: false, zoomToFeatures: false,
leafletMap: this.map.leafletMap, leafletMap: this.map.leafletMap,
layerToShow: this._matching_layer, layerToShow: this._matching_layer,
state: State.state, state: this._state,
selectedElement: State.state.selectedElement, selectedElement: this._state.selectedElement,
}) })
} }
this.mapBackground.map( this.mapBackground.map(
@ -267,7 +283,10 @@ export default class LocationInput
} }
} }
TakeScreenshot(): Promise<string> { TakeScreenshot(format: "image"): Promise<string>;
return this.map.TakeScreenshot() 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, snappedPointTags: tags,
maxSnapDistance: preset.preciseInput.maxSnapDistance, maxSnapDistance: preset.preciseInput.maxSnapDistance,
bounds: mapBounds, bounds: mapBounds,
state: <any> state
}) })
preciseInput.installBounds(preset.boundsFactor ?? 0.25, true) preciseInput.installBounds(preset.boundsFactor ?? 0.25, true)
preciseInput preciseInput

View file

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

View file

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

View file

@ -12,8 +12,8 @@ export class Utils {
url: string, url: string,
headers?: any headers?: any
) => Promise<{ content: string } | { redirect: string }> ) => Promise<{ content: string } | { redirect: string }>
public static Special_visualizations_tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`. public static Special_visualizations_tagsToApplyHelpText = `These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`.
This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature. This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature.
If a value to substitute is undefined, empty string will be used instead. If a value to substitute is undefined, empty string will be used instead.
@ -41,11 +41,11 @@ There are also some technicalities in your theme to keep in mind:
This should be used to change the appearance or even to hide it (eg by changing the icon size to zero) This should be used to change the appearance or even to hide it (eg by changing the icon size to zero)
3. There should be a way for the theme to detect previously imported points, even after reloading. 3. There should be a way for the theme to detect previously imported points, even after reloading.
A reference number to the original dataset is an excellent way to do this A reference number to the original dataset is an excellent way to do this
4. When importing ways, the theme creator is also responsible of avoiding overlapping ways. 4. When importing ways, the theme creator is also responsible of avoiding overlapping ways.
#### Disabled in unofficial themes #### Disabled in unofficial themes
The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md). The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md).
The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console. The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console.
In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org` In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org`
private static knownKeys = [ private static knownKeys = [
@ -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) { } else if (xhr.status === 509 || xhr.status === 429) {
reject("rate limited") reject("rate limited")
} else { } else {
reject(xhr.statusText) reject("Could not download "+url+" due to "+xhr.statusText)
} }
} }
xhr.open("GET", url) xhr.open("GET", url)

View file

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

View file

@ -4,6 +4,11 @@ import {Translation, TypedTranslation} from "../UI/i18n/Translation";
import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; import FeaturePipelineState from "../Logic/State/FeaturePipelineState";
import {PngMapCreator} from "./pngMapCreator"; import {PngMapCreator} from "./pngMapCreator";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; 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 { class SvgToPdfInternals {
private readonly doc: jsPDF; private readonly doc: jsPDF;
@ -179,7 +184,7 @@ class SvgToPdfInternals {
} }
private extractTranslation(text: string) { 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) { if (pathPart === null) {
return text 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 { public handleElement(element: SVGSVGElement | Element): void {
const isTransformed = this.setTransform(element) const isTransformed = this.setTransform(element)
if (element.tagName === "tspan") { if (element.tagName === "tspan") {
@ -331,6 +368,10 @@ class SvgToPdfInternals {
this.drawImage(element) this.drawImage(element)
} }
if (element.tagName === "path") {
this.drawPath(<any>element)
}
if (element.tagName === "g" || element.tagName === "text") { if (element.tagName === "g" || element.tagName === "text") {
for (let child of Array.from(element.children)) { 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 { export class SvgToPdf {
private images: Record<string, HTMLImageElement> = {} private images: Record<string, HTMLImageElement> = {}
@ -379,18 +427,20 @@ export class SvgToPdf {
private readonly _textSubstitutions: Record<string, string>; private readonly _textSubstitutions: Record<string, string>;
private readonly _beforePage: ((i: number) => void) | undefined; private readonly _beforePage: ((i: number) => void) | undefined;
public readonly _usedTranslations: Set<string> = new Set<string>() 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?: { constructor(pages: string[], options?:SvgToPdfOptions) {
freeDivId?: string, this.currentState = this._currentState
textSubstitutions?: Record<string, string>, beforePage?: (i: number) => void
}) {
this._textSubstitutions = options?.textSubstitutions ?? {}; this._textSubstitutions = options?.textSubstitutions ?? {};
this._beforePage = options?.beforePage; this._beforePage = options?.beforePage;
this._freeDivId = options?.freeDivId this._freeDivId = options?.getFreeDiv
this._disableMaps = options.disableMaps ?? false
const parser = new DOMParser(); const parser = new DOMParser();
for (const page of pages) { 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]; const svgRoot = xmlDoc.getElementsByTagName("svg")[0];
this._svgRoots.push(svgRoot) this._svgRoots.push(svgRoot)
} }
@ -423,6 +473,7 @@ export class SvgToPdf {
} }
this.images[xlink] = img this.images[xlink] = img
this.setState("Preparing: loading image " + Object.keys(this.images).length + ": " + img.src.substring(0, 30))
return new Promise((resolve) => { return new Promise((resolve) => {
img.onload = _ => { img.onload = _ => {
resolve() resolve()
@ -466,11 +517,135 @@ export class SvgToPdf {
private _isPrepared = false; 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)
let textElement: Element = mapSpec
// We recurse up to get the actual, full specification
while (textElement.tagName !== "text") {
textElement = textElement.parentElement
}
const spec = textElement.textContent
const match = spec.match(/\$map\(([^)]+)\)$/)
if (match === null) {
throw "Invalid mapspec:" + spec
}
const params = SvgToPdfInternals.parseCss(match[1], ",")
const ctx = `Preparing map (theme ${params["theme"]})`
this.setState(ctx + "...")
let smallestRect: SVGRectElement = undefined
let smallestSurface: number = undefined;
// We iterate over all the rectangles and pick the smallest (by surface area) that contains the upper left point of the tspan
for (const id in this.rects) {
const rect = this.rects[id]
const rx = SvgToPdfInternals.attrNumber(rect, "x")
const ry = SvgToPdfInternals.attrNumber(rect, "y")
const w = SvgToPdfInternals.attrNumber(rect, "width")
const h = SvgToPdfInternals.attrNumber(rect, "height")
const inBounds = rx <= x && x <= rx + w && ry <= y && y <= ry + h
if (!inBounds) {
continue
}
const surface = w * h
if (smallestSurface === undefined || smallestSurface > surface) {
smallestSurface = surface
smallestRect = rect
}
}
if (smallestRect === undefined) {
throw "No rectangle found around " + spec + ". Draw a rectangle around it, the map will be projected on that one"
}
const svgImage = document.createElement('image')
svgImage.setAttribute("x", smallestRect.getAttribute("x"))
svgImage.setAttribute("y", smallestRect.getAttribute("y"))
const width = SvgToPdfInternals.attrNumber(smallestRect, "width")
const height = SvgToPdfInternals.attrNumber(smallestRect, "height")
svgImage.setAttribute("width", "" + width)
svgImage.setAttribute("height", "" + height)
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
if (layout === undefined) {
console.error("Could not show map with parameters", params)
throw "Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
}
layout.widenFactor = 0
layout.overpassTimeout = 180
layout.defaultBackgroundId = params["background"] ?? layout.defaultBackgroundId
const zoom = Number(params["zoom"] ?? params["z"] ?? 14);
const state = new FeaturePipelineState(layout)
state.locationControl.setData({
zoom,
lat: Number(params["lat"] ?? 51.05016),
lon: Number(params["lon"] ?? 3.717842)
})
const fl = state.filteredLayers.data
for (const filteredLayer of fl) {
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 = 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(),
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, [])
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() { public async Prepare() {
if (this._isPrepared) { if (this._isPrepared) {
return return
} }
this._isPrepared = true; this._isPrepared = true;
this.setState("Preparing...")
const mapSpecs: SVGTSpanElement[] = [] const mapSpecs: SVGTSpanElement[] = []
for (const svgRoot of this._svgRoots) { for (const svgRoot of this._svgRoots) {
for (let child of Array.from(svgRoot.children)) { for (let child of Array.from(svgRoot.children)) {
@ -478,114 +653,16 @@ export class SvgToPdf {
} }
} }
for (const mapSpec of mapSpecs) { const self = this;
// Upper left point of the tspan await Promise.all(mapSpecs.map(ms => self.prepareMap(ms)))
const {x, y} = SvgToPdfInternals.GetActualXY(mapSpec)
let textElement: Element = mapSpec
// We recurse up to get the actual, full specification
while (textElement.tagName !== "text") {
textElement = textElement.parentElement
}
const spec = textElement.textContent
const match = spec.match(/\$map\(([^)]+)\)$/)
if (match === null) {
throw "Invalid mapspec:" + spec
}
const params = SvgToPdfInternals.parseCss(match[1], ",")
let smallestRect: SVGRectElement = undefined
let smallestSurface: number = undefined;
// We iterate over all the rectangles and pick the smallest (by surface area) that contains the upper left point of the tspan
for (const id in this.rects) {
const rect = this.rects[id]
const rx = SvgToPdfInternals.attrNumber(rect, "x")
const ry = SvgToPdfInternals.attrNumber(rect, "y")
const w = SvgToPdfInternals.attrNumber(rect, "width")
const h = SvgToPdfInternals.attrNumber(rect, "height")
const inBounds = rx <= x && x <= rx + w && ry <= y && y <= ry + h
if (!inBounds) {
continue
}
const surface = w * h
if (smallestSurface === undefined || smallestSurface > surface) {
smallestSurface = surface
smallestRect = rect
}
}
if (smallestRect === undefined) {
throw "No rectangle found around " + spec + ". Draw a rectangle around it, the map will be projected on that one"
}
const svgImage = document.createElement('image')
svgImage.setAttribute("x", smallestRect.getAttribute("x"))
svgImage.setAttribute("y", smallestRect.getAttribute("y"))
const width = SvgToPdfInternals.attrNumber(smallestRect, "width")
const height = SvgToPdfInternals.attrNumber(smallestRect, "height")
svgImage.setAttribute("width", "" + width)
svgImage.setAttribute("height", "" + height)
let layout = AllKnownLayouts.allKnownLayouts.get(params["theme"])
if (layout === undefined) {
console.error("Could not show map with parameters", params)
throw "Theme not found:" + params["theme"] + ". Use theme: to define which theme to use. "
}
layout.widenFactor = 0
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),
lon: Number(params["lon"] ?? 3.717842)
})
const fl = state.filteredLayers.data
for (const filteredLayer of fl) {
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 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(
isDisplayed
)
}
}
const pngCreator = new PngMapCreator(
state,
{
width,
height,
scaling: Number(params["scaling"] ?? 1.5),
divId: this._freeDivId
}
)
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")
textElement.parentElement.removeChild(textElement)
}
} }
public async ConvertSvg(saveAs: string): Promise<void> { public async ConvertSvg(saveAs: string): Promise<void> {
await this.Prepare() await this.Prepare()
const ctx = "Rendering PDF"
this.setState(ctx + "...")
const firstPage = this._svgRoots[0] const firstPage = this._svgRoots[0]
const width = SvgToPdfInternals.attrNumber(firstPage, "width") const width = SvgToPdfInternals.attrNumber(firstPage, "width")
const height = SvgToPdfInternals.attrNumber(firstPage, "height") const height = SvgToPdfInternals.attrNumber(firstPage, "height")
@ -598,6 +675,7 @@ export class SvgToPdf {
doc.advancedAPI(advancedApi => { doc.advancedAPI(advancedApi => {
const internal = new SvgToPdfInternals(advancedApi, this._textSubstitutions, this.images, this.rects); const internal = new SvgToPdfInternals(advancedApi, this._textSubstitutions, this.images, this.rects);
for (let i = 0; i < this._svgRoots.length; i++) { for (let i = 0; i < this._svgRoots.length; i++) {
this.setState(ctx + ": page " + i + "/" + this._svgRoots.length)
beforePage(i) beforePage(i)
const svgRoot = svgRoots[i]; const svgRoot = svgRoots[i];
for (let child of Array.from(svgRoot.children)) { for (let child of Array.from(svgRoot.children)) {
@ -608,7 +686,9 @@ export class SvgToPdf {
} }
} }
}) })
this.setState("Serving PDF...")
await doc.save(saveAs); await doc.save(saveAs);
this.setState("Done")
} }

View file

@ -21,7 +21,7 @@
"render": "{export_as_geojson()}" "render": "{export_as_geojson()}"
}, },
"wikipedia": { "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}", "render": "{wikipedia():max-height:25rem}",
"question": { "question": {
"en": "What is the corresponding Wikidata entity?", "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" showgrid="false"
showguides="true" showguides="true"
inkscape:guide-bbox="true" inkscape:guide-bbox="true"
inkscape:zoom="1.2400294" inkscape:zoom="1.2563786"
inkscape:cx="712.48308" inkscape:cx="450.10316"
inkscape:cy="537.08402" inkscape:cy="393.98951"
inkscape:window-width="1920" inkscape:window-width="1920"
inkscape:window-height="1007" inkscape:window-height="1007"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
inkscape:current-layer="layer2" inkscape:current-layer="layer1"
inkscape:snap-global="false"> inkscape:snap-global="false">
<sodipodi:guide <sodipodi:guide
position="98.990966,200.95191" 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 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" x="27.585938"
y="594.75243" y="594.75243"
id="tspan37960"><tspan id="tspan39535"><tspan
style="font-size:20px;-inkscape-font-specification:'sans-serif, Normal'" 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 <text
xml:space="preserve" xml:space="preserve"
transform="scale(0.26458333)" 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 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" x="11.105469"
y="116.27391" y="116.27391"
id="tspan37964"><tspan id="tspan39539"><tspan
style="font-weight:bold;font-size:26.6667px;-inkscape-font-specification:'sans-serif, Bold'" 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 <text
xml:space="preserve" xml:space="preserve"
transform="scale(0.26458333)" 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 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" x="8.7285156"
y="34.819131" y="34.819131"
id="tspan37966">$flyer.title</tspan></text> id="tspan39541">$flyer.title</tspan></text>
<text <text
xml:space="preserve" xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,97.529993,-16.582379)" 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 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" x="11.105469"
y="116.27391" y="116.27391"
id="tspan37970"><tspan id="tspan39545"><tspan
style="font-weight:bold;font-size:32px;-inkscape-font-specification:'sans-serif, Bold';fill:#000000" 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 <image
width="55.363865" width="55.363865"
height="21.384291" 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 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" x="11.105469"
y="116.27391" y="116.27391"
id="tspan37974"><tspan id="tspan39549"><tspan
style="font-weight:bold;font-size:26.6667px;-inkscape-font-specification:'sans-serif, Bold'" 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 <text
xml:space="preserve" xml:space="preserve"
transform="matrix(0.26458333,0,0,0.26458333,93.634029,-58.617677)" 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 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" x="27.585938"
y="594.75243" y="594.75243"
id="tspan37978"><tspan id="tspan39553"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'" style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37976">$flyer.mapcomplete.intro id="tspan39551">$flyer.mapcomplete.intro
</tspan></tspan><tspan </tspan></tspan><tspan
x="27.585938" x="27.585938"
y="624.75243" y="624.75243"
id="tspan37982"><tspan id="tspan39557"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'" style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37980"> id="tspan39555">
</tspan></tspan><tspan </tspan></tspan><tspan
x="27.585938" x="27.585938"
y="654.75243" y="654.75243"
id="tspan37986"><tspan id="tspan39561"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'" 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 </tspan></tspan><tspan
x="27.585938" x="27.585938"
y="684.75243" y="684.75243"
id="tspan37990"><tspan id="tspan39565"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'" style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37988"> id="tspan39563">
</tspan></tspan><tspan </tspan></tspan><tspan
x="27.585938" x="27.585938"
y="714.75243" y="714.75243"
id="tspan37994"><tspan id="tspan39569"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'" style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37992"> id="tspan39567">
</tspan></tspan><tspan </tspan></tspan><tspan
x="27.585938" x="27.585938"
y="744.75243" y="744.75243"
id="tspan37998"><tspan id="tspan39573"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'" style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan37996"> id="tspan39571">
</tspan></tspan><tspan </tspan></tspan><tspan
x="27.585938" x="27.585938"
y="774.75243" y="774.75243"
id="tspan38002"><tspan id="tspan39577"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'" style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38000"> id="tspan39575">
</tspan></tspan><tspan </tspan></tspan><tspan
x="27.585938" x="27.585938"
y="804.75243" y="804.75243"
id="tspan38006"><tspan id="tspan39581"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'" style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38004"> id="tspan39579">
</tspan></tspan><tspan </tspan></tspan><tspan
x="27.585938" x="27.585938"
y="834.75243" y="834.75243"
id="tspan38010"><tspan id="tspan39585"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'" style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38008"> id="tspan39583">
</tspan></tspan><tspan </tspan></tspan><tspan
x="27.585938" x="27.585938"
y="864.75243" y="864.75243"
id="tspan38014"><tspan id="tspan39589"><tspan
style="font-size:17.3333px;-inkscape-font-specification:'sans-serif, Normal'" 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 <text
xml:space="preserve" 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" 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 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" x="27.585938"
y="594.75243" y="594.75243"
id="tspan38020"><tspan id="tspan39599"><tspan
style="font-weight:bold;font-size:24px;-inkscape-font-specification:'sans-serif, Bold'" 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'" style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38018"> id="tspan39597">
</tspan></tspan><tspan </tspan></tspan><tspan
x="27.585938" x="27.585938"
y="624.75243" y="624.75243"
id="tspan38024"><tspan id="tspan39603"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'" style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'"
id="tspan38022"> </tspan></tspan></text> id="tspan39601"> </tspan></tspan></text>
<image <image
width="73.774147" width="73.774147"
height="103.17501" 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 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" x="381.5332"
y="126.50438" y="126.50438"
id="tspan38028"><tspan id="tspan39607"><tspan
style="font-size:18.6667px;-inkscape-font-specification:'sans-serif, Normal'" 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> </g>
</svg> </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" "reload": "Reload the data"
}, },
"flyer": { "flyer": {
"aerial": "This map uses a different background, namely aerial imagery by Agentschap Informatie Vlaanderen",
"callToAction": "Test it on mapcomplete.osm.be", "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.", "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": { "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.", "intro": "MapComplete is a website which has {mapCount} interactive maps. Every single map allows to add or update information.",
"li0": "Communicate where POI are", "li0": "Communicate where POI are",
"li1": "Add new points and update information on existing points", "li1": "Add new points and update information on existing points",
@ -56,11 +55,13 @@
"li5": "See aerial imagery and map backgrounds", "li5": "See aerial imagery and map backgrounds",
"li6": "Can be placed in other websites as iFrame", "li6": "Can be placed in other websites as iFrame",
"li7": "Embedded within the OpenStreetMap-ecosystem, which has many tools available", "li7": "Embedded within the OpenStreetMap-ecosystem, which has many tools available",
"li8": "Fully open source (GPL) and free to use",
"title": "What is MapComplete?" "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", "tagline": "Collect geodata easily with OpenStreetMap",
"title": "MapComplete.osm.be", "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?" "whatIsOsm": "What is OpenStreetMap?"
}, },
"general": { "general": {

View file

@ -40,13 +40,12 @@
"reload": "Herlaad de data" "reload": "Herlaad de data"
}, },
"flyer": { "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", "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.", "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": { "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.", "intro": "MapComplete is een website met {mapCount} interactieve kaarten. Op iedere kaart kunnen gebruikers data zien en updaten.",
"li0": "Communiceer waar interessepunten zijn", "li0": "Communiceer waar interessepunten zijn",
"li1": "Voeg nieuwe punten toe en update informatie van reeds bestaande punten", "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", "li5": "Wissel tussen kaart- en luchtfoto's als achtergrond",
"li6": "Eenvoudig te embedden in een website als iFrame", "li6": "Eenvoudig te embedden in een website als iFrame",
"li7": "Deel van het OpenStreetMap-ecosysteem waarbinnen honderden andere tools bestaan", "li7": "Deel van het OpenStreetMap-ecosysteem waarbinnen honderden andere tools bestaan",
"li8": "Volledig Open-Source (GPL) en gratis te gebruiken",
"title": "Wat is MapComplete?" "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.", "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": "Verzamel geodata eenvoudig met OpenStreetMap", "tagline": "Eenvoudig geodata verzamelen met OpenStreetMap",
"title": "MapComplete.osm.be", "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?" "whatIsOsm": "Wat is OpenStreetMap?"
}, },
"general": { "general": {

11
package-lock.json generated
View file

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

55
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 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() 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() { async function main() {
const svg = await Utils.download(window.location.protocol + "//" + window.location.host + "/assets/templates/MapComplete-flyer.svg") 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") 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,
textSubstitutions: {
mapCount: "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length
}
}).ConvertSvg("flyer_en.pdf")
//*/ const options = <SvgToPdfOptions>{
getFreeDiv: createElement,
textSubstitutions: {
mapCount: "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length
},
disableMaps: false
}
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") Locale.language.setData("en")
const svgToPdf = new SvgToPdf([svgBack], { const svgToPdf = new SvgToPdf([svgBack], {
freeDivId: "extradiv", getFreeDiv: createElement,
textSubstitutions: { textSubstitutions: {
mapCount: "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length mapCount: "" + Array.from(AllKnownLayouts.allKnownLayouts.values()).filter(th => !th.hideFromOverview).length
} }
}) })
new VariableUiElement(svgToPdf.currentState).AttachTo("maindiv")
await svgToPdf.Prepare() await svgToPdf.Prepare()
console.log("Used translations", svgToPdf._usedTranslations) console.log("Used translations", svgToPdf._usedTranslations)
await svgToPdf.ConvertSvg("flyer_nl.pdf") await svgToPdf.ConvertSvg("flyer_nl.pdf")