forked from MapComplete/MapComplete
		
	First working version of the notes-layer, add filtering
This commit is contained in:
		
							parent
							
								
									ebb510da04
								
							
						
					
					
						commit
						91d2272861
					
				
					 19 changed files with 282 additions and 109 deletions
				
			
		|  | @ -403,6 +403,10 @@ export class ExtraFunctions { | ||||||
|     ]; |     ]; | ||||||
| 
 | 
 | ||||||
|     public static FullPatchFeature(params: ExtraFuncParams, feature) { |     public static FullPatchFeature(params: ExtraFuncParams, feature) { | ||||||
|  |         if(feature._is_patched){ | ||||||
|  |             return | ||||||
|  |         } | ||||||
|  |         feature._is_patched = true | ||||||
|         for (const func of ExtraFunctions.allFuncs) { |         for (const func of ExtraFunctions.allFuncs) { | ||||||
|             feature[func._name] = func._f(params, feature) |             feature[func._name] = func._f(params, feature) | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -59,6 +59,7 @@ export default class FeaturePipeline { | ||||||
| 
 | 
 | ||||||
|     private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>() |     private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>() | ||||||
|     private readonly metataggingRecalculated = new UIEventSource<void>(undefined) |     private readonly metataggingRecalculated = new UIEventSource<void>(undefined) | ||||||
|  |     private readonly requestMetataggingRecalculation = new UIEventSource<Date>(undefined) | ||||||
|      |      | ||||||
|     /** |     /** | ||||||
|      * Keeps track of all raw OSM-nodes. |      * Keeps track of all raw OSM-nodes. | ||||||
|  | @ -97,6 +98,10 @@ export default class FeaturePipeline { | ||||||
|             } |             } | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|  |         this.requestMetataggingRecalculation.stabilized(500).addCallbackAndRunD(_ => { | ||||||
|  |             self.updateAllMetaTagging("Request stabilized") | ||||||
|  |         }) | ||||||
|  |          | ||||||
|         const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed) |         const neededTilesFromOsm = this.getNeededTilesFromOsm(this.sufficientlyZoomed) | ||||||
| 
 | 
 | ||||||
|         const perLayerHierarchy = new Map<string, TileHierarchyMerger>() |         const perLayerHierarchy = new Map<string, TileHierarchyMerger>() | ||||||
|  | @ -141,7 +146,7 @@ export default class FeaturePipeline { | ||||||
|                     tile => { |                     tile => { | ||||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||||
|                         perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) |                         perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) | ||||||
|                         tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) |                         tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) | ||||||
|                     }); |                     }); | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|  | @ -169,7 +174,10 @@ export default class FeaturePipeline { | ||||||
|             if (id === "current_view") { |             if (id === "current_view") { | ||||||
|                 handlePriviligedFeatureSource(state.currentView) |                 handlePriviligedFeatureSource(state.currentView) | ||||||
|                 state.currentView.features.map(ffs => ffs[0]?.feature?.properties?.id).withEqualityStabilized((x,y) => x === y) |                 state.currentView.features.map(ffs => ffs[0]?.feature?.properties?.id).withEqualityStabilized((x,y) => x === y) | ||||||
|                     .addCallbackAndRunD(_ => self.applyMetaTags(state.currentView, state)) |                     .addCallbackAndRunD(_ => { | ||||||
|  |                             self.applyMetaTags(state.currentView, <any>this.state, `currentview changed`) | ||||||
|  |                         } | ||||||
|  |                     ) | ||||||
|                 continue |                 continue | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -187,7 +195,7 @@ export default class FeaturePipeline { | ||||||
|                         console.debug("Loaded tile ", id, tile.tileIndex, "from local cache") |                         console.debug("Loaded tile ", id, tile.tileIndex, "from local cache") | ||||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||||
|                         hierarchy.registerTile(tile); |                         hierarchy.registerTile(tile); | ||||||
|                         tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) |                         tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) | ||||||
|                     } |                     } | ||||||
|                 ) |                 ) | ||||||
| 
 | 
 | ||||||
|  | @ -207,13 +215,13 @@ export default class FeaturePipeline { | ||||||
|                         registerTile: (tile) => { |                         registerTile: (tile) => { | ||||||
|                             new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |                             new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||||
|                             perLayerHierarchy.get(id).registerTile(tile) |                             perLayerHierarchy.get(id).registerTile(tile) | ||||||
|                             tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) |                             tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) | ||||||
|                         } |                         } | ||||||
|                     }) |                     }) | ||||||
|                 } else { |                 } else { | ||||||
|                     new RegisteringAllFromFeatureSourceActor(src, state.allElements) |                     new RegisteringAllFromFeatureSourceActor(src, state.allElements) | ||||||
|                     perLayerHierarchy.get(id).registerTile(src) |                     perLayerHierarchy.get(id).registerTile(src) | ||||||
|                     src.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(src)) |                     src.features.addCallbackAndRunD(_ => self.onNewDataLoaded(src)) | ||||||
|                 } |                 } | ||||||
|             } else { |             } else { | ||||||
|                 new DynamicGeoJsonTileSource( |                 new DynamicGeoJsonTileSource( | ||||||
|  | @ -221,7 +229,7 @@ export default class FeaturePipeline { | ||||||
|                     tile => { |                     tile => { | ||||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||||
|                         perLayerHierarchy.get(id).registerTile(tile) |                         perLayerHierarchy.get(id).registerTile(tile) | ||||||
|                         tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) |                         tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) | ||||||
|                     }, |                     }, | ||||||
|                     state |                     state | ||||||
|                 ) |                 ) | ||||||
|  | @ -242,7 +250,7 @@ export default class FeaturePipeline { | ||||||
|                     saver?.addTile(tile) |                     saver?.addTile(tile) | ||||||
|                 } |                 } | ||||||
|                 perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) |                 perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile) | ||||||
|                 tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) |                 tile.features.addCallbackAndRunD(_ => self.onNewDataLoaded(tile)) | ||||||
| 
 | 
 | ||||||
|             }, |             }, | ||||||
|             state: state, |             state: state, | ||||||
|  | @ -282,7 +290,12 @@ export default class FeaturePipeline { | ||||||
|                     // We save the tile data for the given layer to local storage - data sourced from overpass
 |                     // We save the tile data for the given layer to local storage - data sourced from overpass
 | ||||||
|                     self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile) |                     self.localStorageSavers.get(tile.layer.layerDef.id)?.addTile(tile) | ||||||
|                     perLayerHierarchy.get(source.layer.layerDef.id).registerTile(new RememberingSource(tile)) |                     perLayerHierarchy.get(source.layer.layerDef.id).registerTile(new RememberingSource(tile)) | ||||||
|                     tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) |                     tile.features.addCallbackAndRunD(f => { | ||||||
|  |                         if(f.length === 0){ | ||||||
|  |                             return | ||||||
|  |                         } | ||||||
|  |                         self.onNewDataLoaded(tile) | ||||||
|  |                     }) | ||||||
| 
 | 
 | ||||||
|                 } |                 } | ||||||
|             }), |             }), | ||||||
|  | @ -302,9 +315,7 @@ export default class FeaturePipeline { | ||||||
|                 // We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
 |                 // We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
 | ||||||
|                 perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) |                 perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) | ||||||
|                 // AT last, we always apply the metatags whenever possible
 |                 // AT last, we always apply the metatags whenever possible
 | ||||||
|                 // @ts-ignore
 |                 perLayer.features.addCallbackAndRunD(_ => self.onNewDataLoaded(perLayer)) | ||||||
|                 perLayer.features.addCallbackAndRunD(_ => self.applyMetaTags(perLayer, state)) |  | ||||||
|                 perLayer.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(perLayer)) |  | ||||||
| 
 | 
 | ||||||
|             }, |             }, | ||||||
|             newGeometry |             newGeometry | ||||||
|  | @ -312,8 +323,8 @@ export default class FeaturePipeline { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         // Whenever fresh data comes in, we need to update the metatagging
 |         // Whenever fresh data comes in, we need to update the metatagging
 | ||||||
|         self.newDataLoadedSignal.stabilized(250).addCallback(_ => { |         self.newDataLoadedSignal.stabilized(250).addCallback(src => { | ||||||
|             self.updateAllMetaTagging() |             self.updateAllMetaTagging(`New data loaded by ${src.name} (and stabilized)`) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @ -325,7 +336,11 @@ export default class FeaturePipeline { | ||||||
|             }, [osmFeatureSource.isRunning] |             }, [osmFeatureSource.isRunning] | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|  |     private onNewDataLoaded(src: FeatureSource){ | ||||||
|  |         this.newDataLoadedSignal.setData(src) | ||||||
|  |         this.requestMetataggingRecalculation.setData(new Date()) | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     public GetAllFeaturesWithin(bbox: BBox): any[][] { |     public GetAllFeaturesWithin(bbox: BBox): any[][] { | ||||||
|  | @ -471,12 +486,16 @@ export default class FeaturePipeline { | ||||||
|         return updater; |         return updater; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private applyMetaTags(src: FeatureSourceForLayer, state: any) { |     private applyMetaTags(src: FeatureSourceForLayer, state: any, reason: string) { | ||||||
|         const self = this |         const self = this | ||||||
|         if(src === undefined){ |         if(src === undefined){ | ||||||
|             throw "Src is undefined" |             throw "Src is undefined" | ||||||
|         } |         } | ||||||
|         const layerDef = src.layer.layerDef; |         const layerDef = src.layer.layerDef; | ||||||
|  |         console.debug(`Applying metatags onto ${src.name} due to ${reason} which has ${src.features.data?.length} features`) | ||||||
|  |         if(src.features.data.length == 0){ | ||||||
|  |             return | ||||||
|  |         } | ||||||
|         MetaTagging.addMetatags( |         MetaTagging.addMetatags( | ||||||
|             src.features.data, |             src.features.data, | ||||||
|             { |             { | ||||||
|  | @ -495,17 +514,14 @@ export default class FeaturePipeline { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     public updateAllMetaTagging() { | 
 | ||||||
|  |     public updateAllMetaTagging(reason: string) { | ||||||
|         const self = this; |         const self = this; | ||||||
|         console.debug("Updating the meta tagging of all tiles as new data got loaded") |  | ||||||
|         this.perLayerHierarchy.forEach(hierarchy => { |         this.perLayerHierarchy.forEach(hierarchy => { | ||||||
|             hierarchy.loadedTiles.forEach(tile => { |             hierarchy.loadedTiles.forEach(tile => { | ||||||
|                 self.applyMetaTags(tile, <any> this.state) |                 self.applyMetaTags(tile, <any> this.state, `${reason} (tile ${tile.tileIndex})`) | ||||||
|             }) |             }) | ||||||
|         }) |         }) | ||||||
|         if(this.state.currentView !== undefined){ |  | ||||||
|             this.applyMetaTags(this.state.currentView, <any> this.state) |  | ||||||
|         } |  | ||||||
|         self.metataggingRecalculated.ping() |         self.metataggingRecalculated.ping() | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -26,7 +26,7 @@ export default class PerLayerFeatureSourceSplitter { | ||||||
|             if (features === undefined) { |             if (features === undefined) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             if (layers.data === undefined) { |             if (layers.data === undefined || layers.data.length === 0) { | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -4,7 +4,6 @@ import {FeatureSourceForLayer, Tiled} from "../FeatureSource"; | ||||||
| import Hash from "../../Web/Hash"; | import Hash from "../../Web/Hash"; | ||||||
| import {BBox} from "../../BBox"; | import {BBox} from "../../BBox"; | ||||||
| import {ElementStorage} from "../../ElementStorage"; | import {ElementStorage} from "../../ElementStorage"; | ||||||
| import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"; |  | ||||||
| 
 | 
 | ||||||
| export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled { | export default class FilteringFeatureSource implements FeatureSourceForLayer, Tiled { | ||||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = |     public features: UIEventSource<{ feature: any; freshness: Date }[]> = | ||||||
|  | @ -71,8 +70,8 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | ||||||
|             self.registerCallback(f.feature) |             self.registerCallback(f.feature) | ||||||
| 
 | 
 | ||||||
|             if ( |             if ( | ||||||
|                 this.state.selectedElement.data?.id === f.feature.id || |                 (this.state.selectedElement !== undefined && this.state.selectedElement.data?.id === f.feature.properties.id) || | ||||||
|                 f.feature.id === Hash.hash.data) { |                 (Hash.hash.data !== undefined && f.feature.properties.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
 |                 // This is the selected object - it gets a free pass even if zoom is not sufficient or it is filtered away
 | ||||||
|                 return true; |                 return true; | ||||||
|             } |             } | ||||||
|  | @ -89,6 +88,7 @@ export default class FilteringFeatureSource implements FeatureSourceForLayer, Ti | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const tagsFilter = layer.appliedFilters.data; |             const tagsFilter = layer.appliedFilters.data; | ||||||
|  |             console.log("Current filters for "+layer.layerDef.id+" are ",tagsFilter) | ||||||
|             for (const filter of tagsFilter ?? []) { |             for (const filter of tagsFilter ?? []) { | ||||||
|                 const neededTags = filter.filter.options[filter.selected].osmTags |                 const neededTags = filter.filter.options[filter.selected].osmTags | ||||||
|                 if (!neededTags.matchesProperties(f.feature.properties)) { |                 if (!neededTags.matchesProperties(f.feature.properties)) { | ||||||
|  |  | ||||||
|  | @ -44,6 +44,10 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | ||||||
|                     return undefined |                     return undefined | ||||||
|                 } |                 } | ||||||
|                 const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) |                 const tileRange = Tiles.TileRangeBetween(zoomlevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) | ||||||
|  |                 if(tileRange.total > 10000){ | ||||||
|  |                     console.error("Got a really big tilerange, bounds and location might be out of sync") | ||||||
|  |                     return undefined | ||||||
|  |                 } | ||||||
|                  |                  | ||||||
|                 const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) |                 const needed = Tiles.MapRange(tileRange, (x, y) => Tiles.tile_index(zoomlevel, x, y)).filter(i => !self._loadedTiles.has(i)) | ||||||
|                 if (needed.length === 0) { |                 if (needed.length === 0) { | ||||||
|  |  | ||||||
|  | @ -28,7 +28,6 @@ export default class MetaTagging { | ||||||
|                                   includeDates?: true | boolean, |                                   includeDates?: true | boolean, | ||||||
|                                   includeNonDates?: true | boolean |                                   includeNonDates?: true | boolean | ||||||
|                               }): boolean { |                               }): boolean { | ||||||
| 
 |  | ||||||
|         if (features === undefined || features.length === 0) { |         if (features === undefined || features.length === 0) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | @ -106,7 +105,6 @@ export default class MetaTagging { | ||||||
|     } |     } | ||||||
|     public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] { |     public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] { | ||||||
|         const functions: ((feature: any) => any)[] = []; |         const functions: ((feature: any) => any)[] = []; | ||||||
|          |  | ||||||
|         for (const entry of calculatedTags) { |         for (const entry of calculatedTags) { | ||||||
|             const key = entry[0] |             const key = entry[0] | ||||||
|             const code = entry[1]; |             const code = entry[1]; | ||||||
|  | @ -148,6 +146,7 @@ export default class MetaTagging { | ||||||
| 
 | 
 | ||||||
|             // Lazy function
 |             // Lazy function
 | ||||||
|             const f = (feature: any) => { |             const f = (feature: any) => { | ||||||
|  |                 const oldValue = feature.properties[key] | ||||||
|                 delete feature.properties[key] |                 delete feature.properties[key] | ||||||
|                 Object.defineProperty(feature.properties, key, { |                 Object.defineProperty(feature.properties, key, { | ||||||
|                     configurable: true, |                     configurable: true, | ||||||
|  |  | ||||||
|  | @ -9,7 +9,6 @@ import MapState from "./MapState"; | ||||||
| import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; | import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; | ||||||
| import Hash from "../Web/Hash"; | import Hash from "../Web/Hash"; | ||||||
| import {BBox} from "../BBox"; | import {BBox} from "../BBox"; | ||||||
| import {FeatureSourceForLayer} from "../FeatureSource/FeatureSource"; |  | ||||||
| 
 | 
 | ||||||
| export default class FeaturePipelineState extends MapState { | export default class FeaturePipelineState extends MapState { | ||||||
| 
 | 
 | ||||||
|  | @ -33,7 +32,7 @@ export default class FeaturePipelineState extends MapState { | ||||||
| 
 | 
 | ||||||
|                 const sourceBBox = source.features.map(allFeatures => BBox.bboxAroundAll(allFeatures.map(f => BBox.get(f.feature)))) |                 const sourceBBox = source.features.map(allFeatures => BBox.bboxAroundAll(allFeatures.map(f => BBox.get(f.feature)))) | ||||||
|                  |                  | ||||||
|                 // Do show features indicates if the 'showDataLayer' should be shown
 |                 // Do show features indicates if the respective 'showDataLayer' should be shown. It can be hidden by e.g. clustering
 | ||||||
|                 const doShowFeatures = source.features.map( |                 const doShowFeatures = source.features.map( | ||||||
|                     f => { |                     f => { | ||||||
|                         const z = self.locationControl.data.zoom |                         const z = self.locationControl.data.zoom | ||||||
|  |  | ||||||
|  | @ -22,7 +22,7 @@ export default class Constants { | ||||||
|     /** |     /** | ||||||
|      * Layer IDs of layers which have special properties through built-in hooks |      * Layer IDs of layers which have special properties through built-in hooks | ||||||
|      */ |      */ | ||||||
|     public static readonly priviliged_layers: string[] = [...Constants.added_by_default, "type_node", ...Constants.no_include] |     public static readonly priviliged_layers: string[] = [...Constants.added_by_default, "type_node", "notes", ...Constants.no_include] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     // The user journey states thresholds when a new feature gets unlocked
 |     // The user journey states thresholds when a new feature gets unlocked
 | ||||||
|  |  | ||||||
|  | @ -3,12 +3,18 @@ import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||||
| import FilterConfigJson from "./Json/FilterConfigJson"; | import FilterConfigJson from "./Json/FilterConfigJson"; | ||||||
| import Translations from "../../UI/i18n/Translations"; | import Translations from "../../UI/i18n/Translations"; | ||||||
| import {TagUtils} from "../../Logic/Tags/TagUtils"; | import {TagUtils} from "../../Logic/Tags/TagUtils"; | ||||||
|  | import ValidatedTextField from "../../UI/Input/ValidatedTextField"; | ||||||
|  | import {Utils} from "../../Utils"; | ||||||
|  | import {TagRenderingConfigJson} from "./Json/TagRenderingConfigJson"; | ||||||
|  | import {AndOrTagConfigJson} from "./Json/TagConfigJson"; | ||||||
| 
 | 
 | ||||||
| export default class FilterConfig { | export default class FilterConfig { | ||||||
|     public readonly id: string |     public readonly id: string | ||||||
|     public readonly options: { |     public readonly options: { | ||||||
|         question: Translation; |         question: Translation; | ||||||
|         osmTags: TagsFilter; |         osmTags: TagsFilter; | ||||||
|  |         originalTagsSpec: string | AndOrTagConfigJson | ||||||
|  |         fields: { name: string, type: string }[] | ||||||
|     }[]; |     }[]; | ||||||
|      |      | ||||||
|     constructor(json: FilterConfigJson, context: string) { |     constructor(json: FilterConfigJson, context: string) { | ||||||
|  | @ -28,23 +34,49 @@ export default class FilterConfig { | ||||||
|         } |         } | ||||||
|         this.id = json.id; |         this.id = json.id; | ||||||
|         this.options = json.options.map((option, i) => { |         this.options = json.options.map((option, i) => { | ||||||
|  |             const ctx = `${context}.options[${i}]`; | ||||||
|             const question = Translations.T( |             const question = Translations.T( | ||||||
|                 option.question, |                 option.question, | ||||||
|                 context + ".options-[" + i + "].question" |                 `${ctx}.question` | ||||||
|             ); |  | ||||||
|             const osmTags = TagUtils.Tag( |  | ||||||
|                 option.osmTags ?? {and: []}, |  | ||||||
|                 `${context}.options-[${i}].osmTags` |  | ||||||
|             ); |             ); | ||||||
|  |             let osmTags = TagUtils.Tag( | ||||||
|  |                     option.osmTags ?? {and: []}, | ||||||
|  |                     `${ctx}.osmTags` | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|             if (question === undefined) { |             if (question === undefined) { | ||||||
|                 throw `Invalid filter: no question given at ${context}[${i}]` |                 throw `Invalid filter: no question given at ${ctx}` | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return {question: question, osmTags: osmTags}; |             const fields: { name: string, type: string }[] = ((option.fields) ?? []).map((f, i) => { | ||||||
|  |                 const type = f.type ?? "string" | ||||||
|  |                 if (!ValidatedTextField.AllTypes.has(type)) { | ||||||
|  |                     throw `Invalid filter: ${type} is not a valid validated textfield type (at ${ctx}.fields[${i}])\n\tTry one of ${Array.from(ValidatedTextField.AllTypes.keys()).join(",")}` | ||||||
|  |                 } | ||||||
|  |                 if (f.name === undefined || f.name === "" || f.name.match(/[a-z0-9_-]+/) == null) { | ||||||
|  |                     throw `Invalid filter: a variable name should match [a-z0-9_-]+ at ${ctx}.fields[${i}]` | ||||||
|  |                 } | ||||||
|  |                 return { | ||||||
|  |                     name: f.name, | ||||||
|  |                     type | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |              | ||||||
|  |             if(fields.length > 0){ | ||||||
|  |                 // erase the tags, they aren't needed
 | ||||||
|  |                 osmTags = TagUtils.Tag({and:[]}) | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             return {question: question, osmTags: osmTags, fields, originalTagsSpec: option.osmTags}; | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|  |         if (this.options.some(o => o.fields.length > 0) && this.options.length > 1) { | ||||||
|  |             throw `Invalid filter at ${context}: a filter with textfields should only offer a single option.` | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         if (this.options.length > 1 && this.options[0].osmTags["and"]?.length !== 0) { |         if (this.options.length > 1 && this.options[0].osmTags["and"]?.length !== 0) { | ||||||
|             throw "Error in " + context + "." + this.id + ": the first option of a multi-filter should always be the 'reset' option and not have any filters" |             throw "Error in " + context + "." + this.id + ": the first option of a multi-filter should always be the 'reset' option and not have any filters" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |      | ||||||
| } | } | ||||||
|  | @ -11,5 +11,12 @@ export default interface FilterConfigJson { | ||||||
|      * If there is only one option this will be a checkbox |      * If there is only one option this will be a checkbox | ||||||
|      * Filtering is done based on the given osmTags that are compared to the objects in that layer. |      * Filtering is done based on the given osmTags that are compared to the objects in that layer. | ||||||
|      */ |      */ | ||||||
|     options: { question: string | any; osmTags?: AndOrTagConfigJson | string }[]; |     options: { | ||||||
|  |         question: string | any;  | ||||||
|  |         osmTags?: AndOrTagConfigJson | string,  | ||||||
|  |         fields?: { | ||||||
|  |             name: string, | ||||||
|  |             type?: string | "string" | ||||||
|  |         }[] | ||||||
|  |     }[]; | ||||||
| } | } | ||||||
|  | @ -111,7 +111,7 @@ export default class TagRenderingConfig { | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|             if (ValidatedTextField.AllTypes[this.freeform.type] === undefined) { |             if (!ValidatedTextField.AllTypes.has(this.freeform.type)) { | ||||||
|                 const knownKeys = ValidatedTextField.tpList.map(tp => tp.name).join(", "); |                 const knownKeys = ValidatedTextField.tpList.map(tp => tp.name).join(", "); | ||||||
|                 throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}` |                 throw `Freeform.key ${this.freeform.key} is an invalid type. Known keys are ${knownKeys}` | ||||||
|             } |             } | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ export class Tiles { | ||||||
|         const result: T[] = [] |         const result: T[] = [] | ||||||
|         const total = tileRange.total |         const total = tileRange.total | ||||||
|         if (total > 100000) { |         if (total > 100000) { | ||||||
|             throw "Tilerange too big (z is "+tileRange.zoomlevel+")" |             throw `Tilerange too big (z is ${tileRange.zoomlevel}, total tiles needed: ${tileRange.total})` | ||||||
|         } |         } | ||||||
|         for (let x = tileRange.xstart; x <= tileRange.xend; x++) { |         for (let x = tileRange.xstart; x <= tileRange.xend; x++) { | ||||||
|             for (let y = tileRange.ystart; y <= tileRange.yend; y++) { |             for (let y = tileRange.ystart; y <= tileRange.yend; y++) { | ||||||
|  |  | ||||||
|  | @ -162,7 +162,7 @@ class AutomationPanel extends Combine{ | ||||||
|                     return true; |                     return true; | ||||||
|                 } |                 } | ||||||
|                 stateToShow.setData("Applying metatags") |                 stateToShow.setData("Applying metatags") | ||||||
|                 pipeline.updateAllMetaTagging() |                 pipeline.updateAllMetaTagging("triggered by automaton") | ||||||
|                 stateToShow.setData("Gathering applicable elements") |                 stateToShow.setData("Gathering applicable elements") | ||||||
| 
 | 
 | ||||||
|                 let handled = 0 |                 let handled = 0 | ||||||
|  |  | ||||||
|  | @ -14,6 +14,9 @@ import FilteredLayer from "../../Models/FilteredLayer"; | ||||||
| import BackgroundSelector from "./BackgroundSelector"; | import BackgroundSelector from "./BackgroundSelector"; | ||||||
| import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; | import FilterConfig from "../../Models/ThemeConfig/FilterConfig"; | ||||||
| import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; | import TilesourceConfig from "../../Models/ThemeConfig/TilesourceConfig"; | ||||||
|  | import {SubstitutedTranslation} from "../SubstitutedTranslation"; | ||||||
|  | import ValidatedTextField from "../Input/ValidatedTextField"; | ||||||
|  | import {QueryParameters} from "../../Logic/Web/QueryParameters"; | ||||||
| 
 | 
 | ||||||
| export default class FilterView extends VariableUiElement { | export default class FilterView extends VariableUiElement { | ||||||
|     constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) { |     constructor(filteredLayer: UIEventSource<FilteredLayer[]>, tileLayers: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[]) { | ||||||
|  | @ -144,7 +147,7 @@ export default class FilterView extends VariableUiElement { | ||||||
|         layer.filters.forEach((f, i) => filterIndexes.set(f.id, i)) |         layer.filters.forEach((f, i) => filterIndexes.set(f.id, i)) | ||||||
| 
 | 
 | ||||||
|         let listFilterElements: [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>][] = layer.filters.map( |         let listFilterElements: [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>][] = layer.filters.map( | ||||||
|             FilterView.createFilter |             filter => FilterView.createFilter(filter) | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         listFilterElements.forEach((inputElement, i) => |         listFilterElements.forEach((inputElement, i) => | ||||||
|  | @ -193,6 +196,71 @@ export default class FilterView extends VariableUiElement { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>] { |     private static createFilter(filterConfig: FilterConfig): [BaseUIElement, UIEventSource<{ filter: FilterConfig, selected: number }>] { | ||||||
|  | 
 | ||||||
|  |         if (filterConfig.options[0].fields.length > 0) { | ||||||
|  | 
 | ||||||
|  |             // Filter which uses one or more textfields
 | ||||||
|  |             const filter = filterConfig.options[0] | ||||||
|  |             const mappings = new Map<string, BaseUIElement>() | ||||||
|  |             let allValid = new UIEventSource(true) | ||||||
|  |             const properties = new UIEventSource<any>({}) | ||||||
|  |             for (const {name, type} of filter.fields) { | ||||||
|  |                 const value = QueryParameters.GetQueryParameter("filter-" + filterConfig.id + "-" + name, "", "Value for filter " + filterConfig.id) | ||||||
|  |                 const field = ValidatedTextField.InputForType(type, { | ||||||
|  |                     value | ||||||
|  |                 }).SetClass("inline-block") | ||||||
|  |                 mappings.set(name, field) | ||||||
|  |                 const stable = value.stabilized(250) | ||||||
|  |                 stable.addCallbackAndRunD(v => { | ||||||
|  |                     properties.data[name] = v.toLowerCase(); | ||||||
|  |                     properties.ping() | ||||||
|  |                 }) | ||||||
|  |                 allValid = allValid.map(previous => previous && field.IsValid(stable.data) && stable.data !== "", [stable]) | ||||||
|  |             } | ||||||
|  |             const tr = new SubstitutedTranslation(filter.question, new UIEventSource<any>({id: filterConfig.id}), State.state, mappings) | ||||||
|  |             const neutral = { | ||||||
|  |                 filter: new FilterConfig({ | ||||||
|  |                     id: filterConfig.id, | ||||||
|  |                     options: [ | ||||||
|  |                         { | ||||||
|  |                             question: "--", | ||||||
|  |                         } | ||||||
|  |                     ] | ||||||
|  |                 }, "While dynamically constructing a filterconfig"), | ||||||
|  |                 selected: 0 | ||||||
|  |             } | ||||||
|  |             const trigger = allValid.map(isValid => { | ||||||
|  |                 if (!isValid) { | ||||||
|  |                     return neutral | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Replace all the field occurences in the tags...
 | ||||||
|  |                 const osmTags = Utils.WalkJson(filter.originalTagsSpec, | ||||||
|  |                     v => { | ||||||
|  |                         if (typeof v !== "string") { | ||||||
|  |                             return v | ||||||
|  |                         } | ||||||
|  |                         return Utils.SubstituteKeys(v, properties.data) | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |                 // ... which we use below to construct a filter!
 | ||||||
|  |                 return { | ||||||
|  |                     filter: new FilterConfig({ | ||||||
|  |                         id: filterConfig.id, | ||||||
|  |                         options: [ | ||||||
|  |                             { | ||||||
|  |                                 question: "--", | ||||||
|  |                                 osmTags | ||||||
|  |                             } | ||||||
|  |                         ] | ||||||
|  |                     }, "While dynamically constructing a filterconfig"), | ||||||
|  |                     selected: 0 | ||||||
|  |                 } | ||||||
|  |             }, [properties]) | ||||||
|  |             return [tr, trigger]; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|         if (filterConfig.options.length === 1) { |         if (filterConfig.options.length === 1) { | ||||||
|             let option = filterConfig.options[0]; |             let option = filterConfig.options[0]; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -436,7 +436,7 @@ export default class ValidatedTextField { | ||||||
|     /** |     /** | ||||||
|      * {string (typename) --> TextFieldDef} |      * {string (typename) --> TextFieldDef} | ||||||
|      */ |      */ | ||||||
|     public static AllTypes = ValidatedTextField.allTypesDict(); |     public static AllTypes: Map<string, TextFieldDef> = ValidatedTextField.allTypesDict(); | ||||||
| 
 | 
 | ||||||
|     public static InputForType(type: string, options?: { |     public static InputForType(type: string, options?: { | ||||||
|         placeholder?: string | BaseUIElement, |         placeholder?: string | BaseUIElement, | ||||||
|  | @ -455,7 +455,7 @@ export default class ValidatedTextField { | ||||||
|     }): InputElement<string> { |     }): InputElement<string> { | ||||||
|         options = options ?? {}; |         options = options ?? {}; | ||||||
|         options.placeholder = options.placeholder ?? type; |         options.placeholder = options.placeholder ?? type; | ||||||
|         const tp: TextFieldDef = ValidatedTextField.AllTypes[type] |         const tp: TextFieldDef = ValidatedTextField.AllTypes.get(type) | ||||||
|         const isValidTp = tp.isValid; |         const isValidTp = tp.isValid; | ||||||
|         let isValid; |         let isValid; | ||||||
|         options.textArea = options.textArea ?? type === "text"; |         options.textArea = options.textArea ?? type === "text"; | ||||||
|  | @ -615,10 +615,11 @@ export default class ValidatedTextField { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     private static allTypesDict() { |     private static allTypesDict(): Map<string, TextFieldDef> { | ||||||
|         const types = {}; |         const types = new Map<string, TextFieldDef>(); | ||||||
|         for (const tp of ValidatedTextField.tpList) { |         for (const tp of ValidatedTextField.tpList) { | ||||||
|             types[tp.name] = tp; |             types[tp.name] = tp; | ||||||
|  |             types.set(tp.name, tp); | ||||||
|         } |         } | ||||||
|         return types; |         return types; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -43,8 +43,8 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | ||||||
|         const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI"), State.state) |         const title = new TagRenderingAnswer(tags, layerConfig.title ?? new TagRenderingConfig("POI"), State.state) | ||||||
|             .SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2"); |             .SetClass("break-words font-bold sm:p-0.5 md:p-1 sm:p-1.5 md:p-2"); | ||||||
|         const titleIcons = new Combine( |         const titleIcons = new Combine( | ||||||
|             layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, |             layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon, State.state, | ||||||
|                 "block w-8 h-8 max-h-8 align-baseline box-content sm:p-0.5", "width: 2rem;") |                 "block w-8 h-8 max-h-8 align-baseline box-content sm:p-0.5 w-10",) | ||||||
|             )) |             )) | ||||||
|             .SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") |             .SetClass("flex flex-row flex-wrap pt-0.5 sm:pt-1 items-center mr-2") | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										91
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										91
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -45,34 +45,31 @@ There are also some technicalities in your theme to keep in mind: | ||||||
| The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md). 
 | The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md). 
 | ||||||
| The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console. | The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console. | ||||||
| In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org`
 | In the case that MapComplete is pointed to the testing grounds, the edit will be made on https://master.apis.dev.openstreetmap.org`
 | ||||||
| 
 |     private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"] | ||||||
|  |     private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"] | ||||||
|  |     private static injectedDownloads = {} | ||||||
|  |     private static _download_cache = new Map<string, { promise: Promise<any>, timestamp: number }>() | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * Parses the arguments for special visualisations |      * Parses the arguments for special visualisations | ||||||
|      */ |      */ | ||||||
|     public static ParseVisArgs(specs: { name: string, defaultValue?: string }[], args: string[]): any { |     public static ParseVisArgs(specs: { name: string, defaultValue?: string }[], args: string[]): any { | ||||||
|         const parsed = {}; |         const parsed = {}; | ||||||
|         if(args.length> specs.length){ |         if (args.length > specs.length) { | ||||||
|             throw "To much arguments for special visualization: got "+args.join(",")+" but expected only "+args.length+" arguments" |             throw "To much arguments for special visualization: got " + args.join(",") + " but expected only " + args.length + " arguments" | ||||||
|         } |         } | ||||||
|         for (let i = 0; i < specs.length; i++){ |         for (let i = 0; i < specs.length; i++) { | ||||||
|             const spec = specs[i]; |             const spec = specs[i]; | ||||||
|             let arg = args[i]?.trim(); |             let arg = args[i]?.trim(); | ||||||
|             if(arg === undefined || arg === ""){ |             if (arg === undefined || arg === "") { | ||||||
|                 arg = spec.defaultValue |                 arg = spec.defaultValue | ||||||
|             } |             } | ||||||
|             parsed[spec.name] =  arg |             parsed[spec.name] = arg | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         return parsed; |         return parsed; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
|     private static knownKeys = ["addExtraTags", "and", "calculatedTags", "changesetmessage", "clustering", "color", "condition", "customCss", "dashArray", "defaultBackgroundId", "description", "descriptionTail", "doNotDownload", "enableAddNewPoints", "enableBackgroundLayerSelection", "enableGeolocation", "enableLayers", "enableMoreQuests", "enableSearch", "enableShareScreen", "enableUserBadge", "freeform", "hideFromOverview", "hideInAnswer", "icon", "iconOverlays", "iconSize", "id", "if", "ifnot", "isShown", "key", "language", "layers", "lockLocation", "maintainer", "mappings", "maxzoom", "maxZoom", "minNeededElements", "minzoom", "multiAnswer", "name", "or", "osmTags", "passAllFeatures", "presets", "question", "render", "roaming", "roamingRenderings", "rotation", "shortDescription", "socialImage", "source", "startLat", "startLon", "startZoom", "tagRenderings", "tags", "then", "title", "titleIcons", "type", "version", "wayHandling", "widenFactor", "width"] |  | ||||||
|     private static extraKeys = ["nl", "en", "fr", "de", "pt", "es", "name", "phone", "email", "amenity", "leisure", "highway", "building", "yes", "no", "true", "false"] |  | ||||||
|     private static injectedDownloads = {} |  | ||||||
|     private static _download_cache = new Map<string, { promise: Promise<any>, timestamp: number }>() |  | ||||||
| 
 |  | ||||||
|     static EncodeXmlValue(str) { |     static EncodeXmlValue(str) { | ||||||
|         if (typeof str !== "string") { |         if (typeof str !== "string") { | ||||||
|             str = "" + str |             str = "" + str | ||||||
|  | @ -198,7 +195,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         const newArr = []; |         const newArr = []; | ||||||
|         const seen = new Set<string>(); |         const seen = new Set<string>(); | ||||||
|         for (const string of arr) { |         for (const string of arr) { | ||||||
|             if(seen.has(string)){ |             if (seen.has(string)) { | ||||||
|                 newArr.push(string) |                 newArr.push(string) | ||||||
|             } |             } | ||||||
|             seen.add(string) |             seen.add(string) | ||||||
|  | @ -238,6 +235,13 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         return [a.substr(0, index), a.substr(index + sep.length)]; |         return [a.substr(0, index), a.substr(index + sep.length)]; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Given a piece of text, will replace any key occuring in 'tags' by the corresponding value | ||||||
|  |      * @param txt | ||||||
|  |      * @param tags | ||||||
|  |      * @param useLang | ||||||
|  |      * @constructor | ||||||
|  |      */ | ||||||
|     public static SubstituteKeys(txt: string | undefined, tags: any, useLang?: string): string | undefined { |     public static SubstituteKeys(txt: string | undefined, tags: any, useLang?: string): string | undefined { | ||||||
|         if (txt === undefined) { |         if (txt === undefined) { | ||||||
|             return undefined |             return undefined | ||||||
|  | @ -249,7 +253,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         while (match) { |         while (match) { | ||||||
|             const key = match[1] |             const key = match[1] | ||||||
|             let v = tags[key] |             let v = tags[key] | ||||||
|             if(v !== undefined ){ |             if (v !== undefined) { | ||||||
| 
 | 
 | ||||||
|                 if (v["toISOString"] != undefined) { |                 if (v["toISOString"] != undefined) { | ||||||
|                     // This is a date, probably the timestamp of the object
 |                     // This is a date, probably the timestamp of the object
 | ||||||
|  | @ -258,17 +262,17 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|                     v = date.toISOString() |                     v = date.toISOString() | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if(useLang !== undefined && v?.translations !== undefined){ |                 if (useLang !== undefined && v?.translations !== undefined) { | ||||||
|                     v = v.translations[useLang] ?? v.translations["*"] ?? (v.textFor !== undefined ? v.textFor(useLang) : v); |                     v = v.translations[useLang] ?? v.translations["*"] ?? (v.textFor !== undefined ? v.textFor(useLang) : v); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if(v.InnerConstructElement !== undefined){ |                 if (v.InnerConstructElement !== undefined) { | ||||||
|                     console.warn("SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is", key,"\nThe value is", v) |                     console.warn("SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is", key, "\nThe value is", v) | ||||||
|                     v = ( <HTMLElement> v.InnerConstructElement())?.innerText |                     v = (<HTMLElement>v.InnerConstructElement())?.innerText | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if(typeof v !== "string"){ |                 if (typeof v !== "string") { | ||||||
|                     v = ""+v |                     v = "" + v | ||||||
|                 } |                 } | ||||||
|                 v = v.replace(/\n/g, "<br/>") |                 v = v.replace(/\n/g, "<br/>") | ||||||
|             } |             } | ||||||
|  | @ -321,7 +325,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const sourceV = source[key]; |             const sourceV = source[key]; | ||||||
|             if(target === null){ |             if (target === null) { | ||||||
|                 return source |                 return source | ||||||
|             } |             } | ||||||
|             const targetV = target[key] |             const targetV = target[key] | ||||||
|  | @ -342,6 +346,27 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         return target; |         return target; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     static WalkJson(json: any, f: (v: number | string | boolean | undefined) => any) { | ||||||
|  |         if(json === undefined){ | ||||||
|  |             return f(undefined) | ||||||
|  |         } | ||||||
|  |         const jtp = typeof json | ||||||
|  |         if (jtp === "boolean" || jtp === "string" || jtp === "number"){ | ||||||
|  |             return f(json) | ||||||
|  |         } | ||||||
|  |         if (json.map !== undefined) { | ||||||
|  |           return json.map(sub => { | ||||||
|  |                 return Utils.WalkJson(sub, f); | ||||||
|  |             }) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const cp = {...json} | ||||||
|  |         for (const key in json) { | ||||||
|  |             cp[key] = Utils.WalkJson(json[key], f) | ||||||
|  |         } | ||||||
|  |         return cp | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     static getOrSetDefault<K, V>(dict: Map<K, V>, k: K, v: () => V) { |     static getOrSetDefault<K, V>(dict: Map<K, V>, k: K, v: () => V) { | ||||||
|         let found = dict.get(k); |         let found = dict.get(k); | ||||||
|         if (found !== undefined) { |         if (found !== undefined) { | ||||||
|  | @ -592,6 +617,18 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|         return "https://osmcha.org/?filters=" + encodeURIComponent("{" + osmcha_link + "}") |         return "https://osmcha.org/?filters=" + encodeURIComponent("{" + osmcha_link + "}") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * Deepclone an object by serializing and deserializing it | ||||||
|  |      * @param x | ||||||
|  |      * @constructor | ||||||
|  |      */ | ||||||
|  |     static Clone<T>(x: T): T { | ||||||
|  |         if (x === undefined) { | ||||||
|  |             return undefined; | ||||||
|  |         } | ||||||
|  |         return JSON.parse(JSON.stringify(x)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) { |     private static colorDiff(c0: { r: number, g: number, b: number }, c1: { r: number, g: number, b: number }) { | ||||||
|         return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b); |         return Math.abs(c0.r - c1.r) + Math.abs(c0.g - c1.g) + Math.abs(c0.b - c1.b); | ||||||
|     } |     } | ||||||
|  | @ -618,17 +655,5 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | ||||||
|             b: parseInt(hex.substr(5, 2), 16), |             b: parseInt(hex.substr(5, 2), 16), | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * Deepclone an object by serializing and deserializing it |  | ||||||
|      * @param x |  | ||||||
|      * @constructor |  | ||||||
|      */ |  | ||||||
|     static Clone<T>(x: T): T { |  | ||||||
|         if(x === undefined){ |  | ||||||
|             return undefined; |  | ||||||
|         } |  | ||||||
|         return JSON.parse(JSON.stringify(x)); |  | ||||||
|     } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ | ||||||
|   "description": "Notes from OpenStreetMap", |   "description": "Notes from OpenStreetMap", | ||||||
|   "icon": "./assets/themes/notes/resolved.svg", |   "icon": "./assets/themes/notes/resolved.svg", | ||||||
|   "clustering": false, |   "clustering": false, | ||||||
|  |   "enableDownload": true, | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     { |     { | ||||||
|       "id": "notes", |       "id": "notes", | ||||||
|  | @ -25,25 +26,29 @@ | ||||||
|         "geoJsonZoomLevel": 12, |         "geoJsonZoomLevel": 12, | ||||||
|         "maxCacheAge": 0 |         "maxCacheAge": 0 | ||||||
|       }, |       }, | ||||||
|       "minzoom": 10, |       "minzoom": 8, | ||||||
|       "title": { |       "title": { | ||||||
|         "render": { |         "render": { | ||||||
|          "en": "Note" |           "en": "Note" | ||||||
|         }, |         }, | ||||||
|         "mappings": [{ |         "mappings": [ | ||||||
|           "if": "closed_at~*", |           { | ||||||
|           "then": { |             "if": "closed_at~*", | ||||||
|             "en": "Closed note" |             "then": { | ||||||
|  |               "en": "Closed note" | ||||||
|  |             } | ||||||
|           } |           } | ||||||
|         }] |         ] | ||||||
|       }, |       }, | ||||||
|       "calculatedTags": [ |       "calculatedTags": [ | ||||||
|         "_first_comment:=feat.get('comments')[0].text", |         "_first_comment:=feat.get('comments')[0].text.toLowerCase()", | ||||||
|         "_conversation=feat.get('comments').map(c => {if(c.user_url == undefined) {return 'anonymous user, '+c.date;} return c.html+'<div class=\"subtle flex justify-end border-b border-gray-500\"><a href=\"'+c.user_url+'\" target=\"_blank\">'+c.user+'</a>  '+c.date+'</div>'}).join('')" |         "_conversation=feat.get('comments').map(c => { let user = 'anonymous user'; if(c.user_url !== undefined){user = '<a href=\"'+c.user_url+'\" target=\"_blank\">'+c.user+'</a>'}; return c.html +'<div class=\"subtle flex justify-end border-t border-gray-500\">' + user + ' '+c.date+'</div>' }).join('')" | ||||||
|  |       ], | ||||||
|  |       "titleIcons": [ | ||||||
|  |         { | ||||||
|  |           "render": "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>" | ||||||
|  |         } | ||||||
|       ], |       ], | ||||||
|       "titleIcons": [{ |  | ||||||
|         "render": "<a href='https://openstreetmap.org/note/{id}' target='_blank'><img src='./assets/svg/osm-logo-us.svg'></a>" |  | ||||||
|       }], |  | ||||||
|       "tagRenderings": [ |       "tagRenderings": [ | ||||||
|         { |         { | ||||||
|           "id": "conversation", |           "id": "conversation", | ||||||
|  | @ -76,18 +81,27 @@ | ||||||
|               } |               } | ||||||
|             ] |             ] | ||||||
|           }, |           }, | ||||||
|            |  | ||||||
|           "iconSize": "40,40,bottom" |           "iconSize": "40,40,bottom" | ||||||
|         } |         } | ||||||
|       ], |       ], | ||||||
|       "filter": [{ |       "filter": [ | ||||||
|         "id": "bookcases", |         { | ||||||
|         "options": [ |           "id": "search", | ||||||
|           { |           "options": [ | ||||||
|           "osmTags": "_first_comment~.*bookcase.*", |             { | ||||||
|           "question": "Should mention 'bookcase' in the first comment" |               "osmTags": "_first_comment~.*{search}.*", | ||||||
|         }] |               "fields": [ | ||||||
|       }] |                 { | ||||||
|  |                   "name": "search" | ||||||
|  |                 } | ||||||
|  |               ], | ||||||
|  |               "question": { | ||||||
|  |                 "en": "Should mention {search} in the first comment" | ||||||
|  |               } | ||||||
|  |             } | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |       ] | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|  | @ -1044,6 +1044,10 @@ video { | ||||||
|   max-height: 1rem; |   max-height: 1rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .max-h-8 { | ||||||
|  |   max-height: 2rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .w-full { | .w-full { | ||||||
|   width: 100%; |   width: 100%; | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue