forked from MapComplete/MapComplete
		
	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
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										75
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										75
									
								
								Utils.ts
									
										
									
									
									
								
							| 
						 | 
					@ -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") {
 | 
				
			||||||
| 
						 | 
					@ -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;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -844,15 +885,5 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    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
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue