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, argument: string[] ): Promise } 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 = 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, 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]] } }