Failed to fetch fork details. Try again later.
-
Delaigue Olivier authored943207d2
Forked from
HYCAR-Hydro / airGR
Source project has a limited visibility.
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();
}
}