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)" | ||||
|     _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. | ||||
|      * | ||||
|  | @ -164,8 +165,11 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
| 
 | ||||
|         const selfCenter = GeoOperations.centerpointCoordinates(feature) | ||||
|         let closestFeatures: { feat: any, distance: number }[] = []; | ||||
|          | ||||
|         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) { | ||||
|                  | ||||
|                 if (otherFeature === feature || otherFeature.properties.id === feature.properties.id) { | ||||
|                     continue; // We ignore self
 | ||||
|                 } | ||||
|  | @ -187,6 +191,7 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
|                 } | ||||
| 
 | ||||
|                 if (closestFeatures.length === 0) { | ||||
|                     // This is the first matching feature we find - always add it
 | ||||
|                     closestFeatures.push({ | ||||
|                         feat: otherFeature, | ||||
|                         distance: distance | ||||
|  | @ -194,6 +199,7 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 if (closestFeatures.length >= maxFeatures && closestFeatures[maxFeatures - 1].distance < distance) { | ||||
|                     // 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!
 | ||||
|  | @ -257,6 +263,8 @@ class ClosestNObjectFunc implements ExtraFunction { | |||
|                     } | ||||
| 
 | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|             } | ||||
|         } | ||||
|         return closestFeatures; | ||||
|  |  | |||
|  | @ -126,7 +126,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | |||
| 
 | ||||
|                 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,31 +107,22 @@ export default class MetaTagging { | |||
|         } | ||||
|         return atLeastOneFeatureChanged | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string][]): ((feature: any) => void)[] { | ||||
|     public static createFunctionsForFeature(layerId: string, calculatedTags: [string, string, boolean][]): ((feature: any) => void)[] { | ||||
|         const functions: ((feature: any) => void)[] = []; | ||||
|          | ||||
|         for (const entry of calculatedTags) { | ||||
|             const key = entry[0] | ||||
|             const code = entry[1]; | ||||
|             const isStrict = entry[2] | ||||
|             if (code === undefined) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             const func = new Function("feat", "return " + code + ";"); | ||||
| 
 | ||||
|             const f = (feature: any) => { | ||||
|             const calculateAndAssign = (feat) => { | ||||
| 
 | ||||
| 
 | ||||
|                 delete feature.properties[key] | ||||
|                 Object.defineProperty(feature.properties, key, { | ||||
|                     configurable: true, | ||||
|                     enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this
 | ||||
|                     get: function () { | ||||
|                 try { | ||||
|                             // Lazyness for the win!
 | ||||
|                             let result = func(feature); | ||||
| 
 | ||||
|                     let result = new Function("feat", "return " + code + ";")(feat); | ||||
|                     if (result === "") { | ||||
|                         result === undefined | ||||
|                     } | ||||
|  | @ -139,19 +130,34 @@ export default class MetaTagging { | |||
|                         // Make sure it is a string!
 | ||||
|                         result = JSON.stringify(result); | ||||
|                     } | ||||
|                             delete feature.properties[key] | ||||
|                             feature.properties[key] = result; | ||||
|                     delete feat.properties[key] | ||||
|                     feat.properties[key] = result; | ||||
|                     return result; | ||||
|                         } catch (e) { | ||||
|                 }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) | ||||
|                         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) => { | ||||
|                 delete feature.properties[key] | ||||
|                 Object.defineProperty(feature.properties, key, { | ||||
|                     configurable: true, | ||||
|                     enumerable: false, // By setting this as not enumerable, the localTileSaver will _not_ calculate this
 | ||||
|                     get: function () { | ||||
|                         return calculateAndAssign(feature) | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
|  | @ -167,7 +173,7 @@ export default class MetaTagging { | |||
|     private static createRetaggingFunc(layer: LayerConfig): | ||||
|         ((params: ExtraFuncParams, feature: any) => void) { | ||||
| 
 | ||||
|         const calculatedTags: [string, string][] = layer.calculatedTags; | ||||
|         const calculatedTags: [string, string, boolean][] = layer.calculatedTags; | ||||
|         if (calculatedTags === undefined || calculatedTags.length === 0) { | ||||
|             return undefined; | ||||
|         } | ||||
|  |  | |||
|  | @ -98,7 +98,7 @@ export class Changes { | |||
|      * Uploads all the pending changes in one go. | ||||
|      * 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) { | ||||
|             return; | ||||
|         } | ||||
|  | @ -108,16 +108,14 @@ export class Changes { | |||
|         } | ||||
|         console.log("Uploading changes due to: ", flushreason) | ||||
|         this.isUploading.setData(true) | ||||
| 
 | ||||
|         this.flushChangesAsync() | ||||
|             .then(_ => { | ||||
|         try { | ||||
|             const csNumber = await this.flushChangesAsync() | ||||
|             this.isUploading.setData(false) | ||||
|             console.log("Changes flushed!"); | ||||
|             }) | ||||
|             .catch(e => { | ||||
|         } catch (e) { | ||||
|             this.isUploading.setData(false) | ||||
|             console.error("Flushing changes failed due to", e); | ||||
|             }) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) { | ||||
|  |  | |||
|  | @ -17,7 +17,6 @@ import {FeatureSourceForLayer, Tiled} from "../FeatureSource/FeatureSource"; | |||
| import SimpleFeatureSource from "../FeatureSource/Sources/SimpleFeatureSource"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains all the leaflet-map related state | ||||
|  | @ -186,6 +185,7 @@ export default class MapState extends UserRelatedState { | |||
| 
 | ||||
| 
 | ||||
|         let i = 0 | ||||
|         const self = this; | ||||
|         const features : UIEventSource<{ feature: any, freshness: Date }[]>= this.currentBounds.map(bounds => { | ||||
|             if(bounds === undefined){ | ||||
|                 return [] | ||||
|  | @ -197,7 +197,8 @@ export default class MapState extends UserRelatedState { | |||
|                     type: "Feature", | ||||
|                     properties:{ | ||||
|                         id:"current_view-"+i, | ||||
|                         "current_view":"yes" | ||||
|                         "current_view":"yes", | ||||
|                         "zoom": ""+self.locationControl.data.zoom | ||||
|                     }, | ||||
|                     geometry:{ | ||||
|                         type:"Polygon", | ||||
|  |  | |||
|  | @ -82,6 +82,15 @@ export interface LayerConfigJson { | |||
|      *  "_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[]; | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ export default class LayerConfig extends WithContextLoader { | |||
|     public readonly name: Translation; | ||||
|     public readonly description: Translation; | ||||
|     public readonly source: SourceConfig; | ||||
|     public readonly calculatedTags: [string, string][]; | ||||
|     public readonly calculatedTags: [string, string, boolean][]; | ||||
|     public readonly doNotDownload: boolean; | ||||
|     public readonly  passAllFeatures: boolean; | ||||
|     public readonly isShown: TagRenderingConfig; | ||||
|  | @ -130,7 +130,11 @@ export default class LayerConfig extends WithContextLoader { | |||
|             this.calculatedTags = []; | ||||
|             for (const kv of json.calculatedTags) { | ||||
|                 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); | ||||
| 
 | ||||
|                 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()" | ||||
|     } | ||||
|      | ||||
|     private constructor() {         | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -72,6 +72,7 @@ export default class LeftControls extends Combine { | |||
|                     return new Lazy(() => { | ||||
|                       const tagsSource=  state.allElements.getEventSourceById(feature.properties.id) | ||||
|                         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( | ||||
|             new AllDownloads( | ||||
|                 guiState.downloadControlIsOpened | ||||
|             ).SetClass("block p-1 rounded-full"), | ||||
|             ).SetClass("block p-1 rounded-full md:floating-element-width"), | ||||
|             new MapControlButton(Svg.download_svg()) | ||||
|                 .onClick(() => guiState.downloadControlIsOpened.setData(true)), | ||||
|             guiState.downloadControlIsOpened | ||||
|  | @ -116,7 +117,7 @@ export default class LeftControls extends Combine { | |||
|                     ), | ||||
|                 "filters", | ||||
|                 guiState.filterViewIsOpened | ||||
|             ).SetClass("rounded-lg"), | ||||
|             ).SetClass("rounded-lg md:floating-element-width"), | ||||
|             new MapControlButton(Svg.filter_svg()) | ||||
|                 .onClick(() => guiState.filterViewIsOpened.setData(true)), | ||||
|             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 CreateMultiPolygonWithPointReuseAction from "../../Logic/Osm/Actions/CreateMultiPolygonWithPointReuseAction"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
| import TagApplyButton from "./TagApplyButton"; | ||||
| 
 | ||||
| 
 | ||||
| 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
 | ||||
|         const newTags = SpecialVisualizations.generateTagsToApply(args.tags, tagSource) | ||||
|         const newTags = TagApplyButton.generateTagsToApply(args.tags, tagSource) | ||||
|         const appliedTags = new Toggle( | ||||
|             new VariableUiElement( | ||||
|                 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[]> } { | ||||
|         const baseArgs = Utils.ParseVisArgs(this.args, argsRaw) | ||||
|         if (originalFeatureTags !== undefined) { | ||||
|             baseArgs["newTags"] = SpecialVisualizations.generateTagsToApply(baseArgs.tags, originalFeatureTags) | ||||
|             baseArgs["newTags"] = TagApplyButton.generateTagsToApply(baseArgs.tags, originalFeatureTags) | ||||
|         } | ||||
|         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) | ||||
|     } | ||||
| } | ||||
|            | ||||
|  | @ -20,7 +20,6 @@ import Histogram from "./BigComponents/Histogram"; | |||
| import Loc from "../Models/Loc"; | ||||
| import {Utils} from "../Utils"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import {Tag} from "../Logic/Tags/Tag"; | ||||
| import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer"; | ||||
| import Minimap from "./Base/Minimap"; | ||||
|  | @ -31,14 +30,13 @@ import MultiApply from "./Popup/MultiApply"; | |||
| import AllKnownLayers from "../Customizations/AllKnownLayers"; | ||||
| import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; | ||||
| import {SubtleButton} from "./Base/SubtleButton"; | ||||
| import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"; | ||||
| import {And} from "../Logic/Tags/And"; | ||||
| import Toggle from "./Input/Toggle"; | ||||
| import {DefaultGuiState} from "./DefaultGuiState"; | ||||
| import {GeoOperations} from "../Logic/GeoOperations"; | ||||
| import Hash from "../Logic/Web/Hash"; | ||||
| import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; | ||||
| import {ConflateButton, ImportPointButton, ImportWayButton} from "./Popup/ImportButton"; | ||||
| import TagApplyButton from "./Popup/TagApplyButton"; | ||||
| import AutoApplyButton from "./Popup/AutoApplyButton"; | ||||
| 
 | ||||
| export interface SpecialVisualization { | ||||
|     funcName: string, | ||||
|  | @ -51,8 +49,11 @@ export interface SpecialVisualization { | |||
| 
 | ||||
| export default class SpecialVisualizations { | ||||
| 
 | ||||
|     static tagsToApplyHelpText = Utils.Special_visualizations_tagsToApplyHelpText | ||||
|     public static specialVisualizations: SpecialVisualization[] = | ||||
|     public static specialVisualizations = SpecialVisualizations.init() | ||||
| 
 | ||||
| 
 | ||||
|     private static init(){ | ||||
|       const  specialVisualizations: SpecialVisualization[] = | ||||
|             [ | ||||
|                 { | ||||
|                     funcName: "all_tags", | ||||
|  | @ -533,76 +534,7 @@ export default class SpecialVisualizations { | |||
| 
 | ||||
|                     } | ||||
|                 }, | ||||
|             { | ||||
|                 funcName: "tag_apply", | ||||
|                 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" + SpecialVisualizations.tagsToApplyHelpText, | ||||
|                 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" | ||||
|                     } | ||||
|                 ], | ||||
|                 example: "`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)", | ||||
|                 constr: (state, tags, args) => { | ||||
|                     const tagsToApply = SpecialVisualizations.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 applied = new UIEventSource(false) | ||||
|                     const applyButton = new SubtleButton(image, new Combine([msg, tagsExplanation]).SetClass("flex flex-col")) | ||||
|                         .onClick(() => { | ||||
|                             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" | ||||
|                                 } | ||||
|                             ) | ||||
|                             state.changes.applyAction(changeAction) | ||||
|                             applied.setData(true) | ||||
|                         }) | ||||
| 
 | ||||
| 
 | ||||
|                     return new Toggle( | ||||
|                         new Toggle( | ||||
|                             t.isApplied.SetClass("thanks"), | ||||
|                             applyButton, | ||||
|                             applied | ||||
|                         ) | ||||
|                         , undefined, state.osmConnection.isLoggedIn) | ||||
|                 } | ||||
|             }, | ||||
|                 new TagApplyButton(), | ||||
|                 { | ||||
|                     funcName: "export_as_gpx", | ||||
|                     docs: "Exports the selected feature as GPX-file", | ||||
|  | @ -643,46 +575,11 @@ export default class SpecialVisualizations { | |||
|                 } | ||||
|             ] | ||||
|          | ||||
|         specialVisualizations.push(new AutoApplyButton(specialVisualizations)) | ||||
|          | ||||
|     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 specialVisualizations; | ||||
|     } | ||||
|      | ||||
|         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 static HelpMessage() { | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										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": [], | ||||
|     "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", | ||||
|     "license": "CC0", | ||||
|  |  | |||
| Before Width: | Height: | Size: 751 B After Width: | Height: | Size: 751 B | 
|  | @ -42,14 +42,19 @@ | |||
|           } | ||||
|         ], | ||||
|         "calculatedTags": [ | ||||
|           "_x='y'", | ||||
|           "_embedded_crab_addresses=undefined // feat.overlapWith('crab_address').length" | ||||
|           "_embedded_crab_addresses= Number(feat.properties.zoom) >= 18 ? feat.overlapWith('crab_address').length : undefined" | ||||
|         ], | ||||
|         "minZoom": 18, | ||||
|         "tagRenderings": [ | ||||
|           { | ||||
|             "id": "hw", | ||||
|             "render": "There are {_embedded_crab_addresses} adresses in view", | ||||
|             "mappings": [{ | ||||
|             "mappings": [ | ||||
|               { | ||||
|                 "if": "zoom<18", | ||||
|                 "then": "Zoom in more..." | ||||
|               }, | ||||
|               { | ||||
|               "if": "_embedded_crab_addresses=", | ||||
|               "then": "Loading..." | ||||
|             },{ | ||||
|  |  | |||
|  | @ -6,15 +6,5 @@ | |||
|       "Pieter Vander Vennet" | ||||
|     ], | ||||
|     "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 @@ | |||
|         } | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "id": "named_streets", | ||||
|       "minzoom": 18, | ||||
|       "source": { | ||||
|         "osmTags": { | ||||
|           "and": [ | ||||
|             "highway~*", | ||||
|             "name~*" | ||||
|           ] | ||||
|         } | ||||
|       }, | ||||
|       "mapRendering": [ | ||||
|         { | ||||
|           "color": { | ||||
|             "render": "#ccc" | ||||
|           }, | ||||
|           "width": { | ||||
|             "render": "0" | ||||
|           } | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
| "named_streets" | ||||
|   ], | ||||
|   "enableShareScreen": false, | ||||
|   "enableMoreQuests": false | ||||
|  |  | |||
|  | @ -1032,6 +1032,10 @@ video { | |||
|   height: 0.75rem; | ||||
| } | ||||
| 
 | ||||
| .h-48 { | ||||
|   height: 12rem; | ||||
| } | ||||
| 
 | ||||
| .max-h-20vh { | ||||
|   max-height: 20vh; | ||||
| } | ||||
|  | @ -1611,6 +1615,10 @@ video { | |||
|   text-decoration: underline; | ||||
| } | ||||
| 
 | ||||
| .line-through { | ||||
|   text-decoration: line-through; | ||||
| } | ||||
| 
 | ||||
| .opacity-50 { | ||||
|   opacity: 0.5; | ||||
| } | ||||
|  | @ -2223,6 +2231,11 @@ li::marker { | |||
|   border: unset !important; | ||||
| } | ||||
| 
 | ||||
| .floating-element-width { | ||||
|   max-width: calc(100vw - 5em); | ||||
|   width: 40em; | ||||
| } | ||||
| 
 | ||||
| .leaflet-div-icon svg { | ||||
|   width: calc(100%); | ||||
|   height: calc(100%); | ||||
|  |  | |||
|  | @ -439,6 +439,10 @@ li::marker { | |||
|     border: unset !important; | ||||
| } | ||||
| 
 | ||||
| .floating-element-width { | ||||
|     max-width: calc(100vw - 5em); | ||||
|     width: 40em; | ||||
| } | ||||
| 
 | ||||
| .leaflet-div-icon svg { | ||||
|     width: calc(100%); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue