diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 7167c1dd2..58f7cf823 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -8,13 +8,13 @@ import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; import {Translation} from "../../UI/i18n/Translation"; import Img from "../../UI/Base/Img"; import Svg from "../../Svg"; -import {SubstitutedTranslation} from "../../UI/SpecialVisualizations"; import {Utils} from "../../Utils"; import Combine from "../../UI/Base/Combine"; import {VariableUiElement} from "../../UI/Base/VariableUIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import {FixedUiElement} from "../../UI/Base/FixedUiElement"; import {UIElement} from "../../UI/UIElement"; +import {SubstitutedTranslation} from "../../UI/SubstitutedTranslation"; export default class LayerConfig { diff --git a/InitUiElements.ts b/InitUiElements.ts index fd7427c9f..1fe16d67f 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -336,7 +336,7 @@ export class InitUiElements { } let features = featuresFreshness.map(ff => ff.feature); features.forEach(feature => { - State.state.allElements.addElement(feature); + State.state.allElements.addOrGetElement(feature); if(Hash.hash.data === feature.properties.id.replace("/","_")){ State.state.selectedElement.setData(feature); diff --git a/Logic/ElementStorage.ts b/Logic/ElementStorage.ts index c127d2c6a..d300ec0bb 100644 --- a/Logic/ElementStorage.ts +++ b/Logic/ElementStorage.ts @@ -16,7 +16,7 @@ export class ElementStorage { } addElement(element): UIEventSource { - const eventSource = new UIEventSource(element.properties); + const eventSource = new UIEventSource(element.properties, "tags of "+element.properties.id); this._elements[element.properties.id] = eventSource; return eventSource; } diff --git a/Logic/Osm/OsmConnection.ts b/Logic/Osm/OsmConnection.ts index 1b614e86d..631a92969 100644 --- a/Logic/Osm/OsmConnection.ts +++ b/Logic/Osm/OsmConnection.ts @@ -69,7 +69,7 @@ export class OsmConnection { } - this.userDetails = new UIEventSource(new UserDetails()); + this.userDetails = new UIEventSource(new UserDetails(), "userDetails"); this.userDetails.data.dryRun = dryRun; this._dryRun = dryRun; diff --git a/Logic/UIEventSource.ts b/Logic/UIEventSource.ts index 3744ab261..aa7ef8ee1 100644 --- a/Logic/UIEventSource.ts +++ b/Logic/UIEventSource.ts @@ -13,7 +13,7 @@ export class UIEventSource { return []; } // @ts-ignore - window.mcperf = () => { + window.mapcomplete_performance = () => { console.log(UIEventSource.allSources.length, "uieventsources created"); const copy = [...UIEventSource.allSources]; copy.sort((a,b) => b._callbacks.length - a._callbacks.length); diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 6f2922fe4..46e2de255 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -1,10 +1,10 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig"; import {UIElement} from "../UIElement"; -import {SubstitutedTranslation} from "../SpecialVisualizations"; import {Utils} from "../../Utils"; import Combine from "../Base/Combine"; import {TagUtils} from "../../Logic/Tags"; +import {SubstitutedTranslation} from "../SubstitutedTranslation"; /*** * Displays the correct value for a known tagrendering @@ -64,7 +64,7 @@ export default class TagRenderingAnswer extends UIElement { const tr = this._configuration.GetRenderValue(tags); if (tr !== undefined) { - this._content = new SubstitutedTranslation(tr, this._tags); + this._content = SubstitutedTranslation.construct(tr, this._tags); return this._content.Render(); } diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 9c2e7eb93..515bbb80b 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -6,7 +6,6 @@ import {InputElement} from "../Input/InputElement"; import {And, Tag, TagsFilter, TagUtils} from "../../Logic/Tags"; import ValidatedTextField from "../Input/ValidatedTextField"; import {FixedInputElement} from "../Input/FixedInputElement"; -import {SubstitutedTranslation} from "../SpecialVisualizations"; import {RadioButton} from "../Input/RadioButton"; import {Utils} from "../../Utils"; import CheckBoxes from "../Input/Checkboxes"; @@ -19,6 +18,7 @@ import Translations from "../i18n/Translations"; import {FixedUiElement} from "../Base/FixedUiElement"; import {Translation} from "../i18n/Translation"; import Constants from "../../Models/Constants"; +import {SubstitutedTranslation} from "../SubstitutedTranslation"; /** * Shows the question element. @@ -203,7 +203,7 @@ export default class TagRenderingQuestion extends UIElement { return undefined; } return new FixedInputElement( - new SubstitutedTranslation(mapping.then, this._tags), + SubstitutedTranslation.construct(mapping.then, this._tags), mapping.if, (t0, t1) => t1.isEquivalent(t0)); } diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index cbf3d1967..ad27c6455 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -116,12 +116,12 @@ export default class ShowDataLayer { const popup = L.popup({ autoPan: true, closeOnEscapeKey: true, + closeButton: false }, leafletLayer); const tags = State.state.allElements.getEventSourceFor(feature); const uiElement: LazyElement = new LazyElement(() =>new Combine([ new FeatureInfoBox(tags, layer, () => { - console.log("Closing the popup!") State.state.selectedElement.setData(undefined); popup.remove(); diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 41d3dd2ed..c37bd9095 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -5,9 +5,7 @@ import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"; import {ImageCarousel} from "./Image/ImageCarousel"; import Combine from "./Base/Combine"; import {FixedUiElement} from "./Base/FixedUiElement"; -import Locale from "../UI/i18n/Locale"; import {ImageUploadFlow} from "./Image/ImageUploadFlow"; -import {Translation} from "./i18n/Translation"; import ShareButton from "./BigComponents/ShareButton"; import Svg from "../Svg"; @@ -19,93 +17,6 @@ import OpeningHoursVisualization from "./OpeningHours/OhVisualization"; import State from "../State"; -export class SubstitutedTranslation extends UIElement { - private readonly tags: UIEventSource; - private readonly translation: Translation; - private content: UIElement[]; - - constructor( - translation: Translation, - tags: UIEventSource) { - super(tags); - this.translation = translation; - this.tags = tags; - const self = this; - tags.addCallbackAndRun(() => { - self.content = self.CreateContent(); - self.Update(); - }); - - Locale.language.addCallback(() => { - self.content = self.CreateContent(); - self.Update(); - }); - - } - - InnerRender(): string { - return new Combine(this.content).Render(); - } - - private CreateContent(): UIElement[] { - let txt = this.translation?.txt; - if (txt === undefined) { - return [] - } - const tags = this.tags.data; - txt = SubstitutedTranslation.SubstituteKeys(txt, tags); - return this.EvaluateSpecialComponents(txt); - } - - public static SubstituteKeys(txt: string, tags: any) { - for (const key in tags) { - // Poor mans replace all - txt = txt.split("{" + key + "}").join(tags[key]); - } - return txt; - } - - private EvaluateSpecialComponents(template: string): UIElement[] { - - for (const knownSpecial of SpecialVisualizations.specialVisualizations) { - - // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' - const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)}(.*)`); - if (matched != null) { - - // We found a special component that should be brought to live - const partBefore = this.EvaluateSpecialComponents(matched[1]); - const argument = matched[2].trim(); - const partAfter = this.EvaluateSpecialComponents(matched[3]); - try { - const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); - if (argument.length > 0) { - const realArgs = argument.split(",").map(str => str.trim()); - for (let i = 0; i < realArgs.length; i++) { - if (args.length <= i) { - args.push(realArgs[i]); - } else { - args[i] = realArgs[i]; - } - } - } - - - const element = knownSpecial.constr(State.state, this.tags, args); - return [...partBefore, element, ...partAfter] - } catch (e) { - console.error(e); - return [...partBefore, new FixedUiElement(`Failed loading ${knownSpecial.funcName}(${matched[2]}): ${e}`), ...partAfter] - } - } - } - - // IF we end up here, no changes have to be made - return [new FixedUiElement(template)]; - } - -} - export default class SpecialVisualizations { public static specialVisualizations: { diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts new file mode 100644 index 000000000..81637d247 --- /dev/null +++ b/UI/SubstitutedTranslation.ts @@ -0,0 +1,113 @@ +import {UIElement} from "./UIElement"; +import {UIEventSource} from "../Logic/UIEventSource"; +import {Translation} from "./i18n/Translation"; +import Locale from "./i18n/Locale"; +import Combine from "./Base/Combine"; +import State from "../State"; +import {FixedUiElement} from "./Base/FixedUiElement"; +import SpecialVisualizations from "./SpecialVisualizations"; + +export class SubstitutedTranslation extends UIElement { + private static cachedTranslations: Map, SubstitutedTranslation>> = new Map, SubstitutedTranslation>>(); + private readonly tags: UIEventSource; + private readonly translation: Translation; + private content: UIElement[]; + + private constructor( + translation: Translation, + tags: UIEventSource) { + super(tags); + this.translation = translation; + this.tags = tags; + const self = this; + tags.addCallbackAndRun(() => { + self.content = self.CreateContent(); + self.Update(); + }); + + Locale.language.addCallback(() => { + self.content = self.CreateContent(); + self.Update(); + }); + + } + + public static construct( + translation: Translation, + tags: UIEventSource): SubstitutedTranslation { + if (!this.cachedTranslations.has(translation)) { + this.cachedTranslations.set(translation, new Map, SubstitutedTranslation>()); + } + const innerMap = this.cachedTranslations.get(translation); + + const cachedTranslation = innerMap.get(tags); + if (cachedTranslation !== undefined) { + return cachedTranslation; + } + const st = new SubstitutedTranslation(translation, tags); + innerMap.set(tags, st); + return st; + } + + public static SubstituteKeys(txt: string, tags: any) { + for (const key in tags) { + // Poor mans replace all + txt = txt.split("{" + key + "}").join(tags[key]); + } + return txt; + } + + InnerRender(): string { + return new Combine(this.content).Render(); + } + + private CreateContent(): UIElement[] { + let txt = this.translation?.txt; + if (txt === undefined) { + return [] + } + const tags = this.tags.data; + txt = SubstitutedTranslation.SubstituteKeys(txt, tags); + return this.EvaluateSpecialComponents(txt); + } + + private EvaluateSpecialComponents(template: string): UIElement[] { + + for (const knownSpecial of SpecialVisualizations.specialVisualizations) { + + // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way' + const matched = template.match(`(.*){${knownSpecial.funcName}\\((.*?)\\)}(.*)`); + if (matched != null) { + + // We found a special component that should be brought to live + const partBefore = this.EvaluateSpecialComponents(matched[1]); + const argument = matched[2].trim(); + const partAfter = this.EvaluateSpecialComponents(matched[3]); + try { + const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); + if (argument.length > 0) { + const realArgs = argument.split(",").map(str => str.trim()); + for (let i = 0; i < realArgs.length; i++) { + if (args.length <= i) { + args.push(realArgs[i]); + } else { + args[i] = realArgs[i]; + } + } + } + + + const element = knownSpecial.constr(State.state, this.tags, args); + return [...partBefore, element, ...partAfter] + } catch (e) { + console.error(e); + return [...partBefore, new FixedUiElement(`Failed loading ${knownSpecial.funcName}(${matched[2]}): ${e}`), ...partAfter] + } + } + } + + // IF we end up here, no changes have to be made + return [new FixedUiElement(template)]; + } + +} \ No newline at end of file diff --git a/test/Tag.spec.ts b/test/Tag.spec.ts index d3a60f1d2..93ce901d4 100644 --- a/test/Tag.spec.ts +++ b/test/Tag.spec.ts @@ -9,10 +9,10 @@ import Translations from "../UI/i18n/Translations"; import {UIEventSource} from "../Logic/UIEventSource"; import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; import EditableTagRendering from "../UI/Popup/EditableTagRendering"; -import {SubstitutedTranslation} from "../UI/SpecialVisualizations"; import {Translation} from "../UI/i18n/Translation"; import {OH, OpeningHour} from "../UI/OpeningHours/OpeningHours"; import PublicHolidayInput from "../UI/OpeningHours/PublicHolidayInput"; +import {SubstitutedTranslation} from "../UI/SubstitutedTranslation"; new T([ @@ -109,7 +109,7 @@ new T([ equal(undefined, tr.GetRenderValue({"foo": "bar"})); equal("Has no name", tr.GetRenderValue({"noname": "yes"})?.txt); equal("Ook een {name}", tr.GetRenderValue({"name": "xyz"})?.txt); - equal("Ook een xyz", new SubstitutedTranslation( tr.GetRenderValue({"name": "xyz"}), + equal("Ook een xyz", SubstitutedTranslation.construct( tr.GetRenderValue({"name": "xyz"}), new UIEventSource({"name":"xyz"})).InnerRender()); equal(undefined, tr.GetRenderValue({"foo": "bar"}));