From 917fb69990d3fc0241b11b0ed66fe3e4f6afba95 Mon Sep 17 00:00:00 2001
From: "mathias.chouet" <mathias.chouet@irstea.fr>
Date: Wed, 16 Oct 2019 16:04:36 +0200
Subject: [PATCH] Solveur GUI

interface for Solveur module
draw Solveur relations in modules diagram view
---
 .../calculators/solveur/solveur.config.json   |  6 +-
 src/app/calculators/solveur/solveur.en.json   |  1 +
 src/app/calculators/solveur/solveur.fr.json   |  1 +
 .../calculator.component.ts                   |  7 ++
 .../modules-diagram.component.ts              | 20 +++-
 .../select-field-line.component.ts            | 12 ++-
 .../definition/concrete/form-solveur.ts       | 76 +++++++++++----
 .../formulaire/definition/form-definition.ts  |  6 ++
 src/app/formulaire/fieldset.ts                | 37 +++++++-
 src/app/formulaire/ngparam.ts                 |  5 +-
 src/app/formulaire/select-field-nub.ts        | 37 ++++++++
 src/app/formulaire/select-field-parameter.ts  | 36 ++++++++
 src/app/formulaire/select-field-reference.ts  | 92 +++++++++++++++++++
 src/app/formulaire/select-field.ts            | 24 +----
 src/app/util.ts                               | 11 +++
 src/locale/messages.en.json                   | 11 ++-
 src/locale/messages.fr.json                   | 11 ++-
 17 files changed, 336 insertions(+), 57 deletions(-)
 create mode 100644 src/app/formulaire/select-field-nub.ts
 create mode 100644 src/app/formulaire/select-field-parameter.ts
 create mode 100644 src/app/formulaire/select-field-reference.ts

diff --git a/src/app/calculators/solveur/solveur.config.json b/src/app/calculators/solveur/solveur.config.json
index 7e6ac3bbd..40b0bbb8c 100644
--- a/src/app/calculators/solveur/solveur.config.json
+++ b/src/app/calculators/solveur/solveur.config.json
@@ -5,7 +5,8 @@
         "fields": [
             {
                 "id": "select_target_nub",
-                "type": "select",
+                "type": "select_reference",
+                "reference": "nub",
                 "source": "solveur_target"
             },
             "Ytarget"
@@ -17,7 +18,8 @@
         "fields": [
             {
                 "id": "select_searched_param",
-                "type": "select",
+                "type": "select_reference",
+                "reference": "parameter",
                 "source": "solveur_searched"
             },
             "Xinit"
diff --git a/src/app/calculators/solveur/solveur.en.json b/src/app/calculators/solveur/solveur.en.json
index cbf67bcf2..df14b3764 100644
--- a/src/app/calculators/solveur/solveur.en.json
+++ b/src/app/calculators/solveur/solveur.en.json
@@ -4,6 +4,7 @@
 
     "Ytarget": "Value of target parameter",
     "Xinit": "Initial value for searched parameter",
+    "X": "Value for searched parameter",
 
     "select_target_nub": "Module and parameter to calculate",
     "select_searched_param": "Searched parameter"
diff --git a/src/app/calculators/solveur/solveur.fr.json b/src/app/calculators/solveur/solveur.fr.json
index 86899fc0d..1439bd8da 100644
--- a/src/app/calculators/solveur/solveur.fr.json
+++ b/src/app/calculators/solveur/solveur.fr.json
@@ -4,6 +4,7 @@
 
     "Ytarget": "Valeur du paramètre cible",
     "Xinit": "Valeur initiale du paramètre recherché",
+    "X": "Valeur du paramètre recherché",
 
     "select_target_nub": "Module et paramètre à calculer",
     "select_searched_param": "Paramètre recherché"
diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts
index 82dc8d735..365fa61e7 100644
--- a/src/app/components/generic-calculator/calculator.component.ts
+++ b/src/app/components/generic-calculator/calculator.component.ts
@@ -376,6 +376,8 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
                     this._calculatorNameComponent.model = this._formulaire;
                     // reload localisation in all cases
                     this.formulaireService.loadUpdateFormulaireLocalisation(this._formulaire);
+                    // call Form init hook
+                    this._formulaire.onCalculatorInit();
                     break;
             }
         } else if (sender instanceof FormulaireDefinition) {
@@ -530,6 +532,11 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
         return (this.isPAB || this.isMRC);
     }
 
+    // true if current Nub is Solveur
+    public get isSolveur() {
+        return this.is(CalculatorType.Solveur);
+    }
+
     // true if current Nub is PAB
     public get isPAB() {
         return this.is(CalculatorType.Pab);
diff --git a/src/app/components/modules-diagram/modules-diagram.component.ts b/src/app/components/modules-diagram/modules-diagram.component.ts
index 68297ad6c..d84bfdac7 100644
--- a/src/app/components/modules-diagram/modules-diagram.component.ts
+++ b/src/app/components/modules-diagram/modules-diagram.component.ts
@@ -16,7 +16,8 @@ import {
     LoiDebit,
     Nub,
     MacrorugoCompound,
-    Pab
+    Pab,
+    Solveur
 } from "jalhyd";
 
 import { I18nService } from "../../services/internationalisation.service";
@@ -43,7 +44,7 @@ export class ModulesDiagramComponent implements AfterContentInit, AfterViewCheck
     private nativeElement: any;
 
     @ViewChild("diagram", { static: true })
-    public diagram;
+    public diagram: any;
 
     public error: boolean;
 
@@ -179,7 +180,7 @@ export class ModulesDiagramComponent implements AfterContentInit, AfterViewCheck
                 // simple Nub (no children)
                 def.push(f.uid + "(\"" + f.calculatorName + "\")");
             }
-            // fnid all linked parameters
+            // find all linked parameters
             for (const p of nub.parameterIterator) {
                 if (p.valueMode === ParamValueMode.LINK && p.isReferenceDefined()) {
                     const target = p.referencedValue.nub;
@@ -190,6 +191,19 @@ export class ModulesDiagramComponent implements AfterContentInit, AfterViewCheck
                     def.push(nub.uid + "-->|" + symb + "|" + target.uid);
                 }
             }
+            // add Solveur links
+            if (nub instanceof Solveur) {
+                const ntc = nub.nubToCalculate;
+                const sp = nub.searchedParameter;
+                const reads = this.intlService.localizeText("INFO_DIAGRAM_SOLVEUR_READS");
+                const finds = this.intlService.localizeText("INFO_DIAGRAM_SOLVEUR_FINDS");
+                if (ntc !== undefined) {
+                    def.push(nub.uid + "-->|" + reads + ":" + ntc.calculatedParam.symbol + "|" + ntc.uid);
+                }
+                if (sp !== undefined) {
+                    def.push(sp.nubUid + "-->|" + finds + ":" + sp.symbol + "|" + nub.uid);
+                }
+            }
         }
 
         return def.join("\n");
diff --git a/src/app/components/select-field-line/select-field-line.component.ts b/src/app/components/select-field-line/select-field-line.component.ts
index 04a69146b..e9615dbed 100644
--- a/src/app/components/select-field-line/select-field-line.component.ts
+++ b/src/app/components/select-field-line/select-field-line.component.ts
@@ -1,8 +1,9 @@
-import { Component, Input } from "@angular/core";
+import { Component, Input, OnInit } from "@angular/core";
 
 import { SelectField } from "../../formulaire/select-field";
 import { SelectEntry } from "../../formulaire/select-entry";
 import { I18nService } from "../../services/internationalisation.service";
+import { SelectFieldReference } from "../../formulaire/select-field-reference";
 
 @Component({
     selector: "select-field-line",
@@ -11,7 +12,7 @@ import { I18nService } from "../../services/internationalisation.service";
         "./select-field-line.component.scss"
     ]
 })
-export class SelectFieldLineComponent {
+export class SelectFieldLineComponent implements OnInit {
 
     /** aide en ligne */
     protected helpLink: string | { [key: string]: string };
@@ -83,4 +84,11 @@ export class SelectFieldLineComponent {
     public get uitextOpenHelp() {
         return this.i18nService.localizeText("INFO_CALCULATOR_OPEN_HELP");
     }
+
+    // called every time we navigate to the module
+    ngOnInit(): void {
+        if (this._select instanceof SelectFieldReference) {
+            this._select.updateEntries();
+        }
+    }
 }
diff --git a/src/app/formulaire/definition/concrete/form-solveur.ts b/src/app/formulaire/definition/concrete/form-solveur.ts
index 09d2fe6ab..e04fc1665 100644
--- a/src/app/formulaire/definition/concrete/form-solveur.ts
+++ b/src/app/formulaire/definition/concrete/form-solveur.ts
@@ -1,7 +1,9 @@
-import { IObservable } from "jalhyd";
+import { IObservable, ParamDefinition, Solveur } from "jalhyd";
 
 import { FormulaireBase } from "./form-base";
-import { FieldSet } from "../../fieldset";
+import { SelectFieldNub } from "../../select-field-nub";
+import { SelectFieldParameter } from "../../select-field-parameter";
+import { NgParameter } from "../../ngparam";
 
 /**
  * Formulaire pour les Solveurs
@@ -20,33 +22,73 @@ export class FormulaireSolveur extends FormulaireBase {
         this._searchedParamSelectId = this.getOption(json, "searchedParamSelectId");
     }
 
-    public afterParseFieldset(fs: FieldSet) {
-        if (this._searchedParamSelectId) {
-            const sel = fs.getFormulaireNodeById(this._searchedParamSelectId);
+    protected completeParse(json: {}) {
+        super.completeParse(json);
+        if (this._targetNubSelectId) {
+            const sel = this.getFormulaireNodeById(this._targetNubSelectId);
             if (sel) {
-                fs.properties.addObserver(this);
+                sel.addObserver(this);
             }
         }
-        if (this._targetNubSelectId) {
-            const sel = fs.getFormulaireNodeById(this._targetNubSelectId);
+        if (this._searchedParamSelectId) {
+            const sel = this.getFormulaireNodeById(this._searchedParamSelectId);
             if (sel) {
-                fs.properties.addObserver(this);
+                sel.addObserver(this);
             }
         }
+
+    }
+
+    private debugState() {
+        const sol = this._currentNub as Solveur;
+        const ntc = sol.nubToCalculate;
+        const spm = sol.searchedParameter;
+        console.log(
+            `ETAT:\n X.singleValue=${sol.prms.X.singleValue}\n Y.singleValue=${sol.prms.Y.singleValue}`
+            + `\n Xinit.singleValue=${sol.prms.Xinit.singleValue}\n Ytarget.singleValue=${sol.prms.Ytarget.singleValue}`
+            + `\n searchedParam.singleValue=${spm.singleValue}`
+        );
     }
 
     // interface Observer
 
     public update(sender: IObservable, data: any) {
         super.update(sender, data);
-        console.log("FormulaireSolveur().update", sender.constructor.name, data);
-        if (data.action === "propertyChange") {
-            if (data.name === "gridType") {
-                this.reset();
-                // Inclined grids have more input fields (OEntH and cIncl)
-                this.getFieldsetById("fs_grille").updateFields();
-                // Alpha and Beta are not always shown
-                this.getFieldsetById("fs_plan").updateFields();
+        if (sender instanceof SelectFieldNub) {
+            if (data.action === "select") {
+                // update Solveur property: Nub to calculate
+                console.log("(i) update Nub to calculate");
+                try {
+                    // if searchedParam is set to a value that won't be available anymore
+                    // once nubToCalculate is updated, setPropValue throws an error, but
+                    // nubToCalculate is updated anyway; here, just inhibit the error
+                    this._currentNub.properties.setPropValue("nubToCalculate", data.value.value);
+                } catch (e) { }
+                // refresh parameters selector
+                const sel = this.getFormulaireNodeById(this._searchedParamSelectId) as SelectFieldParameter;
+                if (sel) {
+                    console.log("(ii) update Parameters entries");
+                    sel.updateEntries();
+                    this.debugState();
+                    // reflect changes in GUI
+                    const inputYtarget = this.getFormulaireNodeById("Ytarget") as NgParameter;
+                    inputYtarget.notifyValueModified(this);
+                }
+            }
+        }
+        if (sender instanceof SelectFieldParameter) {
+            if (data.action === "select") {
+                // update Solveur property: searched Parameter
+                console.log("(i) update searched Parameter");
+                const p: ParamDefinition = data.value.value;
+                this._currentNub.properties.setPropValue(
+                    "searchedParameter",
+                    p.nubUid + "/" + p.symbol
+                );
+                this.debugState();
+                // reflect changes in GUI
+                const inputXinit = this.getFormulaireNodeById("Xinit") as NgParameter;
+                inputXinit.notifyValueModified(this);
             }
         }
     }
diff --git a/src/app/formulaire/definition/form-definition.ts b/src/app/formulaire/definition/form-definition.ts
index b178d3a3d..5aa007c8a 100644
--- a/src/app/formulaire/definition/form-definition.ts
+++ b/src/app/formulaire/definition/form-definition.ts
@@ -452,6 +452,12 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         return new TopFormulaireElementIterator(this);
     }
 
+    /**
+     * Appelé par CalculatorComponent lrosque le Formulaire est chargé dans la vue,
+     * c'est à dire lorsqu'on affiche un module de calcul à l'écran
+     */
+    public onCalculatorInit() {}
+
     //  interface Observer
 
     public update(sender: any, data: any) {
diff --git a/src/app/formulaire/fieldset.ts b/src/app/formulaire/fieldset.ts
index 07c6d3be1..e892c0c2c 100644
--- a/src/app/formulaire/fieldset.ts
+++ b/src/app/formulaire/fieldset.ts
@@ -21,6 +21,8 @@ import { FormulaireDefinition } from "./definition/form-definition";
 import { StringMap } from "../stringmap";
 import { FormulaireNode } from "./formulaire-node";
 import { FieldsetContainer } from "./fieldset-container";
+import { SelectFieldNub } from "./select-field-nub";
+import { SelectFieldParameter } from "./select-field-parameter";
 
 export class FieldSet extends FormulaireElement implements Observer {
     /**
@@ -97,6 +99,28 @@ export class FieldSet extends FormulaireElement implements Observer {
         return res;
     }
 
+    private parse_select_reference(json: {}): SelectField {
+        const refType = json["reference"];
+        const source = json["source"];
+        let res: SelectField;
+        if (source === undefined || source === "") {
+            throw new Error(`Fieldset.parse_select_reference(): "source" must not be empty`);
+        }
+        switch (refType) {
+            case "nub": // @TODO upstreamNub / downstreamNub ?
+                res = new SelectFieldNub(this, source);
+                break;
+            case "parameter":
+                res = new SelectFieldParameter(this, source);
+                break;
+            default:
+                throw new Error(`Fieldset.parse_select_reference(): unknown reference type ${refType}`);
+        }
+        res.parseConfig(json);
+        res.addObserver(this);
+        return res;
+    }
+
     public get properties(): Props {
         return this.nub.properties;
     }
@@ -186,6 +210,11 @@ export class FieldSet extends FormulaireElement implements Observer {
                     this.addField(param);
                     break;
 
+                case "select_reference":
+                    param = this.parse_select_reference(field);
+                    this.addField(param);
+                    break;
+
             }
         }
     }
@@ -260,7 +289,7 @@ export class FieldSet extends FormulaireElement implements Observer {
                 this.setSelectValueFromProperty("select_regime", "regime");
                 break;
 
-            case "fs_target": // Solveur
+            /* case "fs_target": // Solveur
                 this.setSelectValueFromProperty("select_target_nub", "nubToCalculate");
                 break;
 
@@ -274,11 +303,13 @@ export class FieldSet extends FormulaireElement implements Observer {
                         try {
                             selectField.setValue(selectElement);
                         } catch (e) {
-                            console.error(`fieldset.updateFields(): cannot set ${X.parentNub.uid}/${X.symbol} on <select> "select_searched_param"`);
+                            console.error(
+                                `fieldset.updateFields(): cannot set ${X.parentNub.uid}/${X.symbol} on <select> "select_searched_param"`
+                            );
                         }
                     }
                 }
-                break;
+                break; */
         }
     }
 
diff --git a/src/app/formulaire/ngparam.ts b/src/app/formulaire/ngparam.ts
index 5387c058d..b5d15d216 100644
--- a/src/app/formulaire/ngparam.ts
+++ b/src/app/formulaire/ngparam.ts
@@ -113,9 +113,8 @@ export class NgParameter extends InputField implements Observer {
                         const cVal = ref.nub.result.getCalculatedValues();
                         valuePreview = fv(cVal[0]) + " … " + fv(cVal[cVal.length - 1]);
                     } else {
-                        const vCalc = ref.nub.result.vCalc;
-                        if (vCalc) {
-                            valuePreview = fv(vCalc);
+                        if (ref.nub.result.resultElements.length > 0 && ref.nub.result.vCalc) {
+                            valuePreview = fv(ref.nub.result.vCalc);
                         } else {
                             // computation has been run but has failed
                             valuePreview = i18n.localizeText("INFO_PARAMFIELD_CALCULATION_FAILED");
diff --git a/src/app/formulaire/select-field-nub.ts b/src/app/formulaire/select-field-nub.ts
new file mode 100644
index 000000000..345699fc1
--- /dev/null
+++ b/src/app/formulaire/select-field-nub.ts
@@ -0,0 +1,37 @@
+import { SelectFieldReference } from "./select-field-reference";
+import { SelectEntry } from "./select-entry";
+import { ServiceFactory } from "../services/service-factory";
+import { decodeHtml } from "../util";
+
+import { Session } from "jalhyd";
+
+/**
+ * A select field that populates itself with references to Nubs
+ */
+export class SelectFieldNub extends SelectFieldReference {
+
+    protected initSelectedValue() {}
+
+    /**
+     * Populates entries with available references
+     */
+    protected populate() {
+        switch (this._source) {
+            case "solveur_target": // Solveur, paramètre cible (à calculer)
+                // find all Nubs having at least one link to another Nub's result
+                const fs = ServiceFactory.instance.formulaireService;
+                const downstreamNubs = Session.getInstance().getDownstreamNubs();
+                for (const dn of downstreamNubs) {
+                    const calc = fs.getFormulaireFromId(dn.uid).calculatorName;
+                    let label = calc;
+                    if (dn.calculatedParam !== undefined) {
+                        const varName = fs.expandVariableName(dn.calcType, dn.calculatedParam.symbol);
+                        label += ` / ${varName} (${dn.calculatedParam.symbol})`;
+                    }
+                    this.addEntry(new SelectEntry(this._entriesBaseId + dn.uid, dn.uid, decodeHtml(label)));
+                }
+                break;
+        }
+    }
+
+}
diff --git a/src/app/formulaire/select-field-parameter.ts b/src/app/formulaire/select-field-parameter.ts
new file mode 100644
index 000000000..d84ad50ac
--- /dev/null
+++ b/src/app/formulaire/select-field-parameter.ts
@@ -0,0 +1,36 @@
+import { SelectFieldReference } from "./select-field-reference";
+import { SelectEntry } from "./select-entry";
+import { decodeHtml } from "../util";
+import { ServiceFactory } from "../services/service-factory";
+
+import { Nub, Solveur } from "jalhyd";
+
+/**
+ * A select field that populates itself with references to ParamDefinitions
+ */
+export class SelectFieldParameter extends SelectFieldReference {
+
+    protected initSelectedValue() {}
+
+    /**
+     * Populates entries with available references
+     */
+    protected populate() {
+        switch (this._source) {
+            case "solveur_searched": // Solveur, paramètre recherché (à faire varier)
+                // find all non-calculated, non-linked parameters of all Nubs that
+                // the current "target" Nub depends on (if any)
+                const fs = ServiceFactory.instance.formulaireService;
+                const ntc: Nub = (this.parentForm.currentNub as Solveur).nubToCalculate;
+                const searchableParams = Solveur.getDependingNubsSearchableParams(ntc);
+                for (const p of searchableParams) {
+                    const calc = fs.getFormulaireFromId(p.parentNub.uid).calculatorName;
+                    const varName = fs.expandVariableName(p.parentNub.calcType, p.symbol);
+                    const label = `${p.symbol} - ${varName} (${calc})`;
+                    this.addEntry(new SelectEntry(this._entriesBaseId + p.nubUid + "_" + p.symbol, p, decodeHtml(label)));
+                }
+                break;
+        }
+    }
+
+}
diff --git a/src/app/formulaire/select-field-reference.ts b/src/app/formulaire/select-field-reference.ts
new file mode 100644
index 000000000..a84b272ea
--- /dev/null
+++ b/src/app/formulaire/select-field-reference.ts
@@ -0,0 +1,92 @@
+import { SelectField } from "./select-field";
+import { SelectEntry } from "./select-entry";
+import { FormulaireNode } from "./formulaire-node";
+
+/**
+ * A select field that populates itself with references to
+ * available objects (for ex. Nub or ParamDefinition)
+ */
+export abstract class SelectFieldReference extends SelectField {
+
+    /** source identifier for populate() method */
+    protected _source: string;
+
+    constructor(parent: FormulaireNode, source: string) {
+        super(parent);
+        this._source = source;
+        this.initSelectedValue();
+    }
+
+    protected abstract initSelectedValue();
+
+    /**
+     * Populates entries with available references
+     */
+    protected abstract populate();
+
+    /**
+     * Reloads available entries, trying to keep the current selected
+     * value; does not notify observers if value did not change
+     */
+    public updateEntries() {
+        // store previous selected entry
+        const pse = this._selectedEntry;
+        // empty
+        this.clearEntries();
+        // populate
+        this.populate();
+        // keep previously selected entry if possible
+        if (pse && pse.id) {
+            this.setValueFromId(pse.id);
+        }
+        // if no entry is available anymore, unset value
+        if (this.entries.length === 0) {
+            super.setValue(undefined);
+        }
+    }
+
+    /**
+     * Updates selectedValue; notifies observers only if
+     * value.id has changed
+     */
+    public setValue(v: SelectEntry) {
+        const previousSelectedEntry = this._selectedEntry;
+        this._selectedEntry = v;
+        if (
+            ! previousSelectedEntry
+            || (previousSelectedEntry.id !== v.id)
+        ) {
+            console.log(`--> select, setValue: ${v.value}`);
+            this.notifyObservers({
+                "action": "select",
+                "value": v
+            }, this);
+        }
+    }
+
+    /**
+     * Sets value from given ID; if it was not found, sets the
+     * first available entry as selectedValue
+     */
+    public setValueFromId(id: string) {
+        let found = false;
+        for (const e of this._entries) {
+            if (e.id === id) {
+                found = true;
+                this.setValue(e);
+            }
+        }
+        if (! found) {
+            // default to first available entry if any
+            if (this._entries.length > 0) {
+                this.setValue(this._entries[0]);
+            } else {
+                // notify observers that no value is selected anymore
+                this.notifyObservers({
+                    "action": "select",
+                    "value": undefined
+                }, this);
+            }
+        }
+    }
+}
diff --git a/src/app/formulaire/select-field.ts b/src/app/formulaire/select-field.ts
index 065b09633..66545a058 100644
--- a/src/app/formulaire/select-field.ts
+++ b/src/app/formulaire/select-field.ts
@@ -108,7 +108,9 @@ export class SelectField extends Field {
     public updateLocalisation(loc: StringMap) {
         super.updateLocalisation(loc);
         for (const e of this._entries) {
-            e.label = loc[e.id];
+            if (loc[e.id] !== undefined) {
+                e.label = loc[e.id];
+            }
         }
     }
 
@@ -191,26 +193,6 @@ export class SelectField extends Field {
                 this.addEntry(new SelectEntry(this._entriesBaseId + BiefRegime.Fluvial, BiefRegime.Fluvial));
                 this.addEntry(new SelectEntry(this._entriesBaseId + BiefRegime.Torrentiel, BiefRegime.Torrentiel));
                 break;
-
-            case "solveur_target": // Solveur, paramètre cible (à calculer)
-                // find all Nubs having at least one link to another Nub's result
-                console.log(">> update solveur targets");
-                const downstreamNubs = Session.getInstance().getDownstreamNubs();
-                for (const dn of downstreamNubs) {
-                    this.addEntry(new SelectEntry(this._entriesBaseId + dn.uid, dn.uid));
-                }
-                break;
-
-            case "solveur_searched": // Solveur, paramètre recherché (à faire varier)
-                // find all non-calculated, non-linked parameters of all Nubs that
-                // the current "target" Nub depends on (if any)
-                console.log(">> update solveur searched");
-                const ntc: Nub = (nub as Solveur).nubToCalculate;
-                const searchableParams = Solveur.getDependingNubsSearchableParams(ntc);
-                for (const p of searchableParams) {
-                    this.addEntry(new SelectEntry(this._entriesBaseId + p.nubUid + "_" + p.symbol, p));
-                }
-                break;
         }
     }
 }
diff --git a/src/app/util.ts b/src/app/util.ts
index 6d6a10bb1..e4d3e1f1b 100644
--- a/src/app/util.ts
+++ b/src/app/util.ts
@@ -30,3 +30,14 @@ export function fv(p: NgParameter | number): string {
 
     return formattedValue(value, nDigits);
 }
+
+/**
+ * Trick to decode HTML entities in a string
+ * https://stackoverflow.com/a/7394787/5986614
+ * @param html string containing HTML entities, like &nbsp;
+ */
+export function decodeHtml(html: string): string {
+    const txt = document.createElement("textarea");
+    txt.innerHTML = html;
+    return txt.value;
+}
diff --git a/src/locale/messages.en.json b/src/locale/messages.en.json
index ad74567f5..270145ca4 100644
--- a/src/locale/messages.en.json
+++ b/src/locale/messages.en.json
@@ -4,7 +4,7 @@
     "WARNING_DOWNSTREAM_ELEVATION_POSSIBLE_SUBMERSION": "Downstream elevation is higher than weir elevation (possible submersion)",
     "WARNING_NOTCH_SUBMERSION_GREATER_THAN_07": "Notch formula is discouraged when submersion is greater than 0.7",
     "WARNING_SLOT_SUBMERSION_NOT_BETWEEN_07_AND_09": "Slot formula is discouraged when submersion is lower than 0.7 or greater than 0.9",
-    "ERROR_ABSTRACT": "%nb% errors occurred during calculation",
+    "WARNING_ERRORS_ABSTRACT": "%nb% errors occurred during calculation",
     "ERROR_BIEF_Z1_CALC_FAILED": "Unable to calculate upstream elevation (calculation interrupted before upstream)",
     "ERROR_BIEF_Z2_CALC_FAILED": "Unable to calculate downstream elevation (calculation interrupted before downstream)",
     "ERROR_DICHO_CONVERGE": "Dichotomy could not converge",
@@ -15,6 +15,8 @@
     "ERROR_DICHO_TARGET_TOO_HIGH": "Dichotomy: the solution %targetSymbol%=%targetValue% is greater than the maximum computable value %targetSymbol%(%variableSymbol%=%variableExtremeValue%)=%extremeTarget%)",
     "ERROR_DICHO_TARGET_TOO_LOW": "Dichotomy: the solution %targetSymbol%=%targetValue%  is lower than the minimum computable value %targetSymbol%(%variableSymbol%=%variableExtremeValue%)=%extremeTarget%)",
     "ERROR_ELEVATION_ZI_LOWER_THAN_Z2": "Upstream elevation is lower than downstream elevation",
+    "ERROR_IN_CALC_CHAIN": "An error occurred in calculation chain",
+    "WARNING_ERROR_IN_CALC_CHAIN_STEPS": "Errors occurred during chain calculation",
     "ERROR_INTERVAL_OUTSIDE": "Interval: value %value% is outside of %interval%",
     "ERROR_INTERVAL_UNDEF": "Interval: invalid 'undefined' value",
     "ERROR_INVALID_AT_POSITION": "Position %s:",
@@ -80,6 +82,8 @@
     "INFO_COURBEREMOUS_TITRE": "Backwater curves",
     "INFO_DEVER_TITRE_COURT": "Free weir",
     "INFO_DEVER_TITRE": "Free flow weir stage-discharge laws",
+    "INFO_DIAGRAM_SOLVEUR_FINDS": "finds",
+    "INFO_DIAGRAM_SOLVEUR_READS": "reads",
     "INFO_DIAGRAM_TITLE": "Calculation modules diagram",
     "INFO_DIAGRAM_DRAWING_ERROR": "Error while drawing diagram",
     "INFO_DIAGRAM_CALCULATED_PARAM": "calculated parameter",
@@ -455,7 +459,7 @@
     "INFO_EXAMPLE_LABEL_PAB_COMPLETE": "Standard fish ladder",
     "INFO_EXAMPLES_TITLE": "Examples",
     "INFO_EXAMPLES_SUBTITLE": "Load standard examples",
-    "WARNING_ABSTRACT": "%nb% warnings occurred during calculation",
+    "WARNING_WARNINGS_ABSTRACT": "%nb% warnings occurred during calculation",
     "WARNING_REMOUS_ARRET_CRITIQUE": "Calculation stopped: critical elevation reached at abscissa %x%",
     "WARNING_STRUCTUREKIVI_HP_TROP_ELEVE": "h/p must not be greater than 2.5. h/p is forced to 2.5",
     "WARNING_STRUCTUREKIVI_PELLE_TROP_FAIBLE": "Threshold height should be greater than 0.1 m. Beta coefficient is forced to 0",
@@ -468,5 +472,6 @@
     "WARNING_DOWNSTREAM_BOTTOM_HIGHER_THAN_WATER": "Downstream water elevation is lower or equal to bottom elevation",
     "WARNING_YN_SECTION_PENTE_NEG_NULLE_HNORMALE_INF": "Normal depth: slope is negative or zero, normal depth is infinite",
     "WARNING_YN_SECTION_NON_CONVERGENCE_NEWTON_HNORMALE": "Normal depth: non convergence of the calculation (Newton's method)",
-    "WARNING_SESSION_LOAD_NOTES_MERGED": "Notes have been merged"
+    "WARNING_SESSION_LOAD_NOTES_MERGED": "Notes have been merged",
+    "WARNING_VALUE_ROUNDED_TO_INTEGER": "Value of %symbol% was rounded to %rounded%"
 }
diff --git a/src/locale/messages.fr.json b/src/locale/messages.fr.json
index e93d09b95..e04f71c07 100644
--- a/src/locale/messages.fr.json
+++ b/src/locale/messages.fr.json
@@ -4,7 +4,7 @@
     "WARNING_DOWNSTREAM_ELEVATION_POSSIBLE_SUBMERSION": "La cote de l'eau aval est plus élevée que la cote du seuil (ennoiement possible)",
     "WARNING_NOTCH_SUBMERSION_GREATER_THAN_07": "La formule de l'échancrure n'est pas conseillée pour un ennoiement supérieur à 0.7",
     "WARNING_SLOT_SUBMERSION_NOT_BETWEEN_07_AND_09": "La formule de la fente n'est pas conseillée pour un ennoiement inférieur à 0.7 et supérieur à 0.9",
-    "ERROR_ABSTRACT": "%nb% erreurs rencontrées lors du calcul",
+    "WARNING_ERRORS_ABSTRACT": "%nb% erreurs rencontrées lors du calcul",
     "ERROR_BIEF_Z1_CALC_FAILED": "Impossible de calculer la cote amont (calcul interrompu avant l'amont)",
     "ERROR_BIEF_Z2_CALC_FAILED": "Impossible de calculer la cote aval (calcul interrompu avant l'aval)",
     "ERROR_DICHO_CONVERGE": "La dichotomie n'a pas pu converger",
@@ -15,6 +15,8 @@
     "ERROR_DICHO_TARGET_TOO_HIGH": "Dichotomie&nbsp;: la solution %targetSymbol%=%targetValue% est supérieure à la valeur maximale calculable %targetSymbol%(%variableSymbol%=%variableExtremeValue%)=%extremeTarget%)",
     "ERROR_DICHO_TARGET_TOO_LOW": "Dichotomie&nbsp;: la solution %targetSymbol%=%targetValue% est inférieure à la valeur minimale calculable %targetSymbol%(%variableSymbol%=%variableExtremeValue%)=%extremeTarget%)",
     "ERROR_ELEVATION_ZI_LOWER_THAN_Z2": "La cote amont est plus basse que la cote aval",
+    "ERROR_IN_CALC_CHAIN": "Une erreur est survenue dans la chaîne de calcul",
+    "WARNING_ERROR_IN_CALC_CHAIN_STEPS": "Des erreurs sont survenues durant le calcul en chaîne",
     "ERROR_INTERVAL_OUTSIDE": "Intervalle&nbsp;: la valeur %value% est hors de l'intervalle %interval%",
     "ERROR_INTERVAL_UNDEF": "Interval&nbsp;: valeur 'undefined' incorrecte",
     "ERROR_INVALID_AT_POSITION": "Position %s :",
@@ -80,6 +82,8 @@
     "INFO_COURBEREMOUS_TITRE": "Courbes de remous",
     "INFO_DEVER_TITRE_COURT": "Déver. dénoyés",
     "INFO_DEVER_TITRE": "Lois de déversoirs dénoyés",
+    "INFO_DIAGRAM_SOLVEUR_FINDS": "trouve",
+    "INFO_DIAGRAM_SOLVEUR_READS": "lit",
     "INFO_DIAGRAM_TITLE": "Diagramme des modules de calcul",
     "INFO_DIAGRAM_DRAWING_ERROR": "Erreur lors du dessin du diagramme",
     "INFO_DIAGRAM_CALCULATED_PARAM": "paramètre calculé",
@@ -454,7 +458,7 @@
     "INFO_EXAMPLE_LABEL_PAB_COMPLETE": "Passe à bassins type",
     "INFO_EXAMPLES_TITLE": "Exemples",
     "INFO_EXAMPLES_SUBTITLE": "Charger des exemples types",
-    "WARNING_ABSTRACT": "%nb% avertissements rencontrés lors du calcul",
+    "WARNING_WARNINGS_ABSTRACT": "%nb% avertissements rencontrés lors du calcul",
     "WARNING_REMOUS_ARRET_CRITIQUE": "Arrêt du calcul&nbsp;: hauteur critique atteinte à l'abscisse %x%",
     "WARNING_STRUCTUREKIVI_HP_TROP_ELEVE": "h/p ne doit pas être supérieur à 2,5. h/p est forcé à 2,5",
     "WARNING_STRUCTUREKIVI_PELLE_TROP_FAIBLE": "La pelle du seuil doit mesurer au moins 0,1 m. Le coefficient béta est forcé à 0",
@@ -467,5 +471,6 @@
     "WARNING_DOWNSTREAM_BOTTOM_HIGHER_THAN_WATER": "La cote de l'eau à l'aval est plus basse ou égale à la cote de fond",
     "WARNING_YN_SECTION_PENTE_NEG_NULLE_HNORMALE_INF": "Hauteur normale: pente négative ou nulle, hauteur normale infinie",
     "WARNING_YN_SECTION_NON_CONVERGENCE_NEWTON_HNORMALE": "Hauteur normale: non convergence du calcul (méthode de Newton)",
-    "WARNING_SESSION_LOAD_NOTES_MERGED": "Les notes ont été fusionnées"
+    "WARNING_SESSION_LOAD_NOTES_MERGED": "Les notes ont été fusionnées",
+    "WARNING_VALUE_ROUNDED_TO_INTEGER": "La valeur de %symbol% a été arrondie à %rounded%"
 }
-- 
GitLab