From ef0ec5160d050b0f49982cdd86356d774887e41d Mon Sep 17 00:00:00 2001 From: Pieter Vander Vennet Date: Fri, 14 Apr 2023 04:33:06 +0200 Subject: [PATCH] Refactoring: fix download buttons --- Logic/FeatureSource/Actors/GeoIndexedStore.ts | 2 +- .../Sources/FilteringFeatureSource.ts | 20 +- Models/FilteredLayer.ts | 33 ++- Models/MenuState.ts | 2 +- UI/BigComponents/DownloadPanel.ts | 195 ++++++++---------- UI/ThemeViewGUI.svelte | 16 +- langs/en.json | 2 +- package-lock.json | 2 +- package.json | 2 +- theme.html | 2 +- 10 files changed, 142 insertions(+), 134 deletions(-) diff --git a/Logic/FeatureSource/Actors/GeoIndexedStore.ts b/Logic/FeatureSource/Actors/GeoIndexedStore.ts index a4f91eb33..314706536 100644 --- a/Logic/FeatureSource/Actors/GeoIndexedStore.ts +++ b/Logic/FeatureSource/Actors/GeoIndexedStore.ts @@ -23,7 +23,7 @@ export default class GeoIndexedStore implements FeatureSource { * @param bbox * @constructor */ - public GetFeaturesWithin(bbox: BBox): Feature[] { + public GetFeaturesWithin(bbox: BBox, strict: boolean = false): Feature[] { // TODO optimize const bboxFeature = bbox.asGeojsonCached() return this.features.data.filter((f) => { diff --git a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts index 675002211..2a2b52d76 100644 --- a/Logic/FeatureSource/Sources/FilteringFeatureSource.ts +++ b/Logic/FeatureSource/Sources/FilteringFeatureSource.ts @@ -54,7 +54,6 @@ export default class FilteringFeatureSource implements FeatureSource { this.update() } - private update() { const self = this const layer = this._layer @@ -64,26 +63,9 @@ export default class FilteringFeatureSource implements FeatureSource { const newFeatures = (features ?? []).filter((f) => { self.registerCallback(f) - const isShown: TagsFilter = layer.layerDef.isShown - const tags = f.properties - if (isShown !== undefined && !isShown.matchesProperties(tags)) { + if (!layer.isShown(f.properties, globalFilters)) { 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) return true diff --git a/Models/FilteredLayer.ts b/Models/FilteredLayer.ts index 9679f3447..e8ccd60a8 100644 --- a/Models/FilteredLayer.ts +++ b/Models/FilteredLayer.ts @@ -8,6 +8,7 @@ import { TagsFilter } from "../Logic/Tags/TagsFilter" import { Utils } from "../Utils" import { TagUtils } from "../Logic/Tags/TagUtils" import { And } from "../Logic/Tags/And" +import { GlobalFilter } from "./GlobalFilter" export default class FilteredLayer { /** @@ -62,7 +63,7 @@ export default class FilteredLayer { return JSON.stringify(values) } - public static stringToFieldProperties(value: string): Record { + private static stringToFieldProperties(value: string): Record { const values = JSON.parse(value) for (const key in values) { if (values[key] === "") { @@ -208,4 +209,34 @@ export default class FilteredLayer { } return optimized } + + /** + * Returns true if the given tags match the current filters (and the specified 'global filters') + */ + public isShown(properties: Record, 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 + } } diff --git a/Models/MenuState.ts b/Models/MenuState.ts index aa737c2fd..d613b0048 100644 --- a/Models/MenuState.ts +++ b/Models/MenuState.ts @@ -10,7 +10,7 @@ import { Utils } from "../Utils" * Some convenience methods are provided for this as well */ 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 themeViewTabIndex: UIEventSource public readonly themeViewTab: UIEventSource diff --git a/UI/BigComponents/DownloadPanel.ts b/UI/BigComponents/DownloadPanel.ts index a47703ba7..1527d1f96 100644 --- a/UI/BigComponents/DownloadPanel.ts +++ b/UI/BigComponents/DownloadPanel.ts @@ -7,24 +7,21 @@ 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 { Store } 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 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 { - constructor(state: { - filteredLayers: UIEventSource - featurePipeline: FeaturePipeline - layoutToUse: LayoutConfig - currentBounds: UIEventSource - }) { + constructor(state: SpecialVisualizationState) { const t = Translations.t.general.download - const name = state.layoutToUse.id + const name = state.layout.id const includeMetaToggle = new CheckBoxes([t.includeMetaData]) const metaisIncluded = includeMetaToggle.GetValue().map((selected) => selected.length > 0) @@ -71,12 +68,13 @@ export class DownloadPanel extends Toggle { ) ).OnClickWithLoading(t.exporting, async () => { 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, { - layers: state.filteredLayers.data.map((l) => l.layerDef), - mapExtent: state.currentBounds.data, - width: leafletdiv.offsetWidth, - height: leafletdiv.offsetHeight, + layers, + mapExtent: state.mapProperties.bounds.data, + width: maindiv.offsetWidth, + height: maindiv.offsetHeight, }) Utils.offerContentsAsDownloadableFile( @@ -97,7 +95,11 @@ export class DownloadPanel extends Toggle { 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) + super( + downloadButtons, + t.noDataLoaded, + state.dataIsLoading.map((x) => !x) + ) } /** @@ -118,7 +120,7 @@ export class DownloadPanel extends Toggle { * DownloadPanel.asSvg(perLayer).replace(/\n/g, "") // => ` ` */ public static asSvg( - perLayer: Map, + perLayer: Map, options?: { layers?: LayerConfig[] width?: 1000 | number @@ -128,8 +130,11 @@ export class DownloadPanel extends Toggle { } ) { options = options ?? {} - const w = options.width ?? 1000 - const h = options.height ?? 1000 + 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) { @@ -139,7 +144,7 @@ export class DownloadPanel extends Toggle { 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())) { @@ -152,7 +157,7 @@ export class DownloadPanel extends Toggle { const rendering = layerDef?.lineRendering[0] const converter = geojson2svg({ - viewportSize: { width: w, height: h }, + viewportSize: { width, height }, mapExtent, output: "svg", attributes: [ @@ -184,105 +189,85 @@ export class DownloadPanel extends Toggle { elements.push(group) } + const w = width + const h = height 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 + layout: LayoutConfig + mapProperties: { bounds: Store } + perLayer: ReadonlyMap + layerState: LayerState }, includeMetaData: boolean - ) { - const perLayer = DownloadPanel.getCleanGeoJsonPerLayer(state, includeMetaData) - const features = [].concat(...Array.from(perLayer.values())) + ): FeatureCollection { + const featuresPerLayer = DownloadPanel.getCleanGeoJsonPerLayer(state, includeMetaData) + const features = [].concat(...Array.from(featuresPerLayer.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 (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) - } + /** + * 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 }, } - 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 } + 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 } } diff --git a/UI/ThemeViewGUI.svelte b/UI/ThemeViewGUI.svelte index 6d2d4c50f..9b66947ae 100644 --- a/UI/ThemeViewGUI.svelte +++ b/UI/ThemeViewGUI.svelte @@ -28,7 +28,8 @@ import UserRelatedState from "../Logic/State/UserRelatedState"; import LoginToggle from "./Base/LoginToggle.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; let layout = state.layout; @@ -150,12 +151,21 @@ +
+ + +
+
+ new DownloadPanel(state)}/> +
-
+
- new CopyrightPanel(state)}> + new CopyrightPanel(state)}> + + diff --git a/langs/en.json b/langs/en.json index 43cf285b3..3d0f1f673 100644 --- a/langs/en.json +++ b/langs/en.json @@ -173,7 +173,7 @@ "includeMetaData": "Include metadata (last editor, calculated values, …)", "licenseInfo": "

Copyright notice

The provided data is available under ODbL. Reusing it is gratis for any purpose, but
  • the attribution © OpenStreetMap contributors is required
  • Any change must be use the license
Please read the full copyright notice for details.", "noDataLoaded": "No data is loaded yet. Download will be available soon", - "title": "Download visible data", + "title": "Download", "uploadGpx": "Upload your track to OpenStreetMap" }, "error": "Something went wrong", diff --git a/package-lock.json b/package-lock.json index 532d68342..6eff6d6d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "email-validator": "^2.0.4", "escape-html": "^1.0.3", "fake-dom": "^1.0.4", - "geojson2svg": "^1.3.1", + "geojson2svg": "^1.3.3", "html-to-markdown": "^1.0.0", "i18next-client": "^1.11.4", "idb-keyval": "^6.0.3", diff --git a/package.json b/package.json index 771d998b6..ea5c8a9c1 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "email-validator": "^2.0.4", "escape-html": "^1.0.3", "fake-dom": "^1.0.4", - "geojson2svg": "^1.3.1", + "geojson2svg": "^1.3.3", "html-to-markdown": "^1.0.0", "i18next-client": "^1.11.4", "idb-keyval": "^6.0.3", diff --git a/theme.html b/theme.html index 8ef3c47a1..1623a00c9 100644 --- a/theme.html +++ b/theme.html @@ -41,7 +41,7 @@ -
+
Loading MapComplete, hang on...