modules/post/this/this.mjs

export { THisHelper };

import { AssessmentDatums } from "./assessment_datums.mjs";

/**
 * Helper class to carry out tasks in T-HIS
 */
class THisHelper {
    /**
     * Maximum number of graphs
     * @type {number} */
    static get MAX_GRAPHS() {
        return 32;
    }

    /**
     * Maximum number of pages
     * @type {number} */
    static get MAX_PAGES() {
        return 32;
    }

    /**
     * Reads data into a curve, returning null if it fails
     * @param {Model} model Model
     * @param {string} entity_type Entity type
     * @param {string|number} entity_id Entity ID (label of database history name)
     * @param {string} component Component name (used in dialogue command)
     * @param {boolean} [blank = false] Flag to blank the curve after reading it
     * @returns {?Curve}
     * @example
     * // Pass in the entity_type and entity_id parameters explicitly
     *
     * let curve = THisHelper.ReadData(m, "node", 1, "AX");
     *
     * // Typically you will use the OccupantEntity entity_type and
     * // id properties for the entity_type and entity_id parameters
     *
     * let node_x = head.GetEntityByTag(OccupantEntity.HEAD_NODE);
     * let curve = THisHelper.ReadData(m, node_x.entity_type, node_x.id, "AX");
     */
    static ReadData(model, entity_type, entity_id, component, blank = false) {
        /* This uses dialogue commands to read data rather than the Model.GetDataFlagged()
         * API method because the latter uses Model.SetFlag() which only allows entities
         * to be specified using their numerical IDs and there's no way to use the database
         * history name.
         */

        let curve_id = Curve.FirstFreeID();

        // @ts-ignore
        if (!isNaN(entity_id)) {
            DialogueInput(`/MODEL DATA ${entity_type} M${model.id}/${entity_id}`, component, `#${curve_id}`);
        } else {
            DialogueInput(`/MODEL DATA ${entity_type} M${model.id}/"${entity_id}"`, component, `#${curve_id}`);
        }

        if (Curve.Exists(curve_id)) {
            let curve = Curve.GetFromID(curve_id);

            if (blank) {
                curve.RemoveFromGraph();
            }

            return Curve.GetFromID(curve_id);
        } else {
            ErrorMessage(`Unable to read ${component} data for ${entity_type} ${entity_id} in model ${model.id}`);

            return null;
        }
    }

    /**
     * Sets the labels on a curve
     * @param {Curve} curve Curve to set labels on
     * @param {string} label Curve label
     * @param {string} x_label x-axis label
     * @param {string} y_label y-axis label
     * @example
     * THisHelper.SetLabels(curve, "My Curve", "Time (s)", "Acceleration (g)");
     */
    static SetCurveLabels(curve, label, x_label, y_label) {
        if (!(curve instanceof Curve)) {
            throw new Error("curve must be a Curve in THisHelper.SetLabels");
        }

        curve.label = label;
        curve.x_axis_label = x_label;
        curve.y_axis_label = y_label;
    }

    /**
     * Sets the line style of a curve
     * @param {Curve|Curve[]} curve Curve or array of curves to set line style on
     * @param {number} colour Colour to set line to, e.g. Colour.BLACK
     * @param {number} [style=LineStyle.SOLID] Line style to set, e.g. LineStyle.DASH
     * @param {number} [symbol=Symbol.NONE] Symbol, e.g. Symbol.CROSS
     * @example
     * // Set style on one curve
     * THisHelper.SetLineStyle(curve, Colour.BLACK);
     *
     * // Set style on multiple curves at the same time
     * THisHelper.SetLineStyle([curve1, curve2, curve3], Colour.BLACK);
     *
     * // Set line style as well as colour on one curve
     * THisHelper.SetLineStyle(curve, Colour.BLACK, LineStyle.DASH);
     */
    static SetLineStyle(curve, colour, style, symbol) {
        if (curve instanceof Array) {
            for (let c of curve) {
                if (!(c instanceof Curve)) {
                    throw new Error("curve Array does not contain Curves in THisHelper.SetLineStyle");
                }
            }
        } else if (!(curve instanceof Curve)) {
            throw new Error("curve must be a Curve in THisHelper.SetLineStyle");
        }

        if (style == undefined) style = LineStyle.SOLID;
        // @ts-ignore
        if (symbol == undefined) symbol = Symbol.NONE;

        if (curve instanceof Array) {
            for (let c of curve) {
                set_line_style(c, colour, style, symbol);
            }
        } else {
            set_line_style(curve, colour, style, symbol);
        }

        function set_line_style(c, colour, style, symbol) {
            c.colour = colour;
            c.style = style;
            c.width = LineWidth.BOLD;
            c.symbol = symbol;
        }
    }

    /**
     * Blanks all the curves in all graphs
     * @example
     * THisHelper.BlankAllCurves();
     */
    static BlankAllCurves() {
        let id = Curve.HighestID();

        if (id > 0) {
            var c = Curve.First();

            while (c) {
                c.RemoveFromGraph();
                c = c.Next();
            }
        }
    }

    /**
     * Blanks all the datums in all graphs
     * @example
     * THisHelper.BlankAllDatums();
     */
    static BlankAllDatums() {
        let d = Datum.First();

        while (d) {
            d.RemoveFromGraph();
            d = d.Next();
        }
    }

    /**
     * Sets the graph title
     * @param {number} graph_id Index of graph
     * @param {string} title Title for graph
     */
    static SetGraphTitle(graph_id, title) {
        /* Get the graph */

        let graph = Graph.GetFromID(graph_id);

        if (graph == null) {
            ErrorMessage(`Graph (${graph_id}) not found in THisHelper.SetGraphTitle`);
            return;
        }

        /* Set the title */

        graph.title = title;
    }

    /**
     * Scales the graph to show the curves and datums in the graph
     *
     * This is different to just using the dialogue command "/AU" because
     * it scales it so there is space above and below the curve.
     *
     * Also if assessment datums are passed to the function it will be
     * scaled to show all the datums, again with space above and below.
     * @param {number} graph_id Index of graph to scale
     * @param {AssessmentDatums} [assessment_datums] Datums to show
     */
    static ScaleGraph(graph_id, assessment_datums) {
        /* Get the graph */

        let graph = Graph.GetFromID(graph_id);

        if (graph == null) {
            ErrorMessage(`Graph (${graph_id}) not found in THisHelper.ScaleGraph`);
            return;
        }

        /* Get the curves on the graph */

        let curve_ids = graph.GetAllCurveIDs();

        /* Nothin to do if no curves or assessment datums */

        if (curve_ids.length == 0 && !assessment_datums) {
            return;
        }

        /* Get the min and max values of the curves */

        let min_x = Number.MAX_VALUE;
        let max_x = -Number.MAX_VALUE;
        let min_y = Number.MAX_VALUE;
        let max_y = -Number.MAX_VALUE;

        for (let curve_id of curve_ids) {
            let curve = Curve.GetFromID(curve_id);

            if (curve == null) {
                ErrorMessage(`Curve (${curve_id}) not found in THisHelper.ScaleGraph`);
                continue;
            }

            min_x = Math.min(min_x, curve.xmin);
            max_x = Math.max(max_x, curve.xmax);
            min_y = Math.min(min_y, curve.ymin);
            max_y = Math.max(max_y, curve.ymax);
        }

        /* Get the min and max values of the datums */

        if (assessment_datums) {
            min_x = Math.min(min_x, assessment_datums.MinX());
            max_x = Math.max(max_x, assessment_datums.MaxX());
            min_y = Math.min(min_y, assessment_datums.MinY());
            max_y = Math.max(max_y, assessment_datums.MaxY());
        }

        /* Add a bit to the min and max y values */

        if (min_y < 0) {
            min_y *= 1.1;
        } else if (min_y > 0) {
            min_y *= 0.9;
        }

        if (max_y > 0) {
            max_y *= 1.1;
        } else if (max_y < 0) {
            max_y *= 0.9;
        }

        /* Scale the graph
         *
         * If the max value is smaller than the current min value T/HIS
         * sets the max to the current min and the min to the max value
         * (and vice-versa when setting min values)
         *
         * To counter this set the values twice so that the min and max
         * graph values get set correctly.
         */

        for (let i = 0; i < 2; i++) {
            if (max_x != -Number.MAX_VALUE) graph.xmax = max_x;
            if (min_x != Number.MAX_VALUE) graph.xmin = min_x;
            if (max_y != -Number.MAX_VALUE) graph.ymax = max_y;
            if (min_y != Number.MAX_VALUE) graph.ymin = min_y;
        }
    }

    /**
     * Put graphs on a page, removing any that are not required
     * @param {number[]} graphs Graph ids to put on page
     * @param {number} page Page to put graphs on
     * @param {boolean} remove_existing_graphs Switch whether to remove existing graphs from page
     */
    static PutGraphsOnPage(graphs, page, remove_existing_graphs) {
        if (remove_existing_graphs) {
            Page.RemoveGraph(page, -1);
        }

        for (let g of graphs) {
            Page.AddGraph(page, g);
        }

        /* Need to set the page layout to get it to plot the page */

        Page.Layout(page, "tall");
    }

    /**
     * Puts graphs on separate pages
     * @param {number[]} graphs Graph ids to put on separate pages
     * @param {number} [start_page=1] Page to start adding graphs on
     * @return {number} The last page id used
     */
    static PutGraphsOnSeparatePages(graphs, start_page = 1) {
        /* Remove the graphs from all pages */

        for (let p = 1; p <= THisHelper.MAX_PAGES; p++) {
            for (let g of graphs) {
                Page.RemoveGraph(p, g);
            }
        }

        /* Add graphs to separate pages */

        let page = start_page;

        for (let g of graphs) {
            this.PutGraphsOnPage([g], page, true);
            page++;
        }

        return page - 1;
    }

    /**
     * Captures an image of Graph 1 (REPORTER templates always designed to capture Graph 1).
     * @param {string} fname Image filename
     */
    static CaptureImage(fname) {
        /* If file exists, delete it first (dialogue commands can't easily
         * handle overwriting existing files) */

        if (File.Exists(fname)) {
            let deleted = File.Delete(fname);
            if (!deleted) {
                ErrorMessage(`Unable to delete existing image file: ${fname}`);
                return;
            }
        }

        DialogueInput("/IMAGE PNG24", fname, "GRAPH", "1");
    }
}