diff --git a/.eslintrc.js b/.eslintrc.js
index c82462bdc07d13830d67563d58a0a74618197b66..d615175bb634a360119750e65b54d4b457a45887 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/.gitlab-ci.yml b/.gitlab-ci.yml
index af628beb20ce721ffc5427dc169bd9201ad789d0..8868651a1eb5ed9a3e3c4ca0de8f4d91f7a7e5e1 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:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 9a280a042ea2ab778f7ca7edc7ff88865aa15cd3..b8e94e5f7870d2bfcb6d58363103f7e2ca3a9802 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 f65425f3ef5a5558d3a5cfd0012d8d6bd70b587a..27e95e86c118da9a9d845243d520df00507f41af 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,880 @@ 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.slice(-1) !== ".") {
+            cell.model.singleValue = round(cell.model.singleValue, this.nDigits);
+        }
+        return cell.model.singleValue;
     }
 
-    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 +1454,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