From 30be86668e26725fe7c1f47ca613236cd331ce46 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Thu, 10 Feb 2022 23:16:14 +0100 Subject: [PATCH] Fix autoapply for GRB theme --- .../Actors/MetaTagRecalculator.ts | 17 +- Logic/State/MapState.ts | 1 + Models/ThemeConfig/Conversion/Conversion.ts | 39 +-- .../Conversion/CreateNoteImportLayer.ts | 7 +- Models/ThemeConfig/Conversion/FixImages.ts | 2 +- Models/ThemeConfig/Conversion/PrepareLayer.ts | 1 - Models/ThemeConfig/Conversion/PrepareTheme.ts | 32 +-- Models/ThemeConfig/Conversion/Validation.ts | 97 ++++---- UI/BigComponents/MoreScreen.ts | 11 +- UI/Popup/AutoApplyButton.ts | 225 +++++++++++------- UI/Popup/EditableTagRendering.ts | 2 +- UI/Popup/ImportButton.ts | 95 ++++++-- UI/Popup/NoteCommentElement.ts | 2 +- UI/SpecialVisualizations.ts | 8 +- assets/themes/grb_import/grb.json | 59 ++++- assets/themes/grb_import/missing_streets.json | 3 +- 16 files changed, 392 insertions(+), 209 deletions(-) diff --git a/Logic/FeatureSource/Actors/MetaTagRecalculator.ts b/Logic/FeatureSource/Actors/MetaTagRecalculator.ts index 6e1284ba9..f17c27afe 100644 --- a/Logic/FeatureSource/Actors/MetaTagRecalculator.ts +++ b/Logic/FeatureSource/Actors/MetaTagRecalculator.ts @@ -50,7 +50,7 @@ class MetatagUpdater { this.isDirty.setData(true) } - private updateMetaTags() { + public updateMetaTags() { const features = this.source.features.data if (features.length === 0) { @@ -77,12 +77,23 @@ export default class MetaTagRecalculator { private readonly _notifiers: MetatagUpdater[] = [] /** - * The meta tag recalculator receives tiles of layers. + * The meta tag recalculator receives tiles of layers via the 'registerSource'-function. * It keeps track of which sources have had their share calculated, and which should be re-updated if some other data is loaded */ - constructor(state: { allElements?: ElementStorage }, featurePipeline: FeaturePipeline) { + constructor(state: { allElements?: ElementStorage, currentView: FeatureSourceForLayer & Tiled }, featurePipeline: FeaturePipeline) { this._featurePipeline = featurePipeline; this._state = state; + + if(state.currentView !== undefined){ + const currentViewUpdater = new MetatagUpdater(state.currentView, this._state, this._featurePipeline) + this._alreadyRegistered.add(state.currentView) + this._notifiers.push(currentViewUpdater) + state.currentView.features.addCallback(_ => { + console.debug("Requesting an update for currentView") + currentViewUpdater.updateMetaTags(); + }) + } + } public registerSource(source: FeatureSourceForLayer & Tiled, recalculateOnEveryChange = false) { diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index 34b667546..07e45a0e4 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -18,6 +18,7 @@ import {LocalStorageSource} from "../Web/LocalStorageSource"; import {GeoOperations} from "../GeoOperations"; import TitleHandler from "../Actors/TitleHandler"; import {BBox} from "../BBox"; +import MetaTagging from "../MetaTagging"; /** * Contains all the leaflet-map related state diff --git a/Models/ThemeConfig/Conversion/Conversion.ts b/Models/ThemeConfig/Conversion/Conversion.ts index a5e9df2c8..d90bfca0b 100644 --- a/Models/ThemeConfig/Conversion/Conversion.ts +++ b/Models/ThemeConfig/Conversion/Conversion.ts @@ -18,11 +18,15 @@ export abstract class Conversion { this.name = name ?? this.constructor.name } - public static strict(fixed: { errors?: string[], warnings?: string[], result?: T }): T { + public static strict(fixed: { errors?: string[], warnings?: string[], information?: string[], result?: T }): T { if (fixed?.errors !== undefined && fixed?.errors?.length > 0) { throw fixed.errors.join("\n\n"); } - fixed.warnings?.forEach(w => console.warn(w)) + fixed.information?.forEach(i => console.log(" ", i)) + const yellow = (s) => "\x1b[33m"+s+"\x1b[0m" + const red = s => '\x1b[31m'+s+'\x1b[0m' + + fixed.warnings?.forEach(w => console.warn(red(` `), yellow (w))) return fixed.result; } @@ -31,26 +35,29 @@ export abstract class Conversion { return DesugaringStep.strict(fixed) } - abstract convert(json: TIn, context: string): { result: TOut, errors?: string[], warnings?: string[] } + abstract convert(json: TIn, context: string): { result: TOut, errors?: string[], warnings?: string[], information?: string[] } - public convertAll(jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[] } { + public convertAll(jsons: TIn[], context: string): { result: TOut[], errors: string[], warnings: string[], information?: string[] } { if(jsons === undefined){ throw "convertAll received undefined - don't do this (at "+context+")" } const result = [] const errors = [] const warnings = [] + const information = [] for (let i = 0; i < jsons.length; i++) { const json = jsons[i]; const r = this.convert(json, context + "[" + i + "]") result.push(r.result) errors.push(...r.errors ?? []) warnings.push(...r.warnings ?? []) + information.push(...r.information ?? []) } return { result, errors, - warnings + warnings, + information } } @@ -69,16 +76,15 @@ export class OnEvery extends DesugaringStep { this.key = key; } - convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[] } { + convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[], information?: string[] } { json = {...json} const step = this.step const key = this.key; const r = step.convertAll((json[key]), context + "." + key) json[key] = r.result return { + ...r, result: json, - errors: r.errors, - warnings: r.warnings }; } } @@ -94,7 +100,7 @@ export class OnEveryConcat extends DesugaringStep { this.key = key; } - convert(json: T, context: string): { result: T; errors: string[]; warnings: string[] } { + convert(json: T, context: string): { result: T; errors?: string[]; warnings?: string[], information?: string[] } { json = {...json} const step = this.step const key = this.key; @@ -103,17 +109,14 @@ export class OnEveryConcat extends DesugaringStep { // Move on - nothing to see here! return { result: json, - errors: [], - warnings: [] } } const r = step.convertAll((values), context + "." + key) const vals: X[][] = r.result json[key] = [].concat(...vals) return { + ...r, result: json, - errors: r.errors, - warnings: r.warnings }; } @@ -129,14 +132,16 @@ export class Fuse extends DesugaringStep { this.steps = steps; } - convert(json: T, context: string): { result: T; errors: string[]; warnings: string[] } { + convert(json: T, context: string): { result: T; errors: string[]; warnings: string[], information: string[] } { const errors = [] const warnings = [] + const information = [] for (let i = 0; i < this.steps.length; i++) { const step = this.steps[i]; let r = step.convert(json, "While running step " +step.name + ": " + context) errors.push(...r.errors ?? []) warnings.push(...r.warnings ?? []) + information.push(...r.information ?? []) json = r.result if (errors.length > 0) { break; @@ -145,7 +150,8 @@ export class Fuse extends DesugaringStep { return { result: json, errors, - warnings + warnings, + information }; } @@ -163,14 +169,13 @@ export class SetDefault extends DesugaringStep { this._overrideEmptyString = overrideEmptyString; } - convert(json: T, context: string): { result: T; errors: string[]; warnings: string[] } { + convert(json: T, context: string): { result: T } { if (json[this.key] === undefined || (json[this.key] === "" && this._overrideEmptyString)) { json = {...json} json[this.key] = this.value } return { - errors: [], warnings: [], result: json }; } diff --git a/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts b/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts index e5ba8ad76..c51437d7f 100644 --- a/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts +++ b/Models/ThemeConfig/Conversion/CreateNoteImportLayer.ts @@ -20,9 +20,7 @@ export default class CreateNoteImportLayer extends Conversion { this._knownImages = knownImages; } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } { + convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson } { let url: URL; try { url = new URL(json.id) diff --git a/Models/ThemeConfig/Conversion/PrepareLayer.ts b/Models/ThemeConfig/Conversion/PrepareLayer.ts index a362172e4..94b664df0 100644 --- a/Models/ThemeConfig/Conversion/PrepareLayer.ts +++ b/Models/ThemeConfig/Conversion/PrepareLayer.ts @@ -213,7 +213,6 @@ class ExpandGroupRewrite extends Conversion<{ for (let i = 0; i < sourceStrings.length; i++) { const source = sourceStrings[i] const target = targets[i] // This is a string OR a translation - console.log("Replacing every "+source+" with "+JSON.stringify(target)) rewritten = this.prepConfig(source, target, rewritten) } rewritten.group = rewritten.group ?? groupName diff --git a/Models/ThemeConfig/Conversion/PrepareTheme.ts b/Models/ThemeConfig/Conversion/PrepareTheme.ts index a1b337971..f2270c014 100644 --- a/Models/ThemeConfig/Conversion/PrepareTheme.ts +++ b/Models/ThemeConfig/Conversion/PrepareTheme.ts @@ -20,8 +20,9 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig this._state = state; } - convert(json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[], warnings?: string[] } { + convert(json: string | LayerConfigJson, context: string): { result: LayerConfigJson[]; errors: string[], information?: string[] } { const errors = [] + const information = [] const state= this._state function reportNotFound(name: string){ const knownLayers = Array.from(state.sharedLayers.keys()) @@ -55,7 +56,6 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig names = [names] } const layers = [] - const warnings = [] for (const name of names) { const found = Utils.Clone(state.sharedLayers.get(name)) @@ -84,20 +84,20 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig const forbiddenLabel = labels.findIndex(l => hideLabels.has(l)) if(forbiddenLabel >= 0){ usedLabels.add(labels[forbiddenLabel]) - warnings.push(context+": Dropping tagRendering "+tr["id"]+" as it has a forbidden label: "+labels[forbiddenLabel]) + information.push(context+": Dropping tagRendering "+tr["id"]+" as it has a forbidden label: "+labels[forbiddenLabel]) continue } } if(hideLabels.has(tr["id"])){ usedLabels.add(tr["id"]) - warnings.push(context+": Dropping tagRendering "+tr["id"]+" as its id is a forbidden label") + information.push(context+": Dropping tagRendering "+tr["id"]+" as its id is a forbidden label") continue } if(hideLabels.has(tr["group"])){ usedLabels.add(tr["group"]) - warnings.push(context+": Dropping tagRendering "+tr["id"]+" as its group `"+tr["group"]+"` is a forbidden label") + information.push(context+": Dropping tagRendering "+tr["id"]+" as its group `"+tr["group"]+"` is a forbidden label") continue } @@ -113,7 +113,7 @@ class SubstituteLayer extends Conversion<(string | LayerConfigJson), LayerConfig return { result: layers, errors, - warnings + information } } @@ -185,9 +185,8 @@ class AddImportLayers extends DesugaringStep { super("For every layer in the 'layers'-list, create a new layer which'll import notes. (Note that priviliged layers and layers which have a geojson-source set are ignored)", ["layers"]); } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { + convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[] } { const errors = [] - const warnings = [] json = {...json} const allLayers: LayerConfigJson[] = json.layers; @@ -221,8 +220,6 @@ class AddImportLayers extends DesugaringStep { try { const importLayerResult = creator.convert(layer, context + ".(noteimportlayer)[" + i1 + "]") - errors.push(...importLayerResult.errors) - warnings.push(...importLayerResult.warnings) if (importLayerResult.result !== undefined) { json.layers.push(importLayerResult.result) } @@ -234,7 +231,6 @@ class AddImportLayers extends DesugaringStep { return { errors, - warnings, result: json }; } @@ -274,7 +270,7 @@ export class AddMiniMap extends DesugaringStep { return false; } - convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson; errors: string[]; warnings: string[] } { + convert(layerConfig: LayerConfigJson, context: string): { result: LayerConfigJson } { const state = this._state; const hasMinimap = layerConfig.tagRenderings?.some(tr => AddMiniMap.hasMinimap(tr)) ?? true @@ -286,8 +282,6 @@ export class AddMiniMap extends DesugaringStep { } return { - errors: [], - warnings: [], result: layerConfig }; } @@ -384,12 +378,11 @@ class AddDependencyLayersToTheme extends DesugaringStep { }); } - convert(theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[]; warnings: string[] } { + convert(theme: LayoutConfigJson, context: string): { result: LayoutConfigJson; information: string[] } { const state = this._state const allKnownLayers: Map = state.sharedLayers; const knownTagRenderings: Map = state.tagRenderings; - const errors = []; - const warnings = []; + const information = []; const layers: LayerConfigJson[] = theme.layers; // Layers should be expanded at this point knownTagRenderings.forEach((value, key) => { @@ -399,7 +392,7 @@ class AddDependencyLayersToTheme extends DesugaringStep { const dependencies = AddDependencyLayersToTheme.CalculateDependencies(layers, allKnownLayers, theme.id); if (dependencies.length > 0) { - warnings.push(context + ": added " + dependencies.map(d => d.id).join(", ") + " to the theme as they are needed") + information.push(context + ": added " + dependencies.map(d => d.id).join(", ") + " to the theme as they are needed") } layers.unshift(...dependencies); @@ -408,8 +401,7 @@ class AddDependencyLayersToTheme extends DesugaringStep { ...theme, layers: layers }, - errors, - warnings + information }; } } diff --git a/Models/ThemeConfig/Conversion/Validation.ts b/Models/ThemeConfig/Conversion/Validation.ts index 80847fc5f..777e9c614 100644 --- a/Models/ThemeConfig/Conversion/Validation.ts +++ b/Models/ThemeConfig/Conversion/Validation.ts @@ -9,6 +9,7 @@ import LayoutConfig from "../LayoutConfig"; import {TagRenderingConfigJson} from "../Json/TagRenderingConfigJson"; import {TagUtils} from "../../../Logic/Tags/TagUtils"; import {ExtractImages} from "./FixImages"; +import ScriptUtils from "../../../scripts/ScriptUtils"; class ValidateLanguageCompleteness extends DesugaringStep { @@ -55,9 +56,10 @@ class ValidateTheme extends DesugaringStep { this._isBuiltin = isBuiltin; } - convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[], warnings: string[] } { + convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors: string[], warnings: string[], information: string[] } { const errors = [] const warnings = [] + const information = [] { // Legacy format checks if (this._isBuiltin) { @@ -70,7 +72,7 @@ class ValidateTheme extends DesugaringStep { } } { - // Check for remote images + // Check images: are they local, are the licenses there, is the theme icon square, ... const images = new ExtractImages().convertStrict(json, "validation") const remoteImages = images.filter(img => img.indexOf("http") == 0) for (const remoteImage of remoteImages) { @@ -78,14 +80,14 @@ class ValidateTheme extends DesugaringStep { } for (const image of images) { if (image.indexOf("{") >= 0) { - warnings.push("Ignoring image with { in the path: ", image) + information.push("Ignoring image with { in the path: " + image) continue } - - if(image === "assets/SocialImage.png"){ + + if (image === "assets/SocialImage.png") { continue } - if(image.match(/[a-z]*/)){ + if (image.match(/[a-z]*/)) { // This is a builtin img, e.g. 'checkmark' or 'crosshair' continue; } @@ -96,6 +98,22 @@ class ValidateTheme extends DesugaringStep { } } + if (json.icon.endsWith(".svg")) { + try { + ScriptUtils.ReadSvgSync(json.icon, svg => { + const width: string = svg.$.width; + const height: string = svg.$.height; + if (width !== height) { + const e = `the icon for theme ${json.id} is not square. Please square the icon at ${json.icon}` + + ` Width = ${width} height = ${height}`; + (json.hideFromOverview ? warnings : errors).push(e) + } + }) + } catch (e) { + console.error("Could not read " + json.icon + " due to " + e) + } + } + } try { const theme = new LayoutConfig(json, true, "test") @@ -127,7 +145,8 @@ class ValidateTheme extends DesugaringStep { return { result: json, errors, - warnings + warnings, + information }; } } @@ -142,60 +161,60 @@ export class ValidateThemeAndLayers extends Fuse { } -class OverrideShadowingCheck extends DesugaringStep{ - +class OverrideShadowingCheck extends DesugaringStep { + constructor() { super("Checks that an 'overrideAll' does not override a single override"); } convert(json: LayoutConfigJson, context: string): { result: LayoutConfigJson; errors?: string[]; warnings?: string[] } { - + const overrideAll = json.overrideAll; - if(overrideAll === undefined){ + if (overrideAll === undefined) { return {result: json} } - + const errors = [] - const withOverride = json.layers.filter(l => l["override"] !== undefined) + const withOverride = json.layers.filter(l => l["override"] !== undefined) for (const layer of withOverride) { for (const key in overrideAll) { - if(layer["override"][key] !== undefined || layer["override"]["="+key] !== undefined){ - const w = "The override of layer "+JSON.stringify(layer["builtin"])+" has a shadowed property: "+key+" is overriden by overrideAll of the theme"; - errors.push(w) - } + if (layer["override"][key] !== undefined || layer["override"]["=" + key] !== undefined) { + const w = "The override of layer " + JSON.stringify(layer["builtin"]) + " has a shadowed property: " + key + " is overriden by overrideAll of the theme"; + errors.push(w) + } } } - - return {result: json, errors} + + return {result: json, errors} } - + } -export class PrevalidateTheme extends Fuse{ - +export class PrevalidateTheme extends Fuse { + constructor() { super("Various consistency checks on the raw JSON", new OverrideShadowingCheck() - ); - + ); + } } -export class DetectShadowedMappings extends DesugaringStep{ +export class DetectShadowedMappings extends DesugaringStep { constructor() { super("Checks that the mappings don't shadow each other"); } - + convert(json: TagRenderingConfigJson, context: string): { result: TagRenderingConfigJson; errors?: string[]; warnings?: string[] } { const errors = [] - if(json.mappings === undefined || json.mappings.length === 0){ + if (json.mappings === undefined || json.mappings.length === 0) { return {result: json} } const parsedConditions = json.mappings.map(m => TagUtils.Tag(m.if)) - for (let i = 0; i < json.mappings.length; i++){ - if(!parsedConditions[i].isUsableAsAnswer()){ + for (let i = 0; i < json.mappings.length; i++) { + if (!parsedConditions[i].isUsableAsAnswer()) { continue } const keyValues = parsedConditions[i].asChange({}); @@ -203,12 +222,12 @@ export class DetectShadowedMappings extends DesugaringStep { properties[k] = v }) - for (let j = 0; j < i; j++){ + for (let j = 0; j < i; j++) { const doesMatch = parsedConditions[j].matchesProperties(properties) - if(doesMatch){ + if (doesMatch) { // The current mapping is shadowed! errors.push(`Mapping ${i} is shadowed by mapping ${j} and will thus never be shown: - The mapping ${parsedConditions[i].asHumanString(false,false, {})} is fully matched by a previous mapping, which matches: + The mapping ${parsedConditions[i].asHumanString(false, false, {})} is fully matched by a previous mapping, which matches: ${parsedConditions[j].asHumanString(false, false, {})}. Move the mapping up to fix this problem @@ -217,7 +236,7 @@ export class DetectShadowedMappings extends DesugaringStep { } } } - if(json.tagRenderings !== undefined){ - new DetectShadowedMappings().convertAll( json.tagRenderings, context+".tagRenderings") + if (json.tagRenderings !== undefined) { + new DetectShadowedMappings().convertAll(json.tagRenderings, context + ".tagRenderings") } - + } catch (e) { errors.push(e) } - - - - + + return { result: json, errors, diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index 1dd04ecb4..22674db52 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -126,6 +126,9 @@ export default class MoreScreen extends Combine { for (let i = 0; i < length; i++) { str += allPreferences[id + "-" + i] } + if(str === undefined || str === "undefined"){ + return undefined + } try { const value: { id: string @@ -157,13 +160,9 @@ export default class MoreScreen extends Combine { return ids }); - currentIds.addCallback(ids => { - console.log("Current special ids are:", ids) - }) + var stableIds = UIEventSource.ListStabilized(currentIds) - currentIds.addCallback(ids => { - console.log("Stabilized special ids are:", ids) - }) + return new VariableUiElement( stableIds.map(ids => { const allThemes: BaseUIElement[] = [] diff --git a/UI/Popup/AutoApplyButton.ts b/UI/Popup/AutoApplyButton.ts index 0ee17e721..0f3f7c1c6 100644 --- a/UI/Popup/AutoApplyButton.ts +++ b/UI/Popup/AutoApplyButton.ts @@ -19,6 +19,10 @@ 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"; export interface AutoAction extends SpecialVisualization { supportsAutoAction: boolean @@ -29,6 +33,120 @@ export interface AutoAction extends SpecialVisualization { }, 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) + + } + + private async Run() { + this.buttonState.setData("running") + 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}) + } + } + + 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(() => self.Run()); + + 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, + popup: undefined, + zoomToFeatures: true, + features: new StaticFeatureSource(features, false), + 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") + } + )) + } + +} + export default class AutoApplyButton implements SpecialVisualization { public readonly docs: string; public readonly funcName: string = "auto_apply"; @@ -72,109 +190,44 @@ export default class AutoApplyButton implements SpecialVisualization { } constr(state: FeaturePipelineState, tagSource: UIEventSource, 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 = JSON.parse(to_parse) - - if (target_feature_ids.length === 0) { - return new FixedUiElement("No elements found to perform action") + 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 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 options = { + target_layer_id, targetTagRendering, text, icon } - const buttonState = new UIEventSource<"idle" | "running" | "done" | { error: string }>("idle") + return new Lazy(() => { + const to_parse = new UIEventSource(undefined) + // Very ugly hack: read the value every 500ms + UIEventSource.Chronic(500, () => to_parse.data === undefined).addCallback(() => { + const applicable = tagSource.data[argument[1]] + console.log("Current applicable value is: ", applicable) + to_parse.setData(applicable) + }) - 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 = specialRendering.func - await action.applyActionOn(state, featureTags, specialRendering.args) - } + const loading = new Loading("Gathering which elements support auto-apply... "); + return new VariableUiElement(to_parse.map(ids => { + if(ids === undefined){ + return loading } - 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, - popup: undefined, - zoomToFeatures: true, - features: new StaticFeatureSource(features, false), - state, - layerToShow: layer.layerDef, + return new ApplyButton(state, JSON.parse(ids), options); + })) }) + - 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") } } diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index c9dee62c4..0075d2651 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -36,7 +36,7 @@ export default class EditableTagRendering extends Toggle { const editMode = options.editMode ?? new UIEventSource(false) let rendering = EditableTagRendering.CreateRendering(state, tags, configuration, units, editMode); rendering.SetClass(options.innerElementClasses) - if(state.featureSwitchIsDebugging.data){ + if(state.featureSwitchIsDebugging.data || state.featureSwitchIsTesting.data){ rendering = new Combine([ new FixedUiElement(configuration.id).SetClass("self-end subtle"), rendering diff --git a/UI/Popup/ImportButton.ts b/UI/Popup/ImportButton.ts index e2a7ebb22..7ad8842e8 100644 --- a/UI/Popup/ImportButton.ts +++ b/UI/Popup/ImportButton.ts @@ -21,7 +21,7 @@ import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; import CreateWayWithPointReuseAction, {MergePointConfig} from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"; -import OsmChangeAction, {OsmCreateAction} from "../../Logic/Osm/Actions/OsmChangeAction"; +import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"; import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; @@ -37,12 +37,17 @@ import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; import * as conflation_json from "../../assets/layers/conflation/conflation.json"; import {GeoOperations} from "../../Logic/GeoOperations"; import {LoginToggle} from "./LoginButton"; +import {AutoAction} from "./AutoApplyButton"; +import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; +import {Changes} from "../../Logic/Osm/Changes"; +import {ElementStorage} from "../../Logic/ElementStorage"; /** * A helper class for the various import-flows. * An import-flow always starts with a 'Import this'-button. Upon click, a custom confirmation panel is provided */ abstract class AbstractImportButton implements SpecialVisualizations { + protected static importedIds = new Set() public readonly funcName: string public readonly docs: string public readonly args: { name: string, defaultValue?: string, doc: string }[] @@ -157,7 +162,10 @@ ${Utils.special_visualizations_importRequirementDocs} state.featureSwitchUserbadge) - const isImported = tagSource.map(tags => tags._imported === "yes") + const isImported = tagSource.map(tags => { + AbstractImportButton.importedIds.add(tags.id) + return tags._imported === "yes"; + }) /**** THe actual panel showing the import guiding map ****/ @@ -269,7 +277,7 @@ ${Utils.special_visualizations_importRequirementDocs} return new Combine([confirmationMap, confirmButton, cancel]).SetClass("flex flex-col") } - private parseArgs(argsRaw: string[], originalFeatureTags: UIEventSource): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string, newTags: UIEventSource } { + protected parseArgs(argsRaw: string[], originalFeatureTags: UIEventSource): { minzoom: string, max_snap_distance: string, snap_onto_layers: string, icon: string, text: string, tags: string, targetLayer: string, newTags: UIEventSource } { const baseArgs = Utils.ParseVisArgs(this.args, argsRaw) if (originalFeatureTags !== undefined) { @@ -351,7 +359,8 @@ export class ConflateButton extends AbstractImportButton { } -export class ImportWayButton extends AbstractImportButton { +export class ImportWayButton extends AbstractImportButton implements AutoAction { + public readonly supportsAutoAction = true; constructor() { super("import_way_button", @@ -386,6 +395,39 @@ export class ImportWayButton extends AbstractImportButton { ) } + async applyActionOn(state: { layoutToUse: LayoutConfig; changes: Changes, allElements: ElementStorage }, + originalFeatureTags: UIEventSource, + argument: string[]): Promise { + const id = originalFeatureTags.data.id; + if (AbstractImportButton.importedIds.has(originalFeatureTags.data.id) + ) { + return; + } + AbstractImportButton.importedIds.add(originalFeatureTags.data.id) + const args = this.parseArgs(argument, originalFeatureTags) + const feature = state.allElements.ContainingFeatures.get(id) + console.log("Geometry to auto-import is:", feature) + const geom = feature.geometry + let coordinates: [number, number][] + if (geom.type === "LineString") { + coordinates = geom.coordinates + } else if (geom.type === "Polygon") { + coordinates = geom.coordinates[0] + } + + + const mergeConfigs = this.GetMergeConfig(args); + + const action = this.CreateAction( + feature, + args, + state, + mergeConfigs, + coordinates + ) + await state.changes.applyAction(action) + } + canBeImported(feature: any) { const type = feature.geometry.type return type === "LineString" || type === "Polygon" @@ -420,7 +462,24 @@ export class ImportWayButton extends AbstractImportButton { } else if (geom.type === "Polygon") { coordinates = geom.coordinates[0] } + const mergeConfigs = this.GetMergeConfig(args); + + let action = this.CreateAction(feature, args, state, mergeConfigs, coordinates); + + return this.createConfirmPanelForWay( + state, + args, + feature, + originalFeatureTags, + action, + onCancel + ) + + } + + private GetMergeConfig(args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource; targetLayer: string }) + : MergePointConfig[] { const nodesMustMatch = args["snap_to_point_if"]?.split(";")?.map((tag, i) => TagUtils.Tag(tag, "TagsSpec for import button " + i)) const mergeConfigs = [] @@ -446,14 +505,21 @@ export class ImportWayButton extends AbstractImportButton { mergeConfigs.push(mergeConfig) } - let action: OsmCreateAction & { getPreview(): Promise }; + return mergeConfigs; + } + + private CreateAction(feature, + args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource; targetLayer: string }, + state: FeaturePipelineState, + mergeConfigs: any[], + coordinates: [number, number][]) { const coors = feature.geometry.coordinates if (feature.geometry.type === "Polygon" && coors.length > 1) { const outer = coors[0] const inner = [...coors] inner.splice(0, 1) - action = new CreateMultiPolygonWithPointReuseAction( + return new CreateMultiPolygonWithPointReuseAction( args.newTags.data, outer, inner, @@ -463,24 +529,13 @@ export class ImportWayButton extends AbstractImportButton { ) } else { - action = new CreateWayWithPointReuseAction( + return new CreateWayWithPointReuseAction( args.newTags.data, coordinates, state, mergeConfigs ) } - - - return this.createConfirmPanelForWay( - state, - args, - feature, - originalFeatureTags, - action, - onCancel - ) - } } @@ -525,11 +580,11 @@ export class ImportPointButton extends AbstractImportButton { let specialMotivation = undefined let note_id = args.note_id - if (args.note_id !== undefined && isNaN(Number(args.note_id))) { + if (args.note_id !== undefined && isNaN(Number(args.note_id))) { note_id = originalFeatureTags.data[args.note_id] specialMotivation = "source: https://osm.org/note/" + note_id } - + const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon, { theme: state.layoutToUse.id, changeType: "import", diff --git a/UI/Popup/NoteCommentElement.ts b/UI/Popup/NoteCommentElement.ts index 8db51ff81..effb794e7 100644 --- a/UI/Popup/NoteCommentElement.ts +++ b/UI/Popup/NoteCommentElement.ts @@ -29,7 +29,7 @@ export default class NoteCommentElement extends Combine { } else if (comment.action === "closed") { actionIcon = Svg.resolved_svg() } else { - actionIcon = Svg.addSmall_svg() + actionIcon = Svg.speech_bubble_svg() } let user: BaseUIElement diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index a61fb0f46..c33404b1f 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -534,7 +534,7 @@ export default class SpecialVisualizations { funcName: "multi_apply", docs: "A button to apply the tagging of this object onto a list of other features. This is an advanced feature for which you'll need calculatedTags", args: [ - {name: "feature_ids", doc: "A JSOn-serialized list of IDs of features to apply the tagging on"}, + {name: "feature_ids", doc: "A JSON-serialized list of IDs of features to apply the tagging on"}, { name: "keys", doc: "One key (or multiple keys, seperated by ';') of the attribute that should be copied onto the other features." @@ -725,7 +725,7 @@ export default class SpecialVisualizations { textField.SetClass("rounded-l border border-grey") const txt = textField.GetValue() - const addCommentButton = new SubtleButton(Svg.addSmall_svg().SetClass("max-h-7"), t.addCommentPlaceholder) + const addCommentButton = new SubtleButton(Svg.speech_bubble_svg().SetClass("max-h-7"), t.addCommentPlaceholder) .onClick(async () => { const id = tags.data[args[1] ?? "id"] @@ -778,7 +778,9 @@ export default class SpecialVisualizations { new Combine([ new Title("Add a comment"), textField, - new Combine([addCommentButton.SetClass("mr-2"), stateButtons]).SetClass("flex justify-end") + new Combine([ + new Toggle(addCommentButton, undefined, textField.GetValue().map(t => t !==undefined && t.length > 1)).SetClass("mr-2") + , stateButtons]).SetClass("flex justify-end") ]).SetClass("border-2 border-black rounded-xl p-4 block"), t.loginToAddComment, state) } diff --git a/assets/themes/grb_import/grb.json b/assets/themes/grb_import/grb.json index d2c0281d8..e27cc59eb 100644 --- a/assets/themes/grb_import/grb.json +++ b/assets/themes/grb_import/grb.json @@ -12,7 +12,7 @@ "hu": "Ez a sablon a flandriai GRB épületimportálás automatizlását kívánja megkönnyíteni." }, "maintainer": "", - "icon": "./assets/themes/grb_import/housenumber_blank.svg", + "icon": "./assets/themes/grb_import/logo.svg", "version": "0", "startLat": 51.0249, "startLon": 4.026489, @@ -447,7 +447,7 @@ "title": "GRB outline", "calculatedTags": [ "_overlaps_with_buildings=feat.overlapWith('osm-buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)", - "_overlaps_with=feat.get('_overlaps_with_buildings').filter(f => f.overlap > 1 /* square meter */ )[0] ?? ''", + "_overlaps_with=feat.get('_overlaps_with_buildings').find(f => f.overlap > 1 /* square meter */ )", "_osm_obj:source:ref=feat.get('_overlaps_with')?.feat?.properties['source:geometry:ref']", "_osm_obj:id=feat.get('_overlaps_with')?.feat?.properties?.id", "_osm_obj:source:date=feat.get('_overlaps_with')?.feat?.properties['source:geometry:date'].replace(/\\//g, '-')", @@ -650,10 +650,63 @@ } } ] + }, + { + "builtin": "current_view", + "override": { + "calculatedTags": [ + "_overlapping=Number(feat.properties.zoom) >= 16 ? feat.overlapWith('grb').map(ff => ff.feat.properties) : undefined", + "_applicable=feat.get('_overlapping')?.filter(p => (p._imported_osm_object_found !== 'true' && p._intersects_with_other_features === ''))?.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 importable buildins in view" + } + ] + }, + { + "id": "autoapply", + "render": "{auto_apply(grb, _applicable, Import-button, Import or conflate all non-conflicting buildings in view)}", + "mappings": [ + { + "if": "zoom<16", + "then": "Zoom in more to import" + } + ] + } + ], + "+mapRendering": [ + { + "location": [ + "point" + ], + "icon": { + "render": "./assets/svg/robot.svg" + }, + "iconSize": "15,15,center" + } + ] + + + } } ], "hideFromOverview": true, "defaultBackgroundId": "AGIVFlandersGRB", "overpassMaxZoom": 17, "osmApiTileSize": 17 -} \ No newline at end of file +} diff --git a/assets/themes/grb_import/missing_streets.json b/assets/themes/grb_import/missing_streets.json index 38228f780..4bcd22c2a 100644 --- a/assets/themes/grb_import/missing_streets.json +++ b/assets/themes/grb_import/missing_streets.json @@ -74,7 +74,7 @@ "builtin": "crab_address", "override": { "source": { - "geoJson": "http://127.0.0.1:8080/tile_{z}_{x}_{y}.geojson", + "geoJson": "https://raw.githubusercontent.com/pietervdvn/MapComplete-data/main/CRAB_2021_10_26/tile_{z}_{x}_{y}.geojson", "geoJsonZoomLevel": 18 }, "mapRendering": [ @@ -135,7 +135,6 @@ "tagRenderings": [ { "id": "apply_streetname", - "group": "auto", "render": "{tag_apply(addr:street=$_name_to_apply ,Apply the CRAB-street onto this building)}", "mappings": [ {