forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			300 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			300 lines
		
	
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"
 | 
						|
import BaseUIElement from "../BaseUIElement"
 | 
						|
import { Stores, 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"
 | 
						|
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 } from "../SpecialVisualization"
 | 
						|
 | 
						|
export interface AutoAction extends SpecialVisualization {
 | 
						|
    supportsAutoAction: boolean
 | 
						|
 | 
						|
    applyActionOn(
 | 
						|
        state: {
 | 
						|
            layoutToUse: LayoutConfig
 | 
						|
            changes: Changes
 | 
						|
        },
 | 
						|
        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: FeaturePipelineState
 | 
						|
    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
 | 
						|
 | 
						|
    constructor(
 | 
						|
        state: FeaturePipelineState,
 | 
						|
        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.filteredLayers.data.find(
 | 
						|
            (l) => l.layerDef.id === 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 previewMap = Minimap.createMiniMap({
 | 
						|
            allowMoving: false,
 | 
						|
            background: this.state.backgroundLayer,
 | 
						|
            addLayerControl: true,
 | 
						|
        }).SetClass("h-48")
 | 
						|
 | 
						|
        const features = this.target_feature_ids.map((id) =>
 | 
						|
            this.state.allElements.ContainingFeatures.get(id)
 | 
						|
        )
 | 
						|
 | 
						|
        new ShowDataLayer({
 | 
						|
            leafletMap: previewMap.leafletMap,
 | 
						|
            zoomToFeatures: true,
 | 
						|
            features: StaticFeatureSource.fromGeojson(features),
 | 
						|
            state: this.state,
 | 
						|
            layerToShow: 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("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")
 | 
						|
            })
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    private async Run() {
 | 
						|
        try {
 | 
						|
            console.log("Applying auto-action on " + this.target_feature_ids.length + " features")
 | 
						|
 | 
						|
            for (const targetFeatureId of this.target_feature_ids) {
 | 
						|
                const featureTags = this.state.allElements.getEventSourceById(targetFeatureId)
 | 
						|
                const rendering = this.tagRenderingConfig.GetRenderValue(featureTags.data).txt
 | 
						|
                const specialRenderings = Utils.NoNull(
 | 
						|
                    SubstitutedTranslation.ExtractSpecialComponents(rendering).map((x) => x.special)
 | 
						|
                ).filter((v) => 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) {
 | 
						|
                    const action = <AutoAction>specialRendering.func
 | 
						|
                    await action.applyActionOn(this.state, featureTags, specialRendering.args)
 | 
						|
                }
 | 
						|
            }
 | 
						|
            console.log("Flushing changes...")
 | 
						|
            await this.state.changes.flushChanges("Auto button")
 | 
						|
            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: FeaturePipelineState,
 | 
						|
        tagSource: UIEventSource<any>,
 | 
						|
        argument: string[],
 | 
						|
        guistate: DefaultGuiState
 | 
						|
    ): BaseUIElement {
 | 
						|
        try {
 | 
						|
            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 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(undefined)
 | 
						|
                // Very ugly hack: read the value every 500ms
 | 
						|
                Stores.Chronic(500, () => to_parse.data === undefined).addCallback(() => {
 | 
						|
                    const applicable = tagSource.data[argument[1]]
 | 
						|
                    to_parse.setData(applicable)
 | 
						|
                })
 | 
						|
 | 
						|
                const loading = new Loading("Gathering which elements support auto-apply... ")
 | 
						|
                return new VariableUiElement(
 | 
						|
                    to_parse.map((ids) => {
 | 
						|
                        if (ids === undefined) {
 | 
						|
                            return loading
 | 
						|
                        }
 | 
						|
 | 
						|
                        return new ApplyButton(state, JSON.parse(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]]
 | 
						|
    }
 | 
						|
}
 |