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 {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 {
|
||||
|
||||
|
@ -21,9 +24,9 @@ export class DownloadPanel extends Toggle {
|
|||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
featurePipeline: FeaturePipeline,
|
||||
layoutToUse: LayoutConfig,
|
||||
currentBounds: UIEventSource<BBox>
|
||||
}) {
|
||||
currentBounds: UIEventSource<BBox>,
|
||||
|
||||
}) {
|
||||
|
||||
const t = Translations.t.general.download
|
||||
const name = State.state.layoutToUse.id;
|
||||
|
@ -35,7 +38,7 @@ export class DownloadPanel extends Toggle {
|
|||
const buttonGeoJson = new SubtleButton(Svg.floppy_ui(),
|
||||
new Combine([t.downloadGeojson.SetClass("font-bold"),
|
||||
t.downloadGeoJsonHelper]).SetClass("flex flex-col"))
|
||||
.OnClickWithLoading(t.exporting,async () => {
|
||||
.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`, {
|
||||
|
@ -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(
|
||||
[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")
|
||||
|
@ -71,41 +95,152 @@ export class DownloadPanel extends Toggle {
|
|||
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: {
|
||||
featurePipeline: FeaturePipeline,
|
||||
currentBounds: UIEventSource<BBox>,
|
||||
filteredLayers: UIEventSource<FilteredLayer[]>
|
||||
}, 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 bbox = state.currentBounds.data
|
||||
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)
|
||||
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))){
|
||||
|
||||
if (!bbox.overlapsWith(BBox.get(feature))) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (filters !== undefined) {
|
||||
let featureDoesMatchAllFilters = true;
|
||||
for (let key of Array.from(filters.keys())) {
|
||||
const filter: FilterState = filters.get(key)
|
||||
if(filter?.currentFilter === undefined){
|
||||
if (filter?.currentFilter === undefined) {
|
||||
continue
|
||||
}
|
||||
if (!filter.currentFilter.matchesProperties(feature.properties)) {
|
||||
featureDoesMatchAllFilters = false;
|
||||
break
|
||||
continue outer;
|
||||
}
|
||||
}
|
||||
if(!featureDoesMatchAllFilters){
|
||||
continue; // the outer loop
|
||||
}
|
||||
}
|
||||
|
||||
const cleaned = {
|
||||
|
@ -130,14 +265,11 @@ export class DownloadPanel extends Toggle {
|
|||
delete feature.properties[key]
|
||||
}
|
||||
|
||||
resultFeatures.push(feature)
|
||||
featureList.push(feature)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: "FeatureCollection",
|
||||
features: resultFeatures
|
||||
}
|
||||
return perLayer
|
||||
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue