import { Store, UIEventSource } from "../Logic/UIEventSource" import LayerConfig from "./ThemeConfig/LayerConfig" import { OsmConnection } from "../Logic/Osm/OsmConnection" import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" import { QueryParameters } from "../Logic/Web/QueryParameters" import { FilterConfigOption } from "./ThemeConfig/FilterConfig" 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 { /** * Wether or not the specified layer is shown */ readonly isDisplayed: UIEventSource /** * Maps the filter.option.id onto the actual used state. * This state is either the chosen option (as number) or a representation of the fields */ readonly appliedFilters: ReadonlyMap> readonly layerDef: LayerConfig /** * Indicates if some filter is set. * If this is the case, adding a new element of this type might be a bad idea */ readonly hasFilter: Store /** * Contains the current properties a feature should fulfill in order to match the filter */ readonly currentFilter: Store constructor( layer: LayerConfig, appliedFilters?: ReadonlyMap>, isDisplayed?: UIEventSource ) { this.layerDef = layer this.isDisplayed = isDisplayed ?? new UIEventSource(true) if (!appliedFilters) { const appliedFiltersWritable = new Map< string, UIEventSource >() for (const filter of this.layerDef.filters) { appliedFiltersWritable.set(filter.id, new UIEventSource(undefined)) } appliedFilters = appliedFiltersWritable } this.appliedFilters = appliedFilters const self = this const currentTags = new UIEventSource(undefined) this.appliedFilters.forEach((filterSrc) => { filterSrc.addCallbackAndRun((_) => { currentTags.setData(self.calculateCurrentTags()) }) }) this.hasFilter = currentTags.map((ct) => ct !== undefined) this.currentFilter = currentTags } public static fieldsToString(values: Record): string { for (const key in values) { if (values[key] === "") { delete values[key] } } return JSON.stringify(values) } /** * Creates a FilteredLayer which is tied into the QueryParameters and/or user preferences */ public static initLinkedState( layer: LayerConfig, context: string, osmConnection: OsmConnection ) { let isDisplayed: UIEventSource if (layer.syncSelection === "local") { isDisplayed = LocalStorageSource.GetParsed( context + "-layer-" + layer.id + "-enabled", layer.shownByDefault ) } else if (layer.syncSelection === "theme-only") { isDisplayed = FilteredLayer.getPref( osmConnection, context + "-layer-" + layer.id + "-enabled", layer ) } else if (layer.syncSelection === "global") { isDisplayed = FilteredLayer.getPref( osmConnection, "layer-" + layer.id + "-enabled", layer ) } else { isDisplayed = QueryParameters.GetBooleanQueryParameter( "layer-" + layer.id, layer.shownByDefault, "Whether or not layer " + layer.id + " is shown" ) } const appliedFilters = new Map>() for (const subfilter of layer.filters) { appliedFilters.set(subfilter.id, subfilter.initState(layer.id)) } return new FilteredLayer(layer, appliedFilters, isDisplayed) } private static stringToFieldProperties(value: string): Record { const values = JSON.parse(value) for (const key in values) { if (values[key] === "") { delete values[key] } } return values } private static fieldsToTags( option: FilterConfigOption, fieldstate: string | Record ): TagsFilter | undefined { let properties: Record if (typeof fieldstate === "string") { properties = FilteredLayer.stringToFieldProperties(fieldstate) } else { properties = fieldstate } 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 = (v).replace("{" + key + "}", properties[key]) } return v }) return TagUtils.Tag(tagsSpec) } private static getPref( osmConnection: OsmConnection, key: string, layer: LayerConfig ): UIEventSource { return osmConnection.GetPreference(key, layer.shownByDefault + "").sync( (v) => { if (v === undefined) { return undefined } return v === "true" }, [], (b) => { if (b === undefined) { return undefined } return "" + b } ) } public disableAllFilters(): void { this.appliedFilters.forEach((value) => value.setData(undefined)) } /** * Returns true if the given tags match * - the current filters * - the specified 'global filters' * - the 'isShown'-filter set by the layer */ public isShown(properties: Record, globalFilters?: GlobalFilter[]): boolean { if (properties._deleted === "yes") { return false } for (const globalFilter of globalFilters ?? []) { const neededTags = globalFilter.osmTags if (neededTags !== undefined && !neededTags.matchesProperties(properties)) { 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 } } return true } 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(state.data) const asTags = FilteredLayer.fieldsToTags(filter.options[0], fieldProperties) 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 } }