forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			251 lines
		
	
	
	
		
			8.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			251 lines
		
	
	
	
		
			8.3 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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<boolean>
 | 
						|
    /**
 | 
						|
     * 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<string, UIEventSource<undefined | number | string>>
 | 
						|
    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<boolean>
 | 
						|
 | 
						|
    /**
 | 
						|
     * Contains the current properties a feature should fulfill in order to match the filter
 | 
						|
     */
 | 
						|
    readonly currentFilter: Store<TagsFilter | undefined>
 | 
						|
 | 
						|
    constructor(
 | 
						|
        layer: LayerConfig,
 | 
						|
        appliedFilters?: ReadonlyMap<string, UIEventSource<undefined | number | string>>,
 | 
						|
        isDisplayed?: UIEventSource<boolean>
 | 
						|
    ) {
 | 
						|
        this.layerDef = layer
 | 
						|
        this.isDisplayed = isDisplayed ?? new UIEventSource(true)
 | 
						|
        if (!appliedFilters) {
 | 
						|
            const appliedFiltersWritable = new Map<
 | 
						|
                string,
 | 
						|
                UIEventSource<number | string | undefined>
 | 
						|
            >()
 | 
						|
            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<TagsFilter>(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, string>): 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<boolean>
 | 
						|
        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<string, UIEventSource<undefined | number | string>>()
 | 
						|
        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<string, string> {
 | 
						|
        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<string, string>
 | 
						|
    ): TagsFilter | undefined {
 | 
						|
        let properties: Record<string, string>
 | 
						|
        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 = (<string>v).replace("{" + key + "}", properties[key])
 | 
						|
            }
 | 
						|
 | 
						|
            return v
 | 
						|
        })
 | 
						|
        return TagUtils.Tag(tagsSpec)
 | 
						|
    }
 | 
						|
 | 
						|
    private static getPref(
 | 
						|
        osmConnection: OsmConnection,
 | 
						|
        key: string,
 | 
						|
        layer: LayerConfig
 | 
						|
    ): UIEventSource<boolean> {
 | 
						|
        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 (and the specified 'global filters')
 | 
						|
     */
 | 
						|
    public isShown(properties: Record<string, string>, 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(<string>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
 | 
						|
    }
 | 
						|
}
 |