Refactoring: fix filters, remove obsolete code

This commit is contained in:
Pieter Vander Vennet 2023-04-06 02:20:25 +02:00
parent 0241f89d3d
commit 97aaa8e941
9 changed files with 122 additions and 302 deletions

View file

@ -18,6 +18,11 @@ import StaticFeatureSource from "./StaticFeatureSource"
* Note that special layers (with `source=null` will be ignored) * Note that special layers (with `source=null` will be ignored)
*/ */
export default class LayoutSource extends FeatureSourceMerger { export default class LayoutSource extends FeatureSourceMerger {
/**
* Indicates if a data source is loading something
* TODO fixme
*/
public readonly isLoading: Store<boolean> = new ImmutableStore(false)
constructor( constructor(
layers: LayerConfig[], layers: LayerConfig[],
featureSwitches: FeatureSwitchState, featureSwitches: FeatureSwitchState,

View file

@ -31,6 +31,7 @@ export default class FilteredLayer {
* Contains the current properties a feature should fulfill in order to match the filter * Contains the current properties a feature should fulfill in order to match the filter
*/ */
readonly currentFilter: Store<TagsFilter | undefined> readonly currentFilter: Store<TagsFilter | undefined>
constructor( constructor(
layer: LayerConfig, layer: LayerConfig,
appliedFilters?: Map<string, UIEventSource<undefined | number | string>>, appliedFilters?: Map<string, UIEventSource<undefined | number | string>>,
@ -41,104 +42,34 @@ export default class FilteredLayer {
this.appliedFilters = this.appliedFilters =
appliedFilters ?? new Map<string, UIEventSource<number | string | undefined>>() appliedFilters ?? new Map<string, UIEventSource<number | string | undefined>>()
const hasFilter = new UIEventSource<boolean>(false)
const self = this const self = this
const currentTags = new UIEventSource<TagsFilter>(undefined) const currentTags = new UIEventSource<TagsFilter>(undefined)
this.appliedFilters.forEach((filterSrc) => { this.appliedFilters.forEach((filterSrc) => {
filterSrc.addCallbackAndRun((filter) => { filterSrc.addCallbackAndRun((_) => {
if ((filter ?? 0) !== 0) { currentTags.setData(self.calculateCurrentTags())
hasFilter.setData(true)
currentTags.setData(self.calculateCurrentTags())
return
}
const hf = Array.from(self.appliedFilters.values()).some((f) => (f.data ?? 0) !== 0)
if (hf) {
currentTags.setData(self.calculateCurrentTags())
} else {
currentTags.setData(undefined)
}
hasFilter.setData(hf)
}) })
}) })
this.hasFilter = currentTags.map((ct) => ct !== undefined)
currentTags.addCallbackAndRunD((t) => console.log("Current filter is", t))
this.currentFilter = currentTags this.currentFilter = currentTags
} }
private calculateCurrentTags(): TagsFilter {
let needed: TagsFilter[] = []
for (const filter of this.layerDef.filters) {
const state = this.appliedFilters.get(filter.id)
if (state.data === undefined) {
continue
}
if (filter.options[0].fields.length > 0) {
const fieldProperties = FilteredLayer.stringToFieldProperties(<string>state.data)
const asTags = FilteredLayer.fieldsToTags(filter.options[0], fieldProperties)
needed.push(asTags)
continue
}
needed.push(filter.options[state.data].osmTags)
}
needed = Utils.NoNull(needed)
if (needed.length == 0) {
return undefined
}
let tags: TagsFilter
if (needed.length == 1) {
tags = needed[1]
} else {
tags = new And(needed)
}
let optimized = tags.optimize()
if (optimized === true) {
return undefined
}
if (optimized === false) {
return tags
}
return optimized
}
public static fieldsToString(values: Record<string, string>): string { public static fieldsToString(values: Record<string, string>): string {
for (const key in values) {
if (values[key] === "") {
delete values[key]
}
}
return JSON.stringify(values) return JSON.stringify(values)
} }
public static stringToFieldProperties(value: string): Record<string, string> { public static stringToFieldProperties(value: string): Record<string, string> {
return JSON.parse(value) const values = JSON.parse(value)
} for (const key in values) {
if (values[key] === "") {
private static fieldsToTags( delete values[key]
option: FilterConfigOption, }
fieldstate: string | Record<string, string>
): TagsFilter {
let properties: Record<string, string>
if (typeof fieldstate === "string") {
properties = FilteredLayer.stringToFieldProperties(fieldstate)
} else {
properties = fieldstate
} }
console.log("Building tagsspec with properties", properties) return values
const tagsSpec = Utils.WalkJson(option.originalTagsSpec, (v) => {
if (typeof v !== "string") {
return v
}
for (const key in properties) {
v = (<string>v).replace("{" + key + "}", properties[key])
}
return v
})
return TagUtils.Tag(tagsSpec)
}
public disableAllFilters(): void {
this.appliedFilters.forEach((value) => value.setData(undefined))
} }
/** /**
@ -177,10 +108,42 @@ export default class FilteredLayer {
const appliedFilters = new Map<string, UIEventSource<undefined | number | string>>() const appliedFilters = new Map<string, UIEventSource<undefined | number | string>>()
for (const subfilter of layer.filters) { for (const subfilter of layer.filters) {
appliedFilters.set(subfilter.id, subfilter.initState()) appliedFilters.set(subfilter.id, subfilter.initState(layer.id))
} }
return new FilteredLayer(layer, appliedFilters, isDisplayed) return new FilteredLayer(layer, appliedFilters, isDisplayed)
} }
private static fieldsToTags(
option: FilterConfigOption,
fieldstate: string | Record<string, string>
): TagsFilter | undefined {
let properties: Record<string, string>
if (typeof fieldstate === "string") {
properties = FilteredLayer.stringToFieldProperties(fieldstate)
} else {
properties = fieldstate
}
console.log("Building tagsspec with properties", properties)
const missingKeys = option.fields
.map((f) => f.name)
.filter((key) => properties[key] === undefined)
if (missingKeys.length > 0) {
return undefined
}
const tagsSpec = Utils.WalkJson(option.originalTagsSpec, (v) => {
if (typeof v !== "string") {
return v
}
for (const key in properties) {
v = (<string>v).replace("{" + key + "}", properties[key])
}
return v
})
return TagUtils.Tag(tagsSpec)
}
private static getPref( private static getPref(
osmConnection: OsmConnection, osmConnection: OsmConnection,
key: string, key: string,
@ -202,4 +165,49 @@ export default class FilteredLayer {
} }
) )
} }
public disableAllFilters(): void {
this.appliedFilters.forEach((value) => value.setData(undefined))
}
private calculateCurrentTags(): TagsFilter {
let needed: TagsFilter[] = []
for (const filter of this.layerDef.filters) {
const state = this.appliedFilters.get(filter.id)
if (state.data === undefined) {
continue
}
if (filter.options[0].fields.length > 0) {
// This is a filter with fields
// We calculate the fields
const fieldProperties = FilteredLayer.stringToFieldProperties(<string>state.data)
const asTags = FilteredLayer.fieldsToTags(filter.options[0], fieldProperties)
console.log("Current field properties:", state.data, fieldProperties, asTags)
if (asTags) {
needed.push(asTags)
}
continue
}
needed.push(filter.options[state.data].osmTags)
}
needed = Utils.NoNull(needed)
if (needed.length == 0) {
return undefined
}
let tags: TagsFilter
if (needed.length == 1) {
tags = needed[0]
} else {
tags = new And(needed)
}
let optimized = tags.optimize()
if (optimized === true) {
return undefined
}
if (optimized === false) {
return tags
}
return optimized
}
} }

View file

@ -144,10 +144,12 @@ export default class FilterConfig {
}) })
} }
public initState(): UIEventSource<undefined | number | string> { public initState(layerId: string): UIEventSource<undefined | number | string> {
let defaultValue = "" let defaultValue = ""
if (this.options.length > 1) { if (this.options.length > 1) {
defaultValue = "" + (this.defaultSelection ?? 0) defaultValue = "" + (this.defaultSelection ?? 0)
} else if (this.options[0].fields?.length > 0) {
defaultValue = "{}"
} else { } else {
// Only a single option // Only a single option
if (this.defaultSelection === 0) { if (this.defaultSelection === 0) {
@ -157,7 +159,7 @@ export default class FilterConfig {
} }
} }
const qp = QueryParameters.GetQueryParameter( const qp = QueryParameters.GetQueryParameter(
"filter-" + this.id, `filter-${layerId}-${this.id}`,
defaultValue, defaultValue,
"State of filter " + this.id "State of filter " + this.id
) )

View file

@ -116,7 +116,7 @@ export default class ThemeViewState implements SpecialVisualizationState {
const self = this const self = this
this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id) this.layerState = new LayerState(this.osmConnection, layout.layers, layout.id)
this.newFeatures = new SimpleFeatureSource(undefined) this.newFeatures = new SimpleFeatureSource(undefined)
this.indexedFeatures = new LayoutSource( const layoutSource = new LayoutSource(
layout.layers, layout.layers,
this.featureSwitches, this.featureSwitches,
this.newFeatures, this.newFeatures,
@ -124,6 +124,8 @@ export default class ThemeViewState implements SpecialVisualizationState {
this.osmConnection.Backend(), this.osmConnection.Backend(),
(id) => self.layerState.filteredLayers.get(id).isDisplayed (id) => self.layerState.filteredLayers.get(id).isDisplayed
) )
this.indexedFeatures = layoutSource
this.dataIsLoading = layoutSource.isLoading
const lastClick = (this.lastClickObject = new LastClickFeatureSource( const lastClick = (this.lastClickObject = new LastClickFeatureSource(
this.mapProperties.lastClickLocation, this.mapProperties.lastClickLocation,
this.layout this.layout

View file

@ -45,7 +45,7 @@
/*If no snapping needed: the value is simply the map location; /*If no snapping needed: the value is simply the map location;
* If snapping is needed: the value will be set later on by the snapping feature source * If snapping is needed: the value will be set later on by the snapping feature source
* */ * */
location: snapToLayers.length === 0 ? value : new UIEventSource<{ lon: number; lat: number }>(coordinate), location: snapToLayers?.length > 0 ? new UIEventSource<{ lon: number; lat: number }>(coordinate) :value,
bounds: new UIEventSource<BBox>(undefined), bounds: new UIEventSource<BBox>(undefined),
allowMoving: new UIEventSource<boolean>(true), allowMoving: new UIEventSource<boolean>(true),
allowZooming: new UIEventSource<boolean>(true), allowZooming: new UIEventSource<boolean>(true),

View file

@ -17,6 +17,7 @@ import { Tag } from "../../Logic/Tags/Tag"
import { SpecialVisualizationState } from "../SpecialVisualization" import { SpecialVisualizationState } from "../SpecialVisualization"
import { Feature } from "geojson" import { Feature } from "geojson"
import { FixedUiElement } from "../Base/FixedUiElement" import { FixedUiElement } from "../Base/FixedUiElement"
import Combine from "../Base/Combine"
/* /*
* The SimpleAddUI is a single panel, which can have multiple states: * The SimpleAddUI is a single panel, which can have multiple states:
@ -33,83 +34,8 @@ export interface PresetInfo extends PresetConfig {
boundsFactor?: 0.25 | number boundsFactor?: 0.25 | number
} }
export default class SimpleAddUI extends Toggle { export default class SimpleAddUI extends Combine {
constructor(state: SpecialVisualizationState) { constructor(state: SpecialVisualizationState) {
const takeLocationFrom = state.mapProperties.lastClickLocation super([])
const selectedPreset = new UIEventSource<PresetInfo>(undefined)
takeLocationFrom.addCallback((_) => selectedPreset.setData(undefined))
async function createNewPoint(
tags: Tag[],
location: { lat: number; lon: number },
snapOntoWay?: OsmWay
): Promise<void> {
if (snapOntoWay) {
tags.push(new Tag("_referencing_ways", "way/" + snapOntoWay.id))
}
const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, {
theme: state.layout?.id ?? "unkown",
changeType: "create",
snapOnto: snapOntoWay,
})
await state.changes.applyAction(newElementAction)
selectedPreset.setData(undefined)
const selectedFeature: Feature = state.indexedFeatures.featuresById.data.get(
newElementAction.newElementId
)
state.selectedElement.setData(selectedFeature)
Hash.hash.setData(newElementAction.newElementId)
}
const addUi = new VariableUiElement(
selectedPreset.map((preset) => {
function confirm(
tags: any[],
location: { lat: number; lon: number },
snapOntoWayId?: WayId
) {
if (snapOntoWayId === undefined) {
createNewPoint(tags, location, undefined)
} else {
OsmObject.DownloadObject(snapOntoWayId).addCallbackAndRunD((way) => {
createNewPoint(tags, location, way)
return true
})
}
}
function cancel() {
selectedPreset.setData(undefined)
}
const message = Translations.t.general.add.addNew.Subs(
{ category: preset.name },
preset.name["context"]
)
return new FixedUiElement("ConfirmLocationOfPoint...") /*ConfirmLocationOfPoint(
state,
filterViewIsOpened,
preset,
message,
takeLocationFrom.data,
confirm,
cancel,
() => {
selectedPreset.setData(undefined)
},
{
cancelIcon: Svg.back_svg(),
cancelText: Translations.t.general.add.backToSelect,
}
)*/
})
)
super(
new Loading(Translations.t.general.add.stillLoading).SetClass("alert"),
addUi,
state.dataIsLoading
)
} }
} }

View file

@ -1,23 +1,16 @@
import { UIEventSource } from "../../Logic/UIEventSource" import { UIEventSource } from "../../Logic/UIEventSource"
import BaseUIElement from "../BaseUIElement" import BaseUIElement from "../BaseUIElement"
import LocationInput from "../Input/LocationInput"
import { BBox } from "../../Logic/BBox"
import { TagUtils } from "../../Logic/Tags/TagUtils"
import { SubtleButton } from "../Base/SubtleButton" import { SubtleButton } from "../Base/SubtleButton"
import Combine from "../Base/Combine" import Combine from "../Base/Combine"
import Translations from "../i18n/Translations" import Translations from "../i18n/Translations"
import Svg from "../../Svg" import Svg from "../../Svg"
import Toggle from "../Input/Toggle" import Toggle from "../Input/Toggle"
import { PresetInfo } from "../BigComponents/SimpleAddUI" import { PresetInfo } from "../BigComponents/SimpleAddUI"
import Title from "../Base/Title"
import { VariableUiElement } from "../Base/VariableUIElement" import { VariableUiElement } from "../Base/VariableUIElement"
import { Tag } from "../../Logic/Tags/Tag" import { Tag } from "../../Logic/Tags/Tag"
import { WayId } from "../../Models/OsmFeature" import { WayId } from "../../Models/OsmFeature"
import { Translation } from "../i18n/Translation" import { Translation } from "../i18n/Translation"
import { Feature } from "geojson"
import { AvailableRasterLayers } from "../../Models/RasterLayers"
import { SpecialVisualizationState } from "../SpecialVisualization" import { SpecialVisualizationState } from "../SpecialVisualization"
import ClippedFeatureSource from "../../Logic/FeatureSource/Sources/ClippedFeatureSource"
export default class ConfirmLocationOfPoint extends Combine { export default class ConfirmLocationOfPoint extends Combine {
constructor( constructor(
@ -38,76 +31,6 @@ export default class ConfirmLocationOfPoint extends Combine {
cancelText?: string | Translation cancelText?: string | Translation
} }
) { ) {
let preciseInput: LocationInput = undefined
if (preset.preciseInput !== undefined) {
// Create location input
// We uncouple the event source
const zloc = { ...loc, zoom: 19 }
const locationSrc = new UIEventSource(zloc)
let backgroundLayer = new UIEventSource(
state?.mapProperties.rasterLayer?.data ?? AvailableRasterLayers.osmCarto
)
if (preset.preciseInput.preferredBackground) {
const defaultBackground = AvailableRasterLayers.SelectBestLayerAccordingTo(
locationSrc,
new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground)
)
// Note that we _break the link_ here, as the minimap will take care of the switching!
backgroundLayer.setData(defaultBackground.data)
}
let snapToFeatures: UIEventSource<Feature[]> = undefined
let mapBounds: UIEventSource<BBox> = undefined
if (preset.preciseInput.snapToLayers && preset.preciseInput.snapToLayers.length > 0) {
snapToFeatures = new UIEventSource<Feature[]>([])
mapBounds = new UIEventSource<BBox>(undefined)
}
const tags = TagUtils.KVtoProperties(preset.tags ?? [])
preciseInput = new LocationInput({
mapBackground: backgroundLayer,
centerLocation: locationSrc,
snapTo: snapToFeatures,
renderLayerForSnappedPoint: preset.layerToAddTo.layerDef,
snappedPointTags: tags,
maxSnapDistance: preset.preciseInput.maxSnapDistance,
bounds: mapBounds,
state: <any>state,
})
preciseInput.installBounds(preset.boundsFactor ?? 0.25, true)
preciseInput
.SetClass("rounded-xl overflow-hidden border border-gray")
.SetStyle("height: 18rem; max-height: 50vh")
if (preset.preciseInput.snapToLayers && preset.preciseInput.snapToLayers.length > 0) {
// We have to snap to certain layers.
// Lets fetch them
let loadedBbox: BBox = undefined
mapBounds?.addCallbackAndRunD((bbox) => {
if (loadedBbox !== undefined && bbox.isContainedIn(loadedBbox)) {
// All is already there
// return;
}
bbox = bbox.pad(
Math.max(preset.boundsFactor ?? 0.25, 2),
Math.max(preset.boundsFactor ?? 0.25, 2)
)
loadedBbox = bbox
const sources = preset.preciseInput.snapToLayers.map(
(layerId) =>
new ClippedFeatureSource(
state.perLayer.get(layerId),
bbox.asGeoJson({})
)
)
})
}
}
let confirmButton: BaseUIElement = new SubtleButton( let confirmButton: BaseUIElement = new SubtleButton(
preset.icon(), preset.icon(),
new Combine([confirmText]).SetClass("flex flex-col") new Combine([confirmText]).SetClass("flex flex-col")
@ -119,38 +42,12 @@ export default class ConfirmLocationOfPoint extends Combine {
.map((gf) => gf.onNewPoint.tags) .map((gf) => gf.onNewPoint.tags)
const globalTags: Tag[] = [].concat(...globalFilterTagsToAdd) const globalTags: Tag[] = [].concat(...globalFilterTagsToAdd)
console.log("Global tags to add are: ", globalTags) console.log("Global tags to add are: ", globalTags)
confirm(
[...preset.tags, ...globalTags],
preciseInput?.GetValue()?.data ?? loc,
preciseInput?.snappedOnto?.data?.properties?.id
)
}) })
if (preciseInput !== undefined) { confirmButton = new Combine([confirmButton])
confirmButton = new Combine([preciseInput, confirmButton])
} else {
confirmButton = new Combine([confirmButton])
}
let openLayerOrConfirm = confirmButton let openLayerOrConfirm = confirmButton
const disableFilter = new SubtleButton(
new Combine([
Svg.filter_ui().SetClass("absolute w-full"),
Svg.cross_bottom_right_svg().SetClass("absolute red-svg"),
]).SetClass("relative"),
new Combine([
Translations.t.general.add.disableFiltersExplanation.Clone(),
Translations.t.general.add.disableFilters.Clone().SetClass("text-xl"),
]).SetClass("flex flex-col")
).onClick(() => {
const appliedFilters = preset.layerToAddTo.appliedFilters
appliedFilters.data.forEach((_, k) => appliedFilters.data.set(k, undefined))
appliedFilters.ping()
cancel()
closePopup()
})
// We assume the number of global filters won't change during the run of the program // We assume the number of global filters won't change during the run of the program
for (let i = 0; i < state.globalFilters.data.length; i++) { for (let i = 0; i < state.globalFilters.data.length; i++) {
const hasBeenCheckedOf = new UIEventSource(false) const hasBeenCheckedOf = new UIEventSource(false)
@ -178,31 +75,6 @@ export default class ConfirmLocationOfPoint extends Combine {
) )
} }
// If at least one filter is active which _might_ hide a newly added item, this blocks the preset and requests the filter to be disabled super([openLayerOrConfirm])
const disableFiltersOrConfirm = new Toggle(openLayerOrConfirm, disableFilter)
const cancelButton = new SubtleButton(
options?.cancelIcon ?? Svg.close_ui(),
options?.cancelText ?? Translations.t.general.cancel
).onClick(cancel)
let examples: BaseUIElement = undefined
if (preset.exampleImages !== undefined && preset.exampleImages.length > 0) {
examples = new Combine([new Title()])
}
super([
new Toggle(
Translations.t.general.testing.SetClass("alert"),
undefined,
state.featureSwitchIsTesting
),
disableFiltersOrConfirm,
cancelButton,
preset.description,
examples,
])
this.SetClass("flex flex-col")
} }
} }

View file

@ -35,7 +35,7 @@
let confirmedCategory = false; let confirmedCategory = false;
$: if (selectedPreset === undefined) { $: if (selectedPreset === undefined) {
confirmedCategory = false; confirmedCategory = false;
creating = false creating = false;
} }
let flayer: FilteredLayer = undefined; let flayer: FilteredLayer = undefined;
@ -51,6 +51,7 @@
const zoom = state.mapProperties.zoom; const zoom = state.mapProperties.zoom;
const isLoading = state.dataIsLoading;
let preciseCoordinate: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(undefined); let preciseCoordinate: UIEventSource<{ lon: number, lat: number }> = new UIEventSource(undefined);
let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined); let snappedToObject: UIEventSource<string> = new UIEventSource<string>(undefined);
@ -95,7 +96,6 @@
} }
}); });
state.newFeatures.features.ping(); state.newFeatures.features.ping();
console.log("New features:", state.newFeatures.features.data )
{ {
// Set some metainfo // Set some metainfo
const tagsStore = state.featureProperties.getStore(newId); const tagsStore = state.featureProperties.getStore(newId);
@ -114,7 +114,7 @@
abort(); abort();
state.selectedElement.setData(feature); state.selectedElement.setData(feature);
state.selectedLayer.setData(selectedPreset.layer); state.selectedLayer.setData(selectedPreset.layer);
} }
</script> </script>
@ -123,8 +123,13 @@
<LoginButton osmConnection={state.osmConnection} slot="not-logged-in"> <LoginButton osmConnection={state.osmConnection} slot="not-logged-in">
<Tr slot="message" t={Translations.t.general.add.pleaseLogin} /> <Tr slot="message" t={Translations.t.general.add.pleaseLogin} />
</LoginButton> </LoginButton>
{#if $isLoading}
{#if $zoom < Constants.minZoomLevelToAddNewPoint} <div class="alert">
<Loading>
<Tr t={Translations.t.general.add.stillLoading} />
</Loading>
</div>
{:else if $zoom < Constants.minZoomLevelToAddNewPoint}
<div class="alert"> <div class="alert">
<Tr t={Translations.t.general.add.zoomInFurther}></Tr> <Tr t={Translations.t.general.add.zoomInFurther}></Tr>
</div> </div>

View file

@ -2,7 +2,7 @@ import SvelteUIElement from "./UI/Base/SvelteUIElement"
import ThemeViewGUI from "./UI/ThemeViewGUI.svelte" import ThemeViewGUI from "./UI/ThemeViewGUI.svelte"
import { FixedUiElement } from "./UI/Base/FixedUiElement" import { FixedUiElement } from "./UI/Base/FixedUiElement"
import LayoutConfig from "./Models/ThemeConfig/LayoutConfig" import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"
import * as theme from "./assets/generated/themes/aed.json" import * as theme from "./assets/generated/themes/shops.json"
import ThemeViewState from "./Models/ThemeViewState" import ThemeViewState from "./Models/ThemeViewState"
import Combine from "./UI/Base/Combine" import Combine from "./UI/Base/Combine"
import SpecialVisualizations from "./UI/SpecialVisualizations" import SpecialVisualizations from "./UI/SpecialVisualizations"