import {SubtleButton} from "../Base/SubtleButton"; import Svg from "../../Svg"; import Translations from "../i18n/Translations"; import State from "../../State"; import {Utils} from "../../Utils"; import Combine from "../Base/Combine"; import CheckBoxes from "../Input/Checkboxes"; import {GeoOperations} from "../../Logic/GeoOperations"; import Toggle from "../Input/Toggle"; import Title from "../Base/Title"; import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline"; import {UIEventSource} from "../../Logic/UIEventSource"; import SimpleMetaTagger from "../../Logic/SimpleMetaTagger"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import {BBox} from "../../Logic/BBox"; 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 { constructor(state: { filteredLayers: UIEventSource featurePipeline: FeaturePipeline, layoutToUse: LayoutConfig, currentBounds: UIEventSource, }) { const t = Translations.t.general.download const name = State.state.layoutToUse.id; const includeMetaToggle = new CheckBoxes([t.includeMetaData]) const metaisIncluded = includeMetaToggle.GetValue().map(selected => selected.length > 0) const buttonGeoJson = new SubtleButton(Svg.floppy_ui(), new Combine([t.downloadGeojson.SetClass("font-bold"), t.downloadGeoJsonHelper]).SetClass("flex flex-col")) .OnClickWithLoading(t.exporting, async () => { const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) Utils.offerContentsAsDownloadableFile(JSON.stringify(geojson, null, " "), `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.geojson`, { mimetype: "application/vnd.geo+json" }); }) const buttonCSV = new SubtleButton(Svg.floppy_ui(), new Combine( [t.downloadCSV.SetClass("font-bold"), t.downloadCSVHelper]).SetClass("flex flex-col")) .OnClickWithLoading(t.exporting, async () => { const geojson = DownloadPanel.getCleanGeoJson(state, metaisIncluded.data) const csv = GeoOperations.toCSV(geojson.features) Utils.offerContentsAsDownloadableFile(csv, `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.csv`, { mimetype: "text/csv" }); }) 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( [new Title(t.title), buttonGeoJson, buttonCSV, buttonSvg, includeMetaToggle, t.licenseInfo.SetClass("link-underline")]) .SetClass("w-full flex flex-col border-4 border-gray-300 rounded-3xl p-4") super( downloadButtons, t.noDataLoaded, 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([["testlayer", [feature]]]) * DownloadPanel.asSvg(perLayer).replace(/\n/g, "") // => ` ` */ public static asSvg(perLayer: Map, 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 = ` \n` + groupPaths.map(p => " " + p).join("\n") + "\n " elements.push(group) } const header = `` return header + "\n" + elements.join("\n") + "\n" } /** * Gets all geojson as geojson feature * @param state * @param includeMetaData * @private */ private static getCleanGeoJson(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource, filteredLayers: UIEventSource }, includeMetaData: boolean) { const perLayer = DownloadPanel.getCleanGeoJsonPerLayer(state, includeMetaData) const features = [].concat(...Array.from(perLayer.values())) return { type: "FeatureCollection", features } } private static getCleanGeoJsonPerLayer(state: { featurePipeline: FeaturePipeline, currentBounds: UIEventSource, filteredLayers: UIEventSource }, includeMetaData: boolean): Map /*{layerId --> geojsonFeatures[]}*/ { const perLayer = new Map(); const neededLayers = state.filteredLayers.data.map(l => l.layerDef.id) const bbox = state.currentBounds.data const featureList = state.featurePipeline.GetAllFeaturesAndMetaWithin(bbox, new Set(neededLayers)); 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) if (!perLayer.has(tile.layer)) { perLayer.set(tile.layer, []) } const featureList = perLayer.get(tile.layer) const filters = layer.appliedFilters.data for (const feature of tile.features) { if (!bbox.overlapsWith(BBox.get(feature))) { continue } if (filters !== undefined) { for (let key of Array.from(filters.keys())) { const filter: FilterState = filters.get(key) if (filter?.currentFilter === undefined) { continue } if (!filter.currentFilter.matchesProperties(feature.properties)) { continue outer; } } } const cleaned = { type: feature.type, geometry: {...feature.geometry}, properties: {...feature.properties} } if (!includeMetaData) { for (const key in cleaned.properties) { if (key === "_lon" || key === "_lat") { continue; } if (key.startsWith("_")) { delete feature.properties[key] } } } const datedKeys = [].concat(SimpleMetaTagger.metatags.filter(tagging => tagging.includesDates).map(tagging => tagging.keys)) for (const key of datedKeys) { delete feature.properties[key] } featureList.push(feature) } } return perLayer } }