Add download-as-svg options
This commit is contained in:
parent
e6997f9b9d
commit
308ab74a08
9 changed files with 266 additions and 58 deletions
|
@ -14,6 +14,9 @@ import SimpleMetaTagger from "../../Logic/SimpleMetaTagger";
|
||||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
|
||||||
import {BBox} from "../../Logic/BBox";
|
import {BBox} from "../../Logic/BBox";
|
||||||
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
|
import FilteredLayer, {FilterState} from "../../Models/FilteredLayer";
|
||||||
|
import geojson2svg from "geojson2svg"
|
||||||
|
import Constants from "../../Models/Constants";
|
||||||
|
import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
|
||||||
|
|
||||||
export class DownloadPanel extends Toggle {
|
export class DownloadPanel extends Toggle {
|
||||||
|
|
||||||
|
@ -21,9 +24,9 @@ export class DownloadPanel extends Toggle {
|
||||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||||
featurePipeline: FeaturePipeline,
|
featurePipeline: FeaturePipeline,
|
||||||
layoutToUse: LayoutConfig,
|
layoutToUse: LayoutConfig,
|
||||||
currentBounds: UIEventSource<BBox>
|
currentBounds: UIEventSource<BBox>,
|
||||||
}) {
|
|
||||||
|
|
||||||
|
}) {
|
||||||
|
|
||||||
const t = Translations.t.general.download
|
const t = Translations.t.general.download
|
||||||
const name = State.state.layoutToUse.id;
|
const name = State.state.layoutToUse.id;
|
||||||
|
@ -35,7 +38,7 @@ export class DownloadPanel extends Toggle {
|
||||||
const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
|
const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
|
||||||
new Combine([t.downloadGeojson.SetClass("font-bold"),
|
new Combine([t.downloadGeojson.SetClass("font-bold"),
|
||||||
t.downloadGeoJsonHelper]).SetClass("flex flex-col"))
|
t.downloadGeoJsonHelper]).SetClass("flex flex-col"))
|
||||||
.OnClickWithLoading(t.exporting,async () => {
|
.OnClickWithLoading(t.exporting, async () => {
|
||||||
const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
|
const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data)
|
||||||
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "),
|
Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "),
|
||||||
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, {
|
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, {
|
||||||
|
@ -57,10 +60,31 @@ export class DownloadPanel extends Toggle {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const buttonSvg = new SubtleButton(Svg.floppy_ui(), new Combine(
|
||||||
|
[t.downloadAsSvg.SetClass("font-bold"),
|
||||||
|
t.downloadAsSvgHelper]).SetClass("flex flex-col"))
|
||||||
|
.OnClickWithLoading(t.exporting, async () => {
|
||||||
|
const geojson = DownloadPanel.getCleanGeoJsonPerLayer(state, metaisIncluded.data)
|
||||||
|
const leafletdiv = document.getElementById("leafletDiv")
|
||||||
|
const csv = DownloadPanel.asSvg(geojson,
|
||||||
|
{
|
||||||
|
layers: state.filteredLayers.data.map(l => l.layerDef),
|
||||||
|
mapExtent: state.currentBounds.data,
|
||||||
|
width: leafletdiv.offsetWidth,
|
||||||
|
height: leafletdiv.offsetHeight
|
||||||
|
})
|
||||||
|
|
||||||
|
Utils.offerContentsAsDownloadableFile(csv,
|
||||||
|
`MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.svg`, {
|
||||||
|
mimetype: "image/svg+xml"
|
||||||
|
});
|
||||||
|
})
|
||||||
|
|
||||||
const downloadButtons = new Combine(
|
const downloadButtons = new Combine(
|
||||||
[new Title(t.title),
|
[new Title(t.title),
|
||||||
buttonGeoJson,
|
buttonGeoJson,
|
||||||
buttonCSV,
|
buttonCSV,
|
||||||
|
buttonSvg,
|
||||||
includeMetaToggle,
|
includeMetaToggle,
|
||||||
t.licenseInfo.SetClass("link-underline")])
|
t.licenseInfo.SetClass("link-underline")])
|
||||||
.SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
|
.SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4")
|
||||||
|
@ -71,41 +95,152 @@ export class DownloadPanel extends Toggle {
|
||||||
state.featurePipeline.somethingLoaded)
|
state.featurePipeline.somethingLoaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a geojson to an SVG
|
||||||
|
*
|
||||||
|
* const feature = {
|
||||||
|
* "type": "Feature",
|
||||||
|
* "properties": {},
|
||||||
|
* "geometry": {
|
||||||
|
* "type": "LineString",
|
||||||
|
* "coordinates": [
|
||||||
|
* [-180, 80],
|
||||||
|
* [180, -80]
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* const perLayer = new Map<string, any[]>([["testlayer", [feature]]])
|
||||||
|
* DownloadPanel.asSvg(perLayer).replace(/\n/g, "") // => `<svg width="1000px" height="1000px" viewBox="0 0 1000 1000"> <g id="testlayer" inkscape:groupmode="layer" inkscape:label="testlayer"> <path d="M0,27.77777777777778 1000,472.22222222222223" style="fill:none;stroke-width:1" stroke="#ff0000"/> </g></svg>`
|
||||||
|
*/
|
||||||
|
public static asSvg(perLayer: Map<string, any[]>,
|
||||||
|
options?:
|
||||||
|
{
|
||||||
|
layers?: LayerConfig[],
|
||||||
|
width?: 1000 | number,
|
||||||
|
height?: 1000 | number,
|
||||||
|
mapExtent?: BBox
|
||||||
|
unit?: "px" | "mm" | string
|
||||||
|
}) {
|
||||||
|
options = options ?? {}
|
||||||
|
const w = options.width ?? 1000
|
||||||
|
const h = options.height ?? 1000
|
||||||
|
const unit = options.unit ?? "px"
|
||||||
|
const mapExtent = {left: -180, bottom: -90, right: 180, top: 90}
|
||||||
|
if (options.mapExtent !== undefined) {
|
||||||
|
const bbox = options.mapExtent
|
||||||
|
mapExtent.left = bbox.minLon
|
||||||
|
mapExtent.right = bbox.maxLon
|
||||||
|
mapExtent.bottom = bbox.minLat
|
||||||
|
mapExtent.top = bbox.maxLat
|
||||||
|
}
|
||||||
|
|
||||||
|
const elements: string [] = []
|
||||||
|
|
||||||
|
for (const layer of Array.from(perLayer.keys())) {
|
||||||
|
const features = perLayer.get(layer)
|
||||||
|
if(features.length === 0){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const layerDef = options?.layers?.find(l => l.id === layer)
|
||||||
|
const rendering = layerDef?.lineRendering[0]
|
||||||
|
|
||||||
|
const converter = geojson2svg({
|
||||||
|
viewportSize: {width: w, height: h},
|
||||||
|
mapExtent,
|
||||||
|
output: 'svg',
|
||||||
|
attributes:[
|
||||||
|
{
|
||||||
|
property: "style",
|
||||||
|
type:'static',
|
||||||
|
value: "fill:none;stroke-width:1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
property: 'properties.stroke',
|
||||||
|
type:'dynamic',
|
||||||
|
key: 'stroke'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const feature of features) {
|
||||||
|
const stroke = rendering?.color?.GetRenderValue(feature.properties)?.txt ?? "#ff0000"
|
||||||
|
const color = Utils.colorAsHex( Utils.color(stroke))
|
||||||
|
feature.properties.stroke = color
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const groupPaths: string[] = converter.convert({type: "FeatureCollection", features})
|
||||||
|
const group = ` <g id="${layer}" inkscape:groupmode="layer" inkscape:label="${layer}">\n` +
|
||||||
|
groupPaths.map(p => " " + p).join("\n")
|
||||||
|
+ "\n </g>"
|
||||||
|
elements.push(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const header = `<svg width="${w}${unit}" height="${h}${unit}" viewBox="0 0 ${w} ${h}">`
|
||||||
|
return header + "\n" + elements.join("\n") + "\n</svg>"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all geojson as geojson feature
|
||||||
|
* @param state
|
||||||
|
* @param includeMetaData
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
private static getCleanGeoJson(state: {
|
private static getCleanGeoJson(state: {
|
||||||
featurePipeline: FeaturePipeline,
|
featurePipeline: FeaturePipeline,
|
||||||
currentBounds: UIEventSource<BBox>,
|
currentBounds: UIEventSource<BBox>,
|
||||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||||
}, includeMetaData: boolean) {
|
}, includeMetaData: boolean) {
|
||||||
|
const perLayer = DownloadPanel.getCleanGeoJsonPerLayer(state, includeMetaData)
|
||||||
|
const features = [].concat(...Array.from(perLayer.values()))
|
||||||
|
return {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const resultFeatures = []
|
private static getCleanGeoJsonPerLayer(state: {
|
||||||
|
featurePipeline: FeaturePipeline,
|
||||||
|
currentBounds: UIEventSource<BBox>,
|
||||||
|
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||||
|
}, includeMetaData: boolean): Map<string, any[]> /*{layerId --> geojsonFeatures[]}*/ {
|
||||||
|
|
||||||
|
const perLayer = new Map<string, any[]>();
|
||||||
const neededLayers = state.filteredLayers.data.map(l => l.layerDef.id)
|
const neededLayers = state.filteredLayers.data.map(l => l.layerDef.id)
|
||||||
const bbox = state.currentBounds.data
|
const bbox = state.currentBounds.data
|
||||||
const featureList = state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox, new Set(neededLayers));
|
const featureList = state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox, new Set(neededLayers));
|
||||||
for (const tile of featureList) {
|
outer : for (const tile of featureList) {
|
||||||
|
|
||||||
|
if(Constants.priviliged_layers.indexOf(tile.layer) >= 0){
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
const layer = state.filteredLayers.data.find(fl => fl.layerDef.id === tile.layer)
|
const layer = state.filteredLayers.data.find(fl => fl.layerDef.id === tile.layer)
|
||||||
|
if (!perLayer.has(tile.layer)) {
|
||||||
|
perLayer.set(tile.layer, [])
|
||||||
|
}
|
||||||
|
const featureList = perLayer.get(tile.layer)
|
||||||
const filters = layer.appliedFilters.data
|
const filters = layer.appliedFilters.data
|
||||||
for (const feature of tile.features) {
|
for (const feature of tile.features) {
|
||||||
|
|
||||||
if(!bbox.overlapsWith(BBox.get(feature))){
|
if (!bbox.overlapsWith(BBox.get(feature))) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (filters !== undefined) {
|
if (filters !== undefined) {
|
||||||
let featureDoesMatchAllFilters = true;
|
|
||||||
for (let key of Array.from(filters.keys())) {
|
for (let key of Array.from(filters.keys())) {
|
||||||
const filter: FilterState = filters.get(key)
|
const filter: FilterState = filters.get(key)
|
||||||
if(filter?.currentFilter === undefined){
|
if (filter?.currentFilter === undefined) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (!filter.currentFilter.matchesProperties(feature.properties)) {
|
if (!filter.currentFilter.matchesProperties(feature.properties)) {
|
||||||
featureDoesMatchAllFilters = false;
|
continue outer;
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(!featureDoesMatchAllFilters){
|
|
||||||
continue; // the outer loop
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleaned = {
|
const cleaned = {
|
||||||
|
@ -130,14 +265,11 @@ export class DownloadPanel extends Toggle {
|
||||||
delete feature.properties[key]
|
delete feature.properties[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
resultFeatures.push(feature)
|
featureList.push(feature)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return perLayer
|
||||||
type: "FeatureCollection",
|
|
||||||
features: resultFeatures
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
89
Utils.ts
89
Utils.ts
|
@ -111,7 +111,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a number to a string with one number after the comma
|
* Converts a number to a string with one number after the comma
|
||||||
*
|
*
|
||||||
* Utils.Round(15) // => "15.0"
|
* Utils.Round(15) // => "15.0"
|
||||||
* Utils.Round(1) // => "1.0"
|
* Utils.Round(1) // => "1.0"
|
||||||
* Utils.Round(1.5) // => "1.5"
|
* Utils.Round(1.5) // => "1.5"
|
||||||
|
@ -310,17 +310,17 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
/**
|
/**
|
||||||
* Copies all key-value pairs of the source into the target. This will change the target
|
* Copies all key-value pairs of the source into the target. This will change the target
|
||||||
* If the key starts with a '+', the values of the list will be appended to the target instead of overwritten
|
* If the key starts with a '+', the values of the list will be appended to the target instead of overwritten
|
||||||
*
|
*
|
||||||
* const obj = {someValue: 42};
|
* const obj = {someValue: 42};
|
||||||
* const override = {someValue: null};
|
* const override = {someValue: null};
|
||||||
* Utils.Merge(override, obj);
|
* Utils.Merge(override, obj);
|
||||||
* obj.someValue // => null
|
* obj.someValue // => null
|
||||||
*
|
*
|
||||||
* const obj = {someValue: 42};
|
* const obj = {someValue: 42};
|
||||||
* const override = {someValue: null};
|
* const override = {someValue: null};
|
||||||
* const returned = Utils.Merge(override, obj);
|
* const returned = Utils.Merge(override, obj);
|
||||||
* returned == obj // => true
|
* returned == obj // => true
|
||||||
*
|
*
|
||||||
* const source = {
|
* const source = {
|
||||||
* abc: "def",
|
* abc: "def",
|
||||||
* foo: "bar",
|
* foo: "bar",
|
||||||
|
@ -347,7 +347,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
*/
|
*/
|
||||||
static Merge<T, S>(source: S, target: T): (T & S) {
|
static Merge<T, S>(source: S, target: T): (T & S) {
|
||||||
if (target === null) {
|
if (target === null) {
|
||||||
return <T & S> source
|
return <T & S>source
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const key in source) {
|
for (const key in source) {
|
||||||
|
@ -457,9 +457,9 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
return collectedList
|
return collectedList
|
||||||
}
|
}
|
||||||
if (Array.isArray(leaf)) {
|
if (Array.isArray(leaf)) {
|
||||||
for (let i = 0; i < (<any[]>leaf).length; i++){
|
for (let i = 0; i < (<any[]>leaf).length; i++) {
|
||||||
const l = (<any[]>leaf)[i];
|
const l = (<any[]>leaf)[i];
|
||||||
collectedList.push({leaf: l, path: [...travelledPath, ""+i]})
|
collectedList.push({leaf: l, path: [...travelledPath, "" + i]})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
collectedList.push({leaf, path: travelledPath})
|
collectedList.push({leaf, path: travelledPath})
|
||||||
|
@ -489,15 +489,15 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
return f(undefined)
|
return f(undefined)
|
||||||
}
|
}
|
||||||
const jtp = typeof json
|
const jtp = typeof json
|
||||||
if(isLeaf !== undefined) {
|
if (isLeaf !== undefined) {
|
||||||
if(jtp === "object"){
|
if (jtp === "object") {
|
||||||
if(isLeaf(json)){
|
if (isLeaf(json)) {
|
||||||
return f(json)
|
return f(json)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
}else if (jtp === "boolean" || jtp === "string" || jtp === "number") {
|
} else if (jtp === "boolean" || jtp === "string" || jtp === "number") {
|
||||||
return f(json)
|
return f(json)
|
||||||
}
|
}
|
||||||
if (Array.isArray(json)) {
|
if (Array.isArray(json)) {
|
||||||
|
@ -661,7 +661,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
* Triggers a 'download file' popup which will download the contents
|
* Triggers a 'download file' popup which will download the contents
|
||||||
*/
|
*/
|
||||||
public static offerContentsAsDownloadableFile(contents: string | Blob, fileName: string = "download.txt",
|
public static offerContentsAsDownloadableFile(contents: string | Blob, fileName: string = "download.txt",
|
||||||
options?: { mimetype: string | "text/plain" | "text/csv" | "application/vnd.geo+json" | "{gpx=application/gpx+xml}"}) {
|
options?: { mimetype: string | "text/plain" | "text/csv" | "application/vnd.geo+json" | "{gpx=application/gpx+xml}" }) {
|
||||||
const element = document.createElement("a");
|
const element = document.createElement("a");
|
||||||
let file;
|
let file;
|
||||||
if (typeof (contents) === "string") {
|
if (typeof (contents) === "string") {
|
||||||
|
@ -713,7 +713,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reorders an object: creates a new object where the keys have been added alphabetically
|
* Reorders an object: creates a new object where the keys have been added alphabetically
|
||||||
*
|
*
|
||||||
* const sorted = Utils.sortKeys({ x: 'x', abc: {'x': 'x', 'a': 'a'}, def: 'def'})
|
* const sorted = Utils.sortKeys({ x: 'x', abc: {'x': 'x', 'a': 'a'}, def: 'def'})
|
||||||
* JSON.stringify(sorted) // => '{"abc":{"a":"a","x":"x"},"def":"def","x":"x"}'
|
* JSON.stringify(sorted) // => '{"abc":{"a":"a","x":"x"},"def":"def","x":"x"}'
|
||||||
*/
|
*/
|
||||||
|
@ -816,15 +816,56 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
return track[str2.length][str1.length];
|
return track[str2.length][str1.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static MapToObj<T>(d: Map<string, T>, onValue: ((t: T, key: string) => any) = undefined): object {
|
||||||
|
const o = {}
|
||||||
|
d.forEach((value, key) => {
|
||||||
|
if (onValue !== undefined) {
|
||||||
|
value = onValue(value, key)
|
||||||
|
}
|
||||||
|
o[key] = value;
|
||||||
|
})
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) {
|
private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) {
|
||||||
return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b);
|
return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static color(hex: string): { r: number, g: number, b: number } {
|
/**
|
||||||
if (hex.startsWith == undefined) {
|
* Utils.colorAsHex({r: 255, g: 128, b: 0}) // => "#ff8000"
|
||||||
console.trace("WUT?", hex)
|
* Utils.colorAsHex(undefined) // => undefined
|
||||||
throw "wut?"
|
*/
|
||||||
|
public static colorAsHex(c:{ r: number, g: number, b: number } ){
|
||||||
|
if(c === undefined){
|
||||||
|
return undefined
|
||||||
}
|
}
|
||||||
|
function componentToHex(n) {
|
||||||
|
let hex = n.toString(16);
|
||||||
|
return hex.length == 1 ? "0" + hex : hex;
|
||||||
|
}
|
||||||
|
return "#" + componentToHex(c.r) + componentToHex(c.g) + componentToHex(c.b);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Utils.color("#ff8000") // => {r: 255, g:128, b: 0}
|
||||||
|
* Utils.color(" rgba (12,34,56) ") // => {r: 12, g:34, b: 56}
|
||||||
|
* Utils.color(" rgba (12,34,56,0.5) ") // => {r: 12, g:34, b: 56}
|
||||||
|
* Utils.color(undefined) // => undefined
|
||||||
|
*/
|
||||||
|
public static color(hex: string): { r: number, g: number, b: number } {
|
||||||
|
if(hex === undefined){
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
hex = hex.replace(/[ \t]/g, "")
|
||||||
|
if (hex.startsWith("rgba(")) {
|
||||||
|
const match = hex.match(/rgba\(([0-9.]+),([0-9.]+),([0-9.]+)(,[0-9.]*)?\)/)
|
||||||
|
if(match == undefined){
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return {r: Number(match[1]), g: Number(match[2]), b:Number( match[3])}
|
||||||
|
}
|
||||||
|
|
||||||
if (!hex.startsWith("#")) {
|
if (!hex.startsWith("#")) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -842,17 +883,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
|
||||||
b: parseInt(hex.substr(5, 2), 16),
|
b: parseInt(hex.substr(5, 2), 16),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static MapToObj<T>(d : Map<string, T>, onValue: ((t:T, key: string) => any) = undefined): object{
|
|
||||||
const o = {}
|
|
||||||
d.forEach((value, key) => {
|
|
||||||
if(onValue !== undefined){
|
|
||||||
value = onValue(value, key)
|
|
||||||
}
|
|
||||||
o[key] = value;
|
|
||||||
})
|
|
||||||
return o
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -404,4 +404,4 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -538,7 +538,9 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"then": "Beheer door een privépersoon",
|
"then": "Beheer door een privépersoon",
|
||||||
"addExtraTags": ["operator="]
|
"addExtraTags": [
|
||||||
|
"operator="
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"condition": {
|
"condition": {
|
||||||
|
|
|
@ -200,7 +200,9 @@
|
||||||
"pt_BR": "Pode ser usado de graça",
|
"pt_BR": "Pode ser usado de graça",
|
||||||
"de": "Nutzung kostenlos"
|
"de": "Nutzung kostenlos"
|
||||||
},
|
},
|
||||||
"addExtraTags": ["charge="]
|
"addExtraTags": [
|
||||||
|
"charge="
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -258,7 +258,9 @@
|
||||||
{
|
{
|
||||||
"if": "not:addr:unit=yes",
|
"if": "not:addr:unit=yes",
|
||||||
"then": "There is no sub-unit within this address",
|
"then": "There is no sub-unit within this address",
|
||||||
"addExtraTags": ["addr:unit="]
|
"addExtraTags": [
|
||||||
|
"addr:unit="
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": "addr:unit=",
|
"if": "addr:unit=",
|
||||||
|
@ -332,7 +334,9 @@
|
||||||
"nl": "Dit gebouw heeft geen huisnummer",
|
"nl": "Dit gebouw heeft geen huisnummer",
|
||||||
"de": "Dieses Gebäude hat keine Hausnummer"
|
"de": "Dieses Gebäude hat keine Hausnummer"
|
||||||
},
|
},
|
||||||
"addExtraTags": [ "addr:housenumber="]
|
"addExtraTags": [
|
||||||
|
"addr:housenumber="
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": {
|
"if": {
|
||||||
|
@ -368,8 +372,11 @@
|
||||||
"then": {
|
"then": {
|
||||||
"en": "No extra place name is given or needed"
|
"en": "No extra place name is given or needed"
|
||||||
},
|
},
|
||||||
"addExtraTags": ["addr:substreet="]
|
"addExtraTags": [
|
||||||
},{
|
"addr:substreet="
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
"if": "addr:substreet=",
|
"if": "addr:substreet=",
|
||||||
"then": {
|
"then": {
|
||||||
"en": "<div class='subtle'>Place (e.g. \"Castle Mews\", \"West Business Park\")</div>"
|
"en": "<div class='subtle'>Place (e.g. \"Castle Mews\", \"West Business Park\")</div>"
|
||||||
|
@ -399,7 +406,9 @@
|
||||||
"then": {
|
"then": {
|
||||||
"en": "No extra place name is given or needed"
|
"en": "No extra place name is given or needed"
|
||||||
},
|
},
|
||||||
"addExtraTags": ["addr:substreet="]
|
"addExtraTags": [
|
||||||
|
"addr:substreet="
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": "addr:substreet=",
|
"if": "addr:substreet=",
|
||||||
|
@ -473,7 +482,9 @@
|
||||||
{
|
{
|
||||||
"if": "not:addr:parentstreet=yes",
|
"if": "not:addr:parentstreet=yes",
|
||||||
"then": "No parent street name is needed within this address",
|
"then": "No parent street name is needed within this address",
|
||||||
"addExtraTags": ["addr:parentstreet="]
|
"addExtraTags": [
|
||||||
|
"addr:parentstreet="
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"if": "addr:parentstreet:={_closest_street:0:name}",
|
"if": "addr:parentstreet:={_closest_street:0:name}",
|
||||||
|
|
|
@ -102,6 +102,8 @@
|
||||||
"download": {
|
"download": {
|
||||||
"downloadAsPdf": "Download a PDF of the current map",
|
"downloadAsPdf": "Download a PDF of the current map",
|
||||||
"downloadAsPdfHelper": "Ideal to print the current map",
|
"downloadAsPdfHelper": "Ideal to print the current map",
|
||||||
|
"downloadAsSvg": "Download an SVG of the current map",
|
||||||
|
"downloadAsSvgHelper": "Compatible Inkscape or Adobe Illustrator; will need further processing ",
|
||||||
"downloadCSV": "Download visible data as CSV",
|
"downloadCSV": "Download visible data as CSV",
|
||||||
"downloadCSVHelper": "Compatible with LibreOffice Calc, Excel, …",
|
"downloadCSVHelper": "Compatible with LibreOffice Calc, Excel, …",
|
||||||
"downloadFeatureAsGeojson": "Download as GeoJson-file",
|
"downloadFeatureAsGeojson": "Download as GeoJson-file",
|
||||||
|
|
27
package-lock.json
generated
27
package-lock.json
generated
|
@ -29,6 +29,7 @@
|
||||||
"doctest-ts": "^0.5.0",
|
"doctest-ts": "^0.5.0",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"geojson2svg": "^1.3.1",
|
||||||
"i18next-client": "^1.11.4",
|
"i18next-client": "^1.11.4",
|
||||||
"idb-keyval": "^6.0.3",
|
"idb-keyval": "^6.0.3",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
|
@ -7309,6 +7310,14 @@
|
||||||
"quickselect": "^2.0.0"
|
"quickselect": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/geojson2svg": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/geojson2svg/-/geojson2svg-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-rurp7ebV+qG+pKNmPa2DI7J/hqtHsAkweamfbx0UXo/i/rIL7S2miFyRphOR9EzB38Q5DJThFUfa+m1LDILMkA==",
|
||||||
|
"dependencies": {
|
||||||
|
"multigeojson": "~0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-caller-file": {
|
"node_modules/get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
@ -9990,6 +9999,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/multigeojson": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/multigeojson/-/multigeojson-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-8kBKgLbuWpZCq7F9sBqefxgJ7z4="
|
||||||
|
},
|
||||||
"node_modules/mute-stream": {
|
"node_modules/mute-stream": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||||
|
@ -22725,6 +22739,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"geojson2svg": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/geojson2svg/-/geojson2svg-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-rurp7ebV+qG+pKNmPa2DI7J/hqtHsAkweamfbx0UXo/i/rIL7S2miFyRphOR9EzB38Q5DJThFUfa+m1LDILMkA==",
|
||||||
|
"requires": {
|
||||||
|
"multigeojson": "~0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"get-caller-file": {
|
"get-caller-file": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||||
|
@ -24766,6 +24788,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
|
"multigeojson": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/multigeojson/-/multigeojson-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-8kBKgLbuWpZCq7F9sBqefxgJ7z4="
|
||||||
|
},
|
||||||
"mute-stream": {
|
"mute-stream": {
|
||||||
"version": "0.0.8",
|
"version": "0.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz",
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
"doctest-ts": "^0.5.0",
|
"doctest-ts": "^0.5.0",
|
||||||
"email-validator": "^2.0.4",
|
"email-validator": "^2.0.4",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
"geojson2svg": "^1.3.1",
|
||||||
"i18next-client": "^1.11.4",
|
"i18next-client": "^1.11.4",
|
||||||
"idb-keyval": "^6.0.3",
|
"idb-keyval": "^6.0.3",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
|
|
Loading…
Reference in a new issue