forked from MapComplete/MapComplete
More work on the flyers
This commit is contained in:
parent
921132b478
commit
27ccce70c0
16 changed files with 687 additions and 308 deletions
|
@ -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
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
import { ReadonlyInputElement } from "./InputElement"
|
||||
import {ReadonlyInputElement} from "./InputElement"
|
||||
import Loc from "../../Models/Loc"
|
||||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Minimap, { MinimapObj } from "../Base/Minimap"
|
||||
import {Store, UIEventSource} from "../../Logic/UIEventSource"
|
||||
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 {GeoOperations} from "../../Logic/GeoOperations"
|
||||
import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"
|
||||
import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { BBox } from "../../Logic/BBox"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import {BBox} from "../../Logic/BBox"
|
||||
import {FixedUiElement} from "../Base/FixedUiElement"
|
||||
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) {
|
||||
|
@ -129,7 +145,7 @@ export default class LocationInput
|
|||
return {
|
||||
type: "Feature",
|
||||
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.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,
|
||||
|
@ -181,7 +197,7 @@ export default class LocationInput
|
|||
try {
|
||||
const self = this
|
||||
const hasMoved = new UIEventSource(false)
|
||||
const startLocation = { ...this._centerLocation.data }
|
||||
const startLocation = {...this._centerLocation.data}
|
||||
this._centerLocation.addCallbackD((newLocation) => {
|
||||
const f = 100000
|
||||
console.log(newLocation.lon, startLocation.lon)
|
||||
|
@ -204,14 +220,14 @@ 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) => {
|
||||
if (loc === undefined) {
|
||||
return []
|
||||
}
|
||||
return [{ feature: loc }]
|
||||
return [{feature: loc}]
|
||||
})
|
||||
console.log("Constructing the match layer", matchPoint)
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
12
Utils.ts
12
Utils.ts
|
@ -12,8 +12,8 @@ export class Utils {
|
|||
url: string,
|
||||
headers?: any
|
||||
) => 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\`.
|
||||
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.
|
||||
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.
|
||||
|
||||
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)
|
||||
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
|
||||
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
|
||||
|
||||
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.
|
||||
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 = [
|
||||
|
@ -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)
|
||||
|
|
|
@ -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,23 +61,26 @@ 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
|
||||
state.currentBounds.setData(bounds)
|
||||
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!")
|
||||
}
|
||||
|
||||
window.setTimeout(() => {
|
||||
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
|
||||
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) => {
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,11 +517,135 @@ export class SvgToPdf {
|
|||
|
||||
private _isPrepared = false;
|
||||
|
||||
private setState(message: string) {
|
||||
this._currentState.setData(message)
|
||||
}
|
||||
|
||||
private async prepareMap(mapSpec: SVGTSpanElement,): Promise<void> {
|
||||
// Upper left point of the tspan
|
||||
const {x, y} = SvgToPdfInternals.GetActualXY(mapSpec)
|
||||
|
||||
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() {
|
||||
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)) {
|
||||
|
@ -478,114 +653,16 @@ export class SvgToPdf {
|
|||
}
|
||||
}
|
||||
|
||||
for (const mapSpec of mapSpecs) {
|
||||
// Upper left point of the tspan
|
||||
const {x, y} = SvgToPdfInternals.GetActualXY(mapSpec)
|
||||
const self = this;
|
||||
await Promise.all(mapSpecs.map(ms => self.prepareMap(ms)))
|
||||
|
||||
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> {
|
||||
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")
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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 |
|
@ -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": {
|
||||
|
|
|
@ -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
11
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
55
test.ts
55
test.ts
|
@ -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,
|
||||
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")
|
||||
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")
|
||||
|
|
Loading…
Reference in a new issue