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.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) ) 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 perfeature: 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 perfeature } } } 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(cleaned) } } return perLayer } }