| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  | import { Store, UIEventSource } from "../Logic/UIEventSource" | 
					
						
							| 
									
										
										
										
											2021-08-07 23:11:34 +02:00
										 |  |  | import LayerConfig from "./ThemeConfig/LayerConfig" | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  | import { OsmConnection } from "../Logic/Osm/OsmConnection" | 
					
						
							|  |  |  | import { LocalStorageSource } from "../Logic/Web/LocalStorageSource" | 
					
						
							|  |  |  | import { QueryParameters } from "../Logic/Web/QueryParameters" | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  | 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" | 
					
						
							| 
									
										
										
										
											2023-04-14 04:33:06 +02:00
										 |  |  | import { GlobalFilter } from "./GlobalFilter" | 
					
						
							| 
									
										
										
										
											2022-01-08 04:22:50 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  | export default class FilteredLayer { | 
					
						
							|  |  |  |     /** | 
					
						
							|  |  |  |      * Wether or not the specified layer is shown | 
					
						
							|  |  |  |      */ | 
					
						
							| 
									
										
										
										
											2021-07-27 19:39:57 +02:00
										 |  |  |     readonly isDisplayed: UIEventSource<boolean> | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |      * 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 | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |      */ | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |     readonly appliedFilters: ReadonlyMap<string, UIEventSource<undefined | number | string>> | 
					
						
							| 
									
										
										
										
											2021-07-27 19:39:57 +02:00
										 |  |  |     readonly layerDef: LayerConfig | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * 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> | 
					
						
							| 
									
										
										
										
											2023-04-06 02:20:25 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |     constructor( | 
					
						
							|  |  |  |         layer: LayerConfig, | 
					
						
							| 
									
										
										
										
											2023-04-14 17:53:08 +02:00
										 |  |  |         appliedFilters?: ReadonlyMap<string, UIEventSource<undefined | number | string>>, | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |         isDisplayed?: UIEventSource<boolean> | 
					
						
							|  |  |  |     ) { | 
					
						
							|  |  |  |         this.layerDef = layer | 
					
						
							|  |  |  |         this.isDisplayed = isDisplayed ?? new UIEventSource(true) | 
					
						
							| 
									
										
										
										
											2023-04-24 03:22:43 +02:00
										 |  |  |         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 | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |         const self = this | 
					
						
							|  |  |  |         const currentTags = new UIEventSource<TagsFilter>(undefined) | 
					
						
							|  |  |  |         this.appliedFilters.forEach((filterSrc) => { | 
					
						
							| 
									
										
										
										
											2023-04-06 02:20:25 +02:00
										 |  |  |             filterSrc.addCallbackAndRun((_) => { | 
					
						
							|  |  |  |                 currentTags.setData(self.calculateCurrentTags()) | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |             }) | 
					
						
							|  |  |  |         }) | 
					
						
							| 
									
										
										
										
											2023-04-06 02:20:25 +02:00
										 |  |  |         this.hasFilter = currentTags.map((ct) => ct !== undefined) | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |         this.currentFilter = currentTags | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-06 02:20:25 +02:00
										 |  |  |     public static fieldsToString(values: Record<string, string>): string { | 
					
						
							|  |  |  |         for (const key in values) { | 
					
						
							|  |  |  |             if (values[key] === "") { | 
					
						
							|  |  |  |                 delete values[key] | 
					
						
							| 
									
										
										
										
											2023-04-06 01:33:08 +02:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         return JSON.stringify(values) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |     /** | 
					
						
							|  |  |  |      * 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) { | 
					
						
							| 
									
										
										
										
											2023-04-06 02:20:25 +02:00
										 |  |  |             appliedFilters.set(subfilter.id, subfilter.initState(layer.id)) | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |         } | 
					
						
							|  |  |  |         return new FilteredLayer(layer, appliedFilters, isDisplayed) | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-04-06 02:20:25 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-24 03:22:43 +02:00
										 |  |  |     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 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-06 02:20:25 +02:00
										 |  |  |     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) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-03-28 05:13:48 +02:00
										 |  |  |     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 | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2023-04-06 02:20:25 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  |     public disableAllFilters(): void { | 
					
						
							|  |  |  |         this.appliedFilters.forEach((value) => value.setData(undefined)) | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-24 03:22:43 +02:00
										 |  |  |     /** | 
					
						
							| 
									
										
										
										
											2023-06-08 13:34:25 +02:00
										 |  |  |      * Returns true if the given tags match | 
					
						
							|  |  |  |      * - the current filters | 
					
						
							|  |  |  |      * - the specified 'global filters' | 
					
						
							|  |  |  |      * - the 'isShown'-filter set by the layer | 
					
						
							| 
									
										
										
										
											2023-04-24 03:22:43 +02:00
										 |  |  |      */ | 
					
						
							|  |  |  |     public isShown(properties: Record<string, string>, globalFilters?: GlobalFilter[]): boolean { | 
					
						
							|  |  |  |         if (properties._deleted === "yes") { | 
					
						
							|  |  |  |             return false | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-04-26 18:04:42 +02:00
										 |  |  |         for (const globalFilter of globalFilters ?? []) { | 
					
						
							|  |  |  |             const neededTags = globalFilter.osmTags | 
					
						
							|  |  |  |             if (neededTags !== undefined && !neededTags.matchesProperties(properties)) { | 
					
						
							|  |  |  |                 return false | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2023-04-24 03:22:43 +02:00
										 |  |  |         { | 
					
						
							|  |  |  |             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 | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-04-06 02:20:25 +02:00
										 |  |  |     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 | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2021-07-27 19:39:57 +02:00
										 |  |  | } |