forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			162 lines
		
	
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			162 lines
		
	
	
	
		
			5.7 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import FeatureSource from "./FeatureSource";
 | |
| import {UIEventSource} from "../UIEventSource";
 | |
| import Loc from "../../Models/Loc";
 | |
| import Hash from "../Web/Hash";
 | |
| import {TagsFilter} from "../Tags/TagsFilter";
 | |
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig";
 | |
| 
 | |
| export default class FilteringFeatureSource implements FeatureSource {
 | |
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> =
 | |
|         new UIEventSource<{ feature: any; freshness: Date }[]>([]);
 | |
|     public readonly name = "FilteringFeatureSource";
 | |
| 
 | |
|     constructor(
 | |
|         layers: UIEventSource<{
 | |
|             isDisplayed: UIEventSource<boolean>;
 | |
|             layerDef: LayerConfig;
 | |
|             appliedFilters: UIEventSource<TagsFilter>;
 | |
|         }[]>,
 | |
|         location: UIEventSource<Loc>,
 | |
|         selectedElement: UIEventSource<any>,
 | |
|         upstream: FeatureSource
 | |
|     ) {
 | |
|         const self = this;
 | |
| 
 | |
|         function update() {
 | |
|             const layerDict = {};
 | |
|             if (layers.data.length == 0) {
 | |
|                 console.warn("No layers defined!");
 | |
|                 return;
 | |
|             }
 | |
|             for (const layer of layers.data) {
 | |
|                 const prev = layerDict[layer.layerDef.id]
 | |
|                 if (prev !== undefined) {
 | |
|                     // We have seen this layer before!
 | |
|                     // We prefer the one which has a name
 | |
|                     if (layer.layerDef.name === undefined) {
 | |
|                         // This one is hidden, so we skip it
 | |
|                         console.log("Ignoring layer selection from ", layer)
 | |
|                         continue;
 | |
|                     }
 | |
|                 }
 | |
|                 layerDict[layer.layerDef.id] = layer;
 | |
|             }
 | |
| 
 | |
|             const features: { feature: any; freshness: Date }[] =
 | |
|                 upstream.features.data;
 | |
| 
 | |
|             const missingLayers = new Set<string>();
 | |
| 
 | |
|             const newFeatures = features.filter((f) => {
 | |
|                 const layerId = f.feature._matching_layer_id;
 | |
| 
 | |
|                 if (
 | |
|                     selectedElement.data?.id === f.feature.id ||
 | |
|                     f.feature.id === Hash.hash.data) {
 | |
|                     // This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away
 | |
|                     return true;
 | |
|                 }
 | |
| 
 | |
|                 if (layerId === undefined) {
 | |
|                     return false;
 | |
|                 }
 | |
|                 const layer: {
 | |
|                     isDisplayed: UIEventSource<boolean>;
 | |
|                     layerDef: LayerConfig;
 | |
|                     appliedFilters: UIEventSource<TagsFilter>;
 | |
|                 } = layerDict[layerId];
 | |
|                 if (layer === undefined) {
 | |
|                     missingLayers.add(layerId);
 | |
|                     return false;
 | |
|                 }
 | |
| 
 | |
|                 const isShown = layer.layerDef.isShown;
 | |
|                 const tags = f.feature.properties;
 | |
|                 if (isShown.IsKnown(tags)) {
 | |
|                     const result = layer.layerDef.isShown.GetRenderValue(
 | |
|                         f.feature.properties
 | |
|                     ).txt;
 | |
|                     if (result !== "yes") {
 | |
|                         return false;
 | |
|                     }
 | |
|                 } 
 | |
|                 
 | |
|                 const tagsFilter = layer.appliedFilters.data;
 | |
|                 if (tagsFilter) {
 | |
|                     if (!tagsFilter.matchesProperties(f.feature.properties)) {
 | |
|                         // Hidden by the filter on the layer itself - we want to hide it no matter wat
 | |
|                         return false;
 | |
|                     }
 | |
|                 }
 | |
|                 if (!FilteringFeatureSource.showLayer(layer, location)) {
 | |
|                     // The layer itself is either disabled or hidden due to zoom constraints
 | |
|                     // We should return true, but it might still match some other layer
 | |
|                     return false;
 | |
|                 }
 | |
| 
 | |
|                 return true;
 | |
|             });
 | |
| 
 | |
|             self.features.setData(newFeatures);
 | |
|             if (missingLayers.size > 0) {
 | |
|                 console.error(
 | |
|                     "Some layers were not found: ",
 | |
|                     Array.from(missingLayers)
 | |
|                 );
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         upstream.features.addCallback(() => {
 | |
|             update();
 | |
|         });
 | |
|         location
 | |
|             .map((l) => {
 | |
|                 // We want something that is stable for the shown layers
 | |
|                 const displayedLayerIndexes = [];
 | |
|                 for (let i = 0; i < layers.data.length; i++) {
 | |
|                     const layer = layers.data[i];
 | |
|                     if (l.zoom < layer.layerDef.minzoom) {
 | |
|                         continue;
 | |
|                     }
 | |
|                  
 | |
|                     if (!layer.isDisplayed.data) {
 | |
|                         continue;
 | |
|                     }
 | |
|                     displayedLayerIndexes.push(i);
 | |
|                 }
 | |
|                 return displayedLayerIndexes.join(",");
 | |
|             })
 | |
|             .addCallback(() => {
 | |
|                 update();
 | |
|             });
 | |
| 
 | |
|         layers.addCallback(update);
 | |
| 
 | |
|         const registered = new Set<UIEventSource<boolean>>();
 | |
|         layers.addCallbackAndRun((layers) => {
 | |
|             for (const layer of layers) {
 | |
|                 if (registered.has(layer.isDisplayed)) {
 | |
|                     continue;
 | |
|                 }
 | |
|                 registered.add(layer.isDisplayed);
 | |
|                 layer.isDisplayed.addCallback(() => update());
 | |
|                 layer.appliedFilters.addCallback(() => update());
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         update();
 | |
|     }
 | |
| 
 | |
|     private static showLayer(
 | |
|         layer: {
 | |
|             isDisplayed: UIEventSource<boolean>;
 | |
|             layerDef: LayerConfig;
 | |
|         },
 | |
|         location: UIEventSource<Loc>
 | |
|     ) {
 | |
|         return (
 | |
|             layer.isDisplayed.data &&
 | |
|             layer.layerDef.minzoomVisible <= location.data.zoom
 | |
|         );
 | |
|     }
 | |
| }
 |