forked from MapComplete/MapComplete
		
	Add special visualisation for automated actions, add missing_street-theme, various fixes
This commit is contained in:
		
							parent
							
								
									e61c25fd6e
								
							
						
					
					
						commit
						20ec12b23c
					
				
					 23 changed files with 1116 additions and 690 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								Docs/MapComplete-Auto_apply.gif
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								Docs/MapComplete-Auto_apply.gif
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 5.2 MiB | 
|  | @ -134,6 +134,7 @@ class ClosestNObjectFunc implements ExtraFunction { | ||||||
|         "If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)" |         "If a 'unique tag key' is given, the tag with this key will only appear once (e.g. if 'name' is given, all features will have a different name)" | ||||||
|     _args =  ["list of features or layer name or '*' to get all features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"] |     _args =  ["list of features or layer name or '*' to get all features", "amount of features", "unique tag key (optional)", "maxDistanceInMeters (optional)"] | ||||||
| 
 | 
 | ||||||
|  |      | ||||||
|     /** |     /** | ||||||
|      * Gets the closes N features, sorted by ascending distance. |      * Gets the closes N features, sorted by ascending distance. | ||||||
|      * |      * | ||||||
|  | @ -164,8 +165,11 @@ class ClosestNObjectFunc implements ExtraFunction { | ||||||
| 
 | 
 | ||||||
|         const selfCenter = GeoOperations.centerpointCoordinates(feature) |         const selfCenter = GeoOperations.centerpointCoordinates(feature) | ||||||
|         let closestFeatures: { feat: any, distance: number }[] = []; |         let closestFeatures: { feat: any, distance: number }[] = []; | ||||||
|  |          | ||||||
|         for (const featureList of features) { |         for (const featureList of features) { | ||||||
|  |             // Features is provided by 'getFeaturesWithin' which returns a list of lists of features, hence the double loop here
 | ||||||
|             for (const otherFeature of featureList) { |             for (const otherFeature of featureList) { | ||||||
|  |                  | ||||||
|                 if (otherFeature === feature || otherFeature.properties.id === feature.properties.id) { |                 if (otherFeature === feature || otherFeature.properties.id === feature.properties.id) { | ||||||
|                     continue; // We ignore self
 |                     continue; // We ignore self
 | ||||||
|                 } |                 } | ||||||
|  | @ -187,6 +191,7 @@ class ClosestNObjectFunc implements ExtraFunction { | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 if (closestFeatures.length === 0) { |                 if (closestFeatures.length === 0) { | ||||||
|  |                     // This is the first matching feature we find - always add it
 | ||||||
|                     closestFeatures.push({ |                     closestFeatures.push({ | ||||||
|                         feat: otherFeature, |                         feat: otherFeature, | ||||||
|                         distance: distance |                         distance: distance | ||||||
|  | @ -194,6 +199,7 @@ class ClosestNObjectFunc implements ExtraFunction { | ||||||
|                     continue; |                     continue; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|                 if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { |                 if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { | ||||||
|                     // The last feature of the list (and thus the furthest away is still closer
 |                     // The last feature of the list (and thus the furthest away is still closer
 | ||||||
|                     // No use for checking, as we already have plenty of features!
 |                     // No use for checking, as we already have plenty of features!
 | ||||||
|  | @ -257,6 +263,8 @@ class ClosestNObjectFunc implements ExtraFunction { | ||||||
|                     } |                     } | ||||||
| 
 | 
 | ||||||
|                 } |                 } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         return closestFeatures; |         return closestFeatures; | ||||||
|  |  | ||||||
|  | @ -126,7 +126,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | ||||||
| 
 | 
 | ||||||
|                 eventSource.setData(eventSource.data.concat(newFeatures)) |                 eventSource.setData(eventSource.data.concat(newFeatures)) | ||||||
| 
 | 
 | ||||||
|             }).catch(msg => console.error("Could not load geojon layer", url, "due to", msg)) |             }).catch(msg => console.error("Could not load geojson layer", url, "due to", msg)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -107,51 +107,57 @@ export default class MetaTagging { | ||||||
|         } |         } | ||||||
|         return atLeastOneFeatureChanged |         return atLeastOneFeatureChanged | ||||||
|     } |     } | ||||||
| 
 |     public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] { | ||||||
| 
 |  | ||||||
|     public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string][]): ((feature: any) => void)[] { |  | ||||||
|         const functions: ((feature: any) => void)[] = []; |         const functions: ((feature: any) => void)[] = []; | ||||||
|  |          | ||||||
|         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]; | ||||||
|  |             const isStrict = entry[2] | ||||||
|             if (code === undefined) { |             if (code === undefined) { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             const func = new Function("feat", "return " + code + ";"); |             const calculateAndAssign = (feat) => { | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  |                 try { | ||||||
|  |                     let result = new Function("feat", "return " + code + ";")(feat); | ||||||
|  |                     if (result === "") { | ||||||
|  |                         result === undefined | ||||||
|  |                     } | ||||||
|  |                     if (result !== undefined && typeof result !== "string") { | ||||||
|  |                         // Make sure it is a string!
 | ||||||
|  |                         result = JSON.stringify(result); | ||||||
|  |                     } | ||||||
|  |                     delete feat.properties[key] | ||||||
|  |                     feat.properties[key] = result; | ||||||
|  |                     return result; | ||||||
|  |                 }catch(e){ | ||||||
|  |                     if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { | ||||||
|  |                         console.warn("Could not calculate a " + (isStrict ? "strict " : "") + " calculated tag for key " + key + " defined by " + code + " (in layer" + layerId + ") due to \n" + e + "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e, e.stack) | ||||||
|  |                         MetaTagging.errorPrintCount++; | ||||||
|  |                         if (MetaTagging.errorPrintCount == MetaTagging.stopErrorOutputAt) { | ||||||
|  |                             console.error("Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now") | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }  | ||||||
|  |                  | ||||||
|  |              | ||||||
|  |             if(isStrict){ | ||||||
|  |                 functions.push(calculateAndAssign) | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Lazy function
 | ||||||
|             const f = (feature: any) => { |             const f = (feature: any) => { | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|                 delete feature.properties[key] |                 delete feature.properties[key] | ||||||
|                 Object.defineProperty(feature.properties, key, { |                 Object.defineProperty(feature.properties, key, { | ||||||
|                     configurable: true, |                     configurable: true, | ||||||
|                     enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this
 |                     enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this
 | ||||||
|                     get: function () { |                     get: function () { | ||||||
|                         try { |                         return calculateAndAssign(feature) | ||||||
|                             // Lazyness for the win!
 |  | ||||||
|                             let result = func(feature); |  | ||||||
| 
 |  | ||||||
|                             if (result === "") { |  | ||||||
|                                 result === undefined |  | ||||||
|                             } |  | ||||||
|                             if (result !== undefined && typeof result !== "string") { |  | ||||||
|                                 // Make sure it is a string!
 |  | ||||||
|                                 result = JSON.stringify(result); |  | ||||||
|                             } |  | ||||||
|                             delete feature.properties[key] |  | ||||||
|                             feature.properties[key] = result; |  | ||||||
|                             return result; |  | ||||||
|                         } catch (e) { |  | ||||||
|                             if (MetaTagging.errorPrintCount < MetaTagging.stopErrorOutputAt) { |  | ||||||
|                                 console.warn("Could not calculate a calculated tag for key " + key + " defined by " + code + " (in layer" + layerId + ") due to \n" + e + "\n. Are you the theme creator? Doublecheck your code. Note that the metatags might not be stable on new features", e, e.stack) |  | ||||||
|                                 MetaTagging.errorPrintCount++; |  | ||||||
|                                 if (MetaTagging.errorPrintCount == MetaTagging.stopErrorOutputAt) { |  | ||||||
|                                     console.error("Got ", MetaTagging.stopErrorOutputAt, " errors calculating this metatagging - stopping output now") |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                     } |                     } | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|  | @ -167,7 +173,7 @@ export default class MetaTagging { | ||||||
|     private static createRetaggingFunc(layer: LayerConfig): |     private static createRetaggingFunc(layer: LayerConfig): | ||||||
|         ((params: ExtraFuncParams, feature: any) => void) { |         ((params: ExtraFuncParams, feature: any) => void) { | ||||||
| 
 | 
 | ||||||
|         const calculatedTags: [string, string][] = layer.calculatedTags; |         const calculatedTags: [string, string, boolean][] = layer.calculatedTags; | ||||||
|         if (calculatedTags === undefined || calculatedTags.length === 0) { |         if (calculatedTags === undefined || calculatedTags.length === 0) { | ||||||
|             return undefined; |             return undefined; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -98,7 +98,7 @@ export class Changes { | ||||||
|      * Uploads all the pending changes in one go. |      * Uploads all the pending changes in one go. | ||||||
|      * Triggered by the 'PendingChangeUploader'-actor in Actors |      * Triggered by the 'PendingChangeUploader'-actor in Actors | ||||||
|      */ |      */ | ||||||
|     public flushChanges(flushreason: string = undefined) { |     public async flushChanges(flushreason: string = undefined) : Promise<void>{ | ||||||
|         if (this.pendingChanges.data.length === 0) { |         if (this.pendingChanges.data.length === 0) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | @ -108,16 +108,14 @@ export class Changes { | ||||||
|         } |         } | ||||||
|         console.log("Uploading changes due to: ", flushreason) |         console.log("Uploading changes due to: ", flushreason) | ||||||
|         this.isUploading.setData(true) |         this.isUploading.setData(true) | ||||||
| 
 |         try { | ||||||
|         this.flushChangesAsync() |             const csNumber = await this.flushChangesAsync() | ||||||
|             .then(_ => { |             this.isUploading.setData(false) | ||||||
|                 this.isUploading.setData(false) |             console.log("Changes flushed!"); | ||||||
|                 console.log("Changes flushed!"); |         } catch (e) { | ||||||
|             }) |             this.isUploading.setData(false) | ||||||
|             .catch(e => { |             console.error("Flushing changes failed due to", e); | ||||||
|                 this.isUploading.setData(false) |         } | ||||||
|                 console.error("Flushing changes failed due to", e); |  | ||||||
|             }) |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) { |     private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) { | ||||||
|  |  | ||||||
|  | @ -17,7 +17,6 @@ import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource"; | ||||||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; | import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; | ||||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||||
| import {GeoOperations} from "../GeoOperations"; | import {GeoOperations} from "../GeoOperations"; | ||||||
| import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"; |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Contains all the leaflet-map related state |  * Contains all the leaflet-map related state | ||||||
|  | @ -186,6 +185,7 @@ export default class MapState extends UserRelatedState { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         let i = 0 |         let i = 0 | ||||||
|  |         const self = this; | ||||||
|         const features : UIEventSource<{ feature: any, freshness: Date }[]>= this.currentBounds.map(bounds => { |         const features : UIEventSource<{ feature: any, freshness: Date }[]>= this.currentBounds.map(bounds => { | ||||||
|             if(bounds === undefined){ |             if(bounds === undefined){ | ||||||
|                 return [] |                 return [] | ||||||
|  | @ -197,7 +197,8 @@ export default class MapState extends UserRelatedState { | ||||||
|                     type: "Feature", |                     type: "Feature", | ||||||
|                     properties:{ |                     properties:{ | ||||||
|                         id:"current_view-"+i, |                         id:"current_view-"+i, | ||||||
|                         "current_view":"yes" |                         "current_view":"yes", | ||||||
|  |                         "zoom": ""+self.locationControl.data.zoom | ||||||
|                     }, |                     }, | ||||||
|                     geometry:{ |                     geometry:{ | ||||||
|                         type:"Polygon", |                         type:"Polygon", | ||||||
|  |  | ||||||
|  | @ -82,6 +82,15 @@ export interface LayerConfigJson { | ||||||
|      *  "_max_overlap_ratio=Number(feat._max_overlap_m2)/feat.area |      *  "_max_overlap_ratio=Number(feat._max_overlap_m2)/feat.area | ||||||
|      * ] |      * ] | ||||||
|      * |      * | ||||||
|  |      * The specified tags are evaluated lazily. E.g. if a calculated tag is only used in the popup (e.g. the number of nearby features), | ||||||
|  |      * the expensive calculation will only be performed then for that feature. This avoids clogging up the contributors PC when all features are loaded. | ||||||
|  |      *  | ||||||
|  |      * If a tag has to be evaluated strictly, use ':=' instead: | ||||||
|  |      *  | ||||||
|  |      * [ | ||||||
|  |      * "_some_key:=some_javascript_expression" | ||||||
|  |      * ] | ||||||
|  |      *  | ||||||
|      */ |      */ | ||||||
|     calculatedTags?: string[]; |     calculatedTags?: string[]; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|     public readonly name: Translation; |     public readonly name: Translation; | ||||||
|     public readonly description: Translation; |     public readonly description: Translation; | ||||||
|     public readonly source: SourceConfig; |     public readonly source: SourceConfig; | ||||||
|     public readonly calculatedTags: [string, string][]; |     public readonly calculatedTags: [string, string, boolean][]; | ||||||
|     public readonly doNotDownload: boolean; |     public readonly doNotDownload: boolean; | ||||||
|     public readonly  passAllFeatures: boolean; |     public readonly  passAllFeatures: boolean; | ||||||
|     public readonly isShown: TagRenderingConfig; |     public readonly isShown: TagRenderingConfig; | ||||||
|  | @ -130,7 +130,11 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|             this.calculatedTags = []; |             this.calculatedTags = []; | ||||||
|             for (const kv of json.calculatedTags) { |             for (const kv of json.calculatedTags) { | ||||||
|                 const index = kv.indexOf("="); |                 const index = kv.indexOf("="); | ||||||
|                 const key = kv.substring(0, index); |                 let key = kv.substring(0, index); | ||||||
|  |                 const isStrict = key.endsWith(':') | ||||||
|  |                 if(isStrict){ | ||||||
|  |                     key = key.substr(0, key.length - 1) | ||||||
|  |                 } | ||||||
|                 const code = kv.substring(index + 1); |                 const code = kv.substring(index + 1); | ||||||
| 
 | 
 | ||||||
|                 try { |                 try { | ||||||
|  | @ -140,7 +144,7 @@ export default class LayerConfig extends WithContextLoader { | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|                 this.calculatedTags.push([key, code]); |                 this.calculatedTags.push([key, code, isStrict]); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -38,4 +38,8 @@ export default class Minimap { | ||||||
|         throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()" |         throw "CreateMinimap hasn't been initialized yet. Please call MinimapImplementation.initialize()" | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     private constructor() {         | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -72,6 +72,7 @@ export default class LeftControls extends Combine { | ||||||
|                     return new Lazy(() => { |                     return new Lazy(() => { | ||||||
|                       const tagsSource=  state.allElements.getEventSourceById(feature.properties.id) |                       const tagsSource=  state.allElements.getEventSourceById(feature.properties.id) | ||||||
|                         return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, "currentview", guiState.currentViewControlIsOpened) |                         return new FeatureInfoBox(tagsSource, currentViewFL.layerDef, "currentview", guiState.currentViewControlIsOpened) | ||||||
|  |                             .SetClass("md:floating-element-width") | ||||||
|                     }) |                     }) | ||||||
|                 })) |                 })) | ||||||
|                  |                  | ||||||
|  | @ -94,7 +95,7 @@ export default class LeftControls extends Combine { | ||||||
|         const toggledDownload = new Toggle( |         const toggledDownload = new Toggle( | ||||||
|             new AllDownloads( |             new AllDownloads( | ||||||
|                 guiState.downloadControlIsOpened |                 guiState.downloadControlIsOpened | ||||||
|             ).SetClass("block p-1 rounded-full"), |             ).SetClass("block p-1 rounded-full md:floating-element-width"), | ||||||
|             new MapControlButton(Svg.download_svg()) |             new MapControlButton(Svg.download_svg()) | ||||||
|                 .onClick(() => guiState.downloadControlIsOpened.setData(true)), |                 .onClick(() => guiState.downloadControlIsOpened.setData(true)), | ||||||
|             guiState.downloadControlIsOpened |             guiState.downloadControlIsOpened | ||||||
|  | @ -116,7 +117,7 @@ export default class LeftControls extends Combine { | ||||||
|                     ), |                     ), | ||||||
|                 "filters", |                 "filters", | ||||||
|                 guiState.filterViewIsOpened |                 guiState.filterViewIsOpened | ||||||
|             ).SetClass("rounded-lg"), |             ).SetClass("rounded-lg md:floating-element-width"), | ||||||
|             new MapControlButton(Svg.filter_svg()) |             new MapControlButton(Svg.filter_svg()) | ||||||
|                 .onClick(() => guiState.filterViewIsOpened.setData(true)), |                 .onClick(() => guiState.filterViewIsOpened.setData(true)), | ||||||
|             guiState.filterViewIsOpened |             guiState.filterViewIsOpened | ||||||
|  |  | ||||||
							
								
								
									
										182
									
								
								UI/Popup/AutoApplyButton.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								UI/Popup/AutoApplyButton.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,182 @@ | ||||||
|  | import {SpecialVisualization} from "../SpecialVisualizations"; | ||||||
|  | import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; | ||||||
|  | import BaseUIElement from "../BaseUIElement"; | ||||||
|  | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | import {DefaultGuiState} from "../DefaultGuiState"; | ||||||
|  | import {SubtleButton} from "../Base/SubtleButton"; | ||||||
|  | import Img from "../Base/Img"; | ||||||
|  | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
|  | import Combine from "../Base/Combine"; | ||||||
|  | import Link from "../Base/Link"; | ||||||
|  | import {SubstitutedTranslation} from "../SubstitutedTranslation"; | ||||||
|  | import {Utils} from "../../Utils"; | ||||||
|  | import Minimap from "../Base/Minimap"; | ||||||
|  | import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||||
|  | import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||||
|  | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
|  | import Loading from "../Base/Loading"; | ||||||
|  | import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||||
|  | import Translations from "../i18n/Translations"; | ||||||
|  | 
 | ||||||
|  | export interface AutoAction extends SpecialVisualization { | ||||||
|  |     supportsAutoAction: boolean | ||||||
|  | 
 | ||||||
|  |     applyActionOn(state: FeaturePipelineState, tagSource: UIEventSource<any>, argument: string[]): Promise<void> | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default class AutoApplyButton implements SpecialVisualization { | ||||||
|  |     public readonly docs: string; | ||||||
|  |     public readonly funcName: string = "auto_apply"; | ||||||
|  |     public readonly args: { name: string; defaultValue?: string; doc: string }[] = [ | ||||||
|  |         { | ||||||
|  |             name: "target_layer", | ||||||
|  |             doc: "The layer that the target features will reside in" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: "target_feature_ids", | ||||||
|  |             doc: "The key, of which the value contains a list of ids" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: "tag_rendering_id", | ||||||
|  |             doc: "The ID of the tagRendering containing the autoAction. This tagrendering will be calculated. The embedded actions will be executed" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: "text", | ||||||
|  |             doc: "The text to show on the button" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: "icon", | ||||||
|  |             doc: "The icon to show on the button", | ||||||
|  |             defaultValue: "./assets/svg/robot.svg" | ||||||
|  |         } | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     constructor(allSpecialVisualisations: SpecialVisualization[]) { | ||||||
|  |         this.docs = AutoApplyButton.generateDocs(allSpecialVisualisations.filter(sv => sv["supportsAutoAction"] === true).map(sv => sv.funcName)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     constr(state: FeaturePipelineState, tagSource: UIEventSource<any>, argument: string[], guistate: DefaultGuiState): BaseUIElement { | ||||||
|  | 
 | ||||||
|  |         if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) { | ||||||
|  |             const t = Translations.t.general.add.import; | ||||||
|  |             return new Combine([new FixedUiElement("The auto-apply button is only available in official themes (or in testing mode)").SetClass("alert"), t.howToTest]) | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         const to_parse = tagSource.data[argument[1]] | ||||||
|  |         if (to_parse === undefined) { | ||||||
|  |             return new Loading("Gathering which elements support auto-apply... ") | ||||||
|  |         } | ||||||
|  |         try { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             const target_layer_id = argument[0] | ||||||
|  |             const target_feature_ids = <string[]>JSON.parse(to_parse) | ||||||
|  |              | ||||||
|  |             if(target_feature_ids.length === 0){ | ||||||
|  |                 return new FixedUiElement("No elements found to perform action") | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             const targetTagRendering = argument[2] | ||||||
|  |             const text = argument[3] | ||||||
|  |             const icon = argument[4] | ||||||
|  | 
 | ||||||
|  |             const layer = state.filteredLayers.data.filter(l => l.layerDef.id === target_layer_id)[0] | ||||||
|  | 
 | ||||||
|  |             const tagRenderingConfig = layer.layerDef.tagRenderings.filter(tr => tr.id === targetTagRendering)[0] | ||||||
|  | 
 | ||||||
|  |             if (tagRenderingConfig === undefined) { | ||||||
|  |                 return new FixedUiElement("Target tagrendering " + targetTagRendering + " not found").SetClass("alert") | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const buttonState = new UIEventSource<"idle" | "running" | "done" | {error: string}>("idle") | ||||||
|  | 
 | ||||||
|  |             const button = new SubtleButton( | ||||||
|  |                 new Img(icon), | ||||||
|  |                 text | ||||||
|  |             ).onClick(async () => { | ||||||
|  |                 buttonState.setData("running") | ||||||
|  |                 try { | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |                     for (const targetFeatureId of target_feature_ids) { | ||||||
|  |                         const featureTags = state.allElements.getEventSourceById(targetFeatureId) | ||||||
|  |                         const rendering = tagRenderingConfig.GetRenderValue(featureTags.data).txt | ||||||
|  |                         const specialRenderings = Utils.NoNull(SubstitutedTranslation.ExtractSpecialComponents(rendering) | ||||||
|  |                             .map(x => x.special)) | ||||||
|  |                             .filter(v => v.func["supportsAutoAction"] === true) | ||||||
|  | 
 | ||||||
|  |                         for (const specialRendering of specialRenderings) { | ||||||
|  |                             const action = <AutoAction>specialRendering.func | ||||||
|  |                             await action.applyActionOn(state, featureTags, specialRendering.args) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     console.log("Flushing changes...") | ||||||
|  |                     await state.changes.flushChanges("Auto button") | ||||||
|  |                     buttonState.setData("done") | ||||||
|  |                 } catch (e) { | ||||||
|  |                     console.error("Error while running autoApply: ", e) | ||||||
|  |                     buttonState.setData({error: e}) | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             const explanation = new Combine(["The following objects will be updated: ", | ||||||
|  |                 ...target_feature_ids.map(id => new Combine([new Link(id, "https:/  /openstreetmap.org/" + id, true), ", "]))]).SetClass("subtle") | ||||||
|  | 
 | ||||||
|  |             const previewMap = Minimap.createMiniMap({ | ||||||
|  |                 allowMoving: false, | ||||||
|  |                 background: state.backgroundLayer, | ||||||
|  |                 addLayerControl: true, | ||||||
|  |             }).SetClass("h-48") | ||||||
|  | 
 | ||||||
|  |             const features = target_feature_ids.map(id => state.allElements.ContainingFeatures.get(id)) | ||||||
|  | 
 | ||||||
|  |             new ShowDataLayer({ | ||||||
|  |                 leafletMap: previewMap.leafletMap, | ||||||
|  |                 enablePopups: false, | ||||||
|  |                 zoomToFeatures: true, | ||||||
|  |                 features: new StaticFeatureSource(features, false), | ||||||
|  |                 allElements: state.allElements, | ||||||
|  |                 layerToShow: layer.layerDef, | ||||||
|  |             }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             return new VariableUiElement(buttonState.map( | ||||||
|  |                 st => { | ||||||
|  |                     if (st === "idle") { | ||||||
|  |                         return new Combine([button, previewMap, explanation]); | ||||||
|  |                     } | ||||||
|  |                     if (st === "done") { | ||||||
|  |                         return new FixedUiElement("All done!").SetClass("thanks") | ||||||
|  |                     } | ||||||
|  |                     if (st === "running") { | ||||||
|  |                     return new Loading("Applying changes...") | ||||||
|  |                     } | ||||||
|  |                     const error =st.error | ||||||
|  |                         return new Combine([new FixedUiElement("Something went wrong...").SetClass("alert"), new FixedUiElement(error).SetClass("subtle")]).SetClass("flex flex-col") | ||||||
|  |                 } | ||||||
|  |             )) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         } catch (e) { | ||||||
|  |             console.log("To parse is", to_parse) | ||||||
|  |             return new FixedUiElement("Could not generate a auto_apply-button for key " + argument[0] + " due to " + e).SetClass("alert") | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     getLayerDependencies(args: string[]): string[] { | ||||||
|  |         return [args[0]] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private static generateDocs(supportedActions: string[]) { | ||||||
|  |         return [ | ||||||
|  |             "A button to run many actions for many features at once.\n", | ||||||
|  |             "To effectively use this button, you'll need some ingredients:\n" + | ||||||
|  |             "- A target layer with features for which an action is defined in a tag rendering. The following special visualisations support an autoAction: " + supportedActions.join(", "), | ||||||
|  |             "- A host feature to place the auto-action on. This can be a big outline (such as a city). Another good option for this is the [current_view](./BuiltinLayers.md#current_view)", | ||||||
|  |             "- Then, use a calculated tag on the host feature to determine the overlapping object ids", | ||||||
|  |             "- At last, add this component" | ||||||
|  |         ].join("\n") | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | @ -34,6 +34,7 @@ import {And} from "../../Logic/Tags/And"; | ||||||
| import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"; | import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"; | ||||||
| import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"; | import CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"; | ||||||
| import {Tag} from "../../Logic/Tags/Tag"; | import {Tag} from "../../Logic/Tags/Tag"; | ||||||
|  | import TagApplyButton from "./TagApplyButton"; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| abstract class AbstractImportButton implements SpecialVisualizations { | abstract class AbstractImportButton implements SpecialVisualizations { | ||||||
|  | @ -131,7 +132,7 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         // Explanation of the tags that will be applied onto the imported/conflated object
 |         // Explanation of the tags that will be applied onto the imported/conflated object
 | ||||||
|         const newTags = SpecialVisualizations.generateTagsToApply(args.tags, tagSource) |         const newTags = TagApplyButton.generateTagsToApply(args.tags, tagSource) | ||||||
|         const appliedTags = new Toggle( |         const appliedTags = new Toggle( | ||||||
|             new VariableUiElement( |             new VariableUiElement( | ||||||
|                 newTags.map(tgs => { |                 newTags.map(tgs => { | ||||||
|  | @ -198,7 +199,7 @@ ${Utils.special_visualizations_importRequirementDocs} | ||||||
|     private parseArgs(argsRaw: string[], originalFeatureTags: UIEventSource<any>): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string, newTags: UIEventSource<Tag[]> } { |     private parseArgs(argsRaw: string[], originalFeatureTags: UIEventSource<any>): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string, newTags: UIEventSource<Tag[]> } { | ||||||
|         const baseArgs = Utils.ParseVisArgs(this.args, argsRaw) |         const baseArgs = Utils.ParseVisArgs(this.args, argsRaw) | ||||||
|         if (originalFeatureTags !== undefined) { |         if (originalFeatureTags !== undefined) { | ||||||
|             baseArgs["newTags"] = SpecialVisualizations.generateTagsToApply(baseArgs.tags, originalFeatureTags) |             baseArgs["newTags"] = TagApplyButton.generateTagsToApply(baseArgs.tags, originalFeatureTags) | ||||||
|         } |         } | ||||||
|         return baseArgs |         return baseArgs | ||||||
|     } |     } | ||||||
|  |  | ||||||
							
								
								
									
										136
									
								
								UI/Popup/TagApplyButton.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								UI/Popup/TagApplyButton.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,136 @@ | ||||||
|  | import {AutoAction} from "./AutoApplyButton"; | ||||||
|  | import Translations from "../i18n/Translations"; | ||||||
|  | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
|  | import BaseUIElement from "../BaseUIElement"; | ||||||
|  | import {FixedUiElement} from "../Base/FixedUiElement"; | ||||||
|  | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | import {SubtleButton} from "../Base/SubtleButton"; | ||||||
|  | import Combine from "../Base/Combine"; | ||||||
|  | import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||||
|  | import {And} from "../../Logic/Tags/And"; | ||||||
|  | import Toggle from "../Input/Toggle"; | ||||||
|  | import {Utils} from "../../Utils"; | ||||||
|  | import {Tag} from "../../Logic/Tags/Tag"; | ||||||
|  | import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; | ||||||
|  | 
 | ||||||
|  | export default class TagApplyButton implements AutoAction { | ||||||
|  |     public readonly funcName = "tag_apply"; | ||||||
|  |     public readonly docs = "Shows a big button; clicking this button will apply certain tags onto the feature.\n\nThe first argument takes a specification of which tags to add.\n" + Utils.Special_visualizations_tagsToApplyHelpText; | ||||||
|  |     public readonly supportsAutoAction = true; | ||||||
|  |     public readonly args = [ | ||||||
|  |         { | ||||||
|  |             name: "tags_to_apply", | ||||||
|  |             doc: "A specification of the tags to apply" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: "message", | ||||||
|  |             doc: "The text to show to the contributor" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: "image", | ||||||
|  |             doc: "An image to show to the contributor on the button" | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             name: "id_of_object_to_apply_this_one", | ||||||
|  |             defaultValue: undefined, | ||||||
|  |             doc: "If specified, applies the the tags onto _another_ object. The id will be read from properties[id_of_object_to_apply_this_one] of the selected object. The tags are still calculated based on the tags of the _selected_ element" | ||||||
|  |         } | ||||||
|  |     ]; | ||||||
|  | 
 | ||||||
|  |     public static generateTagsToApply(spec: string, tagSource: UIEventSource<any>): UIEventSource<Tag[]> { | ||||||
|  | 
 | ||||||
|  |         const tgsSpec = spec.split(";").map(spec => { | ||||||
|  |             const kv = spec.split("=").map(s => s.trim()); | ||||||
|  |             if (kv.length != 2) { | ||||||
|  |                 throw "Invalid key spec: multiple '=' found in " + spec | ||||||
|  |             } | ||||||
|  |             return kv | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         for (const spec of tgsSpec) { | ||||||
|  |             if (spec[0].endsWith(':')) { | ||||||
|  |                 throw "A tag specification for import or apply ends with ':'. The theme author probably wrote key:=otherkey instead of key=$otherkey" | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return tagSource.map(tags => { | ||||||
|  |             const newTags: Tag [] = [] | ||||||
|  |             for (const [key, value] of tgsSpec) { | ||||||
|  |                 if (value.indexOf('$') >= 0) { | ||||||
|  | 
 | ||||||
|  |                     let parts = value.split("$") | ||||||
|  |                     // THe first of the split won't start with a '$', so no substitution needed
 | ||||||
|  |                     let actualValue = parts[0] | ||||||
|  |                     parts.shift() | ||||||
|  | 
 | ||||||
|  |                     for (const part of parts) { | ||||||
|  |                         const [_, varName, leftOver] = part.match(/([a-zA-Z0-9_:]*)(.*)/) | ||||||
|  |                         actualValue += (tags[varName] ?? "") + leftOver | ||||||
|  |                     } | ||||||
|  |                     newTags.push(new Tag(key, actualValue)) | ||||||
|  |                 } else { | ||||||
|  |                     newTags.push(new Tag(key, value)) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             return newTags | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public readonly example = "`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)"; | ||||||
|  | 
 | ||||||
|  |     async applyActionOn(state: FeaturePipelineState, tags: UIEventSource<any>, args: string[]) : Promise<void>{ | ||||||
|  |         const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) | ||||||
|  |         const targetIdKey = args[3] | ||||||
|  | 
 | ||||||
|  |         const targetId = tags.data[targetIdKey] ?? tags.data.id | ||||||
|  |         const changeAction = new ChangeTagAction(targetId, | ||||||
|  |             new And(tagsToApply.data), | ||||||
|  |             tags.data, // We pass in the tags of the selected element, not the tags of the target element!
 | ||||||
|  |             { | ||||||
|  |                 theme: state.layoutToUse.id, | ||||||
|  |                 changeType: "answer" | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |         await state.changes.applyAction(changeAction) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public constr(state: FeaturePipelineState, tags: UIEventSource<any>, args: string[]): BaseUIElement { | ||||||
|  |         const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) | ||||||
|  |         const msg = args[1] | ||||||
|  |         let image = args[2]?.trim() | ||||||
|  |         if (image === "" || image === "undefined") { | ||||||
|  |             image = undefined | ||||||
|  |         } | ||||||
|  |         const targetIdKey = args[3] | ||||||
|  |         const t = Translations.t.general.apply_button | ||||||
|  | 
 | ||||||
|  |         const tagsExplanation = new VariableUiElement(tagsToApply.map(tagsToApply => { | ||||||
|  |                 const tagsStr = tagsToApply.map(t => t.asHumanString(false, true)).join("&"); | ||||||
|  |                 let el: BaseUIElement = new FixedUiElement(tagsStr) | ||||||
|  |                 if (targetIdKey !== undefined) { | ||||||
|  |                     const targetId = tags.data[targetIdKey] ?? tags.data.id | ||||||
|  |                     el = t.appliedOnAnotherObject.Subs({tags: tagsStr, id: targetId}) | ||||||
|  |                 } | ||||||
|  |                 return el; | ||||||
|  |             } | ||||||
|  |         )).SetClass("subtle") | ||||||
|  |         const self = this | ||||||
|  |         const applied = new UIEventSource(false) | ||||||
|  |         const applyButton = new SubtleButton(image, new Combine([msg, tagsExplanation]).SetClass("flex flex-col")) | ||||||
|  |             .onClick(() => { | ||||||
|  |                 self.applyActionOn(state, tags, args) | ||||||
|  |                 applied.setData(true) | ||||||
|  |             }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         return new Toggle( | ||||||
|  |             new Toggle( | ||||||
|  |                 t.isApplied.SetClass("thanks"), | ||||||
|  |                 applyButton, | ||||||
|  |                 applied | ||||||
|  |             ), | ||||||
|  |             undefined, state.osmConnection.isLoggedIn) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |            | ||||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										24
									
								
								assets/layers/named_streets/named_streets.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								assets/layers/named_streets/named_streets.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,24 @@ | ||||||
|  | { | ||||||
|  |   "id": "named_streets", | ||||||
|  |   "description": "Hidden layer with all streets which have a name. Useful to detect addresses", | ||||||
|  |   "minzoom": 18, | ||||||
|  |   "source": { | ||||||
|  |     "osmTags": { | ||||||
|  |       "and": [ | ||||||
|  |         "highway~*", | ||||||
|  |         "name~*" | ||||||
|  |       ] | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "mapRendering": [ | ||||||
|  |     { | ||||||
|  |       "color": { | ||||||
|  |         "render": "#ccc" | ||||||
|  |       }, | ||||||
|  |       "width": { | ||||||
|  |         "render": "3" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "shownByDefault": false | ||||||
|  | } | ||||||
|  | @ -1135,6 +1135,16 @@ | ||||||
|     "authors": [], |     "authors": [], | ||||||
|     "sources": [] |     "sources": [] | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     "path": "robot.svg", | ||||||
|  |     "license": "CC-BY 4.0 International", | ||||||
|  |     "authors": [ | ||||||
|  |       "Font Awesome" | ||||||
|  |     ], | ||||||
|  |     "sources": [ | ||||||
|  |       "https://commons.wikimedia.org/wiki/File:Font_Awesome_5_solid_robot.svg" | ||||||
|  |     ] | ||||||
|  |   }, | ||||||
|   { |   { | ||||||
|     "path": "satellite.svg", |     "path": "satellite.svg", | ||||||
|     "license": "CC0", |     "license": "CC0", | ||||||
|  |  | ||||||
| Before Width: | Height: | Size: 751 B After Width: | Height: | Size: 751 B | 
|  | @ -42,14 +42,19 @@ | ||||||
|           } |           } | ||||||
|         ], |         ], | ||||||
|         "calculatedTags": [ |         "calculatedTags": [ | ||||||
|           "_x='y'", |           "_embedded_crab_addresses= Number(feat.properties.zoom) >= 18 ? feat.overlapWith('crab_address').length : undefined" | ||||||
|           "_embedded_crab_addresses=undefined // feat.overlapWith('crab_address').length" |  | ||||||
|         ], |         ], | ||||||
|  |         "minZoom": 18, | ||||||
|         "tagRenderings": [ |         "tagRenderings": [ | ||||||
|           { |           { | ||||||
|             "id": "hw", |             "id": "hw", | ||||||
|             "render": "There are {_embedded_crab_addresses} adresses in view", |             "render": "There are {_embedded_crab_addresses} adresses in view", | ||||||
|             "mappings": [{ |             "mappings": [ | ||||||
|  |               { | ||||||
|  |                 "if": "zoom<18", | ||||||
|  |                 "then": "Zoom in more..." | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|               "if": "_embedded_crab_addresses=", |               "if": "_embedded_crab_addresses=", | ||||||
|               "then": "Loading..." |               "then": "Loading..." | ||||||
|             },{ |             },{ | ||||||
|  |  | ||||||
|  | @ -6,15 +6,5 @@ | ||||||
|       "Pieter Vander Vennet" |       "Pieter Vander Vennet" | ||||||
|     ], |     ], | ||||||
|     "sources": [] |     "sources": [] | ||||||
|   }, |  | ||||||
|   { |  | ||||||
|     "path": "robot.svg", |  | ||||||
|     "license": "CC-BY 4.0 International", |  | ||||||
|     "authors": [ |  | ||||||
|       "Font Awesome" |  | ||||||
|     ], |  | ||||||
|     "sources": [ |  | ||||||
|       "https://commons.wikimedia.org/wiki/File:Font_Awesome_5_solid_robot.svg" |  | ||||||
|     ] |  | ||||||
|   } |   } | ||||||
| ] | ] | ||||||
							
								
								
									
										154
									
								
								assets/themes/grb_import/missing_streets.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								assets/themes/grb_import/missing_streets.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,154 @@ | ||||||
|  | { | ||||||
|  |   "id": "missing_streets", | ||||||
|  |   "title": { | ||||||
|  |     "nl": "GRB import helper" | ||||||
|  |   }, | ||||||
|  |   "shortDescription": { | ||||||
|  |     "nl": "Grb import helper tool" | ||||||
|  |   }, | ||||||
|  |   "description": { | ||||||
|  |     "nl": "Dit thema voegt semi-automatisch straatnamen toe aan gebouwen met huisnummer en overeenkomstig CRAB-adres." | ||||||
|  |   }, | ||||||
|  |   "language": [ | ||||||
|  |     "nl" | ||||||
|  |   ], | ||||||
|  |   "maintainer": "", | ||||||
|  |   "icon": "./assets/svg/robot.svg", | ||||||
|  |   "version": "0", | ||||||
|  |   "startLat": 51.0249, | ||||||
|  |   "startLon": 4.026489, | ||||||
|  |   "startZoom": 9, | ||||||
|  |   "widenFactor": 2, | ||||||
|  |   "socialImage": "", | ||||||
|  |   "clustering": { | ||||||
|  |     "maxZoom": 15 | ||||||
|  |   }, | ||||||
|  |   "overrideAll": { | ||||||
|  |     "minzoom": 14 | ||||||
|  |   }, | ||||||
|  |   "layers": [ | ||||||
|  |     { | ||||||
|  |       "builtin": "current_view", | ||||||
|  |       "override": { | ||||||
|  |         "+mapRendering": [ | ||||||
|  |           { | ||||||
|  |             "location": [ | ||||||
|  |               "point" | ||||||
|  |             ], | ||||||
|  |             "icon": { | ||||||
|  |               "render": "./assets/themes/grb_import/robot.svg" | ||||||
|  |             }, | ||||||
|  |             "iconSize": "15,15,center" | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "calculatedTags": [ | ||||||
|  |           "_overlapping=Number(feat.properties.zoom) >= 14 ? feat.overlapWith('OSM-buildings').map(ff => ff.feat.properties) : undefined", | ||||||
|  |           "_applicable=feat.get('_overlapping').filter(p => (p._spelling_is_correct === 'true') && (p._singular_import === 'true')).map(p => p.id)", | ||||||
|  |           "_applicable_count=feat.get('_applicable')?.length" | ||||||
|  |         ], | ||||||
|  |         "tagRenderings": [ | ||||||
|  |           { | ||||||
|  |             "id": "hw", | ||||||
|  |             "render": "There are {_applicable_count} applicable elements in view", | ||||||
|  |             "mappings": [ | ||||||
|  |               { | ||||||
|  |                 "if": "zoom<14", | ||||||
|  |                 "then": "Zoom in more to see the automatic action" | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 "if": "_applicable_count=", | ||||||
|  |                 "then": "Loading..." | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 "if": "_applicable_count=0", | ||||||
|  |                 "then": "No buildings with missing street names in view" | ||||||
|  |               } | ||||||
|  |             ] | ||||||
|  |           }, | ||||||
|  |           { | ||||||
|  |             "id": "autoapply", | ||||||
|  |             "render": "{auto_apply(OSM-buildings, _applicable, apply_streetname, Automatically add all missing streetnames on buildings in view)}" | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     "named_streets", | ||||||
|  |     { | ||||||
|  |       "builtin": "crab_address", | ||||||
|  |       "override": { | ||||||
|  |         "mapRendering": [ | ||||||
|  |           { | ||||||
|  |             "iconSize": "5,5,center", | ||||||
|  |             "icon": "circle:black;" | ||||||
|  |           } | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "id": "OSM-buildings", | ||||||
|  |       "name": "Alle OSM-gebouwen met een huisnummer en zonder straat", | ||||||
|  |       "source": { | ||||||
|  |         "osmTags": { | ||||||
|  |           "and": [ | ||||||
|  |             "building~*", | ||||||
|  |             "addr:housenumber~*", | ||||||
|  |             "addr:street=" | ||||||
|  |           ] | ||||||
|  |         }, | ||||||
|  |         "maxCacheAge": 0 | ||||||
|  |       }, | ||||||
|  |       "calculatedTags": [ | ||||||
|  |         "_embedded_crab_addresses:=Array.from(new Set(feat.overlapWith('crab_address').map(ff => ff.feat.properties).filter(p => p._HNRLABEL.toLowerCase() === (feat.properties['addr:housenumber'] + (feat.properties['addr:unit']??'')).toLowerCase()).map(p => p.STRAATNM)))", | ||||||
|  |         "_singular_import:=feat.get('_embedded_crab_addresses')?.length == 1", | ||||||
|  |         "_name_to_apply:=feat.get('_embedded_crab_addresses')[0]", | ||||||
|  |         "_nearby_street_names:=feat.closestn('named_streets',5,'name', 500).map(ff => ff.feat.properties.name)", | ||||||
|  |         "_spelling_is_correct:= feat.get('_nearby_street_names').indexOf(feat.properties['_name_to_apply']) >= 0" | ||||||
|  |       ], | ||||||
|  |       "mapRendering": [ | ||||||
|  |         { | ||||||
|  |           "width": { | ||||||
|  |             "render": "2", | ||||||
|  |             "mappings": [ | ||||||
|  |               { | ||||||
|  |                 "if": "fixme~*", | ||||||
|  |                 "then": "5" | ||||||
|  |               } | ||||||
|  |             ] | ||||||
|  |           }, | ||||||
|  |           "color": { | ||||||
|  |             "render": "#00c", | ||||||
|  |             "mappings": [ | ||||||
|  |               { | ||||||
|  |                 "if": "_spelling_is_correct=false", | ||||||
|  |                 "then": "#ff00ff" | ||||||
|  |               }, | ||||||
|  |               { | ||||||
|  |                 "if": "_singular_import=ffalse", | ||||||
|  |                 "then": "#f00" | ||||||
|  |               } | ||||||
|  |             ] | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "title": "OSM-gebouw", | ||||||
|  |       "tagRenderings": [ | ||||||
|  |         { | ||||||
|  |           "id": "apply_streetname", | ||||||
|  |           "render": "{tag_apply(addr:street=$_name_to_apply ,Apply the CRAB-street onto this building)}", | ||||||
|  |           "mappings": [ | ||||||
|  |             { | ||||||
|  |               "if": "_spelling_is_correct=false", | ||||||
|  |               "then": "No nearby street has the same name. The CRAB-name is {_name_to_apply}" | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |               "if": "_singular_import=false", | ||||||
|  |               "then": "There are multiple streetnames applicable here" | ||||||
|  |             } | ||||||
|  |           ] | ||||||
|  |         } | ||||||
|  |       ], | ||||||
|  |       "passAllFeatures": true | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "hideFromOverview": true | ||||||
|  | } | ||||||
|  | @ -326,28 +326,7 @@ | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
|     }, |     }, | ||||||
|     { | "named_streets" | ||||||
|       "id": "named_streets", |  | ||||||
|       "minzoom": 18, |  | ||||||
|       "source": { |  | ||||||
|         "osmTags": { |  | ||||||
|           "and": [ |  | ||||||
|             "highway~*", |  | ||||||
|             "name~*" |  | ||||||
|           ] |  | ||||||
|         } |  | ||||||
|       }, |  | ||||||
|       "mapRendering": [ |  | ||||||
|         { |  | ||||||
|           "color": { |  | ||||||
|             "render": "#ccc" |  | ||||||
|           }, |  | ||||||
|           "width": { |  | ||||||
|             "render": "0" |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|       ] |  | ||||||
|     } |  | ||||||
|   ], |   ], | ||||||
|   "enableShareScreen": false, |   "enableShareScreen": false, | ||||||
|   "enableMoreQuests": false |   "enableMoreQuests": false | ||||||
|  |  | ||||||
|  | @ -1032,6 +1032,10 @@ video { | ||||||
|   height: 0.75rem; |   height: 0.75rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .h-48 { | ||||||
|  |   height: 12rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .max-h-20vh { | .max-h-20vh { | ||||||
|   max-height: 20vh; |   max-height: 20vh; | ||||||
| } | } | ||||||
|  | @ -1611,6 +1615,10 @@ video { | ||||||
|   text-decoration: underline; |   text-decoration: underline; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .line-through { | ||||||
|  |   text-decoration: line-through; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .opacity-50 { | .opacity-50 { | ||||||
|   opacity: 0.5; |   opacity: 0.5; | ||||||
| } | } | ||||||
|  | @ -2223,6 +2231,11 @@ li::marker { | ||||||
|   border: unset !important; |   border: unset !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .floating-element-width { | ||||||
|  |   max-width: calc(100vw - 5em); | ||||||
|  |   width: 40em; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .leaflet-div-icon svg { | .leaflet-div-icon svg { | ||||||
|   width: calc(100%); |   width: calc(100%); | ||||||
|   height: calc(100%); |   height: calc(100%); | ||||||
|  |  | ||||||
|  | @ -439,6 +439,10 @@ li::marker { | ||||||
|     border: unset !important; |     border: unset !important; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .floating-element-width { | ||||||
|  |     max-width: calc(100vw - 5em); | ||||||
|  |     width: 40em; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| .leaflet-div-icon svg { | .leaflet-div-icon svg { | ||||||
|     width: calc(100%); |     width: calc(100%); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue