forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			323 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			323 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import BaseUIElement from "../BaseUIElement"
 | |
| import {Stores, UIEventSource} from "../../Logic/UIEventSource"
 | |
| 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 {Utils} from "../../Utils"
 | |
| 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"
 | |
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
 | |
| import {Changes} from "../../Logic/Osm/Changes"
 | |
| import {UIElement} from "../UIElement"
 | |
| import FilteredLayer from "../../Models/FilteredLayer"
 | |
| import TagRenderingConfig from "../../Models/ThemeConfig/TagRenderingConfig"
 | |
| import Lazy from "../Base/Lazy"
 | |
| import List from "../Base/List"
 | |
| import {SpecialVisualization, SpecialVisualizationState} from "../SpecialVisualization"
 | |
| import {IndexedFeatureSource} from "../../Logic/FeatureSource/FeatureSource"
 | |
| import {MapLibreAdaptor} from "../Map/MapLibreAdaptor"
 | |
| import ShowDataLayer from "../Map/ShowDataLayer"
 | |
| import SvelteUIElement from "../Base/SvelteUIElement"
 | |
| import MaplibreMap from "../Map/MaplibreMap.svelte"
 | |
| import SpecialVisualizations from "../SpecialVisualizations"
 | |
| import {Feature} from "geojson";
 | |
| 
 | |
| export interface AutoAction extends SpecialVisualization {
 | |
|     supportsAutoAction: boolean
 | |
| 
 | |
|     applyActionOn(
 | |
|         feature: Feature,
 | |
|         state: {
 | |
|             layout: LayoutConfig
 | |
|             changes: Changes
 | |
|             indexedFeatures: IndexedFeatureSource
 | |
|         },
 | |
|         tagSource: UIEventSource<any>,
 | |
|         argument: string[]
 | |
|     ): Promise<void>
 | |
| }
 | |
| 
 | |
| class ApplyButton extends UIElement {
 | |
|     private readonly icon: string
 | |
|     private readonly text: string
 | |
|     private readonly targetTagRendering: string
 | |
|     private readonly target_layer_id: string
 | |
|     private readonly state: SpecialVisualizationState
 | |
|     private readonly target_feature_ids: string[]
 | |
|     private readonly buttonState = new UIEventSource<
 | |
|         "idle" | "running" | "done" | { error: string }
 | |
|     >("idle")
 | |
|     private readonly layer: FilteredLayer
 | |
|     private readonly tagRenderingConfig: TagRenderingConfig
 | |
|     private readonly appliedNumberOfFeatures = new UIEventSource<number>(0)
 | |
| 
 | |
|     constructor(
 | |
|         state: SpecialVisualizationState,
 | |
|         target_feature_ids: string[],
 | |
|         options: {
 | |
|             target_layer_id: string
 | |
|             targetTagRendering: string
 | |
|             text: string
 | |
|             icon: string
 | |
|         }
 | |
|     ) {
 | |
|         super()
 | |
|         this.state = state
 | |
|         this.target_feature_ids = target_feature_ids
 | |
|         this.target_layer_id = options.target_layer_id
 | |
|         this.targetTagRendering = options.targetTagRendering
 | |
|         this.text = options.text
 | |
|         this.icon = options.icon
 | |
|         this.layer = this.state.layerState.filteredLayers.get(this.target_layer_id)
 | |
|         this.tagRenderingConfig = this.layer.layerDef.tagRenderings.find(
 | |
|             (tr) => tr.id === this.targetTagRendering
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     protected InnerRender(): string | BaseUIElement {
 | |
|         if (this.target_feature_ids.length === 0) {
 | |
|             return new FixedUiElement("No elements found to perform action")
 | |
|         }
 | |
| 
 | |
|         if (this.tagRenderingConfig === undefined) {
 | |
|             return new FixedUiElement(
 | |
|                 "Target tagrendering " + this.targetTagRendering + " not found"
 | |
|             ).SetClass("alert")
 | |
|         }
 | |
|         const self = this
 | |
|         const button = new SubtleButton(new Img(this.icon), this.text).onClick(() => {
 | |
|             this.buttonState.setData("running")
 | |
|             window.setTimeout(() => {
 | |
|                 self.Run()
 | |
|             }, 50)
 | |
|         })
 | |
| 
 | |
|         const explanation = new Combine([
 | |
|             "The following objects will be updated: ",
 | |
|             ...this.target_feature_ids.map(
 | |
|                 (id) => new Combine([new Link(id, "https:/  /openstreetmap.org/" + id, true), ", "])
 | |
|             ),
 | |
|         ]).SetClass("subtle")
 | |
| 
 | |
|         const mlmap = new UIEventSource(undefined)
 | |
|         const mla = new MapLibreAdaptor(mlmap, {
 | |
|             rasterLayer: this.state.mapProperties.rasterLayer,
 | |
|         })
 | |
|         mla.allowZooming.setData(false)
 | |
|         mla.allowMoving.setData(false)
 | |
| 
 | |
|         const previewMap = new SvelteUIElement(MaplibreMap, {map: mlmap}).SetClass("h-48")
 | |
| 
 | |
|         const features = this.target_feature_ids.map((id) =>
 | |
|             this.state.indexedFeatures.featuresById.data.get(id)
 | |
|         )
 | |
| 
 | |
|         new ShowDataLayer(mlmap, {
 | |
|             features: StaticFeatureSource.fromGeojson(features),
 | |
|             zoomToFeatures: true,
 | |
|             layer: this.layer.layerDef,
 | |
|         })
 | |
| 
 | |
|         return new VariableUiElement(
 | |
|             this.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(new VariableUiElement(this.appliedNumberOfFeatures.map(appliedTo => {
 | |
|                         return "Applying changes, currently at " + appliedTo + "/" + this.target_feature_ids.length
 | |
|                     })))
 | |
|                 }
 | |
|                 const error = st.error
 | |
|                 return new Combine([
 | |
|                     new FixedUiElement("Something went wrong...").SetClass("alert"),
 | |
|                     new FixedUiElement(error).SetClass("subtle"),
 | |
|                 ]).SetClass("flex flex-col")
 | |
|             })
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Actually applies all the changes...
 | |
|      */
 | |
|     private async Run() {
 | |
|         try {
 | |
|             console.log("Applying auto-action on " + this.target_feature_ids.length + " features")
 | |
| 
 | |
|             for (let i = 0; i < this.target_feature_ids.length; i++) {
 | |
|                 const targetFeatureId = this.target_feature_ids[i];
 | |
|                 const feature = this.state.indexedFeatures.featuresById.data.get(targetFeatureId)
 | |
|                 const featureTags = this.state.featureProperties.getStore(targetFeatureId)
 | |
|                 const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt
 | |
|                 const specialRenderings = Utils.NoNull(
 | |
|                     SpecialVisualizations.constructSpecification(rendering)
 | |
|                 ).filter((v) => typeof v !== "string" && v.func["supportsAutoAction"] === true)
 | |
| 
 | |
|                 if (specialRenderings.length == 0) {
 | |
|                     console.warn(
 | |
|                         "AutoApply: feature " +
 | |
|                         targetFeatureId +
 | |
|                         " got a rendering without supported auto actions:",
 | |
|                         rendering
 | |
|                     )
 | |
|                 }
 | |
| 
 | |
|                 for (const specialRendering of specialRenderings) {
 | |
|                     if (typeof specialRendering === "string") {
 | |
|                         continue
 | |
|                     }
 | |
|                     const action = <AutoAction>specialRendering.func
 | |
|                     await action.applyActionOn(feature, this.state, featureTags, specialRendering.args)
 | |
|                 }
 | |
|                 if( i % 50 === 0){
 | |
|                     await this.state.changes.flushChanges("Auto button: intermediate save")
 | |
|                 }
 | |
|                 this.appliedNumberOfFeatures.setData(i + 1)
 | |
|             }
 | |
|             console.log("Flushing changes...")
 | |
|             await this.state.changes.flushChanges("Auto button: done")
 | |
|             this.buttonState.setData("done")
 | |
|         } catch (e) {
 | |
|             console.error("Error while running autoApply: ", e)
 | |
|             this.buttonState.setData({error: e})
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| export default class AutoApplyButton implements SpecialVisualization {
 | |
|     public readonly docs: BaseUIElement
 | |
|     public readonly funcName: string = "auto_apply"
 | |
|     public readonly args: {
 | |
|         name: string
 | |
|         defaultValue?: string
 | |
|         doc: string
 | |
|         required?: boolean
 | |
|     }[] = [
 | |
|         {
 | |
|             name: "target_layer",
 | |
|             doc: "The layer that the target features will reside in",
 | |
|             required: true,
 | |
|         },
 | |
|         {
 | |
|             name: "target_feature_ids",
 | |
|             doc: "The key, of which the value contains a list of ids",
 | |
|             required: true,
 | |
|         },
 | |
|         {
 | |
|             name: "tag_rendering_id",
 | |
|             doc: "The ID of the tagRendering containing the autoAction. This tagrendering will be calculated. The embedded actions will be executed",
 | |
|             required: true,
 | |
|         },
 | |
|         {
 | |
|             name: "text",
 | |
|             doc: "The text to show on the button",
 | |
|             required: true,
 | |
|         },
 | |
|         {
 | |
|             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)
 | |
|         )
 | |
|     }
 | |
| 
 | |
|     private static generateDocs(supportedActions: string[]) {
 | |
|         return new Combine([
 | |
|             "A button to run many actions for many features at once.",
 | |
|             "To effectively use this button, you'll need some ingredients:",
 | |
|             new List([
 | |
|                 "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 layer ",
 | |
|                 new Link("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",
 | |
|             ]),
 | |
|         ])
 | |
|     }
 | |
| 
 | |
|     constr(
 | |
|         state: SpecialVisualizationState,
 | |
|         tagSource: UIEventSource<Record<string, string>>,
 | |
|         argument: string[]
 | |
|     ): BaseUIElement {
 | |
|         try {
 | |
|             if (
 | |
|                 !state.layout.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 target_layer_id = argument[0]
 | |
|             const targetTagRendering = argument[2]
 | |
|             const text = argument[3]
 | |
|             const icon = argument[4]
 | |
|             const options = {
 | |
|                 target_layer_id,
 | |
|                 targetTagRendering,
 | |
|                 text,
 | |
|                 icon,
 | |
|             }
 | |
| 
 | |
|             return new Lazy(() => {
 | |
|                 const to_parse = new UIEventSource<string[]>(undefined)
 | |
|                 // Very ugly hack: read the value every 500ms
 | |
|                 Stores.Chronic(500, () => to_parse.data === undefined).addCallback(() => {
 | |
|                     let applicable = <string | string[]> tagSource.data[argument[1]]
 | |
|                     if(typeof applicable === "string"){
 | |
|                         applicable = JSON.parse(applicable)
 | |
|                     }
 | |
|                     to_parse.setData(<string[]> applicable)
 | |
|                 })
 | |
| 
 | |
|                 const loading = new Loading("Gathering which elements support auto-apply... ")
 | |
|                 return new VariableUiElement(
 | |
|                     Stores.ListStabilized(to_parse).map((ids) => {
 | |
|                         if (ids === undefined) {
 | |
|                             return loading
 | |
|                         }
 | |
| 
 | |
|                         if (typeof ids === "string") {
 | |
|                             ids = JSON.parse(ids)
 | |
|                         }
 | |
|                         return new ApplyButton(state, ids, options)
 | |
|                     })
 | |
|                 )
 | |
|             })
 | |
|         } catch (e) {
 | |
|             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]]
 | |
|     }
 | |
| }
 |