Forked from reversaal / OhmPi
Source project has a limited visibility.
pb-schema.component.ts 15.85 KiB
import { Component, Input, Output, EventEmitter, OnInit, AfterViewInit, ViewChild } from "@angular/core";
import { MatDialog } from '@angular/material/dialog';
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";
import { DialogNewPbCloisonComponent } from "../dialog-new-pb-cloison/dialog-new-pb-cloison.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, OnInit {
    @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 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: 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(
        private i18nService: I18nService,
        private newPbCloisonDialog: MatDialog,
        private appSetupService: ApplicationSetupService,
        private notifService: NotificationsService
    ) { }
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
public get selectedItem(): any { return this._selectedItem; } 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(); } /** 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); }); }); } /** * Builds a Mermaid graph text definition, using Nodes * to represent basins as well as walls; sorts connexions * to prevent lines crossings */ 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
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
// 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)); this.model.addChild(new PbCloison(undefined, this.model.children[1] as PbBassin)); this.model.addChild(new PbCloison(undefined, this.model.children[4] as PbBassin)); this.model.addChild(new PbCloison(undefined, this.model.children[5] as PbBassin)); this.model.addChild(new PbCloison(this.model.children[0] as PbBassin, this.model.children[2] as PbBassin)); this.model.addChild(new PbCloison(this.model.children[1] as PbBassin, this.model.children[2] as PbBassin)); this.model.addChild(new PbCloison(this.model.children[1] as PbBassin, this.model.children[3] as PbBassin)); this.model.addChild(new PbCloison(this.model.children[1] as PbBassin, this.model.children[4] as PbBassin)); this.model.addChild(new PbCloison(this.model.children[2] as PbBassin, this.model.children[3] as PbBassin)); this.model.addChild(new PbCloison(this.model.children[3] as PbBassin, this.model.children[4] as PbBassin)); this.model.addChild(new PbCloison(this.model.children[4] as PbBassin, this.model.children[5] as PbBassin)); this.model.addChild(new PbCloison(this.model.children[5] as PbBassin, undefined)); } 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 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 */
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
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 { for (const b of this.model.children) { if (b.uid === item.id) { this._selectedItem = b; } } } } // for debug only 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; }
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
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" + (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(); } public get uitextRemove() { return this.i18nService.localizeText("INFO_FIELDSET_REMOVE"); } public get enableCopyButton() { return (this._selectedItem !== undefined && this._selectedItem instanceof PbCloison); }
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
/** 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(); 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(); 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"); } /** * 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(); this.validChange.emit(); } private clearHighlightedItems() {
421422423424425426427428429430431432
this.nativeElement.querySelectorAll("g.node").forEach(item => { item.classList.remove("node-highlighted"); }); } private unselect() { this._selectedItem = undefined; this.clearHighlightedItems(); } }