import { SubtleButton } from "../Base/SubtleButton" import Svg from "../../Svg" import Translations from "../i18n/Translations" 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 { Store } from "../../Logic/UIEventSource" import SimpleMetaTagger from "../../Logic/SimpleMetaTagger" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" import { BBox } from "../../Logic/BBox" import geojson2svg from "geojson2svg" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import { SpecialVisualizationState } from "../SpecialVisualization" import { Feature, FeatureCollection } from "geojson" import { GeoIndexedStoreForLayer } from "../../Logic/FeatureSource/Actors/GeoIndexedStore" import LayerState from "../../Logic/State/LayerState" import { PriviligedLayerType } from "../../Models/Constants" export class DownloadPanel extends Toggle { constructor(state: SpecialVisualizationState) { const t = Translations.t.general.download const name = state.layout.id const includeMetaToggle = new CheckBoxes([t.includeMetaData]) const metaisIncluded = includeMetaToggle.GetValue().map((selected) => selected.length > 0) const buttonGeoJson = new SubtleButton( Svg.floppy_svg(), 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_svg(), 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_svg(), 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 maindiv = document.getElementById("maindiv") const layers = state.layout.layers.filter((l) => l.source !== null) const csv = DownloadPanel.asSvg(geojson, { layers, mapExtent: state.mapProperties.bounds.data, width: maindiv.offsetWidth, height: maindiv.offsetHeight, }) Utils.offerContentsAsDownloadableFile( csv, `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.svg`, { mimetype: "image/svg+xml", } ) }) const buttonPng = new SubtleButton( Svg.floppy_svg(), new Combine([t.downloadAsPng.SetClass("font-bold"), t.downloadAsPngHelper]) ).OnClickWithLoading(t.exporting, async () => { const gpsLayer = state.layerState.filteredLayers.get( "gps_location" ) const gpsIsDisplayed = gpsLayer.isDisplayed.data try { gpsLayer.isDisplayed.setData(false) const png = await state.mapProperties.exportAsPng() Utils.offerContentsAsDownloadableFile( png, `MapComplete_${name}_export_${new Date().toISOString().substr(0, 19)}.png`, { mimetype: "image/png", } ) } catch (e) { console.error(e) } finally { gpsLayer.isDisplayed.setData(gpsIsDisplayed) } }) const downloadButtons = new Combine([ new Title(t.title), buttonGeoJson, buttonCSV, buttonSvg, buttonPng, includeMetaToggle, t.licenseInfo.SetClass("link-underline"), ]).SetClass("w-full flex flex-col") super( downloadButtons, t.noDataLoaded, state.dataIsLoading.map((x) => !x) ) } /** * 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 width = options.width ?? 1000 const height = options.height ?? 1000 if (width <= 0 || height <= 0) { throw "Invalid width of height, they should be > 0" } 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 } console.log("Generateing svg, extent:", { mapExtent, width, height }) 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, height }, 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 w = width const h = height const header = `` return header + "\n" + elements.join("\n") + "\n" } private static getCleanGeoJson( state: { layout: LayoutConfig mapProperties: { bounds: Store } perLayer: ReadonlyMap layerState: LayerState }, includeMetaData: boolean ): FeatureCollection { const featuresPerLayer = DownloadPanel.getCleanGeoJsonPerLayer(state, includeMetaData) const features = [].concat(...Array.from(featuresPerLayer.values())) return { type: "FeatureCollection", features, } } /** * Returns a new feature of which all the metatags are deleted */ private static cleanFeature(f: Feature): Feature { f = { type: f.type, geometry: { ...f.geometry }, properties: { ...f.properties }, } for (const key in f.properties) { if (key === "_lon" || key === "_lat") { continue } if (key.startsWith("_")) { delete f.properties[key] } } const datedKeys = [].concat( SimpleMetaTagger.metatags .filter((tagging) => tagging.includesDates) .map((tagging) => tagging.keys) ) for (const key of datedKeys) { delete f.properties[key] } return f } private static getCleanGeoJsonPerLayer( state: { layout: LayoutConfig mapProperties: { bounds: Store } perLayer: ReadonlyMap layerState: LayerState }, includeMetaData: boolean ): Map { const featuresPerLayer = new Map() const neededLayers = state.layout.layers.filter((l) => l.source !== null).map((l) => l.id) const bbox = state.mapProperties.bounds.data for (const neededLayer of neededLayers) { const indexedFeatureSource = state.perLayer.get(neededLayer) let features = indexedFeatureSource.GetFeaturesWithin(bbox, true) // The 'indexedFeatureSources' contains _all_ features, they are not filtered yet const filter = state.layerState.filteredLayers.get(neededLayer) features = features.filter((f) => filter.isShown(f.properties, state.layerState.globalFilters.data) ) if (!includeMetaData) { features = features.map((f) => DownloadPanel.cleanFeature(f)) } featuresPerLayer.set(neededLayer, features) } return featuresPerLayer } }