Skip to content
Snippets Groups Projects
modules-diagram.component.ts 7.80 KiB
import {
    Component,
    ViewChild,
    AfterContentInit,
    OnInit,
    AfterViewChecked,
    AfterViewInit
} from "@angular/core";
import { Router } from "@angular/router";

import {
    Session,
    ParamValueMode,
    CalculatorType,
    SectionType,
    LoiDebit,
    Nub,
    MacrorugoCompound,
    Pab,
    Solveur
} from "jalhyd";

import { I18nService } from "../../services/internationalisation.service";
import { FormulaireService } from "../../services/formulaire.service";

import * as mermaid from "mermaid";

import * as SvgPanZoom from "svg-pan-zoom";

import { MatomoTracker } from "ngx-matomo";

import { fv } from "../../util";

@Component({
    selector: "modules-diagram",
    templateUrl: "./modules-diagram.component.html",
    styleUrls: ["./modules-diagram.component.scss"]
})
export class ModulesDiagramComponent implements AfterContentInit, AfterViewChecked, AfterViewInit, OnInit {

    private svgPanZoom: SvgPanZoom.Instance = null;

    private needsToInitSvgPanZoom = false;

    /** handle on SVG container */
    private nativeElement: any;

    @ViewChild("diagram", { static: true })
    public diagram: any;

    public error: boolean;

    public showDebug = false;

    constructor(
        private intlService: I18nService,
        private router: Router,
        private formulaireService: FormulaireService,
        private matomoTracker: MatomoTracker
    ) {
        this.error = false;
        this.matomoTracker.trackPageView("diagram");
    }

    public get uitextTitle(): string {
        return this.intlService.localizeText("INFO_DIAGRAM_TITLE");
    }

    public get uitextDrawingError(): string {
        return this.intlService.localizeText("INFO_DIAGRAM_DRAWING_ERROR");
    }

    public get uitextCalculatedParam(): string {
        return this.intlService.localizeText("INFO_DIAGRAM_CALCULATED_PARAM");
    }

    public ngAfterViewChecked() {
        if (this.needsToInitSvgPanZoom) {
            this.initSvgPanZoom();
        }
    }

    public initSvgPanZoom() {
        if (this.svgPanZoom) {
            this.svgPanZoom.destroy();
        }
        this.svgPanZoom = SvgPanZoom("#graphDiv", {
            minZoom: 1,
            maxZoom: 10,
            zoomScaleSensitivity: 0.8,
            contain: true
        });
        this.needsToInitSvgPanZoom = false;
    }

    public ngOnInit() {
        // if app is started on this page but session is empty, redirect to home page
        if (! this.hasModules) {
            this.router.navigate([ "/list" ]);
        }
    }

    public ngAfterViewInit(): void {
        // add click listener on every calculator node in the graph, that
        // corresponds to an open module
        this.nativeElement.querySelectorAll("g.node").forEach(item => {
            if (item.id && this.formIsOpen(item.id)) {
                item.style.cursor = "pointer";
                item.addEventListener("click", () => {
                    this.openCalc(item.id);
                });
            }
        });
    }

    public ngAfterContentInit(): void {
        this.error = false;
        mermaid.initialize({
            // theme: "forest"
            flowchart: {
                curve: "basis"
            }
        });
        this.nativeElement = this.diagram.nativeElement;

        if (this.hasModules) {
            // 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 resetZoom() {
        if (this.svgPanZoom) {
            this.svgPanZoom.resetZoom();
            // this.svgPanZoom.contain();
            // this.svgPanZoom.fit();
        }
    }

    public get hasModules(): boolean {
        return Session.getInstance().getNumberOfNubs() > 0;
    }

    /**
     * Builds a Mermaid graph text definition
     */
    private graphDefinition() {
        const def: string[] = [ "graph TB" ];
        const forms = this.formulaireService.formulaires;

        for (const f of forms) {
            // register Nub in diagram
            const nub = f.currentNub;
            const children = nub.getChildren();
            if (children.length > 0) {
                // subgraph for Nubs having children
                def.push("subgraph \"" + f.calculatorName + "\"");
                def.push(f.uid + "(\"" + f.calculatorName + "\")");

                if (nub instanceof MacrorugoCompound || nub instanceof Pab) {
                    // PAB or MRC : gather all children in one node
                    def.push(f.uid + "_children" + "[\"" + this.describe(children[0]) + " (x" + children.length + ")\"]");
                    def.push(f.uid + " --- " + f.uid + "_children");
                } else {
                    // other Nub with children: display all children
                    for (const c of children) {
                        def.push(c.uid + "[\"" + this.describe(c) + "\"]");
                        def.push(f.uid + " --- " + c.uid);
                    }
                }
                def.push("end");
            } else {
                // simple Nub (no children)
                def.push(f.uid + "(\"" + f.calculatorName + "\")");
            }
            // find all linked parameters
            for (const p of nub.parameterIterator) {
                if (p.valueMode === ParamValueMode.LINK && p.isReferenceDefined()) {
                    const target = p.referencedValue.nub;
                    let symb = p.symbol;
                    const rv = p.referencedValue;
                    if (rv.isCalculated()) {
                        symb += "*";
                    } else {
                        if (rv.getParamValues().valueMode === ParamValueMode.SINGLE) {
                            symb += "=" + fv(rv.getValue());
                        }
                    }
                    def.push(target.uid + "-->|" + symb + "|" + nub.uid);
                }
            }
            // add Solveur links
            if (nub instanceof Solveur) {
                const ntc = nub.nubToCalculate;
                const sp = nub.searchedParameter;
                const reads = this.intlService.localizeText("INFO_DIAGRAM_SOLVEUR_READS");
                const finds = this.intlService.localizeText("INFO_DIAGRAM_SOLVEUR_FINDS");
                if (ntc !== undefined) {
                    def.push(ntc.uid + "-->|" + reads + ":" + ntc.calculatedParam.symbol + "|" + nub.uid);
                }
                if (sp !== undefined) {
                    def.push(nub.uid + "-->|" + finds + ":" + sp.symbol + "|" + sp.parentNub.uid);
                }
            }
        }

        return def.join("\n");
    }

    public get graphDef(): string {
        return this.graphDefinition();
    }

    public openCalc(uid: string) {
        this.router.navigate(["/calculator", uid]);
    }

    /**
     * Returns a very short "description" of the given Nub,
     * based on the most specific of its properties
     */
    private describe(n: Nub) {
        let type = CalculatorType[n.calcType];
        const nt = n.properties.getPropValue("nodeType");
        if (nt) {
            type = SectionType[nt];
        } else {
            const ld = n.properties.getPropValue("loiDebit");
            if (ld !== undefined) {
                type = LoiDebit[ld];
            }
        }
        return type;
    }

    /**
     * Returns true if uid is the id of the main Nub of any
     * of the open modules
     */
    private formIsOpen(uid: string) {
        for (const f of this.formulaireService.formulaires) {
            if (f.currentNub.uid === uid) {
                return true;
            }
        }
        return false;
    }
}