pb-schema.component.ts 22.43 KiB
import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, ViewChild, Inject, forwardRef, AfterContentInit } from "@angular/core";
import { MatDialog } from "@angular/material/dialog";
import {
    PreBarrage, PbBassin, PbBassinParams, PbCloison, CreateStructure, LoiDebit, Structure, Observer, IObservable
 } from "jalhyd";
import * as mermaid from "mermaid";
import { I18nService } from "../../services/internationalisation.service";
import { PbSchema } from "../../formulaire/elements/pb-schema";
import { DialogNewPbCloisonComponent } from "../dialog-new-pb-cloison/dialog-new-pb-cloison.component";
import { GenericCalculatorComponent } from "../generic-calculator/calculator.component";
/**
 * 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, AfterContentInit, OnInit, Observer {
    @Input()
    private pbSchema: PbSchema;
    @ViewChild("schema", { static: true })
    public schema: any;
    /** handle on SVG container */
    private nativeElement: any;
    /** flag de validité du composant */
    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 sélection d'un nœud du graphique Mermaid */
    @Output()
    private nodeSelected = 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: PbCloison | PbBassin;
    /** Records existing walls as they are built, to detect if multiple walls connect the same pair of basins */
    private existingWalls: { [key: string]: number };
    /** Stores appropriate number suffix for a given wall uid (related to existingWalls above) */
    private wallsSuffixes: { [key: string]: number };
    public constructor(
        @Inject(forwardRef(() => GenericCalculatorComponent)) private calculatorComponent: GenericCalculatorComponent,
        private i18nService: I18nService,
        private newPbCloisonDialog: MatDialog
    ) { }
    public get selectedItem(): any {
        return this._selectedItem;
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
} public ngAfterContentInit(): void { mermaid.initialize({ flowchart: { curve: "basis" } }); this.nativeElement = this.schema.nativeElement; this.render(); } private render() { this.nativeElement.innerHTML = ""; // or diagram goes blank when refreshing… // generate graph description const graphDefinition = this.graphDefinition(); // draw try { mermaid.render("graphDiv", graphDefinition, (svgCode, bindFunctions) => { this.nativeElement.innerHTML = svgCode; }); } catch (e) { console.error(e); } } /** * Builds the interactive schema from the PreBarrage model */ private refresh() { this.render(); this.refreshEventListeners(); this.updateValidity(); } public ngAfterViewInit(): void { this.refreshEventListeners(); this.updateValidity(); // subscribe to "refresh" event passed indirectly by FormulairePbCloison (change upstream/downstream basin) this.pbSchema.addObserver(this); } /** 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.selectNode(item); }); }); } // debug private createStructure(l: LoiDebit, v: number[]): Structure { const s: Structure = CreateStructure(l); s.prms.Q.singleValue = v[0]; s.prms.ZDV.singleValue = v[1]; s.prms.Z1.singleValue = v[2]; s.prms.Z2.singleValue = v[3]; s.getParameter("L").singleValue = v[4]; s.getParameter("CdGR").singleValue = v[5]; s.prms.W.singleValue = v[6]; return s; } /** * Builds a Mermaid graph text definition, using Nodes * to represent basins as well as walls; sorts connexions * to prevent lines crossings */
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
private graphDefinition() { this.existingWalls = {}; this.wallsSuffixes = {}; const def: string[] = [ "graph TB" ]; // river upstream / downstream def.push(`${this.upstreamId}("${this.i18nService.localizeText("INFO_LIB_AMONT")}")`); def.push(`${this.downstreamId}("${this.i18nService.localizeText("INFO_LIB_AVAL")}")`); // styles def.push("classDef wall fill:#e8e8e8,stroke-width:0;"); def.push("classDef basin fill:#e0f3fb,stroke:#003A80;"); // irstea-ocean 50 / 500 def.push("classDef node-highlighted fill:#4DBBE9;"); // irstea-ocean (material "accent"), 300 // debug if (this.model.children.length === 0) { // EXEMPLE 1 (petit) /* 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)); */ // EXEMPLE 2 (grand) this.model.addChild(new PbBassin(new PbBassinParams(13.80, 95))); this.model.addChild(new PbBassin(new PbBassinParams(15.40, 94.70))); this.model.addChild(new PbBassin(new PbBassinParams(16.20, 94.70))); this.model.addChild(new PbBassin(new PbBassinParams(17.50, 94.40))); this.model.addChild(new PbBassin(new PbBassinParams(32.10, 94.25))); this.model.addChild(new PbBassin(new PbBassinParams(35.00, 94.10))); this.model.addChild(new PbCloison(undefined, this.model.children[0] as PbBassin)); // Session.getInstance().createNub(p, this.currentNub as ParallelStructure) as Structure this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 95.30, 0, 0, 0.4, 1.04 ]) ); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 96.25, 0, 0, 4.40, 1.04 ]) ); // Wall between upstream and basin 2 this.model.addChild(new PbCloison(undefined, this.model.children[1] as PbBassin)); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 96.00, 0, 0, 1.00, 1.04 ]) ); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 96.25, 0, 0, 5.00, 0.91 ]) ); // Wall between upstream and basin 5 this.model.addChild(new PbCloison(undefined, this.model.children[4] as PbBassin)); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 96.25, 0, 0, 3.50, 0.99 ]) ); // Wall between upstream and basin 6 this.model.addChild(new PbCloison(undefined, this.model.children[5] as PbBassin)); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 96.25, 0, 0, 3.60, 0.99 ]) ); // Wall between basin 1 & 3 this.model.addChild(new PbCloison(this.model.children[0] as PbBassin, this.model.children[2] as PbBassin)); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 95.00, 0, 0, 0.40, 1.04 ]) ); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 96.25, 0, 0, 5.20, 0.99 ]) ); // Wall between basin 2 & 3 this.model.addChild(new PbCloison(this.model.children[1] as PbBassin, this.model.children[2] as PbBassin)); this.model.children[this.model.children.length - 1].addChild(
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
this.createStructure(LoiDebit.WeirCunge80, [ 0, 95.85, 0, 0, 4.38, 0.91 ]) ); // Wall between basin 2 & 4 this.model.addChild(new PbCloison(this.model.children[1] as PbBassin, this.model.children[3] as PbBassin)); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 95.85, 0, 0, 3.00, 0.99 ]) ); // Wall between basin 2 & 5 this.model.addChild(new PbCloison(this.model.children[1] as PbBassin, this.model.children[4] as PbBassin)); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 95.50, 0, 0, 1.00, 1.04 ]) ); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 95.75, 0, 0, 3.00, 0.99 ]) ); // Wall between basin 3 & 4 this.model.addChild(new PbCloison(this.model.children[2] as PbBassin, this.model.children[3] as PbBassin)); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 94.70, 0, 0, 0.40, 1.04 ]) ); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 95.65, 0, 0, 5.74, 0.99 ]) ); // Wall between basin 4 & 5 this.model.addChild(new PbCloison(this.model.children[3] as PbBassin, this.model.children[4] as PbBassin)); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 94.40, 0, 0, 0.40, 1.04 ]) ); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 95.35, 0, 0, 6.00, 0.99 ]) ); // Wall between basin 5 & 6 this.model.addChild(new PbCloison(this.model.children[4] as PbBassin, this.model.children[5] as PbBassin)); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 94.25, 0, 0, 0.70, 1.04 ]) ); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 95.05, 0, 0, 9.50, 0.99 ]) ); // Wall between basin 6 & downstream this.model.addChild(new PbCloison(this.model.children[5] as PbBassin, undefined)); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 94.10, 0, 0, 0.95, 1.04 ]) ); this.model.children[this.model.children.length - 1].addChild( this.createStructure(LoiDebit.WeirCunge80, [ 0, 94.75, 0, 0, 10.20, 0.99 ]) ); } const sortedWalls: PbCloison[] = []; for (const c of this.model.children) { if (c instanceof PbBassin) { def.push(`${c.uid}("${this.itemDesription(c)}")`); // rounded edges def.push(`class ${c.uid} basin;`); } else if (c instanceof PbCloison) { // store, to draw later sortedWalls.push(c); } } // sort then draw walls sortedWalls.sort(this.triCloisonsGaucheDroite); for (const c of sortedWalls) { const upstreamBasinId = c.bassinAmont === undefined ? this.upstreamId : c.bassinAmont.uid; const downstreamBasinId = c.bassinAval === undefined ? this.downstreamId : c.bassinAval.uid; // record this wall const basinsPair = upstreamBasinId + "-" + downstreamBasinId; if (! (basinsPair in this.existingWalls)) { this.existingWalls[basinsPair] = 0; } // affect suffix if needed
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
if (this.existingWalls[basinsPair] > 0) { this.wallsSuffixes[c.uid] = this.existingWalls[basinsPair]; } this.existingWalls[basinsPair]++; // draw wall Node def.push(`${c.uid}["${this.itemDesription(c)}"]`); // square edges def.push(`class ${c.uid} wall;`); // draw "arrow" with 2 lines def.push(`${upstreamBasinId}---${c.uid}-->${downstreamBasinId}`); } return def.join("\n"); } /** gauche d'abord, droite ensuite */ private triCloisonsGaucheDroite(a: PbCloison, b: PbCloison) { // ultra-gauchistes if (a.bassinAmont === undefined && a.bassinAval === undefined) { return -1; } if (b.bassinAmont === undefined && b.bassinAval === undefined) { return 1; } // si A est un super-gauchiste if (a.bassinAmont === undefined || a.bassinAval === undefined) { // B est-il aussi un super-gauchiste ? if (b.bassinAmont === undefined || b.bassinAval === undefined) { // comparer le bassin restant const bassinA = (a.bassinAmont === undefined ? a.bassinAval : a.bassinAmont); const bassinB = (b.bassinAmont === undefined ? b.bassinAval : b.bassinAmont); return (bassinA.findPositionInParent() <= bassinB.findPositionInParent()) ? -1 : 1; } // sinon A gagne return -1; } // si B est un super-gauchiste if (b.bassinAmont === undefined || b.bassinAval === undefined) { // B gagne (le cas de A super-gauchiste est éliminé avant) return 1; } // sinon, aucun des deux n'est super-gauchiste, comparaison des bassins amont et aval const sommeA = a.bassinAmont.findPositionInParent() + a.bassinAval.findPositionInParent(); const sommeB = b.bassinAmont.findPositionInParent() + b.bassinAval.findPositionInParent(); return (sommeA <= sommeB ? -1 : 1); } private selectNode(item: any) { // highlight clicked element this.clearHighlightedItems(); item.classList.add("node-highlighted"); // find what was clicked if ([ this.upstreamId, this.downstreamId ].includes(item.id)) { this._selectedItem = undefined; } else { this._selectedItem = this.model.findChild(item.id); } // show proper form and hide results this.nodeSelected.emit({ node: this._selectedItem }); } // for debug only public get graphDef(): string { return this.graphDefinition(); } public get title(): string { return this.i18nService.localizeText("INFO_PB_SCHEMA"); }
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
/** Global Pb validity */ public get isValid() { return this._isValid; } /** used for a cosmetics CSS trick only (mat-card-header right margin) */ public get showInputData(): boolean { return this.calculatorComponent.showPBInputData; } 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" + (this.findBasinPosition(item.bassinAmont) + 1); const downstreamBasinName = item.bassinAval === undefined ? this.i18nService.localizeText("INFO_LIB_AVAL") : "B" + (this.findBasinPosition(item.bassinAval) + 1); desc = upstreamBasinName + "-" + downstreamBasinName; // if a similar wall already exists, suffix ! if (item.uid in this.wallsSuffixes) { desc += " (" + this.wallsSuffixes[item.uid] + ")"; } } else if (item instanceof PbBassin) { desc = this.i18nService.localizeText("INFO_PB_BASSIN_N") + (this.findBasinPosition(item) + 1); } // else undefined return desc; } private findBasinPosition(basin: PbBassin): number { let i = 0; for (const b of this.model.bassins) { if (b === basin) { break; } i++; } return i; } // at this time @Input data is supposed to be already populated public ngOnInit() { this.model = this.pbSchema.pb; } public get enableRemoveButton() { return (this._selectedItem !== undefined); } /** Removes a basin or wall, and all related items */ public onRemoveClick() { this.model.deleteChild(this._selectedItem.findPositionInParent()); this.unselect(); this.refresh(); }
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
public get uitextRemove() { return this.i18nService.localizeText("INFO_FIELDSET_REMOVE"); } public get enableCopyButton() { return (this._selectedItem !== undefined && this._selectedItem instanceof PbCloison); } /** Copies a wall */ public onCopyClick() { const wall = this._selectedItem as PbCloison; const wallCopy = new PbCloison(wall.bassinAmont, wall.bassinAval); this.model.addChild(wallCopy); this.unselect(); // @TODO select new wall ? this.refresh(); } public get uitextCopy() { return this.i18nService.localizeText("INFO_FIELDSET_COPY"); } /** Adds a new lone basin */ public onAddBasinClick() { this.model.addChild(new PbBassin(new PbBassinParams(20, 99))); this.unselect(); // @TODO select new basin ? this.refresh(); } public get uitextAddBasin() { return this.i18nService.localizeText("INFO_PB_ADD_BASIN"); } public get enableAddWallButton(): boolean { return (this.model.bassins.length > 0); } /** Adds a new lone wall, opening a modal to choose connected basins */ public onAddWallClick() { // open dialog const dialogRef = this.newPbCloisonDialog.open( DialogNewPbCloisonComponent, { data: { basins: this.model.bassins }, disableClose: true } ); // apply modifications dialogRef.afterClosed().subscribe(result => { if (result.up !== undefined && result.down !== undefined) { const wall = new PbCloison( result.up === 0 ? undefined : this.model.bassins[result.up - 1], result.down === 0 ? undefined : this.model.bassins[result.down - 1] ); this.model.addChild(wall); this.unselect(); this.refresh(); } }); } public get uitextAddWall() { return this.i18nService.localizeText("INFO_PB_ADD_WALL"); } public get enableUpButton() { return ( this._selectedItem instanceof PbBassin && this.findBasinPosition(this._selectedItem) !== 0
491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
&& this.isStandaloneBasin(this._selectedItem) ); } public onMoveBasinUpClick() { if (this._selectedItem instanceof PbBassin) { this.model.moveBasin(this._selectedItem.uid, this.findBasinPosition(this._selectedItem) - 1); } this.refresh(); } public get uitextMoveBasinUp() { return this.i18nService.localizeText("INFO_PB_MOVE_BASIN_UP"); } public get enableDownButton() { return ( this._selectedItem instanceof PbBassin && this.findBasinPosition(this._selectedItem) !== this.model.bassins.length - 1 && this.isStandaloneBasin(this._selectedItem) ); } public onMoveBasinDownClick() { if (this._selectedItem instanceof PbBassin) { this.model.moveBasin(this._selectedItem.uid, this.findBasinPosition(this._selectedItem) + 1); } this.refresh(); } public get uitextMoveBasinDown() { return this.i18nService.localizeText("INFO_PB_MOVE_BASIN_DOWN"); } /** * Returns true if given basin is either connected to nothing, or only to * river upstream or downstream */ private isStandaloneBasin(basin: PbBassin) { return ( ( basin.cloisonsAmont.length === 0 || basin.cloisonsAmont.map(c => c.bassinAmont).every(e => e === undefined) ) && ( basin.cloisonsAval.length === 0 || basin.cloisonsAval.map(c => c.bassinAval).every(e => e === undefined) ) ); } /** * Computes the global Pab validity : validity of every cell of every row */ private updateValidity() { // check that at least 1 basin is present and a route from river // upstream to river downstream exists (2nd check includes 1st) this._isValid = this.model.hasUpDownConnection(); console.log("schéma valide", this._isValid); this.validChange.emit(); } private clearHighlightedItems() { this.nativeElement.querySelectorAll("g.node").forEach(item => { item.classList.remove("node-highlighted"); }); } private unselect() { this._selectedItem = undefined; this.clearHighlightedItems();
561562563564565566567568569570571572573574575576
this.nodeSelected.emit({}); // nothing selected } // interface Observer public update(sender: IObservable, data: any) { if (sender instanceof PbSchema) { if (data.action === "refresh") { this.unselect(); this.refresh(); } } } }