pb-schema.component.ts 18.53 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, Observer, IObservable
 } from "jalhyd";
import * as mermaid from "mermaid";
import { HotkeysService, Hotkey } from "angular2-hotkeys";
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";
import { AppComponent } from "../../app.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 };
    public constructor(
        @Inject(forwardRef(() => GenericCalculatorComponent)) private calculatorComponent: GenericCalculatorComponent,
        private i18nService: I18nService,
        private hotkeysService: HotkeysService,
        private newPbCloisonDialog: MatDialog
    ) {
        this.hotkeysService.add(new Hotkey("del", AppComponent.onHotkey(this.removeOnHotkey, this)));
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
public get selectedItem(): any { return this._selectedItem; } public ngAfterContentInit(): void { mermaid.initialize({ flowchart: { curve: "basis" } }); this.nativeElement = this.schema.nativeElement; this.render(); // restore previously selected item this._selectedItem = this.pbSchema.form.selectedItem; if (this._selectedItem !== undefined) { // @WARNING clodo timeout to prevent ExpressionChangedAfterItHasBeenCheckedError // and select schema node after schema is refreshed by ngAfterViewInit() setTimeout(() => { this.selectNodeOnSchema(this._selectedItem); }, 20); // timeout has to be greater than the 10ms of ngAfterViewInit() } } 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 { // subscribe to "refresh" event passed indirectly by FormulairePbCloison (change upstream/downstream basin) this.pbSchema.addObserver(this); // @WARNING clodo trick to prevent blank diagram when switching from a PreBarrage to another setTimeout(() => { this.refresh(); }, 10); } /** 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
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
*/ private graphDefinition() { this.existingWalls = {}; this.pbSchema.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 const sortedWalls: PbCloison[] = []; for (const c of this.model.children) { if (c instanceof PbBassin) { def.push(`${c.uid}("${this.itemDescription(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.pbSchema.wallsSuffixes[c.uid] = this.existingWalls[basinsPair]; } this.existingWalls[basinsPair]++; // draw wall Node def.push(`${c.uid}["${this.itemDescription(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;
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
} // 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"); } /** 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.itemDescription(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 itemDescription(item: PbCloison | PbBassin): string { let desc = ""; if (item !== undefined) { desc = this.i18nService.localizeMessage(item.description); if (item instanceof PbCloison) { // there might be multiple walls between the same pair of basins if (item.uid in this.pbSchema.wallsSuffixes) { desc += " (" + this.pbSchema.wallsSuffixes[item.uid] + ")"; } } }
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
return desc; } /** * Selects and highlights on the schema the given wall or basin */ private selectNodeOnSchema(element: PbBassin | PbCloison) { this.nativeElement.querySelectorAll("g.node").forEach(item => { if (item.id === element.uid) { this.selectNode(item); } }); } // at this time @Input data is supposed to be already populated public ngOnInit() { this.model = this.pbSchema.pb; } public get enableRemoveButton() { if (this._selectedItem === undefined) { return false; } // if deleting a PbCloison would replace it by a new one at // the same place (@see onRemoveClick), make it not deletable if (this._selectedItem instanceof PbCloison) { if (( this._selectedItem.bassinAmont !== undefined && this._selectedItem.bassinAmont.cloisonsAval.length === 1 && this._selectedItem.bassinAval === undefined ) || ( this._selectedItem.bassinAval !== undefined && this._selectedItem.bassinAval.cloisonsAmont.length === 1 && this._selectedItem.bassinAmont === undefined )) { return false; } } return true; } /** Removes a basin or wall, and all related items */ public onRemoveClick() { this.model.deleteChild(this._selectedItem.findPositionInParent()); // never let an unconnected basin ! (not done in model to prevent unwanted // automatic child addition when clearing children) if (this._selectedItem instanceof PbCloison) { // if no downstream connections remain, connect to river downstream if (this._selectedItem.bassinAmont?.cloisonsAval.length === 0) { this.model.addChild(new PbCloison(this._selectedItem.bassinAmont, undefined)); } // if no upstream connections remain, connect to river upstream if (this._selectedItem.bassinAval?.cloisonsAmont.length === 0) { this.model.addChild(new PbCloison(undefined, this._selectedItem.bassinAval)); } } this.unselect(); this.refresh(); this.clearResults(); this.calculatorComponent.showPBInputData = true; } public get uitextRemove() { return this.i18nService.localizeText("INFO_FIELDSET_REMOVE"); } // listener for "del" hotkey protected removeOnHotkey() { if (this.enableRemoveButton) { this.onRemoveClick();
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
} } 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); wallCopy.loadObjectRepresentation(wall.objectRepresentation()); this.model.addChild(wallCopy); this.unselect(); this.refresh(); this.selectNodeOnSchema(wallCopy); this.clearResults(); } public get uitextCopy() { return this.i18nService.localizeText("INFO_FIELDSET_COPY"); } /** Adds a new lone basin */ public onAddBasinClick() { const newBasin = new PbBassin(new PbBassinParams(20, 99)); this.model.addChild(newBasin); this.unselect(); this.refresh(); this.selectNodeOnSchema(newBasin); this.clearResults(); } 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(); this.selectNodeOnSchema(wall); this.clearResults(); } }); } public get uitextAddWall() {
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
return this.i18nService.localizeText("INFO_PB_ADD_WALL"); } public get enableUpButton() { return ( this._selectedItem instanceof PbBassin && this.model.findBasinPosition(this._selectedItem.uid) !== 0 && this.isStandaloneBasin(this._selectedItem) ); } public onMoveBasinUpClick() { if (this._selectedItem instanceof PbBassin) { this.model.moveBasin(this._selectedItem.uid, this.model.findBasinPosition(this._selectedItem.uid) - 1); } const basin = this._selectedItem; this.unselect(); this.refresh(); this.selectNodeOnSchema(basin); this.clearResults(); } public get uitextMoveBasinUp() { return this.i18nService.localizeText("INFO_PB_MOVE_BASIN_UP"); } public get enableDownButton() { return ( this._selectedItem instanceof PbBassin && this.model.findBasinPosition(this._selectedItem.uid) !== this.model.bassins.length - 1 && this.isStandaloneBasin(this._selectedItem) ); } public onMoveBasinDownClick() { if (this._selectedItem instanceof PbBassin) { this.model.moveBasin(this._selectedItem.uid, this.model.findBasinPosition(this._selectedItem.uid) + 1); } const basin = this._selectedItem; this.unselect(); this.refresh(); this.selectNodeOnSchema(basin); this.clearResults(); } 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
491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
// upstream to river downstream exists (2nd check includes 1st) this._isValid = ( this.model.hasUpDownConnection() && ! this.model.hasBasinNotConnected() ); this.validChange.emit(); } private clearHighlightedItems() { this.nativeElement.querySelectorAll("g.node").forEach(item => { item.classList.remove("node-highlighted"); }); } private unselect() { this._selectedItem = undefined; this.clearHighlightedItems(); this.nodeSelected.emit({}); // nothing selected } /** clear all PB form results whenever the basins / walls layout is changed */ private clearResults() { this.pbSchema.form.reset(); } // interface Observer public update(sender: IObservable, data: any) { if (sender instanceof PbSchema) { if (data.action === "refresh") { this.unselect(); this.refresh(); // select a node on the schema ? if (data.value !== undefined) { this.selectNodeOnSchema(this.model.findChild(data.value)); } } } } }