modules/shared/base.mjs

// module: TRUE

/* Classes to be used as base classes for child classes to extend */

import { Measurement } from "./measurement.mjs";

export { BaseOccupant, BaseEntity, BaseComponent, BaseEntityWidgets, BaseComponentWidgets };

/**
 *
 */
class BaseOccupant {
    constructor(position) {
        this.position = position;
    }

    /**
     * Occupant position constant
     * @type {string} */
    get position() {
        return this._position;
    }
    set position(new_position) {
        if (BaseOccupant.Positions().indexOf(new_position) == -1) {
            throw new Error(`Invalid position: ${new_position}`);
        }

        this._position = new_position;
    }

    // #region Class constant getters (read-only)

    // #region position getters

    /** Driver
     * @type {string} */
    static get DRIVER() {
        return "Driver";
    }
    /** Front passenger
     * @type {string} */
    static get FRONT_PASSENGER() {
        return "Front passenger";
    }

    /** Rear driver side
     * @type {string} */
    static get REAR_DRIVER_SIDE() {
        return "Rear driver side";
    }

    /** Rear passenger side
     * @type {string} */
    static get REAR_PASSENGER_SIDE() {
        return "Rear passenger side";
    }

    /** Middle passenger
     * @type {string} */
    static get REAR_MIDDLE() {
        return "Rear middle passenger";
    }

    // #region side getters

    /** Left side
     * @type {string} */
    static get LEFT() {
        return "left";
    }
    /** Middle side
     * @type {string} */
    static get MIDDLE() {
        return "middle";
    }
    /** Right side
     * @type {string} */
    static get RIGHT() {
        return "right";
    }

    /** Front
     * @type {string} */
    static get FRONT() {
        return "front";
    }
    /** Rear
     * @type {string} */
    static get REAR() {
        return "rear";
    }

    /**
     * Get the row of an occupant based on position
     * @returns {string}
     */
    GetRow() {
        switch (this.position) {
            case BaseOccupant.DRIVER:
            case BaseOccupant.FRONT_PASSENGER:
                return BaseOccupant.FRONT;
            default:
                return BaseOccupant.REAR;
        }
    }

    /**
     * Return an array of all the available position strings
     * @returns {string[]}
     * @example
     * let positions = WorkflowOccupant.Positions();
     */
    static Positions() {
        return [
            BaseOccupant.DRIVER,
            BaseOccupant.FRONT_PASSENGER,
            BaseOccupant.REAR_DRIVER_SIDE,
            BaseOccupant.REAR_MIDDLE,
            BaseOccupant.REAR_PASSENGER_SIDE
        ];
    }
}

/** Class to represent an entity for extracting data<br><br>
 * This is intended to be a base class for other classes to extend, so shouldn't
 * be used directly.<br><br>
 * To extend it the static EntityTags() method needs to be implemented in the child class
 * as well as the entity tag constants.
 */
class BaseEntity {
    /**
     * @param {string} entity_type Entity type constant
     * @param {string|number} id Entity id (label or database history name)
     * @param {string} name Entity name
     * @param {string} tag Entity tag
     * @param {Measurement[]} measurements List of raw measurements that can be read from the entity
     * @param {string} [history_title=""] The entity database history title
     * @param {string} [iso=""] The entity iso name
     */
    constructor(entity_type, id, name, tag, measurements, history_title = "", iso = "") {
        this.entity_type = entity_type;
        this.id = id;
        this.name = name;
        this.tag = tag;
        this.measurements = measurements;
        this.history_title = history_title;
        this.iso = iso;
    }

    /* Instance property getter and setters */

    /** Entity type constant
     * @type {string} */
    get entity_type() {
        return this._entity_type;
    }
    set entity_type(new_entity_type) {
        if (BaseEntity.EntityTypes().indexOf(new_entity_type) == -1) {
            throw new Error(`Invalid entity_type: ${new_entity_type}`);
        }

        this._entity_type = new_entity_type;
    }

    /** Entity id (label or database history name)
     * @type {string|number} */
    get id() {
        return this._id;
    }
    set id(new_id) {
        if (typeof new_id != "number" && typeof new_id != "string") {
            throw new Error(`id must be a number or string: ${new_id}`);
        }

        this._id = new_id;
    }

    /** Entity name
     * @type {string} */
    get name() {
        return this._name;
    }
    set name(new_name) {
        if (typeof new_name != "string") {
            throw new Error(`name must be a string: ${new_name}`);
        }

        this._name = new_name;
    }

    /** Entity tag
     * @type {string} */
    get tag() {
        return this._tag;
    }
    set tag(new_tag) {
        /* Need to call the child class static EntityTags() method.
         * I don't know if this is the right way to do it, but it
         * seems to work although VSCode highlights it as an error,
         * hence the @ts-ignore comment.
         */
        // @ts-ignore
        if (this.constructor.EntityTags().indexOf(new_tag) == -1) {
            throw new Error(`Invalid tag: ${new_tag}`);
        }

        this._tag = new_tag;
    }

    /**
     * Measurements
     * @type {Measurement[]} */
    get measurements() {
        return this._measurements;
    }
    set measurements(new_measurements) {
        if (!(new_measurements instanceof Array)) {
            throw new Error("measurements must be an array");
        }

        for (let new_entity of new_measurements) {
            if (!(new_entity instanceof Measurement)) {
                throw new Error("measurements must be an array of Measurement instances");
            }
        }

        this._measurements = new_measurements;
    }

    /** The history_title text for the label
     * @type {string} */
    get history_title() {
        return this._history_title;
    }
    set history_title(new_history_title) {
        if (typeof new_history_title != "string") {
            throw new Error(`Invalid history_title: ${new_history_title}`);
        }

        //try and add a history title if it can be found
        if (this.history_title == "") new_history_title = BaseEntity.GetHistoryTitleForId(this.entity_type, this.id);

        this._history_title = new_history_title;
    }

    /** The iso text for the label
     * @type {string} */
    get iso() {
        return this._iso;
    }
    set iso(new_iso) {
        if (typeof new_iso != "string") {
            throw new Error(`Invalid iso: ${new_iso}`);
        }

        this._iso = new_iso;
    }

    /* Class constant getters (read-only) */

    // #region Types

    /**
     * Node entity type
     * @type {string} */
    static get NODE() {
        return "node";
    }
    /**
     * Beam entity type
     * @type {string} */
    static get BEAM_BASIC() {
        return "beam basic";
    }
    /**
     * X-Section entity type
     * @type {string} */
    static get XSECTION() {
        return "section";
    }
    /**
     * Translational spring entity type
     * @type {string} */
    static get SPRING_TRANSLATIONAL() {
        return "spring tr";
    }
    /**
     * Rotational spring entity type
     * @type {string} */
    static get SPRING_ROTATIONAL() {
        return "spring rot";
    }
    /**
     * Joint entity type
     * @type {string} */
    static get JOINT() {
        return "joint tr";
    }
    /**
     * Part entity type
     * @type {string} */
    static get PART() {
        return "part";
    }

    // #endregion

    /* Class methods */

    /**
     * this function returns the history title of the entity from the current model (gui.model) if the title exists
     * @param {string} entity_type
     * @param {string|number} id integer id of entity
     * @returns {string} if blank then it means no history title set or id was not int
     */
    static GetHistoryTitleForId(entity_type, id) {
        let id_number = parseInt(id.toString());

        Window.Warning(
            "GetHistoryTitleForId hardwired model",
            "Function GetHistoryTitleForId is still using hardwired Model.First."
        );

        if (!isNaN(id_number)) {
            let history = History.First(Model.First());
            let xsec = CrossSection.First(Model.First());

            /* It's text, so check the Database history items or Databse cross sections */
            switch (entity_type) {
                case BaseEntity.NODE:
                    while (history) {
                        if (history.type == History.NODE && history.id == id_number) return history.heading;
                        history = history.Next();
                    }
                    break;

                case BaseEntity.BEAM_BASIC:
                    while (history) {
                        if (history.type == History.BEAM && history.id == id_number) return history.heading;
                        history = history.Next();
                    }
                    break;

                case BaseEntity.SPRING_TRANSLATIONAL:
                case BaseEntity.SPRING_ROTATIONAL:
                    while (history) {
                        if (history.type == History.DISCRETE && history.id == id_number) return history.heading;
                        history = history.Next();
                    }
                    break;

                case BaseEntity.XSECTION:
                    while (xsec) {
                        if (xsec.label == id_number) return history.heading;
                        xsec = xsec.Next();
                    }

                    break;
            }
        }
        return ""; //return blank string
    }

    /**
     * Returns an array of all the available entity tag strings
     * This needs to be overridden by child classes
     * @returns {string[]}
     */
    static EntityTags() {
        throw new Error("BaseEntity.EntityTags() method must be overridden by child class");
    }

    /**
     * Return an array of all the available entity type strings
     * @returns {string[]}
     * @example
     * let entity_types = BaseEntity.EntityTypes();
     */
    static EntityTypes() {
        return [
            BaseEntity.NODE,
            BaseEntity.BEAM_BASIC,
            BaseEntity.XSECTION,
            BaseEntity.JOINT,
            BaseEntity.SPRING_TRANSLATIONAL,
            BaseEntity.SPRING_ROTATIONAL,
            BaseEntity.PART
        ];
    }

    /**
     * Interactively pick an entity by type
     * @param {string} entity_type Entity type constant
     * @param {Model} model Restrict picking to this model
     * @returns {string|number}
     * @example
     * let entity_id = BaseEntity.Pick(BaseEntity.NODE);
     */
    static Pick(entity_type, model) {
        let picked_object = null;

        if (entity_type == BaseEntity.NODE) {
            picked_object = Node.Pick("Pick a node", model);
        } else if (entity_type == BaseEntity.BEAM_BASIC) {
            picked_object = Beam.Pick("Pick a beam", model);
        } else if (entity_type == BaseEntity.XSECTION) {
            picked_object = CrossSection.Pick("Pick a database cross section", model);
        } else if (entity_type == BaseEntity.JOINT) {
            picked_object = Joint.Pick("Pick a joint", model);
        } else if (entity_type == BaseEntity.SPRING_TRANSLATIONAL) {
            picked_object = Discrete.Pick("Pick a translational spring", model);
        } else if (entity_type == BaseEntity.SPRING_ROTATIONAL) {
            picked_object = Discrete.Pick("Pick a rotational spring", model);
        } else if (entity_type == BaseEntity.PART) {
            picked_object = Part.Pick("Pick a part", model);
        }

        if (picked_object == null) return null;

        return picked_object.label;
    }

    /**
     * Interactively select an entity by type
     * @param {string} entity_type Entity type constant
     * @param {Model} model Restrict selection to this model
     * @param {boolean} [allow_multiple=false] Allow multiple selection
     * @returns {?number|number[]}
     * @example
     * let entity_id = BaseEntity.Select(BaseEntity.NODE);
     */
    static Select(entity_type, model, allow_multiple = false) {
        let flag = AllocateFlag();

        let selected = null;

        if (entity_type == BaseEntity.NODE) {
            selected = Node.Select(flag, "Select a node", model);
        } else if (entity_type == BaseEntity.BEAM_BASIC) {
            selected = Beam.Select(flag, "Select a beam", model);
        } else if (entity_type == BaseEntity.XSECTION) {
            selected = CrossSection.Select(flag, "Select a database cross section", model);
        } else if (entity_type == BaseEntity.JOINT) {
            selected = Joint.Select(flag, "Select a joint", model);
        } else if (entity_type == BaseEntity.SPRING_TRANSLATIONAL) {
            selected = Discrete.Select(flag, "Select a translational spring", model);
        } else if (entity_type == BaseEntity.SPRING_ROTATIONAL) {
            selected = Discrete.Select(flag, "Select a rotational spring", model);
        } else if (entity_type == BaseEntity.PART) {
            selected = Part.Select(flag, "Select a part", model);
        }

        if (selected == null) {
            ReturnFlag(flag);
            return null;
        }

        if (!allow_multiple && selected > 1) {
            Window.Warning("Multiple entities selected", "Please select only one entity");
            ReturnFlag(flag);
            return null;
        }

        let selected_objects = [];

        if (entity_type == BaseEntity.NODE) {
            selected_objects = Node.GetFlagged(model, flag);
        } else if (entity_type == BaseEntity.BEAM_BASIC) {
            selected_objects = Beam.GetFlagged(model, flag);
        } else if (entity_type == BaseEntity.XSECTION) {
            selected_objects = CrossSection.GetFlagged(model, flag);
        } else if (entity_type == BaseEntity.JOINT) {
            selected_objects = Joint.GetFlagged(model, flag);
        } else if (entity_type == BaseEntity.SPRING_TRANSLATIONAL) {
            selected_objects = Discrete.GetFlagged(model, flag);
        } else if (entity_type == BaseEntity.SPRING_ROTATIONAL) {
            selected_objects = Discrete.GetFlagged(model, flag);
        } else if (entity_type == BaseEntity.PART) {
            selected_objects = Part.GetFlagged(model, flag);
        }

        ReturnFlag(flag);

        /* Return a scalar value if only one item can be selected,
         * otherwise return an array of labels for the selected items. */

        if (!allow_multiple) {
            return selected_objects[0].label;
        } else {
            let ids = [];

            for (let object of selected_objects) {
                ids.push(object.label);
            }

            return ids;
        }
    }

    /* Instance methods */

    /**
     * JSON representation
     * @returns {object}
     * @example
     * let json = entity.toJSON();
     */
    toJSON() {
        return {
            entity_type: this.entity_type,
            id: this.id,
            name: this.name,
            tag: this.tag,
            measurements: this.measurements
        };
    }
}

/** Class to represent a component that data should be extracted from<br><br>
 * This is intended to be a base class for other classes to extend, so shouldn't
 * be used directly.<br><br>
 * To extend it the static Types() method needs to be implemented in the child class
 * as well as the type constants.
 */
class BaseComponent {
    /**
     * @param {string} component_type Component type constant
     * @param {BaseEntity[]} entities Array of BaseEntity instances
     */
    constructor(component_type, entities) {
        this.component_type = component_type;
        this.entities = entities;
    }

    /**
     * Component constant
     * @type {string} */
    get component_type() {
        return this._component_type;
    }
    set component_type(new_component_type) {
        /* Need to call the child class static Types() method.
         * I don't know if this is the right way to do it, but it
         * seems to work although VSCode highlights it as an error,
         * hence the @ts-ignore comment.
         */
        // @ts-ignore
        if (this.constructor.Types().indexOf(new_component_type) == -1) {
            throw new Error(`Invalid component_type: ${new_component_type}`);
        }

        this._component_type = new_component_type;
    }

    /**
     * Array of BaseEntity instances
     * @type {BaseEntity[]} */
    get entities() {
        return this._entities;
    }
    set entities(new_entities) {
        if (!(new_entities instanceof Array)) {
            throw new Error("entities must be an array");
        }

        for (let new_entity of new_entities) {
            if (!(new_entity instanceof BaseEntity)) {
                throw new Error("entities must be an array of OccupantEntity instances");
            }
        }

        this._entities = new_entities;
    }

    /**
     * Returns an array of all the available component type strings
     * This needs to be overridden by child classes
     * @returns {string[]}
     */
    static Types() {
        throw new Error("BaseComponent.Types() method must be overridden by child class");
    }

    /**
     * JSON representation
     * @returns {object}
     * @example
     * let json = component.toJSON();
     */
    toJSON() {
        return {
            component_type: this.component_type,
            entities: this.entities
        };
    }

    /**
     * Get an BaseEntity by tag
     * @param {string} tag Entity tag
     * @returns {?BaseEntity}
     * @example
     * let entity = component.GetEntityByTag(OccupantEntity.HEAD_NODE);
     */
    GetEntityByTag(tag) {
        for (let entity of this.entities) {
            if (entity.tag == tag) return entity;
        }

        return null;
    }
}

/** Class to represent a label and textbox widget for selecting Entity ids<br><br>
 * This is intended to be a base class for other classes to extend, so shouldn't
 * be used directly.<br><br>
 */
class BaseEntityWidgets {
    /**
     * Create a new BaseEntityWidgets
     * @param {string} entity_type Entity type constant
     * @param {Widget} label The entity label widget
     * @param {Widget} textbox The entity textbox widget
     * @param {string} tag The entity tag
     * @param {string[]} valid_tags Valid entity tags
     * @param {string} [hover=""] The entity label hover text
     * @example
     * let entity_widgets = new BaseEntityWidgets(label, textbox, "MY_TAG", ["MY_TAG", "MY_OTHER_TAG"], hover_text);
     */
    constructor(entity_type, label, textbox, tag, valid_tags, hover = "") {
        /* Needs to be defined first as it's used to check the tag property is valid  */
        this.valid_tags = valid_tags;

        this.entity_type = entity_type;
        this.label = label;
        this.textbox = textbox;
        this.tag = tag;
        this.hover = hover;
    }

    /* Instance property getter and setters */

    /** Entity type constant
     * @type {string} */
    get entity_type() {
        return this._entity_type;
    }
    set entity_type(new_entity_type) {
        if (BaseEntity.EntityTypes().indexOf(new_entity_type) == -1) {
            throw new Error(`Invalid entity_type: ${new_entity_type}`);
        }

        this._entity_type = new_entity_type;
    }

    /** The entity label widget
     * @type {Widget} */
    get label() {
        return this._label;
    }
    set label(new_label) {
        if (!(new_label instanceof Widget)) {
            throw new Error("label must be a Widget");
        }

        this._label = new_label;
    }

    /** The entity textbox widget
     * @type {Widget} */
    get textbox() {
        return this._textbox;
    }
    set textbox(new_textbox) {
        if (!(new_textbox instanceof Widget)) {
            throw new Error("textbox must be a Widget");
        }

        this._textbox = new_textbox;
    }

    /** The entity tag
     * @type {string} */
    get tag() {
        return this._tag;
    }
    set tag(new_tag) {
        if (this.valid_tags.indexOf(new_tag) == -1) {
            throw new Error(`Invalid tag: ${new_tag}`);
        }

        this._tag = new_tag;
    }

    /** The hover text for the label
     * @type {string} */
    get hover() {
        return this.label.hover;
    }
    set hover(new_hover) {
        if (typeof new_hover != "string") {
            throw new Error(`Invalid hover_text: ${new_hover}`);
        }

        this.label.hover = new_hover;
    }

    /** The valid tags
     * @type {string[]} */
    get valid_tags() {
        return this._valid_tags;
    }
    set valid_tags(new_valid_tags) {
        if (!(new_valid_tags instanceof Array)) {
            throw new Error("valid_tags must be an array");
        }

        this._valid_tags = new_valid_tags;
    }

    /* Instance methods */

    /**
     * Hide the label and textbox widgets
     * @example
     * entity_widgets.Hide();
     */
    Hide() {
        this.label.Hide();
        this.textbox.Hide();
    }

    /**
     * Show the label and textbox widgets
     * @example
     * entity_widgets.Show();
     */
    Show() {
        this.label.Show();
        this.textbox.Show();
    }
}

/** Class to represent all the label and textbox widgets for selecting Entity ids for a component<br><br>
 * This is intended to be a base class for other classes to extend, so shouldn't
 * be used directly.<br><br>
 */
class BaseComponentWidgets {
    /**
     * Create a new BaseComponentWidgets
     * @param {Widget} label The component label widget
     * @param {BaseEntityWidgets[]} entity_widgets Array of entity widgets
     * @example
     * let component_widgets = new BaseComponentWidgets(label, entity_widgets);
     */
    constructor(label, entity_widgets) {
        this.label = label;
        this.entity_widgets = entity_widgets;
    }

    /* Instance property getter and setters */

    /** The component label widget
     * @type {Widget} */
    get label() {
        return this._label;
    }
    set label(new_label) {
        if (!(new_label instanceof Widget)) {
            throw new Error("label must be a Widget");
        }

        this._label = new_label;
    }

    /** Array of entity widgets
     * @type {BaseEntityWidgets[]} */
    get entity_widgets() {
        return this._entity_widgets;
    }
    set entity_widgets(new_entity_widgets) {
        if (!(new_entity_widgets instanceof Array)) {
            throw new Error("entity_widgets must be an Array");
        }

        for (let new_entity_widget of new_entity_widgets) {
            if (!(new_entity_widget instanceof BaseEntityWidgets)) {
                throw new Error("entities must be an array of BaseEntityWidgets instances");
            }
        }

        this._entity_widgets = new_entity_widgets;
    }

    /* Instance methods */

    /**
     * Hide all the entity widgets for this component
     * @example
     * component_widgets.Hide();
     */
    Hide() {
        this.label.Hide();

        for (let entity_widgets of this.entity_widgets) {
            entity_widgets.Hide();
        }
    }

    /**
     * Show all the entity widgets for this component
     * @example
     * component_widgets.Show();
     */
    Show() {
        this.label.Show();

        for (let entity_widgets of this.entity_widgets) {
            entity_widgets.Show();
        }
    }
}