From fc8e66dba698444cf9919cd68d5c442dd191d66d Mon Sep 17 00:00:00 2001
From: Mathias Chouet <mathias.chouet@irstea.fr>
Date: Tue, 26 May 2020 09:59:17 +0200
Subject: [PATCH] Work on GUI for Prebarrage

---
 src/app/app.module.ts                         |   1 +
 src/app/calculators/prebarrage/config.json    |  34 ++
 src/app/calculators/prebarrage/en.json        |  12 +
 src/app/calculators/prebarrage/fr.json        |  10 +
 .../calculator-list.component.ts              |  12 +-
 .../calculator.component.html                 |  52 ++-
 .../calculator.component.scss                 |   4 +
 .../calculator.component.ts                   |  22 +-
 .../pb-schema/pb-schema.component.html        |  62 +++
 .../pb-schema/pb-schema.component.scss        |  58 +++
 .../pb-schema/pb-schema.component.ts          | 399 ++++++++++++++++++
 src/app/config.json                           |   2 +-
 .../formulaire/definition/form-definition.ts  |  12 +
 src/app/formulaire/elements/pab-table.ts      |   1 -
 src/app/formulaire/elements/pb-schema.ts      |  32 ++
 src/locale/messages.en.json                   |   5 +
 src/locale/messages.fr.json                   |   5 +
 17 files changed, 694 insertions(+), 29 deletions(-)
 create mode 100644 src/app/calculators/prebarrage/config.json
 create mode 100644 src/app/calculators/prebarrage/en.json
 create mode 100644 src/app/calculators/prebarrage/fr.json
 create mode 100644 src/app/components/pb-schema/pb-schema.component.html
 create mode 100644 src/app/components/pb-schema/pb-schema.component.scss
 create mode 100644 src/app/components/pb-schema/pb-schema.component.ts
 create mode 100644 src/app/formulaire/elements/pb-schema.ts

diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index d7fa9db45..3dc37b5ae 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -86,6 +86,7 @@ import { LogDrawerComponent } from "./components/log-drawer/log-drawer.component
 import { ParamLinkComponent } from "./components/param-link/param-link.component";
 import { PabProfileChartComponent } from "./components/pab-profile-chart/pab-profile-chart.component";
 import { PabTableComponent } from "./components/pab-table/pab-table.component";
+import { PbSchemaComponent } from './components/pb-schema/pb-schema.component';
 import { VariableResultsSelectorComponent } from "./components/variable-results-selector/variable-results-selector.component";
 import { QuicknavComponent } from "./components/quicknav/quicknav.component";
 import { ModulesDiagramComponent } from "./components/modules-diagram/modules-diagram.component";
diff --git a/src/app/calculators/prebarrage/config.json b/src/app/calculators/prebarrage/config.json
new file mode 100644
index 000000000..754fbc81d
--- /dev/null
+++ b/src/app/calculators/prebarrage/config.json
@@ -0,0 +1,34 @@
+[
+    {
+        "id": "schema_interactif_pb",
+        "type": "pb_schema"
+    },
+    {
+        "id": "fs_params",
+        "type": "fieldset",
+        "fields": [
+            {
+                "id": "select_upstream",
+                "type": "select_reference",
+                "reference": "nub",
+                "source": "upstream_stuff"
+            },
+            {
+                "id": "select_downstream",
+                "type": "select_reference",
+                "reference": "nub",
+                "source": "downstream_stuff"
+            },
+            "Q",
+            "Z1",
+            "Z2"
+        ]
+    },
+    {
+        "type": "options",
+        "selectIds": [ ],
+        "upstreamSelectId": "select_upstream",
+        "downstreamSelectId": "select_downstream",
+        "_help": "prebarrage.html"
+    }
+]
diff --git a/src/app/calculators/prebarrage/en.json b/src/app/calculators/prebarrage/en.json
new file mode 100644
index 000000000..a8e5ea027
--- /dev/null
+++ b/src/app/calculators/prebarrage/en.json
@@ -0,0 +1,12 @@
+{
+    "fs_target": "Target parameter characteristics",
+    "fs_searched": "Searched parameter characteristics",
+
+    "Ytarget": "Value of target parameter",
+    "Xinit": "Initial value for searched parameter",
+    "X": "Value for searched parameter",
+
+    "select_target_nub": "Module and parameter to calculate",
+    "select_target_result": "Targetted result",
+    "select_searched_param": "Searched parameter"
+}
\ No newline at end of file
diff --git a/src/app/calculators/prebarrage/fr.json b/src/app/calculators/prebarrage/fr.json
new file mode 100644
index 000000000..e10e65996
--- /dev/null
+++ b/src/app/calculators/prebarrage/fr.json
@@ -0,0 +1,10 @@
+{
+    "fs_params": "Édition du bassin / de la cloison",
+
+    "Ytarget": "Valeur du paramètre cible",
+    "Xinit": "Valeur initiale du paramètre recherché",
+    "X": "Valeur du paramètre recherché",
+
+    "select_upstream": "Bassin / cloison amont",
+    "select_downstream": "Bassin / cloison aval"
+}
\ No newline at end of file
diff --git a/src/app/components/calculator-list/calculator-list.component.ts b/src/app/components/calculator-list/calculator-list.component.ts
index 6dc05bea1..e4c9a47b2 100644
--- a/src/app/components/calculator-list/calculator-list.component.ts
+++ b/src/app/components/calculator-list/calculator-list.component.ts
@@ -114,10 +114,14 @@ export class CalculatorListComponent implements OnInit {
 
                 for (const t of unusedCalculators) {
                     if ( // those sub-Nub types cannot be built outside a parent
-                        t !== CalculatorType.Structure
-                        && t !== CalculatorType.Section
-                        && t !== CalculatorType.CloisonAval
-                        && t !== CalculatorType.YAXN
+                        ! [
+                            CalculatorType.Structure,
+                            CalculatorType.Section,
+                            CalculatorType.CloisonAval,
+                            CalculatorType.YAXN,
+                            CalculatorType.PbBassin,
+                            CalculatorType.PbCloison
+                        ].includes(t)
                     ) {
                         unusedTheme.calculators.push({
                             type: t,
diff --git a/src/app/components/generic-calculator/calculator.component.html b/src/app/components/generic-calculator/calculator.component.html
index f5ccefc0f..7660f8a21 100644
--- a/src/app/components/generic-calculator/calculator.component.html
+++ b/src/app/components/generic-calculator/calculator.component.html
@@ -62,22 +62,42 @@
                     [fxFlex.lt-md]="isWide ? '1 0 auto' : '1 0 500px'"
                     [fxFlex.lt-sm]="isWide ? '1 0 auto' : '1 0 300px'">
 
-                    <ng-template ngFor let-fe [ngForOf]="formElements">
-                        <field-set *ngIf="isFieldset(fe)" [style.display]="getElementStyleDisplay(fe.id)" [fieldSet]=fe
-                            (radio)=onRadioClick($event) (validChange)=onElementValid()
-                            (inputChange)=onInputChange($event) (tabPressed)="onTabPressed($event)">
-                        </field-set>
-
-                        <fieldset-container *ngIf="isFieldsetContainer(fe)"
-                            [style.display]="getElementStyleDisplay(fe.id)" [_container]=fe (radio)=onRadioClick($event)
-                            (validChange)=onElementValid() (inputChange)=onInputChange($event)
-                            (tabPressed)="onTabPressed($event)">
-                        </fieldset-container>
-
-                        <pab-table *ngIf="isPabTable(fe)" [pabTable]=fe (radio)=onRadioClick($event)
-                            (validChange)=onElementValid() (inputChange)=onInputChange($event)>
-                        </pab-table>
-                    </ng-template>
+                    <div id="calc-card-field-sets-container" [fxLayout]="isPB ? 'row wrap' : 'column'">
+                        
+                        <ng-template ngFor let-fe [ngForOf]="formElements">
+                            <field-set *ngIf="isFieldset(fe)" [style.display]="getElementStyleDisplay(fe.id)" [fieldSet]=fe
+                                (radio)=onRadioClick($event) (validChange)=onElementValid() (inputChange)=onInputChange($event)
+                                (tabPressed)="onTabPressed($event)"
+                                [fxFlex.gt-sm]="isPB ? '1 0 400px' : '1 0 auto'"
+                                [fxFlex.lt-md]="isPB ? '1 0 500px' : '1 0 auto'"
+                                [fxFlex.lt-sm]="isPB ? '1 0 300px' : '1 0 auto'">
+                            </field-set>
+
+                            <fieldset-container *ngIf="isFieldsetContainer(fe)" [style.display]="getElementStyleDisplay(fe.id)" [_container]=fe
+                                (radio)=onRadioClick($event) (validChange)=onElementValid() (inputChange)=onInputChange($event)
+                                (tabPressed)="onTabPressed($event)"
+                                fxFlex="1 0 auto">
+                            </fieldset-container>
+
+                            <pab-table *ngIf="isPabTable(fe)" [pabTable]=fe (radio)=onRadioClick($event)
+                                (validChange)=onElementValid() (inputChange)=onInputChange($event)
+                                fxFlex="1 0 auto">
+                            </pab-table>
+
+                            <div *ngIf="isPbSchema(fe)" id="pb-schema-container"
+                                [fxFlex.gt-sm]="isPB ? '1 0 400px' : '1 0 auto'"
+                                [fxFlex.lt-md]="isPB ? '1 0 500px' : '1 0 auto'"
+                                [fxFlex.lt-sm]="isPB ? '1 0 300px' : '1 0 auto'">
+
+                                <pb-schema *ngIf="isPbSchema(fe)" [pbSchema]=fe (radio)=onRadioClick($event)
+                                    (validChange)=onElementValid() (inputChange)=onInputChange($event)>
+                                </pb-schema>
+
+                                <div fxHide.sm fxFlex.gt-sm="0 0 16px"></div>
+                            </div>
+
+                        </ng-template>
+                    </div>
 
                     <mat-card-actions>
                         <!-- bouton calculer -->
diff --git a/src/app/components/generic-calculator/calculator.component.scss b/src/app/components/generic-calculator/calculator.component.scss
index 1aa9e9e26..14be9aa77 100644
--- a/src/app/components/generic-calculator/calculator.component.scss
+++ b/src/app/components/generic-calculator/calculator.component.scss
@@ -26,6 +26,10 @@
     margin-bottom: 1em;
 }
 
+#pb-schema-container {
+    display: block;
+}
+
 mat-card {
     margin-bottom: 2em;
 
diff --git a/src/app/components/generic-calculator/calculator.component.ts b/src/app/components/generic-calculator/calculator.component.ts
index b662a5000..a377bd939 100644
--- a/src/app/components/generic-calculator/calculator.component.ts
+++ b/src/app/components/generic-calculator/calculator.component.ts
@@ -47,6 +47,7 @@ import { PabTable } from "../../formulaire/elements/pab-table";
 import { MultiDimensionResults } from "../../results/multidimension-results";
 import { NgParameter } from "../../formulaire/elements/ngparam";
 import { FormulaireFixedVar } from "../../formulaire/definition/form-fixedvar";
+import { PbSchema } from "../../formulaire/elements/pb-schema";
 
 import { HotkeysService, Hotkey } from "angular2-hotkeys";
 
@@ -168,24 +169,26 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
     /**
      * détermine si un FormulaireElement est du type FieldSet
      */
+    /** détermine si un FormulaireElement est du type FieldSet */
     public isFieldset(fe: any): boolean {
         return fe instanceof FieldSet;
     }
 
-    /**
-     * détermine si un FormulaireElement est du type FieldsetContainer
-     */
+    /** détermine si un FormulaireElement est du type FieldsetContainer */
     public isFieldsetContainer(fe: any): boolean {
         return fe instanceof FieldsetContainer;
     }
 
-    /**
-     * détermine si un FormulaireElement est du type PabTable
-     */
+    /** détermine si un FormulaireElement est du type PabTable */
     public isPabTable(fe: any): boolean {
         return fe instanceof PabTable;
     }
 
+    /** détermine si un FormulaireElement est du type PbSchema */
+    public isPbSchema(fe: any): boolean {
+        return fe instanceof PbSchema;
+    }
+
     public get hasForm() {
         return this._formulaire !== undefined;
     }
@@ -583,7 +586,7 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
 
     // for "one wide column" layout
     public get isWide() {
-        return (this.isPAB || this.isMRC);
+        return (this.isPAB || this.isMRC || this.isPB);
     }
 
     // true if current Nub is Solveur
@@ -601,6 +604,11 @@ export class GenericCalculatorComponent implements OnInit, DoCheck, AfterViewChe
         return this.is(CalculatorType.MacroRugoCompound);
     }
 
+    // true if current Nub is PreBarrage
+    public get isPB() {
+        return this.is(CalculatorType.PreBarrage);
+    }
+
     // true if current Nub is Jet
     public get isJet() {
         return this.is(CalculatorType.Jet);
diff --git a/src/app/components/pb-schema/pb-schema.component.html b/src/app/components/pb-schema/pb-schema.component.html
new file mode 100644
index 000000000..80b06a0f1
--- /dev/null
+++ b/src/app/components/pb-schema/pb-schema.component.html
@@ -0,0 +1,62 @@
+<mat-card-header class="mat-card-header-text-margin-0 bg-accent-light">
+    <mat-card-title>
+        {{ title }}
+    </mat-card-title>
+</mat-card-header>
+
+<mat-card-content>
+
+    <div id="pb-schema-toolbar">
+
+        <div class="hyd-window-btns">
+            <span class="related-entity-title">
+                {{ prefixedItemDescription }}
+            </span>
+            <!-- <mat-select id="add-many-children" [(value)]="childrenToAdd">
+                <mat-option *ngFor="let i of addManyOptionsList" [value]="i">
+                    {{ i }}
+                </mat-option>
+            </mat-select> -->
+            <button type="button" mat-icon-button color="primary" title="example button">
+              <mat-icon>add_box</mat-icon>
+            </button>
+            <!-- <button type="button" mat-icon-button color="primary" [disabled]="! enableAddButton" (click)="onAddClick()"
+              [title]="uitextAdd">
+                <mat-icon>add_box</mat-icon>
+            </button>
+            <button type="button" mat-icon-button color="primary" [disabled]="! enableCopyButton" (click)="onCopyClick()"
+              [title]="uitextCopy">
+                <mat-icon>content_copy</mat-icon>
+            </button>
+            |
+            <button type="button" mat-icon-button color="primary" [disabled]="! enableRemoveButton" (click)="onRemoveClick()"
+              [title]="uitextRemove">
+                <mat-icon>delete</mat-icon>
+            </button>
+            <button type="button" mat-icon-button color="primary" [disabled]="! enableUpButton" (click)="onMoveUpClick()"
+              [title]="uitextMoveUp">
+                <mat-icon *ngIf="! selectionIsOneDevice">arrow_upward</mat-icon>
+                <mat-icon *ngIf="selectionIsOneDevice">arrow_back</mat-icon>
+            </button>
+            <button type="button" mat-icon-button color="primary" [disabled]="! enableDownButton" (click)="onMoveDownClick()"
+              [title]="uitextMoveDown">
+                <mat-icon *ngIf="! selectionIsOneDevice">arrow_downward</mat-icon>
+                <mat-icon *ngIf="selectionIsOneDevice">arrow_forward</mat-icon>
+            </button>
+            |
+            <button type="button" mat-icon-button color="primary" (click)="exportAsSpreadsheet()"
+              [title]="uitextExportAsSpreadsheet">
+                <mat-icon color="primary">file_download</mat-icon>
+            </button> -->
+        </div>
+    </div>
+
+    <div *ngIf="error">{{ uitextDrawingError }}</div>
+
+    <div id="schema" #schema></div>
+
+    <div *ngIf="showDebug">
+        <pre>{{ graphDef }}</pre>
+    </div>
+
+</mat-card-content>
diff --git a/src/app/components/pb-schema/pb-schema.component.scss b/src/app/components/pb-schema/pb-schema.component.scss
new file mode 100644
index 000000000..1b30459a2
--- /dev/null
+++ b/src/app/components/pb-schema/pb-schema.component.scss
@@ -0,0 +1,58 @@
+/** @see additional styles in src/styles.css */
+
+:host {
+    display: block;
+    width: 100%;
+    // reduce margins to avoid inner field-sets being too narrow on 360px display
+    /* margin-left: -8px;
+    margin-right: -8px; */
+}
+
+mat-card-header {
+    /* margin-left: -8px;
+    margin-right: -8px; */
+    margin-left: -16px;
+    margin-right: -16px;
+    padding-left: 16px;
+    padding-top: 8px;
+    color: white;
+
+    // Pourquoi n'est-ce pas hérité de calculator.component.scss ?
+    // À cause de la surcharge de mat-card-header ci-dessus ?
+    mat-card-title {
+        font-size: 16px !important;
+        margin-bottom: 8px;
+    }
+}
+
+mat-card-content {
+    margin-top: 1em;
+}
+
+#pb-schema-toolbar {
+    #edit-pab-table {
+        float: left;
+    }
+    .related-entity-title {
+        vertical-align: middle;
+        font-weight: bold;
+    }
+    .hyd-window-btns {
+        text-align: right;
+
+        #add-many-children {
+            width: 3em;
+            vertical-align: middle;
+        }
+
+        button.mat-icon-button {
+            width: 32px;
+        }
+    }
+}
+
+#schema {
+    margin-top: .5em;
+    margin-bottom: .5em;
+    text-align: center;
+}
diff --git a/src/app/components/pb-schema/pb-schema.component.ts b/src/app/components/pb-schema/pb-schema.component.ts
new file mode 100644
index 000000000..02a921050
--- /dev/null
+++ b/src/app/components/pb-schema/pb-schema.component.ts
@@ -0,0 +1,399 @@
+import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, ViewChild } from "@angular/core";
+
+import {
+    PreBarrage, PbBassin, PbBassinParams, PbCloison
+ } from "jalhyd";
+
+import * as mermaid from "mermaid";
+
+import { I18nService } from "../../services/internationalisation.service";
+import { ApplicationSetupService } from "../../services/app-setup.service";
+import { NotificationsService } from "../../services/notifications.service";
+import { PbSchema } from "../../formulaire/elements/pb-schema";
+
+/**
+ * The interactive schema for calculator type "PreBarrage" (component)
+ */
+@Component({
+    selector: "pb-schema",
+    templateUrl: "./pb-schema.component.html",
+    styleUrls: [
+        "./pb-schema.component.scss"
+    ]
+})
+export class PbSchemaComponent implements AfterViewInit, OnInit {
+
+    @Input()
+    private pbSchema: PbSchema;
+
+    @ViewChild("schema", { static: true })
+    public schema: any;
+
+    /** handle on SVG container */
+    private nativeElement: any;
+
+    public error: boolean;
+
+    /** flag de validité des FieldSet enfants */
+    private _isValid = false;
+
+    private upstreamId = "amont";
+
+    private downstreamId = "aval";
+
+    /** événément de changement de validité */
+    @Output()
+    private validChange = new EventEmitter();
+
+    /** événément de changement de valeur d'un input */
+    @Output()
+    private inputChange = new EventEmitter();
+
+    /** underlying PB */
+    private model: PreBarrage;
+
+    /** Latest clicked item: a PbCloison, a PbBassin or undefined if river "Upstream" or "Downstream" was clicked */
+    private _selectedItem: any;
+
+    public constructor(
+        private i18nService: I18nService,
+        private appSetupService: ApplicationSetupService,
+        private notifService: NotificationsService
+    ) { }
+
+    public get selectedItem(): any {
+        return this._selectedItem;
+    }
+
+    public ngAfterContentInit(): void {
+        this.error = false;
+        mermaid.initialize({
+            flowchart: {
+                curve: "basis"
+            }
+        });
+        this.nativeElement = this.schema.nativeElement;
+
+        // generate graph description
+        const graphDefinition = this.graphDefinition();
+        // draw
+        try {
+            mermaid.render("graphDiv", graphDefinition, (svgCode, bindFunctions) => {
+                this.nativeElement.innerHTML = svgCode;
+            });
+        } catch (e) {
+            console.error(e);
+            this.error = true;
+        }
+    }
+
+    public ngAfterViewInit(): void {
+        this.refreshEventListeners();
+        this.updateValidity();
+    }
+
+    /** Add click listener on every node and link in the graph */
+    private refreshEventListeners() {
+        this.nativeElement.querySelectorAll("g.node").forEach(item => {
+            item.style.cursor = "pointer";
+            item.addEventListener("click", () => {
+                this.selectBasin(item.id);
+            });
+        });
+        this.nativeElement.querySelectorAll("g.edgeLabel").forEach(item => {
+            item.style.cursor = "pointer";
+            item.addEventListener("click", () => {
+                this.selectWall(item);
+            });
+        });
+    }
+
+    /**
+     * Builds a Mermaid graph text definition
+     */
+    private graphDefinition() {
+        const def: string[] = [ "graph TB" ];
+
+        def.push(`${this.upstreamId}("${this.i18nService.localizeText("INFO_LIB_AMONT")}")`);
+        def.push(`${this.downstreamId}("${this.i18nService.localizeText("INFO_LIB_AVAL")}")`);
+
+        // debug
+        const b1 = new PbBassin(new PbBassinParams(0.1, 42));
+        this.model.addChild(b1);
+        const b2 = new PbBassin(new PbBassinParams(0.15, 38));
+        this.model.addChild(b2);
+        this.model.addChild(new PbCloison(undefined, b1));
+        this.model.addChild(new PbCloison(b1, b2));
+        this.model.addChild(new PbCloison(b2, undefined));
+        this.model.addChild(new PbCloison(b1, undefined));
+
+        for (const b of this.model.bassins) {
+            // basin
+            def.push(`${b.uid}("${this.itemDesription(b)}")`);
+            // upstream walls
+            for (const uw of b.cloisonsAmont) {
+                const upstreamBasinId = uw.bassinAmont === undefined ? this.upstreamId : uw.bassinAmont.uid;
+                // upstream wall unique identifier
+                const uwString = `${upstreamBasinId}-->|${this.itemDesription(uw)}|${b.uid}`;
+                if (! def.includes(uwString)) {
+                    def.push(uwString);
+                }
+            }
+            // downstream walls
+            for (const dw of b.cloisonsAval) {
+                const downstreamBasinId = dw.bassinAval === undefined ? this.downstreamId : dw.bassinAval.uid;
+                // downstream wall unique identifier
+                const dwString = `${b.uid}-->|${this.itemDesription(dw)}|${downstreamBasinId}`;
+                if (! def.includes(dwString)) {
+                    def.push(dwString);
+                }
+            }
+        }
+
+        return def.join("\n");
+    }
+
+    private selectBasin(id: string) {
+        if ([ this.upstreamId, this.downstreamId ].includes(id)) {
+            console.log("YOU CLICKED EITHER UPSTREAM OR DOWNSTREAM");
+            this._selectedItem = undefined;
+        } else {
+            let basin: PbBassin;
+            for (const b of this.model.bassins) {
+                if (b.uid === id) {
+                    basin = b;
+                }
+            }
+            this._selectedItem = basin;
+            // @TODO highlight node in schema
+            console.log("BASIN FOUND !", basin);
+        }
+    }
+
+    private selectWall(item: SVGGElement) {
+        // Mermaid does not allow to assign IDs to connectors and labels…
+        const text: string = item.querySelector("span.edgeLabel").textContent;
+        if (text) {
+            const [ uBs, dBs ] = text.split("-");
+            let wall: PbCloison;
+            // clodo test: is there an upstream basin or is it upstream river ?
+            if (uBs === this.i18nService.localizeText("INFO_LIB_AMONT")) {
+                // find wall from downstream basin
+                const dBi = Number(dBs.substring(1));
+                const dB = this.model.bassins[dBi - 1];
+                for (const w of dB.cloisonsAmont) {
+                    // find the one that is connected to upstream river
+                    if (w.bassinAmont === undefined) {
+                        wall = w;
+                    }
+                }
+            } else {
+                // find wall from upstream basin
+                const uBi = Number(uBs.substring(1));
+                const uB = this.model.bassins[uBi - 1];
+                // clodo test again
+                let dB: PbBassin;
+                if (dBs !== this.i18nService.localizeText("INFO_LIB_AVAL")) {
+                    const dBi = Number(dBs.substring(1));
+                    dB = this.model.bassins[dBi - 1];
+                }
+                for (const w of uB.cloisonsAval) {
+                    // find the one that is connected to dB (either a basin or downstream river)
+                    if (w.bassinAval === dB) {
+                        wall = w;
+                    }
+                }
+            }
+            if (wall === undefined) {
+                throw new Error(`PbSchemaComponent.selectWall(): cannot find wall for label "${text}"`);
+            }
+            this._selectedItem = wall;
+            // @TODO highlight label and edge in schema
+            console.log("WALL FOUND !", wall);
+        }
+    }
+
+    public get graphDef(): string {
+        return this.graphDefinition();
+    }
+
+    public get title(): string {
+        return this.i18nService.localizeText("INFO_PB_SCHEMA");
+    }
+
+    /** Global Pb validity */
+    public get isValid() {
+        return this._isValid;
+    }
+
+    /**
+     * Checks that input value is a valid number, according to input[type="number"] algorithm,
+     * and stores it in cell.uiValidity, so that the <td> element can access it and get angry
+     * if input is invalid
+     */
+    public inputValueChanged($event, cell) {
+        if ($event && $event.target && $event.target.validity) {
+            cell.uiValidity = $event.target.validity.valid;
+        }
+        this.updateValidity();
+        // send input change event (used to reset form results)
+        this.inputChange.emit();
+    }
+
+    public get prefixedItemDescription(): string {
+        let desc = this.itemDesription(this._selectedItem);
+        if (this._selectedItem instanceof PbCloison) {
+            desc = this.i18nService.localizeText("INFO_PB_CLOISON") + " " + desc;
+        }
+        if (desc !== "") {
+            desc += " : ";
+        }
+        return desc;
+    }
+
+    /** Returns a short description of the given item: wall or basin */
+    private itemDesription(item: PbCloison | PbBassin): string {
+        let desc = "";
+        if (item instanceof PbCloison) {
+            const upstreamBasinName = item.bassinAmont === undefined
+                ? this.i18nService.localizeText("INFO_LIB_AMONT")
+                : "B" + (item.bassinAmont.findPositionInParent() + 1);
+            const downstreamBasinName = item.bassinAval === undefined
+                ? this.i18nService.localizeText("INFO_LIB_AVAL")
+                : "B" + (item.bassinAval.findPositionInParent() + 1);
+            desc = upstreamBasinName + "-" + downstreamBasinName;
+
+        } else if (item instanceof PbBassin) {
+            desc = this.i18nService.localizeText("INFO_PB_BASSIN_N") + (item.findPositionInParent() + 1);
+        } // else undefined
+        return desc;
+    }
+
+    /**
+     * Returns true if current cell is bound to a model that says its input value is
+     * no valid, or if characters typed in the input field are not a valid number
+     * (read from cell.uiValidity, see inputValueChanged() above)
+     */
+    /* public isInvalid(cell: any): boolean {
+        let valid = true;
+        if (this.hasModel(cell) && cell.model instanceof ParamDefinition) {
+            valid = valid && cell.model.isValid;
+        }
+        if (cell.uiValidity !== undefined) {
+            valid = valid && cell.uiValidity;
+        }
+        return ! valid;
+    } */
+
+    /**
+     * returns true if every wall (including downwall) has its nth device
+     * selected (or has no nth device)
+     */
+    /* public isDeviceColumnSelected(n: number): boolean {
+        let ok = true;
+        for (const c of this.model.children) {
+            const nthChild = c.getChildren()[n];
+            if (nthChild) {
+                ok = ok && this.selectedItems.includes(nthChild);
+            }
+        }
+        const nthChildDW = this.model.downWall.getChildren()[n];
+        if (nthChildDW) {
+            ok = ok && this.selectedItems.includes(nthChildDW);
+        }
+        return ok;
+    } */
+
+    // 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];
+    } */
+
+    // at this time @Input data is supposed to be already populated
+    public ngOnInit() {
+        this.model = this.pbSchema.pb;
+        this.refresh();
+    }
+
+    /** Unselects all selected text (side-effect of shift+clicking) */
+    /* private clearSelection() {
+        if (window.getSelection) {
+            const sel = window.getSelection();
+            sel.removeAllRanges();
+        }
+    } */
+
+    /**
+     * Builds the interactive schema from the PreBarrage model
+     */
+    private refresh() {
+        this.updateValidity();
+    }
+
+    /* 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;
+    } */
+
+    /**
+     * Computes the global Pab validity : validity of every cell of every row
+     */
+    private updateValidity() {
+        this._isValid = true;
+        /* for (const r of this.rows) {
+            for (const c of r.cells) {
+                this._isValid = this._isValid && ! this.isInvalid(c);
+            }
+        } */
+        this.validChange.emit();
+    }
+
+    /* 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];
+                    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");
+    } */
+}
diff --git a/src/app/config.json b/src/app/config.json
index 35925d769..6a7b7f921 100644
--- a/src/app/config.json
+++ b/src/app/config.json
@@ -15,7 +15,7 @@
                 "path": "passe-bassin.jpg",
                 "credits": "S. Richard / OFB"
             },
-            "calculators": [ 12, 13, 6, 5, 10, 15 ]
+            "calculators": [ 12, 13, 6, 5, 10, 15, 30 ]
         },
         {
             "name": "PASSE_A_RALENTISSEURS",
diff --git a/src/app/formulaire/definition/form-definition.ts b/src/app/formulaire/definition/form-definition.ts
index 2baaff2cf..707f0268c 100644
--- a/src/app/formulaire/definition/form-definition.ts
+++ b/src/app/formulaire/definition/form-definition.ts
@@ -24,6 +24,7 @@ import { CalculatorResults } from "../../results/calculator-results";
 import { ServiceFactory } from "../../services/service-factory";
 import { PabTable } from "../elements/pab-table";
 import { SelectEntry } from "../elements/select-entry";
+import { PbSchema } from '../elements/pb-schema';
 
 /**
  * classe de base pour tous les formulaires
@@ -217,6 +218,13 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
         this.kids.push(tab);
     }
 
+    private parse_pb_schema(json: {}) {
+        const sch: PbSchema = new PbSchema(this);
+        sch.parseConfig(json);
+        this.kids.push(sch);
+    }
+
+
     /**
      * 1ère passe d'analyse de la configuration
      */
@@ -274,6 +282,10 @@ export abstract class FormulaireDefinition extends FormulaireNode implements Obs
                     this.parse_pab_table(conf);
                     break;
 
+                case "pb_schema": // not generic at all
+                    this.parse_pb_schema(conf);
+                    break;
+
                 default:
                     throw new Error(`type d'objet de module de calcul ${type} non pris en charge`);
             }
diff --git a/src/app/formulaire/elements/pab-table.ts b/src/app/formulaire/elements/pab-table.ts
index 3601cf19b..e9934bb9c 100644
--- a/src/app/formulaire/elements/pab-table.ts
+++ b/src/app/formulaire/elements/pab-table.ts
@@ -1,7 +1,6 @@
 import { Pab } from "jalhyd";
 
 import { FormulaireElement } from "./formulaire-element";
-import { FormulaireNode } from "./formulaire-node";
 import { FormulairePab } from "../definition/form-pab";
 
 /**
diff --git a/src/app/formulaire/elements/pb-schema.ts b/src/app/formulaire/elements/pb-schema.ts
new file mode 100644
index 000000000..0d4243781
--- /dev/null
+++ b/src/app/formulaire/elements/pb-schema.ts
@@ -0,0 +1,32 @@
+import { PreBarrage } from "jalhyd";
+
+import { FormulaireElement } from "./formulaire-element";
+
+/**
+ * The interactive schema for calculator type "PreBarrage" (form element).
+ *
+ * This is just a gateway between the model (Prebarrage)
+ * and the user interface (PbSchemaComponent)
+ */
+export class PbSchema extends FormulaireElement {
+
+    public parseConfig(json: {}) {
+        this._confId = json["id"];
+    }
+
+    /**
+     * Returns the parent FormulairePab
+     */
+    /* public get form(): FormulairePab {
+        return this.parentForm as FormulairePab;
+    } */
+
+    /**
+     * Returns the Prebarrage model associated to the parent form
+     */
+    public get pb(): PreBarrage {
+        if (this.parentForm) {
+            return this.parentForm.currentNub as PreBarrage;
+        }
+    }
+}
diff --git a/src/locale/messages.en.json b/src/locale/messages.en.json
index 8f48e654c..09b6c4cfe 100644
--- a/src/locale/messages.en.json
+++ b/src/locale/messages.en.json
@@ -516,6 +516,11 @@
     "INFO_PARAMFIELD_VARIATED": "Variated",
     "INFO_PARAMMODE_LIST": "Values list",
     "INFO_PARAMMODE_MINMAX": "Min/max",
+    "INFO_PB_BASSIN_N": "Basin #",
+    "INFO_PB_CLOISON": "Wall",
+    "INFO_PB_SCHEMA": "Basins layout",
+    "INFO_PREBARRAGE_TITRE": "Pre-dams",
+    "INFO_PREBARRAGE_TITRE_COURT": "Pre-dams",
     "INFO_QUICKNAV_CHARTS": "charts",
     "INFO_QUICKNAV_INPUT": "input",
     "INFO_QUICKNAV_RESULTS": "results",
diff --git a/src/locale/messages.fr.json b/src/locale/messages.fr.json
index 6ce751141..8e418c381 100644
--- a/src/locale/messages.fr.json
+++ b/src/locale/messages.fr.json
@@ -517,6 +517,11 @@
     "INFO_PARAMFIELD_VARIATED": "Varié",
     "INFO_PARAMMODE_LIST": "Liste de valeurs",
     "INFO_PARAMMODE_MINMAX": "Min/max",
+    "INFO_PB_BASSIN_N": "Bassin n°",
+    "INFO_PB_CLOISON": "Cloison",
+    "INFO_PB_SCHEMA": "Organisation des bassins",
+    "INFO_PREBARRAGE_TITRE": "Prébarrages",
+    "INFO_PREBARRAGE_TITRE_COURT": "Prébarrages",
     "INFO_QUICKNAV_CHARTS": "graphiques",
     "INFO_QUICKNAV_INPUT": "données",
     "INFO_QUICKNAV_RESULTS": "résultats",
-- 
GitLab