From 3dc7bfd401e8177fff104be13911aad76f006b49 Mon Sep 17 00:00:00 2001 From: David Dorchies <david.dorchies@inrae.fr> Date: Tue, 20 Feb 2024 15:25:24 +0000 Subject: [PATCH 1/3] lint(pab): lint, lint, lint! Refs #662 --- .eslintrc.js | 2 +- .vscode/settings.json | 1 + .../pab-table/pab-table.component.ts | 1942 ++++++++--------- 3 files changed, 969 insertions(+), 976 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index c82462bdc..d615175bb 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -123,7 +123,7 @@ module.exports = { "id-denylist": "off", "id-match": "off", "import/no-deprecated": "warn", - "indent": "error", + "indent": ["error", 4, { "SwitchCase": 1 }], "max-len": [ "error", { diff --git a/.vscode/settings.json b/.vscode/settings.json index 9a280a042..b8e94e5f7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "nghyd", "prms" ], "cSpell.language": "en,fr-FR" diff --git a/src/app/components/pab-table/pab-table.component.ts b/src/app/components/pab-table/pab-table.component.ts index f65425f3e..daa731730 100644 --- a/src/app/components/pab-table/pab-table.component.ts +++ b/src/app/components/pab-table/pab-table.component.ts @@ -16,13 +16,12 @@ import { ParamDefinition, round, CloisonAval - } from "jalhyd"; +} from "jalhyd"; - import { sprintf } from "sprintf-js"; +import { sprintf } from "sprintf-js"; import { I18nService } from "../../services/internationalisation.service"; import { FormulaireService } from "../../services/formulaire.service"; -import { ApplicationSetupService } from "../../services/app-setup.service"; import { NotificationsService } from "../../services/notifications.service"; import { PabTable } from "../../formulaire/elements/pab-table"; import { DialogEditPabComponent } from "../dialog-edit-pab/dialog-edit-pab.component"; @@ -45,32 +44,32 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni @Input() private pabTable: PabTable; - /** flag de validité des FieldSet enfants */ - private _isValid: DefinedBoolean; - - /** événément de changement de validité */ + /** change of validity event */ @Output() private validChange = new EventEmitter(); - /** événément de changement de valeur d'un input */ + /** input value change event */ @Output() private inputChange = new EventEmitter(); - /** underlying Pab, binded to the rows */ - private model: Pab; - /** general headers above the columns */ public headers: any[]; /** columns headers description */ public cols: any[]; - /** data binded to the table */ + /** data bound to the table */ public rows: any[]; /** number of children to add when clicking "add" or "clone" button */ public childrenToAdd = 1; + /** flag de validité des FieldSet enfants */ + private _isValid: DefinedBoolean; + + /** underlying Pab, bound to the rows */ + private model: Pab; + /** items currently selected */ private selectedItems: Nub[]; @@ -90,14 +89,103 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni this._isValid = new DefinedBoolean(); } - /** update vary value from pab fish ladder and unable compute Button */ - ngAfterViewChecked(): void { - this.updateValidity(); - } + public get addManyOptionsList() { + return Array(20).fill(0).map((value, index) => index + 1); + } + + public get enableAddButton() { + return ( + this.onlyDevicesOfTheSameColumnAreSelected() + || ( + this.selectedItems.length === 1 + && ! (this.selectedItem instanceof CloisonAval) // exclude downwall + ) + ); + } + public get enableCopyButton() { + return this.enableAddButton; + } - public get title(): string { - return this.i18nService.localizeText("INFO_PAB_TABLE"); + public get enableUpButton() { + return ( + this.selectedItems.length === 1 + && ! (this.selectedItem instanceof CloisonAval) // exclude downwall + && this.selectedItem.parent + && this.selectedItem.findPositionInParent() !== 0 + ); + } + + public get enableDownButton() { + return ( + this.selectedItems.length === 1 + && ! (this.selectedItem instanceof CloisonAval) // exclude downwall + && this.selectedItem.parent + && this.selectedItem.findPositionInParent() < (this.selectedItem.parent.getChildren().length - 1) + ); + } + + public get enableRemoveButton() { + let containsDownwall = false; + let containsOrphanNub = false; + let tooFewDevices = false; + let wallsCount = 0; + const devicesCountById = {}; + const deletedWallsUids = []; + + for (const se of this.selectedItems) { + if (se instanceof Structure) { // device + if (devicesCountById[se.parent.uid] === undefined) { + devicesCountById[se.parent.uid] = 0; + } + devicesCountById[se.parent.uid]++; + } else { // wall + wallsCount++; + deletedWallsUids.push(se.uid); + } + if (se instanceof CloisonAval) { + containsDownwall = true; // cannot remove downwall + } + if (! se.parent) { + containsOrphanNub = true; // not supposed to happen but who knows + } + } + + // at least one device must remain in each basin, unless this basin is removed too + for (const structureId in devicesCountById) { + if (! deletedWallsUids.includes(structureId)) { + let wall: Nub; + if (this.model.downWall.uid === structureId) { + wall = this.model.downWall; + } else { + wall = this.model.getChild(structureId); + } + if (wall.getChildren().length <= devicesCountById[structureId]) { + tooFewDevices = true; + } + } + } + + return ( + this.selectedItems.length > 0 + && wallsCount < this.model.children.length // at least one basin must remain + && ! containsDownwall + && ! containsOrphanNub + && ! tooFewDevices + ); + } + + /** + * returns true if at least one object is selected + */ + public get enableEditPabButton() { + return ( + this.selectedItems.length > 0 + && ( + this.onlyDevicesAreSelected() + || this.onlyWallsAreSelected(false) + ) + ); } /** Global Pab validity */ @@ -105,6 +193,80 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni return this._isValid.value; } + public get relatedEntityTitle() { + let title = ""; + if (this.onlyDevicesAreSelected()) { + title = this.i18nService.localizeText("INFO_PAB_OUVRAGES"); + } else if (this.onlyWallsAreSelected()) { + title = this.i18nService.localizeText("INFO_PAB_BASSINS"); + } + if (title !== "") { + title += " :"; + } + return title; + } + + // quick getter for 1st selected item + public get selectedItem() { + if (this.selectedItems.length === 0) { + throw new Error("get selectedItem() : no item selected"); + } + return this.selectedItems[0]; + } + + /** returns true if exactly one device is selected, and nothing else */ + public get selectionIsOneDevice() { + return ( + this.selectedItems.length === 1 + && this.selectedItem instanceof Structure + ); + } + + public get title(): string { + return this.i18nService.localizeText("INFO_PAB_TABLE"); + } + + public get uitextEditPabTable() { + return this.i18nService.localizeText("INFO_PAB_EDIT_VALUES"); + } + + public get uitextAdd(): string { + return this.i18nService.localizeText("INFO_FIELDSET_ADD"); + } + + public get uitextCopy(): string { + return this.i18nService.localizeText("INFO_FIELDSET_COPY"); + } + + public get uitextRemove(): string { + return this.i18nService.localizeText("INFO_FIELDSET_REMOVE"); + } + + public get uitextMoveUp(): string { + if (this.selectionIsOneDevice) { + return this.i18nService.localizeText("INFO_FIELDSET_MOVE_LEFT"); + } else { + return this.i18nService.localizeText("INFO_FIELDSET_MOVE_UP"); + } + } + + public get uitextMoveDown(): string { + if (this.selectionIsOneDevice) { + return this.i18nService.localizeText("INFO_FIELDSET_MOVE_RIGHT"); + } else { + return this.i18nService.localizeText("INFO_FIELDSET_MOVE_DOWN"); + } + } + + public get uitextExportAsSpreadsheet() { + return this.i18nService.localizeText("INFO_RESULTS_EXPORT_AS_SPREADSHEET"); + } + + /** update vary value from pab fish ladder and unable compute Button */ + ngAfterViewChecked(): void { + this.updateValidity(); + } + /** returns true if the cell has an underlying model (ie. is editable) */ public hasModel(cell: any): boolean { return (cell?.model !== undefined); @@ -274,7 +436,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni ) { if ($event.shiftKey && cell !== this.latestClickedCell) { // shift + click // interpolate from this.latestClickedCell to this one - if (! Array.isArray(cell.selectable)) { // multiselectable cells are not managed + if (! Array.isArray(cell.selectable)) { // multiselect cells are not managed const wallsUIDs = this.getSortedWallsUIDs(); let posOld: number; let posNew: number; @@ -295,7 +457,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni // push regular wall this.selectedItems.push(this.model.children[i]); } else { - // push downwall + // push down wall this.selectedItems.push(this.model.downWall); } this.latestClickedCell = cell; @@ -322,7 +484,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni // push regular wall this.selectedItems.push(this.model.children[i].structures[columnOld]); } else { - // push downwall + // push down wall this.selectedItems.push(this.model.downWall.structures[columnOld]); } } @@ -368,7 +530,7 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni // select nothing this.selectedItems = []; } else { - // select this cell / thses cells only + // select this cell / theses cells only if (Array.isArray(cell.selectable)) { this.selectedItems = cell.selectable.slice(); // array copy } else { @@ -388,14 +550,6 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni } } - // quick getter for 1st selected item - public get selectedItem() { - if (this.selectedItems.length === 0) { - throw new Error("get selectedItem() : no item selected"); - } - return this.selectedItems[0]; - } - // prevents Firefox to display weird cell border when ctrl+clicking public preventCtrlClickBorder($event) { if ($event.ctrlKey) { @@ -403,1010 +557,879 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni } } - public get addManyOptionsList() { - return Array(20).fill(0).map((value, index) => index + 1); - } - // at this time @Input data is supposed to be already populated public ngOnInit() { this.model = this.pabTable.pab; this.refresh(); } - /** Unselects all selected text (side-effect of shift+clicking) */ - private clearSelection() { - if (window.getSelection) { - const sel = window.getSelection(); - sel.removeAllRanges(); + public onAddClick() { + // add default item + for (let i = 0; i < this.childrenToAdd; i++) { + for (const si of this.selectedItems) { + if (si instanceof Structure) { + // add new default device for wall parent + const newDevice = Session.getInstance().createNub( + new Props({ + calcType: CalculatorType.Structure, + loiDebit: (si.parent as ParallelStructure).getDefaultLoiDebit() + }) + ); + si.parent.addChild(newDevice, si.findPositionInParent()); + + } else { + // add new default wall for PAB parent + const newWall = Session.getInstance().createNub( + new Props({ + calcType: CalculatorType.Cloisons + }) + ); + // add new default device for new wall + const newDevice = Session.getInstance().createNub( + new Props({ + calcType: CalculatorType.Structure, + loiDebit: (newWall as ParallelStructure).getDefaultLoiDebit() + }) + ); + newWall.addChild(newDevice); + this.model.addChild(newWall, si.findPositionInParent()); + } + } } - } + this.refresh(); - // extract PAB walls order - private getSortedWallsUIDs(): string[] { - const wallsUIDs: string[] = []; - for (const c of this.pabTable.pab.children) { - wallsUIDs.push(c.uid); + // notify + let msg: string; + if (this.childrenToAdd === 1 && this.selectedItems.length === 1) { + if (this.selectedItem instanceof Structure) { + msg = this.i18nService.localizeText("INFO_DEVICE_ADDED"); + } else { + msg = this.i18nService.localizeText("INFO_WALL_ADDED"); + } + } else { + const size = (this.childrenToAdd * this.selectedItems.length); + if (this.selectedItem instanceof Structure) { + msg = sprintf(this.i18nService.localizeText("INFO_DEVICE_ADDED_N_TIMES"), size); + } else { + msg = sprintf(this.i18nService.localizeText("INFO_WALL_ADDED_N_TIMES"), size); + } } - wallsUIDs.push(this.pabTable.pab.downWall.uid); - return wallsUIDs; - } + this.notifService.notify(msg); - /** - * Ensures that this.selectedItems elements are ordered according to - * the walls order in the PAB (important for interpolation) - */ - private sortSelectedItems() { - const wallsUIDs = this.getSortedWallsUIDs(); - // are items walls or devices ? - if (this.onlyWallsAreSelected(false)) { - // 1. walls : order by uid, according to model - this.selectedItems.sort((a, b) => { - const posA = wallsUIDs.indexOf(a.uid); - const posB = wallsUIDs.indexOf(b.uid); - return posA - posB; - }); + this.childrenToAdd = 1; // reinit to avoid confusion + } + + public onCopyClick() { + // cloned selected item + for (let i = 0; i < this.childrenToAdd; i++) { + for (const si of this.selectedItems) { + const newChild = Session.getInstance().createNub( + si, + si.parent + ); + // copy parameter values + for (const p of si.prms) { + if (p.visible) { + newChild.getParameter(p.symbol).loadObjectRepresentation(p.objectRepresentation()); + } + } + // copy children + if (si instanceof ParallelStructure) { + for (const c of si.getChildren()) { + const newGrandChild = Session.getInstance().createNub( + c, + newChild + ); + // copy children parameters values + for (const p of c.prms) { + newGrandChild.getParameter(p.symbol).singleValue = p.singleValue; + } + // add to parent + newChild.addChild( + newGrandChild, + c.findPositionInParent() + ); + } + } + // add to parent + si.parent.addChild( + newChild, + si.findPositionInParent() + ); + } + } + this.refresh(); + + // notify + const pos = this.selectedItem.findPositionInParent() + 1; + let msg: string; + if (this.childrenToAdd === 1 && this.selectedItems.length === 1) { + if (this.selectedItem instanceof Structure) { + msg = sprintf(this.i18nService.localizeText("INFO_DEVICE_COPIED"), pos); + } else { + msg = sprintf(this.i18nService.localizeText("INFO_WALL_COPIED"), pos); + } } else { - // 2. devices : order by parent (wall) uid, according to model - this.selectedItems.sort((a, b) => { - const posA = wallsUIDs.indexOf(a.parent.uid); - const posB = wallsUIDs.indexOf(b.parent.uid); - return posA - posB; - }); + const size = (this.childrenToAdd * this.selectedItems.length); + if (this.selectedItem instanceof Structure) { + msg = sprintf(this.i18nService.localizeText("INFO_DEVICE_COPIED_N_TIMES"), pos, size); + } else { + msg = sprintf(this.i18nService.localizeText("INFO_WALL_COPIED_N_TIMES"), pos, size); + } } - return this.selectedItems; + this.notifService.notify(msg); + + this.childrenToAdd = 1; // reinit to avoid confusion } - /** - * Builds the editable data grid from the Pab model - */ - private refresh() { - const maxNbDevices = this.findMaxNumberOfDevices(); + public onMoveUpClick() { + const pos = this.selectedItem.findPositionInParent() + 1; + this.selectedItem.parent.moveChildUp(this.selectedItem); + if (this.selectedItem instanceof Structure) { + this.notifService.notify(sprintf(this.i18nService.localizeText("INFO_DEVICE_MOVED"), pos)); + } else { + this.notifService.notify(sprintf(this.i18nService.localizeText("INFO_WALL_MOVED"), pos)); + } + this.refresh(); + } - // 0. build spanned headers over real columns - this.headers = []; - // 1 column for basin number - let bs: any[] = this.model.children; - bs = bs.concat(this.model.downWall); - this.headers.push({ - title: this.i18nService.localizeText("INFO_PAB_NUM_BASSIN"), - selectable: bs, - rowspan: 2 - }); - // 3 columns for basin information - this.headers.push({ - title: this.i18nService.localizeText("INFO_PAB_BASSIN"), - colspan: 3, - selectable: bs - }); - // 1 col for wall - this.headers.push({ - title: this.i18nService.localizeText("INFO_PB_CLOISON"), - selectable: bs - }); - // 1 header for each device of the wall having the most devices (including downwall) - for (let i = 0; i < maxNbDevices; i++) { - this.headers.push({ - title: sprintf(this.i18nService.localizeText("INFO_PAB_CLOISON_OUVRAGE_N"), (i + 1)), - colspan: 2, - selectable: this.model.children.map(c => c.getChildren()[i]).concat(this.model.downWall.getChildren()[i]), - selectableColumn: i - }); + public onMoveDownClick() { + const pos = this.selectedItem.findPositionInParent() + 1; + this.selectedItem.parent.moveChildDown(this.selectedItem); + if (this.selectedItem instanceof Structure) { + this.notifService.notify(sprintf(this.i18nService.localizeText("INFO_DEVICE_MOVED"), pos)); + } else { + this.notifService.notify(sprintf(this.i18nService.localizeText("INFO_WALL_MOVED"), pos)); } + this.refresh(); + } - // A. build columns set - this.cols = []; - const headerRow1 = { cells: [] }; - const headerRow2 = { cells: [] }; - this.cols.push(headerRow1); - this.cols.push(headerRow2); + public onRemoveClick() { + let wallsCount = 0; + let devicesCount = 0; + const deletedWallsUids = []; - // 3 cols for basin information - headerRow1.cells.push({ - title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "LB"), - selectable: bs - }); - headerRow1.cells.push({ - title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "BB"), - selectable: bs - }); - headerRow1.cells.push({ - title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "ZRMB"), - selectable: bs - }); + // first pass: gather deleted structures UIDs + for (const se of this.selectedItems) { + if (! (se instanceof Structure)) { + wallsCount++; + deletedWallsUids.push(se.uid); + } + } - // 2 cols for each device of the wall having the most devices (including downwall) - for (let i = 0; i < maxNbDevices; i++) { - const sel = this.model.children.map(c => c.getChildren()[i]).concat(this.model.downWall.getChildren()[i]); - if (i == 0) { - headerRow1.cells.push({ - title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "ZRAM"), - selectable: bs, - }); + // second pass: remove + for (const se of this.selectedItems) { + if (se instanceof Structure) { // device + // do not remove device if parent structure is to be removed too + if (! deletedWallsUids.includes(se.parent.uid)) { + se.parent.deleteChild(se.findPositionInParent()); + devicesCount++; + } + } else { + // remove wall + se.parent.deleteChild(se.findPositionInParent()); } - headerRow1.cells.push({ - title: this.i18nService.localizeText("INFO_PAB_HEADER_PARAMETERS"), - selectable: sel, - selectableColumn: i - }); - headerRow1.cells.push({ - title: this.i18nService.localizeText("INFO_PAB_HEADER_VALUES"), - selectable: sel, - selectableColumn: i - }); } + this.selectedItems = []; + this.refresh(); - // B. Build rows set - this.rows = []; - // admissible LoiDebit (same for all cloisons) - const loisCloisons = this.model.children[0].getLoisAdmissiblesArray().map(l => { - return { - label: this.localizeLoiDebit(l), - value: l - }; - }); + // notify + let msg: string; + if (wallsCount === 0) { + msg = sprintf(this.i18nService.localizeText("INFO_DEVICES_REMOVED"), devicesCount); + } else if (devicesCount === 0) { + msg = sprintf(this.i18nService.localizeText("INFO_WALLS_REMOVED"), wallsCount); + } else { + msg = sprintf(this.i18nService.localizeText("INFO_WALLS_AND_DEVICES_REMOVED"), wallsCount, devicesCount); + } + this.notifService.notify(msg); + } - // NOTE : EB = empty cell (3 columns wide) for LB,BB,ZRMB - // EZRAM = empty cell below ZRAM value (QA editor height + 1) + public exportAsSpreadsheet() { + const elem: any = document.getElementById("geometry"); + const elemCopy = (elem as HTMLElement).cloneNode(true) as HTMLElement; + // enrich element copy: replace inputs by their values, so that it appears in the exported spreadsheet + const tables: any = elemCopy.getElementsByTagName("table"); + for (const table of tables) { + const tds: any = table.getElementsByTagName("td"); + for (const td of tds) { + // if it contains an input, replace it with the input value + const inputs = td.getElementsByTagName("input"); + if (inputs.length > 0) { + const input = inputs[0]; + if (input.id.split("_")[1] === "QA") { + td.innerHTML = NgParameter.preview(this.model.children[input.id.split("_")[0]].prms.QA); + } else { + td.innerHTML = input.value; + } + } + } + } + // export the enriched element copy + AppComponent.exportAsSpreadsheet(elemCopy as any); + } - const minQAEditorRowCount: number = 1; + /** Replace device Nub when LoiDebit is changed */ + public loiDebitSelected($event: any, cell: any) { + const device = cell.model as Nub; + // create new child device + const newDevice = Session.getInstance().createNub( + new Props({ + calcType: CalculatorType.Structure, + loiDebit: $event.value + }) + ); + // replace the current one + device.parent.replaceChildInplace(device, newDevice); + this.refresh(); + // send input change event (used to reset form results) + this.inputChange.emit(); + } - // B.1 many rows for each wall - let childIndex = 0; - for (const cloison of this.model.children) { - // maximum device parameter count for all devices in this wall - const maxDeviceParamCount = this.findMaxNumberOfDeviceParameters(cloison); - - // total row count for this wall = max device parameter row count + 1 line for device type - // minimum = 1 row (EB) + 1 row (LB,BB,ZRMB cells) + QA editor - const totalRowCount = Math.max(maxDeviceParamCount + 1, 1 + 1 + minQAEditorRowCount); - - // QA editor row count : total row count - 1 (LB,BB,ZRMB cells) - 1 (EB, see note) - const QAEditorRowCount = Math.max(totalRowCount - 2, minQAEditorRowCount); - - // total parameter rows (all parameters without device type) = total row count - 1 - const paramRowCount = totalRowCount - 1; - - for (let r = 0; r < totalRowCount; r++) { - const deviceParamRow = { selectable: cloison, cells: [] }; - if (r === 0) { - // basin number - deviceParamRow.cells.push({ - value: childIndex + 1, - rowspan: totalRowCount, - class: "basin_number", - selectable: cloison - }); - // empty line (EB cell, see note) - deviceParamRow.cells.push({ - colspan: 3, - selectable: cloison - }); - // ZRAM - deviceParamRow.cells.push({ - model: cloison.prms.ZRAM, - title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "ZRAM") - }); - } - // LB, BB, ZRMB, EZRAM cell (see note) - else if (r === 1) { - // Longueur bassin - deviceParamRow.cells.push({ - model: cloison.prms.LB, - title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "LB") - }); - // Largeur bassin - deviceParamRow.cells.push({ - model: cloison.prms.BB, - title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "BB") - }); - // Cote radier mi bassin - deviceParamRow.cells.push({ - model: cloison.prms.ZRMB, - title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "ZRMB") - }); - // empty cell (EZRAM cell, see note) - deviceParamRow.cells.push({ - rowspan: paramRowCount, - selectable: cloison - }); - } - else if (r === 2) { - // rows for QA editor - const qaParam = new NgParameter(cloison.prms.QA, this.pabTable.form); - qaParam.radioConfig = ParamRadioConfig.VAR; - deviceParamRow.cells.push({ - model: qaParam, - colspan: 3, - rowspan: QAEditorRowCount, - qa: true, - title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "QA") - }); - } - - // devices - this.fillParallelStructureCells(deviceParamRow, r, paramRowCount, loisCloisons); - } - childIndex++; - } - - // B.2 many rows for downwall - // admissible LoiDebit - const loisAval = this.model.downWall.getLoisAdmissiblesArray().map(l => { - return { - label: this.localizeLoiDebit(l), - value: l - }; - }); - // as much rows as the greatest number of parameters among its devices - const dwParamCount = this.findMaxNumberOfDeviceParameters(this.model.downWall); // device parameter count - const paramRowCount = dwParamCount + 1; // max line number for parameters (without device type) - for (let r = 0; r < paramRowCount; r++) { - // build device params row - const deviceParamRowDW = { selectable: this.model.downWall, cells: [] }; - if (r === 0) { - // "downstream" - deviceParamRowDW.cells.push({ - value: "Aval", - rowspan: paramRowCount, - class: "basin_number", - selectable: this.model.downWall - }); - // 3 empty cells - deviceParamRowDW.cells.push({ - colspan: 3, - rowspan: paramRowCount, - selectable: this.model.downWall - }); - // ZRAM - deviceParamRowDW.cells.push({ - model: this.model.downWall.prms.ZRAM, - title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "ZRAM") - }); - } - if (r === 1) { - // 1 empty cell (in place of the QA editor) - deviceParamRowDW.cells.push({ - rowspan: dwParamCount, - selectable: this.model.downWall - }); - } - - // devices - this.fillParallelStructureCells(deviceParamRowDW, r, paramRowCount, loisAval); - } - - this.updateValidity(); - } - - private fillParallelStructureCells(tableRow: any, rowIndex: number, maxStructParamRowCount: number, loisAdmissibles: any[]) { - const ps: ParallelStructure = tableRow.selectable; - for (const struct of ps.structures) { // for each device - const structParamCount = this.nubVisibleParameterCount(struct); - if (rowIndex === 0) { - // 1st row : device type - tableRow.cells.push({ - model: struct, - modelValue: struct.getPropValue("loiDebit"), - options: loisAdmissibles, - selectable: struct, - colspan: 2 - }); - } - else if (rowIndex === structParamCount + 1) { - // fill remaining space - const remaining = maxStructParamRowCount - structParamCount; - if (remaining > 0) { - tableRow.cells.push({ - colspan: 2, - rowspan: remaining, - selectable: struct - }); - } - } - else { - // parameter row - const nvParam = struct.getNthVisibleParam(rowIndex - 1); - if (nvParam) { - const nvParamTitle = this.formService.expandVariableNameAndUnit(CalculatorType.Pab, nvParam.symbol); - // parameter name - tableRow.cells.push({ - value: nvParam.symbol, - title: nvParamTitle, - selectable: struct - }); - // parameter value - tableRow.cells.push({ - model: nvParam, - title: nvParamTitle, - selectable: struct - }); - } - } - } - // done ! - this.rows.push(tableRow); - } - - /** - * Finds the localized title for a LoiDebit item - */ - private localizeLoiDebit(l: LoiDebit) { - return this.i18nService.localizeText("INFO_PAB_LOIDEBIT_" + LoiDebit[l].toUpperCase()); - } - - private findMaxNumberOfDevices(): number { - let maxNbDevices = 1; - for (const w of this.model.children) { - maxNbDevices = Math.max(maxNbDevices, w.getChildren().length); - } - maxNbDevices = Math.max(maxNbDevices, this.model.downWall.getChildren().length); - return maxNbDevices; - } - - private nubVisibleParameterCount(n: Nub) { - let res = 0; - for (const p of n.parameterIterator) { - if (p.visible) { - res++; - } - } - return res; - } - - private findMaxNumberOfDeviceParameters(struct: ParallelStructure): number { - let maxNbParams = 1; - for (const child of struct.getChildren()) { - maxNbParams = Math.max(maxNbParams, this.nubVisibleParameterCount(child)); - } - return maxNbParams; - } - - /** returns true if exactly one device is selected, and nothing else */ - public get selectionIsOneDevice() { - return ( - this.selectedItems.length === 1 - && this.selectedItem instanceof Structure - ); - } - - /** - * Returns true if there is at least one selected item, - * and all selected items are devices - */ - private onlyDevicesAreSelected() { - let ok = false; - if (this.selectedItems.length > 0) { - ok = true; - for (const s of this.selectedItems) { - ok = ok && (s instanceof Structure); - } - } - return ok; - } - - /** - * Returns true if there is at least one selected item, - * all selected items are devices, and belong to the same column - */ - private onlyDevicesOfTheSameColumnAreSelected() { - let ok = false; - let columnIndex: number; - if (this.selectedItems.length > 0) { - ok = true; - for (const s of this.selectedItems) { - if (s instanceof Structure) { - const ci = s.findPositionInParent(); - ok = ok && (columnIndex === undefined || columnIndex === ci); - columnIndex = ci; - } else { - ok = false; - } - } - } - return ok; - } - - /** - * Returns true if there is at least one selected item, - * and all selected items are walls - */ - private onlyWallsAreSelected(excludeDownwall: boolean = true) { - let ok = false; - if (this.selectedItems.length > 0) { - ok = true; - for (const s of this.selectedItems) { - if (excludeDownwall) { - ok = ok && (s instanceof Cloisons); - } else { - ok = ok && (s instanceof ParallelStructure); - } - } - } - return ok; - } - - public get relatedEntityTitle() { - let title = ""; - if (this.onlyDevicesAreSelected()) { - title = this.i18nService.localizeText("INFO_PAB_OUVRAGES"); - } else if (this.onlyWallsAreSelected()) { - title = this.i18nService.localizeText("INFO_PAB_BASSINS"); - } - if (title !== "") { - title += " :"; - } - return title; - } - - public get enableAddButton() { - return ( - this.onlyDevicesOfTheSameColumnAreSelected() - || ( - this.selectedItems.length === 1 - && ! (this.selectedItem instanceof CloisonAval) // exclude downwall - ) - ); - } - - public get enableCopyButton() { - return this.enableAddButton; - } - - public get enableUpButton() { - return ( - this.selectedItems.length === 1 - && ! (this.selectedItem instanceof CloisonAval) // exclude downwall - && this.selectedItem.parent - && this.selectedItem.findPositionInParent() !== 0 - ); - } - - public get enableDownButton() { - return ( - this.selectedItems.length === 1 - && ! (this.selectedItem instanceof CloisonAval) // exclude downwall - && this.selectedItem.parent - && this.selectedItem.findPositionInParent() < (this.selectedItem.parent.getChildren().length - 1) - ); - } - - public get enableRemoveButton() { - let containsDownwall = false; - let containsOrphanNub = false; - let tooFewDevices = false; - let wallsCount = 0; - const devicesCountById = {}; - const deletedWallsUids = []; + // show modal dialog for values edition + public showEditPab() { + if (this.selectedItems.length > 0) { - for (const se of this.selectedItems) { - if (se instanceof Structure) { // device - if (devicesCountById[se.parent.uid] === undefined) { - devicesCountById[se.parent.uid] = 0; + // list variables eligible to modification + const availableVariables: { label: string; value: string; occurrences: number; first: number; last: number }[] = []; + for (const c of this.selectedItems) { + for (const p of c.parameterIterator) { // deep one + if ( + p.visible && + ! availableVariables.map(av => av.value).includes(p.symbol) + ) { + availableVariables.push({ + label: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, p.symbol), + value: p.symbol, + occurrences: 0, + first: undefined, + last: undefined + }); + } } - devicesCountById[se.parent.uid]++; - } else { // wall - wallsCount++; - deletedWallsUids.push(se.uid); } - if (se instanceof CloisonAval) { - containsDownwall = true; // cannot remove downwall - } - if (! se.parent) { - containsOrphanNub = true; // not supposed to happen but who knows + // find their min/max values (2nd pass) + for (const av of availableVariables) { + for (const c of this.selectedItems) { + for (const p of c.parameterIterator) { + // @TODO what todo when p varies (QA only) ? + if (p.visible && p.symbol === av.value && ! p.hasMultipleValues) { + av.occurrences ++; + if (av.first === undefined) { + av.first = p.singleValue; + } + av.last = p.singleValue; + } + } + } } - } - // at least one device must remain in each basin, unless this basin is removed too - for (const structureId in devicesCountById) { - if (! deletedWallsUids.includes(structureId)) { - let wall: Nub; - if (this.model.downWall.uid === structureId) { - wall = this.model.downWall; - } else { - wall = this.model.getChild(structureId); + // sum up selected items + const walls: ParallelStructure[] = []; + const wallsDevices: Structure[] = []; + const devices: Structure[] = []; + let vertical = true; // @TODO vertical AND consecutive ! + let firstDevicePosition: number; + // 1st pass + for (const s of this.selectedItems) { + if (s instanceof ParallelStructure) { + walls.push(s); + for (const c of s.structures) { + if (firstDevicePosition === undefined) { + firstDevicePosition = c.findPositionInParent(); + } else { + vertical = (vertical && (c.findPositionInParent() === firstDevicePosition)); + } + wallsDevices.push(c); + } } - if (wall.getChildren().length <= devicesCountById[structureId]) { - tooFewDevices = true; + } + const reallySelectedWalls = [...walls]; // array copy + // 2nd pass + for (const c of this.selectedItems) { + if (c instanceof Structure) { + if (! wallsDevices.includes(c)) { + if (firstDevicePosition === undefined) { + firstDevicePosition = c.findPositionInParent(); + } else { + vertical = (vertical && (c.findPositionInParent() === firstDevicePosition)); + } + // add parent wall for basin-length based interpolation + const parentWall = (c.parent as ParallelStructure); + if (parentWall && ! walls.includes(parentWall)) { + walls.push(parentWall); + } + devices.push(c); + } } } - } - return ( - this.selectedItems.length > 0 - && wallsCount < this.model.children.length // at least one basin must remain - && ! containsDownwall - && ! containsOrphanNub - && ! tooFewDevices - ); - } + // open dialog + const dialogRef = this.editPabDialog.open( + DialogEditPabComponent, + { + data: { + availableVariables: availableVariables, + selectedItemsAbstract: { + walls: reallySelectedWalls.length, + wallsDevices: wallsDevices.length, + devices: devices.length + }, + vertical: vertical // used to enable interpolation + }, + disableClose: true + } + ); - /** - * returns true if at least one object is selected - */ - public get enableEditPabButton() { - return ( - this.selectedItems.length > 0 - && ( - this.onlyDevicesAreSelected() - || this.onlyWallsAreSelected(false) - ) - ); - } + // apply modifications + dialogRef.afterClosed().subscribe(result => { + if (result) { + /* console.log("Apply values in parent !!", result.action, result.variable, result.value, + result.delta, result.variableDetails); */ + switch (result.action) { + case "set-value": + for (const s of this.selectedItems) { + for (const p of s.parameterIterator) { // deep + // force single mode (QA only) + if (p.hasMultipleValues) { + p.valueMode = ParamValueMode.SINGLE; + } + if (p.symbol === result.variable) { + p.singleValue = result.value; + } + } + } + break; - public onAddClick() { - // add default item - for (let i = 0; i < this.childrenToAdd; i++) { - for (const si of this.selectedItems) { - if (si instanceof Structure) { - // add new default device for wall parent - const newDevice = Session.getInstance().createNub( - new Props({ - calcType: CalculatorType.Structure, - loiDebit: (si.parent as ParallelStructure).getDefaultLoiDebit() - }) - ); - si.parent.addChild(newDevice, si.findPositionInParent()); + case "delta": + for (const s of this.selectedItems) { + for (const p of s.parameterIterator) { // deep + // force single mode (QA only) + if (p.hasMultipleValues) { + p.valueMode = ParamValueMode.SINGLE; + } + if (p.symbol === result.variable) { + p.singleValue += result.delta; + } + } + } + break; - } else { - // add new default wall for PAB parent - const newWall = Session.getInstance().createNub( - new Props({ - calcType: CalculatorType.Cloisons - }) - ); - // add new default device for new wall - const newDevice = Session.getInstance().createNub( - new Props({ - calcType: CalculatorType.Structure, - loiDebit: (newWall as ParallelStructure).getDefaultLoiDebit() - }) - ); - newWall.addChild(newDevice); - this.model.addChild(newWall, si.findPositionInParent()); + case "interpolate": + if (result.variableDetails.occurrences > 1) { + const interpolatedValues: number[] = []; + const variableRange = result.variableDetails.last - result.variableDetails.first; + let totalBasinsLengths = 0; + for (let wi = 0; wi < walls.length; wi++) { + const w = walls[wi]; + if (w instanceof Cloisons) { + if (result.variable === "ZRMB") { + // for ZRMB, exclude 1st basin + if (wi > 0) { + // half the previous basin length, half the current basin length + totalBasinsLengths += ( + (walls[wi - 1] as Cloisons).prms.LB.singleValue / 2 + + w.prms.LB.singleValue / 2 + ); + } + } else { + // for other interpolable elevations, exclude last basin + if (wi < walls.length - 1) { + totalBasinsLengths += w.prms.LB.singleValue; + } + } + } + } + // console.log(`TOTAL BASINS LENGTHS: ${totalBasinsLengths}, VARIABLE RANGE: ${variableRange}`); + // generate interpolated values list + interpolatedValues.push(result.variableDetails.first); + let currentValue: number = result.variableDetails.first; + for (let i = 0; i < result.variableDetails.occurrences - 1; i++) { + if (result.variable === "ZRMB") { + // for ZRMB, exclude 1st basin + if (i > 0) { + // compute step as percentage of total length, related to sum of + // half the previous basin length and half the current basin length + const currentLength = ( + (walls[i - 1] as Cloisons).prms.LB.singleValue / 2 + + (walls[i] as Cloisons).prms.LB.singleValue / 2 + ); + const currentBasinLengthPercentage = currentLength / totalBasinsLengths; + const step = variableRange * currentBasinLengthPercentage; + /* console.log(`Wall ${i} : length = ${currentLength} / ${totalBasinsLengths}` + + ` (${currentBasinLengthPercentage}), applying step of ${step}`); */ + currentValue += step; + interpolatedValues.push(currentValue); + } + } else { + // for other interpolable elevations, exclude last basin + if (i < result.variableDetails.occurrences - 2) { + // compute step as percentage of total length, related to current basin length + const currentBasinLength = (walls[i] as Cloisons).prms.LB.singleValue; + const currentBasinLengthPercentage = currentBasinLength / totalBasinsLengths; + const step = variableRange * currentBasinLengthPercentage; + /* console.log(`Wall ${i} : length = ${currentBasinLength} / ${totalBasinsLengths}` + + ` (${currentBasinLengthPercentage}), applying step of ${step}`); */ + currentValue += step; + interpolatedValues.push(currentValue); + } + } + } + // console.log("INTERPOPOLATED VALUES", interpolatedValues); + // interpolatedValues.push(result.variableDetails.last); + // apply + let idx = 0; + for (const s of this.selectedItems) { + // for ZRMB, interpolatedValues length is shorter by 1 element + if (interpolatedValues[idx] !== undefined) { + for (const p of s.parameterIterator) { // deep + // force single mode (QA only) + if (p.hasMultipleValues) { + p.valueMode = ParamValueMode.SINGLE; + } + if (p.symbol === result.variable) { + p.singleValue = interpolatedValues[idx]; + idx ++; + } + } + } + } + } else { + throw new Error( + `showEditPab() : cannot interpolate, too few occurrences (${result.variableDetails.occurrences})` + ); + } + break; + } } - } + }); } - this.refresh(); + } - // notify - let msg: string; - if (this.childrenToAdd === 1 && this.selectedItems.length === 1) { - if (this.selectedItem instanceof Structure) { - msg = this.i18nService.localizeText("INFO_DEVICE_ADDED"); - } else { - msg = this.i18nService.localizeText("INFO_WALL_ADDED"); - } - } else { - const size = (this.childrenToAdd * this.selectedItems.length); - if (this.selectedItem instanceof Structure) { - msg = sprintf(this.i18nService.localizeText("INFO_DEVICE_ADDED_N_TIMES"), size); - } else { - msg = sprintf(this.i18nService.localizeText("INFO_WALL_ADDED_N_TIMES"), size); - } - } - this.notifService.notify(msg); + public ngAfterViewInit() { + this.updateValidity(); + } - this.childrenToAdd = 1; // reinit to avoid confusion + public getCellValue(cell) { + if (typeof cell.model.singleValue === "string") { + cell.model.singleValue = +cell.model.singleValue; + } + return round(cell.model.singleValue, this.nDigits); } - public onCopyClick() { - // cloned selected item - for (let i = 0; i < this.childrenToAdd; i++) { - for (const si of this.selectedItems) { - const newChild = Session.getInstance().createNub( - si, - si.parent - ); - // copy parameter values - for (const p of si.prms) { - if (p.visible) { - newChild.getParameter(p.symbol).loadObjectRepresentation(p.objectRepresentation()); - } - } - // copy children - if (si instanceof ParallelStructure) { - for (const c of si.getChildren()) { - const newGrandChild = Session.getInstance().createNub( - c, - newChild - ); - // copy children parameters values - for (const p of c.prms) { - newGrandChild.getParameter(p.symbol).singleValue = p.singleValue; - } - // add to parent - newChild.addChild( - newGrandChild, - c.findPositionInParent() - ); - } - } - // add to parent - si.parent.addChild( - newChild, - si.findPositionInParent() - ); + public setCellValue(cell, $event) { + if ($event !== "-" && $event !== "") { + try { + cell.model.singleValue = $event; + cell.modelValidity = undefined; + } catch (error) { + cell.modelValidity = false; } } - this.refresh(); + } - // notify - const pos = this.selectedItem.findPositionInParent() + 1; - let msg: string; - if (this.childrenToAdd === 1 && this.selectedItems.length === 1) { - if (this.selectedItem instanceof Structure) { - msg = sprintf(this.i18nService.localizeText("INFO_DEVICE_COPIED"), pos); - } else { - msg = sprintf(this.i18nService.localizeText("INFO_WALL_COPIED"), pos); - } - } else { - const size = (this.childrenToAdd * this.selectedItems.length); - if (this.selectedItem instanceof Structure) { - msg = sprintf(this.i18nService.localizeText("INFO_DEVICE_COPIED_N_TIMES"), pos, size); - } else { - msg = sprintf(this.i18nService.localizeText("INFO_WALL_COPIED_N_TIMES"), pos, size); - } + public invalidNANInputValue(e: any) { + const rgx = /^-?[0-9]*\.?[0-9]*$/; + if (e.key.match(rgx) === null) { + e.preventDefault(); } - this.notifService.notify(msg); + } - this.childrenToAdd = 1; // reinit to avoid confusion + /** Unselects all selected text (side-effect of shift+clicking) */ + private clearSelection() { + if (window.getSelection) { + const sel = window.getSelection(); + sel.removeAllRanges(); + } } - public onMoveUpClick() { - const pos = this.selectedItem.findPositionInParent() + 1; - this.selectedItem.parent.moveChildUp(this.selectedItem); - if (this.selectedItem instanceof Structure) { - this.notifService.notify(sprintf(this.i18nService.localizeText("INFO_DEVICE_MOVED"), pos)); - } else { - this.notifService.notify(sprintf(this.i18nService.localizeText("INFO_WALL_MOVED"), pos)); + // extract PAB walls order + private getSortedWallsUIDs(): string[] { + const wallsUIDs: string[] = []; + for (const c of this.pabTable.pab.children) { + wallsUIDs.push(c.uid); } - this.refresh(); + wallsUIDs.push(this.pabTable.pab.downWall.uid); + return wallsUIDs; } - public onMoveDownClick() { - const pos = this.selectedItem.findPositionInParent() + 1; - this.selectedItem.parent.moveChildDown(this.selectedItem); - if (this.selectedItem instanceof Structure) { - this.notifService.notify(sprintf(this.i18nService.localizeText("INFO_DEVICE_MOVED"), pos)); + /** + * Ensures that this.selectedItems elements are ordered according to + * the walls order in the PAB (important for interpolation) + */ + private sortSelectedItems() { + const wallsUIDs = this.getSortedWallsUIDs(); + // are items walls or devices ? + if (this.onlyWallsAreSelected(false)) { + // 1. walls : order by uid, according to model + this.selectedItems.sort((a, b) => { + const posA = wallsUIDs.indexOf(a.uid); + const posB = wallsUIDs.indexOf(b.uid); + return posA - posB; + }); } else { - this.notifService.notify(sprintf(this.i18nService.localizeText("INFO_WALL_MOVED"), pos)); + // 2. devices : order by parent (wall) uid, according to model + this.selectedItems.sort((a, b) => { + const posA = wallsUIDs.indexOf(a.parent.uid); + const posB = wallsUIDs.indexOf(b.parent.uid); + return posA - posB; + }); } - this.refresh(); + return this.selectedItems; } - public onRemoveClick() { - let wallsCount = 0; - let devicesCount = 0; - const deletedWallsUids = []; + /** + * Builds the editable data grid from the Pab model + */ + private refresh() { + const maxNbDevices = this.findMaxNumberOfDevices(); - // first pass: gather deleted structures UIDs - for (const se of this.selectedItems) { - if (! (se instanceof Structure)) { - wallsCount++; - deletedWallsUids.push(se.uid); - } + // 0. build spanned headers over real columns + this.headers = []; + // 1 column for basin number + let bs: any[] = this.model.children; + bs = bs.concat(this.model.downWall); + this.headers.push({ + title: this.i18nService.localizeText("INFO_PAB_NUM_BASSIN"), + selectable: bs, + rowspan: 2 + }); + // 3 columns for basin information + this.headers.push({ + title: this.i18nService.localizeText("INFO_PAB_BASSIN"), + colspan: 3, + selectable: bs + }); + // 1 col for wall + this.headers.push({ + title: this.i18nService.localizeText("INFO_PB_CLOISON"), + selectable: bs + }); + // 1 header for each device of the wall having the most devices (including downwall) + for (let i = 0; i < maxNbDevices; i++) { + this.headers.push({ + title: sprintf(this.i18nService.localizeText("INFO_PAB_CLOISON_OUVRAGE_N"), (i + 1)), + colspan: 2, + selectable: this.model.children.map(c => c.getChildren()[i]).concat(this.model.downWall.getChildren()[i]), + selectableColumn: i + }); } - // second pass: remove - for (const se of this.selectedItems) { - if (se instanceof Structure) { // device - // do not remove device if parent structure is to be removed too - if (! deletedWallsUids.includes(se.parent.uid)) { - se.parent.deleteChild(se.findPositionInParent()); - devicesCount++; - } - } else { - // remove wall - se.parent.deleteChild(se.findPositionInParent()); - } - } - this.selectedItems = []; - this.refresh(); + // A. build columns set + this.cols = []; + const headerRow1 = { cells: [] }; + const headerRow2 = { cells: [] }; + this.cols.push(headerRow1); + this.cols.push(headerRow2); - // notify - let msg: string; - if (wallsCount === 0) { - msg = sprintf(this.i18nService.localizeText("INFO_DEVICES_REMOVED"), devicesCount); - } else if (devicesCount === 0) { - msg = sprintf(this.i18nService.localizeText("INFO_WALLS_REMOVED"), wallsCount); - } else { - msg = sprintf(this.i18nService.localizeText("INFO_WALLS_AND_DEVICES_REMOVED"), wallsCount, devicesCount); + // 3 cols for basin information + headerRow1.cells.push({ + title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "LB"), + selectable: bs + }); + headerRow1.cells.push({ + title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "BB"), + selectable: bs + }); + headerRow1.cells.push({ + title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "ZRMB"), + selectable: bs + }); + + // 2 cols for each device of the wall having the most devices (including downwall) + for (let i = 0; i < maxNbDevices; i++) { + const sel = this.model.children.map(c => c.getChildren()[i]).concat(this.model.downWall.getChildren()[i]); + if (i === 0) { + headerRow1.cells.push({ + title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "ZRAM"), + selectable: bs, + }); + } + headerRow1.cells.push({ + title: this.i18nService.localizeText("INFO_PAB_HEADER_PARAMETERS"), + selectable: sel, + selectableColumn: i + }); + headerRow1.cells.push({ + title: this.i18nService.localizeText("INFO_PAB_HEADER_VALUES"), + selectable: sel, + selectableColumn: i + }); } - this.notifService.notify(msg); - } - public get uitextAdd(): string { - return this.i18nService.localizeText("INFO_FIELDSET_ADD"); - } + // B. Build rows set + this.rows = []; + // admissible LoiDebit (same for all cloisons) + const loisCloisons = this.model.children[0].getLoisAdmissiblesArray().map(l => ({ + label: this.localizeLoiDebit(l), + value: l + })); - public get uitextCopy(): string { - return this.i18nService.localizeText("INFO_FIELDSET_COPY"); - } + // NOTE : EB = empty cell (3 columns wide) for LB,BB,ZRMB + // EZRAM = empty cell below ZRAM value (QA editor height + 1) - public get uitextRemove(): string { - return this.i18nService.localizeText("INFO_FIELDSET_REMOVE"); - } + const minQAEditorRowCount = 1; - public get uitextMoveUp(): string { - if (this.selectionIsOneDevice) { - return this.i18nService.localizeText("INFO_FIELDSET_MOVE_LEFT"); - } else { - return this.i18nService.localizeText("INFO_FIELDSET_MOVE_UP"); - } - } + // B.1 many rows for each wall + let childIndex = 0; + for (const cloison of this.model.children) { + // maximum device parameter count for all devices in this wall + const maxDeviceParamCount = this.findMaxNumberOfDeviceParameters(cloison); - public get uitextMoveDown(): string { - if (this.selectionIsOneDevice) { - return this.i18nService.localizeText("INFO_FIELDSET_MOVE_RIGHT"); - } else { - return this.i18nService.localizeText("INFO_FIELDSET_MOVE_DOWN"); - } - } + // total row count for this wall = max device parameter row count + 1 line for device type + // minimum = 1 row (EB) + 1 row (LB,BB,ZRMB cells) + QA editor + const totalRowCount = Math.max(maxDeviceParamCount + 1, 1 + 1 + minQAEditorRowCount); - /** Replace device Nub when LoiDebit is changed */ - public loiDebitSelected($event: any, cell: any) { - const device = cell.model as Nub; - // create new child device - const newDevice = Session.getInstance().createNub( - new Props({ - calcType: CalculatorType.Structure, - loiDebit: $event.value - }) - ); - // replace the current one - device.parent.replaceChildInplace(device, newDevice); - this.refresh(); - // send input change event (used to reset form results) - this.inputChange.emit(); - } + // QA editor row count : total row count - 1 (LB,BB,ZRMB cells) - 1 (EB, see note) + const QAEditorRowCount = Math.max(totalRowCount - 2, minQAEditorRowCount); - // show modal dialog for values edition - public showEditPab() { - if (this.selectedItems.length > 0) { + // total parameter rows (all parameters without device type) = total row count - 1 + const paramRowCount = totalRowCount - 1; - // list variables eligible to modification - const availableVariables: { label: string, value: string, occurrences: number, first: number, last: number }[] = []; - for (const c of this.selectedItems) { - for (const p of c.parameterIterator) { // deep one - if ( - p.visible && - ! availableVariables.map(av => av.value).includes(p.symbol) - ) { - availableVariables.push({ - label: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, p.symbol), - value: p.symbol, - occurrences: 0, - first: undefined, - last: undefined - }); - } + for (let r = 0; r < totalRowCount; r++) { + const deviceParamRow = { selectable: cloison, cells: [] }; + if (r === 0) { + // basin number + deviceParamRow.cells.push({ + value: childIndex + 1, + rowspan: totalRowCount, + class: "basin_number", + selectable: cloison + }); + // empty line (EB cell, see note) + deviceParamRow.cells.push({ + colspan: 3, + selectable: cloison + }); + // ZRAM + deviceParamRow.cells.push({ + model: cloison.prms.ZRAM, + title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "ZRAM") + }); } - } - // find their min/max values (2nd pass) - for (const av of availableVariables) { - for (const c of this.selectedItems) { - for (const p of c.parameterIterator) { - // @TODO what todo when p varies (QA only) ? - if (p.visible && p.symbol === av.value && ! p.hasMultipleValues) { - av.occurrences ++; - if (av.first === undefined) { - av.first = p.singleValue; - } - av.last = p.singleValue; - } - } + // LB, BB, ZRMB, EZRAM cell (see note) + else if (r === 1) { + // Longueur bassin + deviceParamRow.cells.push({ + model: cloison.prms.LB, + title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "LB") + }); + // Largeur bassin + deviceParamRow.cells.push({ + model: cloison.prms.BB, + title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "BB") + }); + // Cote radier mi bassin + deviceParamRow.cells.push({ + model: cloison.prms.ZRMB, + title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "ZRMB") + }); + // empty cell (EZRAM cell, see note) + deviceParamRow.cells.push({ + rowspan: paramRowCount, + selectable: cloison + }); + } else if (r === 2) { + // rows for QA editor + const qaParam = new NgParameter(cloison.prms.QA, this.pabTable.form); + qaParam.radioConfig = ParamRadioConfig.VAR; + deviceParamRow.cells.push({ + model: qaParam, + colspan: 3, + rowspan: QAEditorRowCount, + qa: true, + title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "QA") + }); } + + // devices + this.fillParallelStructureCells(deviceParamRow, r, paramRowCount, loisCloisons); } + childIndex++; + } - // sum up selected items - const walls: ParallelStructure[] = []; - const wallsDevices: Structure[] = []; - const devices: Structure[] = []; - let vertical = true; // @TODO vertical AND consecutive ! - let firstDevicePosition: number; - // 1st pass - for (const s of this.selectedItems) { - if (s instanceof ParallelStructure) { - walls.push(s); - for (const c of s.structures) { - if (firstDevicePosition === undefined) { - firstDevicePosition = c.findPositionInParent(); - } else { - vertical = (vertical && (c.findPositionInParent() === firstDevicePosition)); - } - wallsDevices.push(c); - } - } + // B.2 many rows for downwall + // admissible LoiDebit + const loisAval = this.model.downWall.getLoisAdmissiblesArray().map(l => ({ + label: this.localizeLoiDebit(l), + value: l + })); + // as much rows as the greatest number of parameters among its devices + const dwParamCount = this.findMaxNumberOfDeviceParameters(this.model.downWall); // device parameter count + const paramRowCount = dwParamCount + 1; // max line number for parameters (without device type) + for (let r = 0; r < paramRowCount; r++) { + // build device params row + const deviceParamRowDW = { selectable: this.model.downWall, cells: [] }; + if (r === 0) { + // "downstream" + deviceParamRowDW.cells.push({ + value: "Aval", + rowspan: paramRowCount, + class: "basin_number", + selectable: this.model.downWall + }); + // 3 empty cells + deviceParamRowDW.cells.push({ + colspan: 3, + rowspan: paramRowCount, + selectable: this.model.downWall + }); + // ZRAM + deviceParamRowDW.cells.push({ + model: this.model.downWall.prms.ZRAM, + title: this.formService.expandVariableNameAndUnit(CalculatorType.Pab, "ZRAM") + }); } - const reallySelectedWalls = [...walls]; // array copy - // 2nd pass - for (const c of this.selectedItems) { - if (c instanceof Structure) { - if (! wallsDevices.includes(c)) { - if (firstDevicePosition === undefined) { - firstDevicePosition = c.findPositionInParent(); - } else { - vertical = (vertical && (c.findPositionInParent() === firstDevicePosition)); - } - // add parent wall for basin-length based interpolation - const parentWall = (c.parent as ParallelStructure); - if (parentWall && ! walls.includes(parentWall)) { - walls.push(parentWall); - } - devices.push(c); - } + if (r === 1) { + // 1 empty cell (in place of the QA editor) + deviceParamRowDW.cells.push({ + rowspan: dwParamCount, + selectable: this.model.downWall + }); + } + + // devices + this.fillParallelStructureCells(deviceParamRowDW, r, paramRowCount, loisAval); + } + + this.updateValidity(); + } + + private fillParallelStructureCells(tableRow: any, rowIndex: number, maxStructParamRowCount: number, loisAdmissibles: any[]) { + const ps: ParallelStructure = tableRow.selectable; + for (const struct of ps.structures) { // for each device + const structParamCount = this.nubVisibleParameterCount(struct); + if (rowIndex === 0) { + // 1st row : device type + tableRow.cells.push({ + model: struct, + modelValue: struct.getPropValue("loiDebit"), + options: loisAdmissibles, + selectable: struct, + colspan: 2 + }); + } else if (rowIndex === structParamCount + 1) { + // fill remaining space + const remaining = maxStructParamRowCount - structParamCount; + if (remaining > 0) { + tableRow.cells.push({ + colspan: 2, + rowspan: remaining, + selectable: struct + }); + } + } else { + // parameter row + const nvParam = struct.getNthVisibleParam(rowIndex - 1); + if (nvParam) { + const nvParamTitle = this.formService.expandVariableNameAndUnit(CalculatorType.Pab, nvParam.symbol); + // parameter name + tableRow.cells.push({ + value: nvParam.symbol, + title: nvParamTitle, + selectable: struct + }); + // parameter value + tableRow.cells.push({ + model: nvParam, + title: nvParamTitle, + selectable: struct + }); } } + } + // done ! + this.rows.push(tableRow); + } - // open dialog - const dialogRef = this.editPabDialog.open( - DialogEditPabComponent, - { - data: { - availableVariables: availableVariables, - selectedItemsAbstract: { - walls: reallySelectedWalls.length, - wallsDevices: wallsDevices.length, - devices: devices.length - }, - vertical: vertical // used to enable interpolation - }, - disableClose: true - } - ); - - // apply modifications - dialogRef.afterClosed().subscribe(result => { - if (result) { - /* console.log("Apply values in parent !!", result.action, result.variable, result.value, - result.delta, result.variableDetails); */ - switch (result.action) { - case "set-value": - for (const s of this.selectedItems) { - for (const p of s.parameterIterator) { // deep - // force single mode (QA only) - if (p.hasMultipleValues) { - p.valueMode = ParamValueMode.SINGLE; - } - if (p.symbol === result.variable) { - p.singleValue = result.value; - } - } - } - break; + /** + * Finds the localized title for a LoiDebit item + */ + private localizeLoiDebit(l: LoiDebit) { + return this.i18nService.localizeText("INFO_PAB_LOIDEBIT_" + LoiDebit[l].toUpperCase()); + } - case "delta": - for (const s of this.selectedItems) { - for (const p of s.parameterIterator) { // deep - // force single mode (QA only) - if (p.hasMultipleValues) { - p.valueMode = ParamValueMode.SINGLE; - } - if (p.symbol === result.variable) { - p.singleValue += result.delta; - } - } - } - break; + private findMaxNumberOfDevices(): number { + let maxNbDevices = 1; + for (const w of this.model.children) { + maxNbDevices = Math.max(maxNbDevices, w.getChildren().length); + } + maxNbDevices = Math.max(maxNbDevices, this.model.downWall.getChildren().length); + return maxNbDevices; + } - case "interpolate": - if (result.variableDetails.occurrences > 1) { - const interpolatedValues: number[] = []; - const variableRange = result.variableDetails.last - result.variableDetails.first; - let totalBasinsLengths = 0; - for (let wi = 0; wi < walls.length; wi++) { - const w = walls[wi]; - if (w instanceof Cloisons) { - if (result.variable === "ZRMB") { - // for ZRMB, exclude 1st basin - if (wi > 0) { - // half the previous basin length, half the current basin length - totalBasinsLengths += ( - (walls[wi - 1] as Cloisons).prms.LB.singleValue / 2 - + w.prms.LB.singleValue / 2 - ); - } - } else { - // for other interpolable elevations, exclude last basin - if (wi < walls.length - 1) { - totalBasinsLengths += w.prms.LB.singleValue; - } - } - } - } - // console.log(`TOTAL BASINS LENGTHS: ${totalBasinsLengths}, VARIABLE RANGE: ${variableRange}`); - // generate interpolated values list - interpolatedValues.push(result.variableDetails.first); - let currentValue: number = result.variableDetails.first; - for (let i = 0; i < result.variableDetails.occurrences - 1; i++) { - if (result.variable === "ZRMB") { - // for ZRMB, exclude 1st basin - if (i > 0) { - // compute step as percentage of total length, related to sum of - // half the previous basin length and half the current basin length - const currentLength = ( - (walls[i - 1] as Cloisons).prms.LB.singleValue / 2 - + (walls[i] as Cloisons).prms.LB.singleValue / 2 - ); - const currentBasinLengthPercentage = currentLength / totalBasinsLengths; - const step = variableRange * currentBasinLengthPercentage; - /* console.log(`Wall ${i} : length = ${currentLength} / ${totalBasinsLengths}` - + ` (${currentBasinLengthPercentage}), applying step of ${step}`); */ - currentValue += step; - interpolatedValues.push(currentValue); - } - } else { - // for other interpolable elevations, exclude last basin - if (i < result.variableDetails.occurrences - 2) { - // compute step as percentage of total length, related to current basin length - const currentBasinLength = (walls[i] as Cloisons).prms.LB.singleValue; - const currentBasinLengthPercentage = currentBasinLength / totalBasinsLengths; - const step = variableRange * currentBasinLengthPercentage; - /* console.log(`Wall ${i} : length = ${currentBasinLength} / ${totalBasinsLengths}` - + ` (${currentBasinLengthPercentage}), applying step of ${step}`); */ - currentValue += step; - interpolatedValues.push(currentValue); - } - } - } - // console.log("INTERPOPOLATED VALUES", interpolatedValues); - // interpolatedValues.push(result.variableDetails.last); - // apply - let idx = 0; - for (const s of this.selectedItems) { - // for ZRMB, interpolatedValues length is shorter by 1 element - if (interpolatedValues[idx] !== undefined) { - for (const p of s.parameterIterator) { // deep - // force single mode (QA only) - if (p.hasMultipleValues) { - p.valueMode = ParamValueMode.SINGLE; - } - if (p.symbol === result.variable) { - p.singleValue = interpolatedValues[idx]; - idx ++; - } - } - } - } - } else { - throw new Error( - `showEditPab() : cannot interpolate, too few occurrences (${result.variableDetails.occurrences})` - ); - } - break; - } - } - }); + private nubVisibleParameterCount(n: Nub) { + let res = 0; + for (const p of n.parameterIterator) { + if (p.visible) { + res++; + } } + return res; } - public ngAfterViewInit() { - this.updateValidity(); + private findMaxNumberOfDeviceParameters(struct: ParallelStructure): number { + let maxNbParams = 1; + for (const child of struct.getChildren()) { + maxNbParams = Math.max(maxNbParams, this.nubVisibleParameterCount(child)); + } + return maxNbParams; } - public getCellValue(cell) { - if(typeof cell.model.singleValue === "string") { - cell.model.singleValue = +cell.model.singleValue; + /** + * Returns true if there is at least one selected item, + * and all selected items are devices + */ + private onlyDevicesAreSelected() { + let ok = false; + if (this.selectedItems.length > 0) { + ok = true; + for (const s of this.selectedItems) { + ok = ok && (s instanceof Structure); + } } - return round(cell.model.singleValue, this.nDigits); + return ok; } - public setCellValue(cell, $event) { - if($event !== "-" && $event !== "") { - try { - cell.model.singleValue = $event; - cell.modelValidity = undefined; - } catch (error) { - cell.modelValidity = false - } - } + /** + * Returns true if there is at least one selected item, + * all selected items are devices, and belong to the same column + */ + private onlyDevicesOfTheSameColumnAreSelected() { + let ok = false; + let columnIndex: number; + if (this.selectedItems.length > 0) { + ok = true; + for (const s of this.selectedItems) { + if (s instanceof Structure) { + const ci = s.findPositionInParent(); + ok = ok && (columnIndex === undefined || columnIndex === ci); + columnIndex = ci; + } else { + ok = false; + } + } + } + return ok; } - public invalidNANInputValue(e: any) { - var rgx = /^-?[0-9]*\.?[0-9]*$/; - if(e.key.match(rgx) === null) { - e.preventDefault(); + /** + * Returns true if there is at least one selected item, + * and all selected items are walls + */ + private onlyWallsAreSelected(excludeDownwall: boolean = true) { + let ok = false; + if (this.selectedItems.length > 0) { + ok = true; + for (const s of this.selectedItems) { + if (excludeDownwall) { + ok = ok && (s instanceof Cloisons); + } else { + ok = ok && (s instanceof ParallelStructure); + } + } } + return ok; } /** @@ -1430,35 +1453,4 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni } } - public get uitextEditPabTable() { - return this.i18nService.localizeText("INFO_PAB_EDIT_VALUES"); - } - - public exportAsSpreadsheet() { - const elem: any = document.getElementById("geometry"); - const elemCopy = (elem as HTMLElement).cloneNode(true) as HTMLElement; - // enrich element copy: replace inputs by their values, so that it appears in the exported spreadsheet - const tables: any = elemCopy.getElementsByTagName("table"); - for (const table of tables) { - const tds: any = table.getElementsByTagName("td"); - for (const td of tds) { - // if it contains an input, replace it with the input value - const inputs = td.getElementsByTagName("input"); - if (inputs.length > 0) { - const input = inputs[0]; - if (input.id.split("_")[1] === "QA") { - td.innerHTML = NgParameter.preview(this.model.children[input.id.split("_")[0]].prms.QA); - } else { - td.innerHTML = input.value; - } - } - } - } - // export the enriched element copy - AppComponent.exportAsSpreadsheet(elemCopy as any); - } - - public get uitextExportAsSpreadsheet() { - return this.i18nService.localizeText("INFO_RESULTS_EXPORT_AS_SPREADSHEET"); - } -} +} \ No newline at end of file -- GitLab From 19df4ae392b2d9c6d77071aee475f6faebec8bfa Mon Sep 17 00:00:00 2001 From: David Dorchies <david.dorchies@inrae.fr> Date: Tue, 20 Feb 2024 16:20:40 +0000 Subject: [PATCH 2/3] fix(pab): loss of decimal separator when no more decimal remain Refs #662 --- src/app/components/pab-table/pab-table.component.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/components/pab-table/pab-table.component.ts b/src/app/components/pab-table/pab-table.component.ts index daa731730..27e95e86c 100644 --- a/src/app/components/pab-table/pab-table.component.ts +++ b/src/app/components/pab-table/pab-table.component.ts @@ -1011,10 +1011,11 @@ export class PabTableComponent implements AfterViewInit, AfterViewChecked, OnIni } public getCellValue(cell) { - if (typeof cell.model.singleValue === "string") { - cell.model.singleValue = +cell.model.singleValue; + if (typeof cell.model.singleValue === "string" && + cell.model.singleValue.slice(-1) !== ".") { + cell.model.singleValue = round(cell.model.singleValue, this.nDigits); } - return round(cell.model.singleValue, this.nDigits); + return cell.model.singleValue; } public setCellValue(cell, $event) { -- GitLab From 03005f118e8be18b3fc8ba43588c3db1d567caab Mon Sep 17 00:00:00 2001 From: David Dorchies <david.dorchies@inrae.fr> Date: Tue, 20 Feb 2024 16:26:20 +0000 Subject: [PATCH 3/3] ci: use test runners instead of stable - due to current bug https://forgemia.inra.fr/adminforgemia/support/-/issues/216 --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index af628beb2..8868651a1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,7 +9,7 @@ stages: - releases-version default: - tags: [docker] + tags: [test] image: $CI_REGISTRY/cassiopee/nghyd:latest variables: -- GitLab