diff --git a/e2e/calculate-all-params.e2e-spec.ts b/e2e/calculate-all-params.e2e-spec.ts index 6f5632baf39b9aacf2333cb68154865592796f64..9533c2fca374524be2406530d9c2c63526783d37 100644 --- a/e2e/calculate-all-params.e2e-spec.ts +++ b/e2e/calculate-all-params.e2e-spec.ts @@ -18,7 +18,12 @@ describe("ngHyd − calculate all parameters of all calculators", () => { }); // get calculators list (IDs) @TODO read it from config, but can't import jalhyd here :/ - const calcTypes = [ 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 15, 17, 18, 19, 20, 21 ]; + const calcTypes = [ + 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, + 11, 12, 13, 15, 17, 18, 19, 20, + 21, + // 22 - Solveur is not calculated here because it is not independent + ]; // for each calculator for (const ct of calcTypes) { diff --git a/e2e/check-translations.e2e-spec.ts b/e2e/check-translations.e2e-spec.ts index 2aef4ff34220586ff8209a79fdc03202028d900e..b0c10f97084467c212a1497b3425d4b21b9cbe47 100644 --- a/e2e/check-translations.e2e-spec.ts +++ b/e2e/check-translations.e2e-spec.ts @@ -25,7 +25,7 @@ describe("ngHyd − check translation of all calculators", () => { }); // get calculators list (IDs) @TODO read it from config, but can't import jalhyd here :/ - const calcTypes = [ 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 15, 17, 18, 19, 20, 21 ]; + const calcTypes = [ 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 15, 17, 18, 19, 20, 21, 22 ]; // options of "Language" selector on preferences page const langs = [ "English", "Français" ]; @@ -67,12 +67,13 @@ describe("ngHyd − check translation of all calculators", () => { // check that "compute" button is active const calcButton = calcPage.getCalculateButton(); const disabledState = await calcButton.getAttribute("disabled"); - expect(disabledState).not.toBe("true"); - // click "compute" button - await calcButton.click(); - // check that result is not empty - const hasResults = await calcPage.hasResults(); - expect(hasResults).toBe(true); + if (! disabledState) { + // click "compute" button + await calcButton.click(); + // check that result is not empty + const hasResults = await calcPage.hasResults(); + expect(hasResults).toBe(true); + } // check absence of "*** message not found" in whole DOM expect(await browser.getPageSource()).not.toContain("*** message not found"); diff --git a/e2e/clone-all-calc.e2e-spec.ts b/e2e/clone-all-calc.e2e-spec.ts index e5b77dcd1b84d0d34687a628f4c52f930eab779b..8c32c1ff622ac258da4f6cd15fc6e03d7c434146 100644 --- a/e2e/clone-all-calc.e2e-spec.ts +++ b/e2e/clone-all-calc.e2e-spec.ts @@ -18,7 +18,12 @@ describe("ngHyd − clone all calculators with all possible <select> values", () }); // get calculators list (IDs) @TODO read it from config, but can't import jalhyd here :/ - const calcTypes = [ 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 15, 17, 18, 19, 20, 21 ]; + const calcTypes = [ + 0, 1, 2, 3, 4, 5, 6, 8, 9, 10, + 11, 12, 13, 15, 17, 18, 19, 20, + 21, + // 22 - Solveur is not cloned here because it is not independent + ]; // for each calculator for (const ct of calcTypes) { diff --git a/e2e/session/session-solveur-chutes.json b/e2e/session/session-solveur-chutes.json new file mode 100644 index 0000000000000000000000000000000000000000..3142108ec2b5a85559d8ba422d3ad0c7a618705b --- /dev/null +++ b/e2e/session/session-solveur-chutes.json @@ -0,0 +1,124 @@ +{ + "header": { + "source": "jalhyd", + "format_version": "1.3", + "created": "2019-10-22T08:08:13.816Z" + }, + "settings": { + "precision": 0.0001, + "maxIterations": 100, + "displayPrecision": 3 + }, + "documentation": "", + "session": [ + { + "uid": "NjRvcG", + "props": { + "calcType": "PabChute" + }, + "meta": { + "title": "PAB : chute" + }, + "children": [], + "parameters": [ + { + "symbol": "Z1", + "mode": "SINGLE", + "value": 2.1513761467889907 + }, + { + "symbol": "Z2", + "mode": "SINGLE", + "value": 0.8669724770642202 + }, + { + "symbol": "DH", + "mode": "CALCUL" + } + ] + }, + { + "uid": "dnV4bD", + "props": { + "calcType": "PabNombre" + }, + "meta": { + "title": "PAB : nombre" + }, + "children": [], + "parameters": [ + { + "symbol": "DHT", + "mode": "LINK", + "targetNub": "NjRvcG", + "targetParam": "DH" + }, + { + "symbol": "N", + "mode": "SINGLE", + "value": 10 + }, + { + "symbol": "DH", + "mode": "CALCUL" + } + ] + }, + { + "uid": "OHBpcz", + "props": { + "calcType": "PabPuissance" + }, + "meta": { + "title": "PAB : puissance" + }, + "children": [], + "parameters": [ + { + "symbol": "DH", + "mode": "LINK", + "targetNub": "dnV4bD", + "targetParam": "DH" + }, + { + "symbol": "Q", + "mode": "SINGLE", + "value": 0.1 + }, + { + "symbol": "V", + "mode": "SINGLE", + "value": 0.5 + }, + { + "symbol": "PV", + "mode": "CALCUL" + } + ] + }, + { + "uid": "ODM0Z2", + "props": { + "calcType": "Solveur", + "nubToCalculate": "OHBpcz", + "searchedParameter": "NjRvcG/Z2" + }, + "meta": { + "title": "Solveur" + }, + "children": [], + "parameters": [ + { + "symbol": "Xinit", + "mode": "SINGLE", + "value": 0.5 + }, + { + "symbol": "Ytarget", + "mode": "SINGLE", + "value": 252 + } + ] + } + ] +} \ No newline at end of file diff --git a/e2e/solveur.e2e-spec.ts b/e2e/solveur.e2e-spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..84b3df0e9d0b143a9ee460b07be960195e762a82 --- /dev/null +++ b/e2e/solveur.e2e-spec.ts @@ -0,0 +1,135 @@ +import { AppPage } from "./app.po"; +import { ListPage } from "./list.po"; +import { CalculatorPage } from "./calculator.po"; +import { Navbar } from "./navbar.po"; +import { browser } from "protractor"; +import { SideNav } from "./sidenav.po"; + +/** + * Clone calculators + */ +describe("Solveur - ", () => { + let startPage: AppPage; + let listPage: ListPage; + let calcPage: CalculatorPage; + let navbar: Navbar; + let sidenav: SideNav; + + beforeEach(() => { + startPage = new AppPage(); + listPage = new ListPage(); + calcPage = new CalculatorPage(); + navbar = new Navbar(); + sidenav = new SideNav(); + }); + + it("load > calculate", async () => { + await startPage.navigateTo(); + + await navbar.clickMenuButton(); + await browser.sleep(200); + + await sidenav.clickLoadSessionButton(); + await browser.sleep(200); + + await sidenav.loadSessionFile("./session/session-solveur-chutes.json"); + await browser.sleep(200); + + expect(await navbar.getAllCalculatorTabs().count()).toBe(4); + await navbar.clickCalculatorTab(3); // n°3 should be the latest + + // check input values + expect(await calcPage.getInputById("Xinit").getAttribute("value")).toBe("0.5"); + expect(await calcPage.getInputById("Ytarget").getAttribute("value")).toBe("252"); + // check Nub to calculate + const ntc = calcPage.getSelectById("select_target_nub"); + const ntcV = await calcPage.getSelectValueText(ntc); + expect(ntcV).toContain("PAB : puissance / Puissance dissipée (PV)"); + // check searched Parameter + const sp = calcPage.getSelectById("select_searched_param"); + const spV = await calcPage.getSelectValueText(sp); + expect(spV).toContain("Z2 - Cote aval (PAB : chute)"); + + // check that "compute" button is active + const calcButton = calcPage.getCalculateButton(); + const disabledState = await calcButton.getAttribute("disabled"); + expect(disabledState).not.toBe("true"); + // click "compute" button + await calcButton.click(); + // check that result is not empty + const hasResults = await calcPage.hasResults(); + expect(hasResults).toBe(true); + }); + + it("create > feed > calculate > clone > calculate clone", async () => { + await startPage.navigateTo(); + + // 1. create empty Solveur + await listPage.clickMenuEntryForCalcType(22); // Solveur + await browser.sleep(500); + + // 2. create PAB:Chute, PAB:Nombre and PAB:Puissance linked to one another + await navbar.clickNewCalculatorButton(); + await listPage.clickMenuEntryForCalcType(12); // PAB:Chute + await browser.sleep(500); + await navbar.clickNewCalculatorButton(); + + await listPage.clickMenuEntryForCalcType(13); // PAB:Nombre + await browser.sleep(500); + // link DHT to PAB:Chute.DH + const dht = calcPage.getInputById("DHT"); + await calcPage.setParamMode(dht, "link"); + // Calculate DH + const dh_nombre = calcPage.getInputById("DH"); + await calcPage.setParamMode(dh_nombre, "cal"); + + await navbar.clickNewCalculatorButton(); + await listPage.clickMenuEntryForCalcType(6); // PAB:Puissance + await browser.sleep(500); + // link DH to PAB:Nombre.DH + const dh_puiss = calcPage.getInputById("DH"); + await calcPage.setParamMode(dh_puiss, "link"); + + // Go back to Solveur + await navbar.clickCalculatorTab(0); + + await calcPage.changeSelectValue(calcPage.getSelectById("select_target_nub"), 1); // "Puissance / PV" + await browser.sleep(500); + await calcPage.changeSelectValue(calcPage.getSelectById("select_searched_param"), 2); // "Chute / Z2" + await browser.sleep(500); + await calcPage.getInputById("Ytarget").sendKeys("318"); + + // check that "compute" button is active + const calcButton = calcPage.getCalculateButton(); + const disabledState = await calcButton.getAttribute("disabled"); + expect(disabledState).not.toBe("true"); + // click "compute" button + await calcButton.click(); + // check that result is not empty + const hasResults = await calcPage.hasResults(); + expect(hasResults).toBe(true); + + // otherwise clickCloneCalcButton() fails with "Element is not clickable at point" + await browser.executeScript("window.scrollTo(0, 0);"); + await calcPage.clickCloneCalcButton(); + await browser.sleep(500); + + // 4. check existence of the cloned module + expect(await navbar.getAllCalculatorTabs().count()).toBe(5); + await navbar.clickCalculatorTab(4); // n°4 should be the latest + + // check that result is empty + const hasResultsClone1 = await calcPage.hasResults(); + expect(hasResultsClone1).toBe(false); + + // check that "compute" button is active + const calcButtonClone = calcPage.getCalculateButton(); + const disabledStateClone = await calcButtonClone.getAttribute("disabled"); + expect(disabledStateClone).not.toBe("true"); + // click "compute" button + await calcButtonClone.click(); + // check that result is not empty + const hasResultsClone2 = await calcPage.hasResults(); + expect(hasResultsClone2).toBe(true); + }); +}); diff --git a/jalhyd_branch b/jalhyd_branch index 0c6550147d6ae5cd2a34cbe0d6523c6fde1c533a..4517376e60041f9bc7c35409eab062c5cd55c1d3 100644 --- a/jalhyd_branch +++ b/jalhyd_branch @@ -1 +1 @@ -55-ajout-d-un-module-de-calcul-des-cotes-amont-aval-d-un-bief +152-solveur-multi-modules diff --git a/src/app/calculators/solveur/solveur.config.json b/src/app/calculators/solveur/solveur.config.json new file mode 100644 index 0000000000000000000000000000000000000000..40b0bbb8cb7976a6da52b2e0f6491d08aa6e5f48 --- /dev/null +++ b/src/app/calculators/solveur/solveur.config.json @@ -0,0 +1,34 @@ +[ + { + "id": "fs_target", + "type": "fieldset", + "fields": [ + { + "id": "select_target_nub", + "type": "select_reference", + "reference": "nub", + "source": "solveur_target" + }, + "Ytarget" + ] + }, + { + "id": "fs_searched", + "type": "fieldset", + "fields": [ + { + "id": "select_searched_param", + "type": "select_reference", + "reference": "parameter", + "source": "solveur_searched" + }, + "Xinit" + ] + }, + { + "type": "options", + "targetNubSelectId": "select_target_nub", + "searchedParamSelectId": "select_searched_param", + "_help": "solveur.html" + } +] \ No newline at end of file diff --git a/src/app/calculators/solveur/solveur.en.json b/src/app/calculators/solveur/solveur.en.json new file mode 100644 index 0000000000000000000000000000000000000000..df14b3764ae45a10c2ec502dfe8076ec409ed7f6 --- /dev/null +++ b/src/app/calculators/solveur/solveur.en.json @@ -0,0 +1,11 @@ +{ + "fs_target": "Target parameter characteristics", + "fs_searched": "Searched parameter characteristics", + + "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" +} \ No newline at end of file diff --git a/src/app/calculators/solveur/solveur.fr.json b/src/app/calculators/solveur/solveur.fr.json new file mode 100644 index 0000000000000000000000000000000000000000..1439bd8daa2cf73e9c871a5a9a6fc989d5747ab3 --- /dev/null +++ b/src/app/calculators/solveur/solveur.fr.json @@ -0,0 +1,11 @@ +{ + "fs_target": "Caractéristiques du paramètre cible", + "fs_searched": "Caractéristiques du paramètre recherché", + + "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é" +} \ No newline at end of file diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts index 82dc8d735da50552c627ec2263a82298cfd7c9c1..18b6f57aa9eb8905666c87cbfe410a841ed8f47b 100644 --- a/src/app/components/generic-calculator/calculator.component.ts +++ b/src/app/components/generic-calculator/calculator.component.ts @@ -122,8 +122,6 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe this.intlService = ServiceFactory.instance.i18nService; this.formulaireService = ServiceFactory.instance.formulaireService; - this.matomoTracker.trackPageView("calculator"); - // hotkeys listeners this.hotkeysService.add(new Hotkey("alt+w", AppComponent.onHotkey(this.closeCalculator, this))); this.hotkeysService.add(new Hotkey("alt+d", AppComponent.onHotkey(this.cloneCalculator, this))); @@ -376,6 +374,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 +530,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/generic-input/generic-input.component.ts b/src/app/components/generic-input/generic-input.component.ts index 2e076ed1c7725df657f8b104bf4be15a728e073c..f49b62c0e4ebdfc7d6f8815b6266a017f9db1ae6 100644 --- a/src/app/components/generic-input/generic-input.component.ts +++ b/src/app/components/generic-input/generic-input.component.ts @@ -220,7 +220,11 @@ export abstract class GenericInputComponent implements OnChanges { * MAJ et validation de l'UI */ protected updateAndValidateUI() { - this._uiValue = String(this.getModelValue()); + if (this.getModelValue() !== undefined) { + this._uiValue = String(this.getModelValue()); + } else { + this._uiValue = ""; + } this.validateUI(); } diff --git a/src/app/components/modules-diagram/modules-diagram.component.ts b/src/app/components/modules-diagram/modules-diagram.component.ts index 68297ad6cf25be2f61179a9aa57158fea3c4c688..d84bfdac71cab208f840fd3c9cdded1b60b64e2d 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 04a69146bdd988ebf4d4f52a1a7322e5dc56294e..e9615dbed86deef53b350b622f965be998275511 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 new file mode 100644 index 0000000000000000000000000000000000000000..231f815b08b1f5a9030be690e26929cfad44e831 --- /dev/null +++ b/src/app/formulaire/definition/concrete/form-solveur.ts @@ -0,0 +1,95 @@ +import { IObservable, ParamDefinition, Solveur } from "jalhyd"; + +import { FormulaireBase } from "./form-base"; +import { SelectFieldNub } from "../../select-field-nub"; +import { SelectFieldParameter } from "../../select-field-parameter"; +import { NgParameter } from "../../ngparam"; + +/** + * Formulaire pour les Solveurs + */ +export class FormulaireSolveur extends FormulaireBase { + + /** id of select configuring target Nub */ + private _targetNubSelectId: string; + + /** id of select configuring searched param */ + private _searchedParamSelectId: string; + + protected parseOptions(json: {}) { + super.parseOptions(json); + this._targetNubSelectId = this.getOption(json, "targetNubSelectId"); + this._searchedParamSelectId = this.getOption(json, "searchedParamSelectId"); + } + + protected completeParse(json: {}) { + super.completeParse(json); + if (this._targetNubSelectId) { + const sel = this.getFormulaireNodeById(this._targetNubSelectId); + if (sel) { + sel.addObserver(this); + // force 1st observation + (sel as SelectFieldNub).notifySelectValueChanged(); + } + } + if (this._searchedParamSelectId) { + const sel = this.getFormulaireNodeById(this._searchedParamSelectId); + if (sel) { + sel.addObserver(this); + // force 1st observation + (sel as SelectFieldNub).notifySelectValueChanged(); + } + } + + } + + private debugState() { + const sol = this._currentNub as Solveur; + const spm = sol.searchedParameter; + console.log( + `ETAT:\n X.singleValue=${sol.prms.X ? sol.prms.X.singleValue : "UNDEF"}\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 ? spm.singleValue : "UNDEF"}` + ); + } + + // interface Observer + + public update(sender: IObservable, data: any) { + super.update(sender, data); + if (sender instanceof SelectFieldNub) { + if (data.action === "select") { + // update Solveur property: 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) { + sel.updateEntries(); + // 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 + try { + const p: ParamDefinition = data.value.value; + this._currentNub.properties.setPropValue( + "searchedParameter", + p.nubUid + "/" + p.symbol + ); + } catch (e) { } + // 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 b178d3a3db9ca3098f58050f2ffb1223d05d484f..5aa007c8aff8314b63f92235241162864c108912 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 b5d4978c5be82673ce8d59cccf9b0b20c41c9266..0681164586f2f3c9ac4b8e798ea6dd2963ee1772 100644 --- a/src/app/formulaire/fieldset.ts +++ b/src/app/formulaire/fieldset.ts @@ -10,6 +10,7 @@ import { GrilleType, GrilleProfile, BiefRegime, + Solveur, } from "jalhyd"; import { FormulaireElement } from "./formulaire-element"; @@ -20,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 { /** @@ -96,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; } @@ -185,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; + } } } diff --git a/src/app/formulaire/ngparam.ts b/src/app/formulaire/ngparam.ts index 5387c058dffb94a62d1419095b4fc2dffc98781d..b5d15d216c23923cdb955c86d91854f4d6f64744 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 0000000000000000000000000000000000000000..b19492cc0abbdb67f34c4c8325cd3fc694fbdaca --- /dev/null +++ b/src/app/formulaire/select-field-nub.ts @@ -0,0 +1,45 @@ +import { SelectFieldReference } from "./select-field-reference"; +import { SelectEntry } from "./select-entry"; +import { ServiceFactory } from "../services/service-factory"; +import { decodeHtml } from "../util"; + +import { Session, Solveur } from "jalhyd"; + +/** + * A select field that populates itself with references to Nubs + */ +export class SelectFieldNub extends SelectFieldReference { + + protected initSelectedValue() { + const nub = this.parentForm.currentNub; + if (nub instanceof Solveur) { + const ntc = nub.nubToCalculate; + if (ntc !== undefined) { + this.setValueFromId(this._entriesBaseId + ntc.uid); + } + } + } + + /** + * 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 0000000000000000000000000000000000000000..b979a98f72b292ff818869c847939c70c29a5232 --- /dev/null +++ b/src/app/formulaire/select-field-parameter.ts @@ -0,0 +1,44 @@ +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() { + const nub = this.parentForm.currentNub; + if (nub instanceof Solveur) { + const sp = nub.searchedParameter; + if (sp !== undefined) { + this.setValueFromId(this._entriesBaseId + sp.nubUid + "_" + sp.symbol); + } + } + } + + /** + * 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 0000000000000000000000000000000000000000..3a0d8396c06c242fdde23616513868aade5d7023 --- /dev/null +++ b/src/app/formulaire/select-field-reference.ts @@ -0,0 +1,107 @@ +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; + } + + protected abstract initSelectedValue(); + + /** + * Populates entries with available references + */ + protected abstract populate(); + + /** + * Once config is parsed, init original value from model + * (needs config, for this._entriesBaseId to be set) + */ + protected afterParseConfig() { + this.populate(); + this.initSelectedValue(); + } + + /** + * 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); + } else { + // if no entry is available anymore, unset value + if (this.entries.length === 0) { + super.setValue(undefined); + } else { + this.setDefaultValue(); + } + } + } + + /** + * 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) + ) { + this.notifySelectValueChanged(); + } + } + + public notifySelectValueChanged() { + this.notifyObservers({ + "action": "select", + "value": this._selectedEntry + }, 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) { + this.setDefaultValue(); + } + } + + protected setDefaultValue() { + // 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.notifySelectValueChanged(); + } + } +} diff --git a/src/app/formulaire/select-field.ts b/src/app/formulaire/select-field.ts index 308b319b08bab11b65ededa940cc947143cb7e7c..b2ea54a8c44b80617e149fd9e6f05dc907a57239 100644 --- a/src/app/formulaire/select-field.ts +++ b/src/app/formulaire/select-field.ts @@ -8,7 +8,10 @@ import { StructureType, LoiDebit, GrilleType, - GrilleProfile + GrilleProfile, + Solveur, + ParamValueMode, + Session } from "jalhyd"; import { Field } from "./field"; @@ -74,6 +77,11 @@ export class SelectField extends Field { */ protected populate() { } + /** + * Triggered at the end of parseConfig() + */ + protected afterParseConfig() { } + public getSelectedEntryFromValue(val: any): SelectEntry { for (const se of this._entries) { if (se.value === val) { @@ -105,7 +113,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]; + } } } @@ -189,5 +199,7 @@ export class SelectField extends Field { this.addEntry(new SelectEntry(this._entriesBaseId + BiefRegime.Torrentiel, BiefRegime.Torrentiel)); break; } + + this.afterParseConfig(); } } diff --git a/src/app/services/formulaire.service.ts b/src/app/services/formulaire.service.ts index 65a60d25bc8ea319c99c4b369e0ce228ff0ba21a..02593060993d7f2112d7d06628cf42225afc0d2d 100644 --- a/src/app/services/formulaire.service.ts +++ b/src/app/services/formulaire.service.ts @@ -39,6 +39,7 @@ import { FormulaireMacrorugoCompound } from "../formulaire/definition/concrete/f import { FormulaireLechaptCalmon } from "../formulaire/definition/concrete/form-lechapt-calmon"; import { FormulaireGrille } from "../formulaire/definition/concrete/form-grille"; import { FormulaireBief } from "../formulaire/definition/concrete/form-bief"; +import { FormulaireSolveur } from "../formulaire/definition/concrete/form-solveur"; @Injectable() export class FormulaireService extends Observable { @@ -84,6 +85,7 @@ export class FormulaireService extends Observable { this.calculatorPaths[CalculatorType.Grille] = "grille"; this.calculatorPaths[CalculatorType.Pente] = "pente"; this.calculatorPaths[CalculatorType.Bief] = "bief"; + this.calculatorPaths[CalculatorType.Solveur] = "solveur"; } private get _intlService(): I18nService { @@ -329,6 +331,10 @@ export class FormulaireService extends Observable { f = new FormulaireBief(); break; + case CalculatorType.Solveur: + f = new FormulaireSolveur(); + break; + default: f = new FormulaireBase(); } diff --git a/src/app/util.ts b/src/app/util.ts index 6d6a10bb17488544685d8517037e8b4896274ae3..e4d3e1f1bf9eecaf247a42521ae971bdaf86320f 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 + */ +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 5a3e63c6445c7fe7a9f8b2fe07197f56a4bd164e..b6db3ee3226f5c823c7a3251e76536054c448cb6 100644 --- a/src/locale/messages.en.json +++ b/src/locale/messages.en.json @@ -4,10 +4,10 @@ "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", + "ERROR_DICHO_CONVERGE": "Dichotomy could not converge. Last approximation: %lastApproximation%", "ERROR_DICHO_FUNCTION_VARIATION": "unable to determinate function direction of variation", "ERROR_DICHO_INIT_DOMAIN": "Dichotomy: target %targetSymbol%=%targetValue% does not exist for variable %variableSymbol% valued in interval %variableInterval%", "ERROR_DICHO_INVALID_STEP_GROWTH": "Dichotomy (initial interval search): invalid null step growth", @@ -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", @@ -431,6 +435,8 @@ "INFO_SNACKBAR_RESULTS_CALCULATED": "Results calculated for", "INFO_SNACKBAR_RESULTS_INVALIDATED": "Results invalidated for", "INFO_SNACKBAR_SETTINGS_SAVED": "Settings saved on this device", + "INFO_SOLVEUR_TITRE": "Multimodule solver", + "INFO_SOLVEUR_TITRE_COURT": "Solver", "INFO_THEME_CREDITS": "Credit", "INFO_THEME_DEVALAISON_TITRE": "Downstream migration", "INFO_THEME_DEVALAISON_DESCRIPTION": "Tools for dimensioning the structures present on the water intakes of hydroelectric power plants known as \"ichthyocompatible\" and consisting of fine grid planes associated with one or more outlets.", @@ -453,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", @@ -466,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 523d6cd37a8604dc9edf94669385423c0cd835f2..f7dd7975fd553ea703cb40945bf09ca14980a8a8 100644 --- a/src/locale/messages.fr.json +++ b/src/locale/messages.fr.json @@ -4,10 +4,10 @@ "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", + "ERROR_DICHO_CONVERGE": "La dichotomie n'a pas pu converger. Dernière approximation: %lastApproximation%", "ERROR_DICHO_FUNCTION_VARIATION": "Dichotomie : impossible de determiner le sens de variation de la fonction", "ERROR_DICHO_INIT_DOMAIN": "Dichotomie : la valeur cible %targetSymbol%=%targetValue% n'existe pas pour la variable %variableSymbol% prise dans l'intervalle %variableInterval%", "ERROR_DICHO_INVALID_STEP_GROWTH": "Dichotomie : l'augmentation du pas pour la recherche de l'intervalle de départ est incorrecte (=0)", @@ -15,6 +15,8 @@ "ERROR_DICHO_TARGET_TOO_HIGH": "Dichotomie : la solution %targetSymbol%=%targetValue% est supérieure à la valeur maximale calculable %targetSymbol%(%variableSymbol%=%variableExtremeValue%)=%extremeTarget%)", "ERROR_DICHO_TARGET_TOO_LOW": "Dichotomie : 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 : la valeur %value% est hors de l'intervalle %interval%", "ERROR_INTERVAL_UNDEF": "Interval : 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é", @@ -430,6 +434,8 @@ "INFO_SNACKBAR_RESULTS_CALCULATED": "Résultats calculés pour", "INFO_SNACKBAR_RESULTS_INVALIDATED": "Résultats invalidés pour", "INFO_SNACKBAR_SETTINGS_SAVED": "Paramètres enregistrés sur cet appareil", + "INFO_SOLVEUR_TITRE": "Solveur multimodule", + "INFO_SOLVEUR_TITRE_COURT": "Solveur", "INFO_THEME_CREDITS": "Crédit", "INFO_THEME_DEVALAISON_TITRE": "Dévalaison", "INFO_THEME_DEVALAISON_DESCRIPTION": "Outils de dimensionnements des ouvrages présents sur les prises d'eau des centrales hydroélectriques dites \"ichtyocompatibles\" et constituées de plans de grilles fines associés à un ou plusieurs exutoires.", @@ -452,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 : 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", @@ -465,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%" }