Refactoring: fix download buttons

This commit is contained in:
Pieter Vander Vennet 2023-04-14 04:33:06 +02:00
parent 8a1f0599d9
commit ef0ec5160d
10 changed files with 142 additions and 134 deletions

View file

@ -23,7 +23,7 @@ export default class GeoIndexedStore implements FeatureSource {
* @param bbox * @param bbox
* @constructor * @constructor
*/ */
public GetFeaturesWithin(bbox: BBox): Feature[] { public GetFeaturesWithin(bbox: BBox, strict: boolean = false): Feature[] {
// TODO optimize // TODO optimize
const bboxFeature = bbox.asGeojsonCached() const bboxFeature = bbox.asGeojsonCached()
return this.features.data.filter((f) => { return this.features.data.filter((f) => {

View file

@ -54,7 +54,6 @@ export default class FilteringFeatureSource implements FeatureSource {
this.update() this.update()
} }
private update() { private update() {
const self = this const self = this
const layer = this._layer const layer = this._layer
@ -64,26 +63,9 @@ export default class FilteringFeatureSource implements FeatureSource {
const newFeatures = (features ?? []).filter((f) => { const newFeatures = (features ?? []).filter((f) => {
self.registerCallback(f) self.registerCallback(f)
const isShown: TagsFilter = layer.layerDef.isShown if (!layer.isShown(f.properties, globalFilters)) {
const tags = f.properties
if (isShown !== undefined && !isShown.matchesProperties(tags)) {
return false return false
} }
if (tags._deleted === "yes") {
return false
}
let neededTags: TagsFilter = layer.currentFilter.data
if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) {
return false
}
for (const globalFilter of globalFilters ?? []) {
const neededTags = globalFilter.osmTags
if (neededTags !== undefined && !neededTags.matchesProperties(f.properties)) {
return false
}
}
includedFeatureIds.add(f.properties.id) includedFeatureIds.add(f.properties.id)
return true return true

View file

@ -8,6 +8,7 @@ import { TagsFilter } from "../Logic/Tags/TagsFilter"
import { Utils } from "../Utils" import { Utils } from "../Utils"
import { TagUtils } from "../Logic/Tags/TagUtils" import { TagUtils } from "../Logic/Tags/TagUtils"
import { And } from "../Logic/Tags/And" import { And } from "../Logic/Tags/And"
import { GlobalFilter } from "./GlobalFilter"
export default class FilteredLayer { export default class FilteredLayer {
/** /**
@ -62,7 +63,7 @@ export default class FilteredLayer {
return JSON.stringify(values) return JSON.stringify(values)
} }
public static stringToFieldProperties(value: string): Record<string, string> { private static stringToFieldProperties(value: string): Record<string, string> {
const values = JSON.parse(value) const values = JSON.parse(value)
for (const key in values) { for (const key in values) {
if (values[key] === "") { if (values[key] === "") {
@ -208,4 +209,34 @@ export default class FilteredLayer {
} }
return optimized return optimized
} }
/**
* Returns true if the given tags match the current filters (and the specified 'global filters')
*/
public isShown(properties: Record<string, string>, globalFilters?: GlobalFilter[]): boolean {
if (properties._deleted === "yes") {
return false
}
{
const isShown: TagsFilter = this.layerDef.isShown
if (isShown !== undefined && !isShown.matchesProperties(properties)) {
return false
}
}
{
let neededTags: TagsFilter = this.currentFilter.data
if (neededTags !== undefined && !neededTags.matchesProperties(properties)) {
return false
}
}
for (const globalFilter of globalFilters ?? []) {
const neededTags = globalFilter.osmTags
if (neededTags !== undefined && !neededTags.matchesProperties(properties)) {
return false
}
}
return true
}
} }

View file

@ -10,7 +10,7 @@ import { Utils } from "../Utils"
* Some convenience methods are provided for this as well * Some convenience methods are provided for this as well
*/ */
export class MenuState { export class MenuState {
private static readonly _themeviewTabs = ["intro", "filters"] as const private static readonly _themeviewTabs = ["intro", "filters", "download", "copyright"] as const
public readonly themeIsOpened = new UIEventSource(true) public readonly themeIsOpened = new UIEventSource(true)
public readonly themeViewTabIndex: UIEventSource<number> public readonly themeViewTabIndex: UIEventSource<number>
public readonly themeViewTab: UIEventSource<typeof MenuState._themeviewTabs[number]> public readonly themeViewTab: UIEventSource<typeof MenuState._themeviewTabs[number]>

View file

@ -7,24 +7,21 @@ import CheckBoxes from "../Input/Checkboxes"
import { GeoOperations } from "../../Logic/GeoOperations" import { GeoOperations } from "../../Logic/GeoOperations"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import Title from "../Base/Title" import Title from "../Base/Title"
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline" import { Store } from "../../Logic/UIEventSource"
import { UIEventSource } from "../../Logic/UIEventSource"
import SimpleMetaTagger from "../../Logic/SimpleMetaTagger" 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 geojson2svg from "geojson2svg" import geojson2svg from "geojson2svg"
import LayerConfig from "../../Models/ThemeConfig/LayerConfig" 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"
export class DownloadPanel extends Toggle { export class DownloadPanel extends Toggle {
constructor(state: { constructor(state: SpecialVisualizationState) {
filteredLayers: UIEventSource<FilteredLayer[]>
featurePipeline: FeaturePipeline
layoutToUse: LayoutConfig
currentBounds: UIEventSource<BBox>
}) {
const t = Translations.t.general.download const t = Translations.t.general.download
const name = state.layoutToUse.id const name = state.layout.id
const includeMetaToggle = new CheckBoxes([t.includeMetaData]) const includeMetaToggle = new CheckBoxes([t.includeMetaData])
const metaisIncluded = includeMetaToggle.GetValue().map((selected) => selected.length > 0) const metaisIncluded = includeMetaToggle.GetValue().map((selected) => selected.length > 0)
@ -71,12 +68,13 @@ export class DownloadPanel extends Toggle {
) )
).OnClickWithLoading(t.exporting, async () => { ).OnClickWithLoading(t.exporting, async () => {
const geojson = DownloadPanel.getCleanGeoJsonPerLayer(state, metaisIncluded.data) const geojson = DownloadPanel.getCleanGeoJsonPerLayer(state, metaisIncluded.data)
const leafletdiv = document.getElementById("leafletDiv") const maindiv = document.getElementById("maindiv")
const layers = state.layout.layers.filter((l) => l.source !== null)
const csv = DownloadPanel.asSvg(geojson, { const csv = DownloadPanel.asSvg(geojson, {
layers: state.filteredLayers.data.map((l) => l.layerDef), layers,
mapExtent: state.currentBounds.data, mapExtent: state.mapProperties.bounds.data,
width: leafletdiv.offsetWidth, width: maindiv.offsetWidth,
height: leafletdiv.offsetHeight, height: maindiv.offsetHeight,
}) })
Utils.offerContentsAsDownloadableFile( Utils.offerContentsAsDownloadableFile(
@ -97,7 +95,11 @@ export class DownloadPanel extends Toggle {
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")
super(downloadButtons, t.noDataLoaded, state.featurePipeline.somethingLoaded) super(
downloadButtons,
t.noDataLoaded,
state.dataIsLoading.map((x) => !x)
)
} }
/** /**
@ -118,7 +120,7 @@ export class DownloadPanel extends Toggle {
* 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>` * 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( public static asSvg(
perLayer: Map<string, any[]>, perLayer: Map<string, Feature[]>,
options?: { options?: {
layers?: LayerConfig[] layers?: LayerConfig[]
width?: 1000 | number width?: 1000 | number
@ -128,8 +130,11 @@ export class DownloadPanel extends Toggle {
} }
) { ) {
options = options ?? {} options = options ?? {}
const w = options.width ?? 1000 const width = options.width ?? 1000
const h = options.height ?? 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 unit = options.unit ?? "px"
const mapExtent = { left: -180, bottom: -90, right: 180, top: 90 } const mapExtent = { left: -180, bottom: -90, right: 180, top: 90 }
if (options.mapExtent !== undefined) { if (options.mapExtent !== undefined) {
@ -139,7 +144,7 @@ export class DownloadPanel extends Toggle {
mapExtent.bottom = bbox.minLat mapExtent.bottom = bbox.minLat
mapExtent.top = bbox.maxLat mapExtent.top = bbox.maxLat
} }
console.log("Generateing svg, extent:", { mapExtent, width, height })
const elements: string[] = [] const elements: string[] = []
for (const layer of Array.from(perLayer.keys())) { for (const layer of Array.from(perLayer.keys())) {
@ -152,7 +157,7 @@ export class DownloadPanel extends Toggle {
const rendering = layerDef?.lineRendering[0] const rendering = layerDef?.lineRendering[0]
const converter = geojson2svg({ const converter = geojson2svg({
viewportSize: { width: w, height: h }, viewportSize: { width, height },
mapExtent, mapExtent,
output: "svg", output: "svg",
attributes: [ attributes: [
@ -184,105 +189,85 @@ export class DownloadPanel extends Toggle {
elements.push(group) elements.push(group)
} }
const w = width
const h = height
const header = `<svg width="${w}${unit}" height="${h}${unit}" viewBox="0 0 ${w} ${h}">` const header = `<svg width="${w}${unit}" height="${h}${unit}" viewBox="0 0 ${w} ${h}">`
return header + "\n" + elements.join("\n") + "\n</svg>" return header + "\n" + elements.join("\n") + "\n</svg>"
} }
/**
* Gets all geojson as geojson feature
* @param state
* @param includeMetaData
* @private
*/
private static getCleanGeoJson( private static getCleanGeoJson(
state: { state: {
featurePipeline: FeaturePipeline layout: LayoutConfig
currentBounds: UIEventSource<BBox> mapProperties: { bounds: Store<BBox> }
filteredLayers: UIEventSource<FilteredLayer[]> perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
layerState: LayerState
}, },
includeMetaData: boolean includeMetaData: boolean
) { ): FeatureCollection {
const perLayer = DownloadPanel.getCleanGeoJsonPerLayer(state, includeMetaData) const featuresPerLayer = DownloadPanel.getCleanGeoJsonPerLayer(state, includeMetaData)
const features = [].concat(...Array.from(perLayer.values())) const features = [].concat(...Array.from(featuresPerLayer.values()))
return { return {
type: "FeatureCollection", type: "FeatureCollection",
features, features,
} }
} }
private static getCleanGeoJsonPerLayer( /**
state: { * Returns a new feature of which all the metatags are deleted
featurePipeline: FeaturePipeline */
currentBounds: UIEventSource<BBox> private static cleanFeature(f: Feature): Feature {
filteredLayers: UIEventSource<FilteredLayer[]> f = {
}, type: f.type,
includeMetaData: boolean geometry: { ...f.geometry },
): Map<string, any[]> /*{layerId --> geojsonFeatures[]}*/ { properties: { ...f.properties },
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) {
if (tile.layer !== undefined) {
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 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<BBox> }
perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
layerState: LayerState
},
includeMetaData: boolean
): Map<string, Feature[]> {
const featuresPerLayer = new Map<string, any[]>()
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
} }
} }

View file

@ -28,7 +28,8 @@
import UserRelatedState from "../Logic/State/UserRelatedState"; import UserRelatedState from "../Logic/State/UserRelatedState";
import LoginToggle from "./Base/LoginToggle.svelte"; import LoginToggle from "./Base/LoginToggle.svelte";
import LoginButton from "./Base/LoginButton.svelte"; import LoginButton from "./Base/LoginButton.svelte";
import CopyrightPanel from "./BigComponents/CopyrightPanel.js"; import CopyrightPanel from "./BigComponents/CopyrightPanel";
import { DownloadPanel } from "./BigComponents/DownloadPanel";
export let state: ThemeViewState; export let state: ThemeViewState;
let layout = state.layout; let layout = state.layout;
@ -150,12 +151,21 @@
<RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker> <RasterLayerPicker {availableLayers} value={mapproperties.rasterLayer}></RasterLayerPicker>
</If> </If>
</div> </div>
<div slot="title2" class="flex">
<img src="./assets/svg/download.svg" class="w-4 h-4"/>
<Tr t={Translations.t.general.download.title}/>
</div>
<div slot="content2">
<ToSvelte construct={() => new DownloadPanel(state)}/>
</div>
<div slot="title2"> <div slot="title3">
<Tr t={Translations.t.general.attribution.title}/> <Tr t={Translations.t.general.attribution.title}/>
</div> </div>
<ToSvelte slot="content2" construct={() => new CopyrightPanel(state)}></ToSvelte> <ToSvelte slot="content3" construct={() => new CopyrightPanel(state)}></ToSvelte>
</TabbedGroup> </TabbedGroup>
</FloatOver> </FloatOver>
</If> </If>

View file

@ -173,7 +173,7 @@
"includeMetaData": "Include metadata (last editor, calculated values, …)", "includeMetaData": "Include metadata (last editor, calculated values, …)",
"licenseInfo": "<h3>Copyright notice</h3>The provided data is available under ODbL. Reusing it is gratis for any purpose, but <ul><li>the attribution <b>© OpenStreetMap contributors</b> is required</li><li>Any change must be use the license</li></ul> Please read the full <a href='https://www.openstreetmap.org/copyright' target='_blank'>copyright notice</a> for details.", "licenseInfo": "<h3>Copyright notice</h3>The provided data is available under ODbL. Reusing it is gratis for any purpose, but <ul><li>the attribution <b>© OpenStreetMap contributors</b> is required</li><li>Any change must be use the license</li></ul> Please read the full <a href='https://www.openstreetmap.org/copyright' target='_blank'>copyright notice</a> for details.",
"noDataLoaded": "No data is loaded yet. Download will be available soon", "noDataLoaded": "No data is loaded yet. Download will be available soon",
"title": "Download visible data", "title": "Download",
"uploadGpx": "Upload your track to OpenStreetMap" "uploadGpx": "Upload your track to OpenStreetMap"
}, },
"error": "Something went wrong", "error": "Something went wrong",

2
package-lock.json generated
View file

@ -27,7 +27,7 @@
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"fake-dom": "^1.0.4", "fake-dom": "^1.0.4",
"geojson2svg": "^1.3.1", "geojson2svg": "^1.3.3",
"html-to-markdown": "^1.0.0", "html-to-markdown": "^1.0.0",
"i18next-client": "^1.11.4", "i18next-client": "^1.11.4",
"idb-keyval": "^6.0.3", "idb-keyval": "^6.0.3",

View file

@ -79,7 +79,7 @@
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"fake-dom": "^1.0.4", "fake-dom": "^1.0.4",
"geojson2svg": "^1.3.1", "geojson2svg": "^1.3.3",
"html-to-markdown": "^1.0.0", "html-to-markdown": "^1.0.0",
"i18next-client": "^1.11.4", "i18next-client": "^1.11.4",
"idb-keyval": "^6.0.3", "idb-keyval": "^6.0.3",

View file

@ -41,7 +41,7 @@
<span class="absolute" id="belowmap" style="z-index: -1; visibility: hidden">Below</span> <span class="absolute" id="belowmap" style="z-index: -1; visibility: hidden">Below</span>
<div id="maindiv"> <div id="maindiv" class="h-full">
<div id="default-main"> <div id="default-main">
<span id="default-title">Loading MapComplete, hang on...</span> <span id="default-title">Loading MapComplete, hang on...</span>