modules/shared/workflow_occupant.mjs

// module: TRUE

/**
 * The workflow_occupant module provides the WorkflowOccupant class for storing the
 * properties of a user defined occupant with the location (position, side and row) ids of entities
 * in each body part of an occupant that can be used to extract
 * data from LS-DYNA results.<br><br>
 */

import {
    Occupant,
    OccupantBodyPart,
    OccupantEntity,
    OccupantPhysiology,
    OccupantProduct,
    OccupantSupplier,
    OccupantChestRotationFactors
} from "./occupant.mjs";
import { OccupantVersion } from "./occupant_version.mjs";

import { THisHelper } from "../post/this.mjs";
import { ComponentMeasurementCurves, Measurement } from "./measurement.mjs";

export { WorkflowOccupant };

/** Class representing a occupant */
class WorkflowOccupant {
    /**
     *
     * @param {string|Occupant} name_or_occupant Occupant or name constant
     * @param {string} position Occupant position constant (WorkflowOccupant.DRIVER|WorkflowOccupant.PASSENGER)
     * @param {string} side Occupant side constant
     * @param {string} front_rear Front/Rear constant
     * @param {OccupantBodyPart[]} body_parts Array of OccupantBodyPart instances
     * @example
     * let occupant = new WorkflowOccupant(
     *                              WorkflowOccupant.HUMANETICS_HIII_50M_V1_5,
     *                              WorkflowOccupant.DRIVER,
     *                              WorkflowOccupant.LEFT,
     *                              WorkflowOccupant.FRONT,
     *                              [body_part1, body_part2]);
     */
    constructor(name_or_occupant, position, side, front_rear, body_parts) {
        this.SetOccupantFields = name_or_occupant;
        this.position = position;
        this.side = side;
        this.front_rear = front_rear;
        this.body_parts = body_parts;

        this.upper_rib_irtracc_length = 0;
        this.mid_rib_irtracc_length = 0;
        this.bottom_rib_irtracc_length = 0;

        this.upper_abdomen_irtracc_length = 0;
        this.bottom_abdomen_irtracc_length = 0;
    }

    // #region Instance property getter and setters

    /** Pritate getters (these properties can only be written through SetOccupantFields) */

    /**
     * Occupant name constant
     * @type {string} */
    get name() {
        return this._name;
    }

    /**
     * Occupant supplier constant
     * @type {string} */
    get supplier() {
        return this._supplier;
    }

    /**
     * Occupant product constant
     * @type {string} */
    get product() {
        return this._product;
    }

    /**
     * Occupant physiology constant
     * @type {string} */
    get physiology() {
        return this._physiology;
    }

    /**
     * Occupant version constant
     * @type {string} */
    get version() {
        return this._version;
    }

    /**
     * Set the occupant supplier, product, physiology and version fields
     * this is because the supplier, product, physiology and version are all a set of variables
     * that should not be changed independently.
     * This is why there are no specific setters for each of them individually
     *
     * NOTE: This function takes an occupant name string or Occupant class
     *
     * @param {string|Occupant} name_or_occupant Occupant or name string
     */
    set SetOccupantFields(name_or_occupant) {
        let occupant;

        if (typeof name_or_occupant == "string") {
            occupant = OccupantVersion.GetFromName(name_or_occupant);
            this._name = name_or_occupant;
        } else if (name_or_occupant instanceof Occupant) {
            occupant = name_or_occupant;
            this._name = occupant.name; //TODO add name field to occupant
        } else {
            throw new Error(`Invalid name_or_occupant passed to the WorkflowOccupant constructor: ${name_or_occupant}`);
        }
        this._supplier = occupant.supplier;
        this._product = occupant.product;
        this._physiology = occupant.physiology;
        this._version = occupant.version;
    }

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

        this._position = new_position;
    }

    /**
     * Occupant side constant
     * @type {string} */
    get side() {
        return this._side;
    }
    set side(new_side) {
        if (WorkflowOccupant.Sides().indexOf(new_side) == -1) {
            throw new Error(`Invalid side: ${new_side}`);
        }

        this._side = new_side;
    }

    /**
     * Occupant front/Rear constant
     * @type {string} */
    get front_rear() {
        return this._front_rear;
    }
    set front_rear(new_front_rear) {
        if (WorkflowOccupant.FrontRear().indexOf(new_front_rear) == -1) {
            throw new Error(`Invalid front_rear: ${new_front_rear}`);
        }

        this._front_rear = new_front_rear;
    }

    /** Array of OccupantBodyPart instances
     * @type {OccupantBodyPart[]} */
    get body_parts() {
        return this._body_parts;
    }
    set body_parts(new_body_parts) {
        if (!(new_body_parts instanceof Array)) {
            throw new Error("body_parts must be an array");
        }

        for (let new_body_part of new_body_parts) {
            if (!(new_body_part instanceof OccupantBodyPart)) {
                throw new Error("body_parts must be an array of OccupantBodyPart instances");
            }
        }

        this._body_parts = new_body_parts;

        /* Set the parent occupant for each body part */

        for (let body_part of this.body_parts) {
            body_part.occupant = this;
        }
    }

    /**
     * Upper rib irtracc length
     * @type {number}
     */
    get upper_rib_irtracc_length() {
        return this._upper_rib_irtracc_length;
    }

    set upper_rib_irtracc_length(new_upper_rib_irtracc_length) {
        if (typeof new_upper_rib_irtracc_length != "number") {
            throw new Error("upper_rib_irtracc_length must be a number");
        }

        this._upper_rib_irtracc_length = new_upper_rib_irtracc_length;
    }

    /**
     * Middle rib irtracc length
     * @type {number}
     */
    get mid_rib_irtracc_length() {
        return this._mid_rib_irtracc_length;
    }

    set mid_rib_irtracc_length(new_mid_rib_irtracc_length) {
        if (typeof new_mid_rib_irtracc_length != "number") {
            throw new Error("mid_rib_irtracc_length must be a number");
        }

        this._mid_rib_irtracc_length = new_mid_rib_irtracc_length;
    }

    /**
     * Bottom rib irtracc length
     * @type {number}
     */
    get bottom_rib_irtracc_length() {
        return this._bottom_rib_irtracc_length;
    }

    set bottom_rib_irtracc_length(new_bottom_rib_irtracc_length) {
        if (typeof new_bottom_rib_irtracc_length != "number") {
            throw new Error("bottom_rib_irtracc_length must be a number");
        }

        this._bottom_rib_irtracc_length = new_bottom_rib_irtracc_length;
    }

    /**
     * Upper abdomen irtracc length
     * @type {number}
     */
    get upper_abdomen_irtracc_length() {
        return this._upper_abdomen_irtracc_length;
    }

    set upper_abdomen_irtracc_length(new_upper_abdomen_irtracc_length) {
        if (typeof new_upper_abdomen_irtracc_length != "number") {
            throw new Error("upper_abdomen_irtracc_length must be a number");
        }

        this._upper_abdomen_irtracc_length = new_upper_abdomen_irtracc_length;
    }

    /**
     * Bottom abdomen irtracc length
     * @type {number}
     */
    get bottom_abdomen_irtracc_length() {
        return this._bottom_abdomen_irtracc_length;
    }

    set bottom_abdomen_irtracc_length(new_bottom_abdomen_irtracc_length) {
        if (typeof new_bottom_abdomen_irtracc_length != "number") {
            throw new Error("bottom_abdomen_irtracc_length must be a number");
        }

        this._bottom_abdomen_irtracc_length = new_bottom_abdomen_irtracc_length;
    }

    // #endregion

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

    /* #region occupant name getters
     *
     * IMPORTANT:
     * ---------
     * The values need to match the names of the .csv files with the default entity ids
     * in reporter_dir/library/templates/dummy_info as they are used to find the file
     * and read the default entity ids from it.
     */

    // #region position getters

    /** Driver
     * @type {string} */
    static get DRIVER() {
        return "Driver";
    }
    /** Passenger
     * @type {string} */
    static get PASSENGER() {
        return "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";
    }

    // #endregion

    /* Class methods */

    /**
     * Return an array of all the available occupant name strings
     * @returns {string[]}
     * @example
     * let version_names = WorkflowOccupant.Versions();
     */
    static Versions() {
        return OccupantVersion.GetAllNames();
    }

    /**
     * Return an array of all the available supplier strings
     * @returns {string[]}
     * @example
     * let suppliers = WorkflowOccupant.Suppliers();
     */
    static Suppliers() {
        return OccupantSupplier.GetAll();
    }

    /**
     * Return an array of all the available Product type strings
     * @returns {string[]}
     * @example
     * let Products = WorkflowOccupant.Products();
     */
    static Products() {
        return OccupantProduct.GetAll();
    }

    /**
     * Return an array of all the available physiology strings
     * @returns {string[]}
     * @example
     * let physiologies = WorkflowOccupant.Physiologies();
     */
    static Physiologies() {
        return OccupantPhysiology.GetAll();
    }

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

    /**
     * Return an array of all the available side strings
     * @returns {string[]}
     * @example
     * let sides = WorkflowOccupant.Sides();
     */
    static Sides() {
        return [WorkflowOccupant.LEFT, WorkflowOccupant.MIDDLE, WorkflowOccupant.RIGHT];
    }

    /**
     * Return an array of all the available front/rear strings
     * @returns {string[]}
     * @example
     * let front_rear = WorkflowOccupant.FrontRear();
     */
    static FrontRear() {
        return [WorkflowOccupant.FRONT, WorkflowOccupant.REAR];
    }

    /**
     * Creates a Workflow occupant from user data.
     * @param {Object} user_data_occupant A single occupant object from user data
     * @returns {WorkflowOccupant}
     * @example
     * for (let o of user_data.occupants) {
     *     let occupant = WorkflowOccupant.CreateFromUserData(o);
     * }
     */
    static CreateFromUserData(user_data_occupant) {
        let o = user_data_occupant; /* Shorten variable name for convenience */

        /** @type {OccupantBodyPart[]} */
        let body_parts = [];

        for (let b of o.body_parts) {
            /** @type {OccupantEntity[]} */
            let entities = [];

            for (let e of b.entities) {
                if (e.measurements) {
                    let measurements = [];

                    for (let m of e.measurements) {
                        measurements.push(new Measurement(m.name, m.component));
                    }

                    entities.push(new OccupantEntity(e.entity_type, e.id, e.name, e.tag, measurements));
                }
            }

            body_parts.push(new OccupantBodyPart(b.component_type, entities));
        }

        /* Create a WorkflowOccupant instance with the required data */

        if (!o.name) {
            if (o.supplier && o.product && o.physiology && o.version) {
                if (!OccupantSupplier.Valid(o.supplier)) {
                    throw new Error(`Occupant supplier ${o.supplier} is not supported`);
                }

                if (!OccupantProduct.Valid(o.product)) {
                    throw new Error(`Occupant product ${o.product} is not supported`);
                }

                if (!OccupantPhysiology.Valid(o.physiology)) {
                    throw new Error(`Occupant physiology ${o.physiology} is not supported`);
                }

                if (typeof o.supplier != "string") {
                    throw new Error(`Occupant version ${o.version} is not a string`);
                }

                o.name = `${o.supplier} ${o.product} ${o.physiology} ${o.version}`;
            } else {
                throw new Error(`Not all occupant inputs provided`);
            }
        }

        let occupant = new WorkflowOccupant(o.name, o.position, o.side, o.front_rear, body_parts);

        if (o.upper_rib_irtracc_length) {
            occupant.upper_rib_irtracc_length = o.upper_rib_irtracc_length;
        } else {
            occupant.upper_rib_irtracc_length = 0;
        }

        if (o.mid_rib_irtracc_length) {
            occupant.mid_rib_irtracc_length = o.mid_rib_irtracc_length;
        } else {
            occupant.mid_rib_irtracc_length = 0;
        }

        if (o.bottom_rib_irtracc_length) {
            occupant.bottom_rib_irtracc_length = o.bottom_rib_irtracc_length;
        } else {
            occupant.bottom_rib_irtracc_length = 0;
        }

        if (o.upper_abdomen_irtracc_length) {
            occupant.upper_abdomen_irtracc_length = o.upper_abdomen_irtracc_length;
        } else {
            occupant.upper_abdomen_irtracc_length = 0;
        }

        if (o.bottom_abdomen_irtracc_length) {
            occupant.bottom_abdomen_irtracc_length = o.bottom_abdomen_irtracc_length;
        } else {
            occupant.bottom_abdomen_irtracc_length = 0;
        }

        return occupant;
    }

    //TODO Merge with CreateFromUserData() or create occupant in class ablove then call CreateWorkflowOccupantFromOccupant?
    /**
     * Returns a WorkflowOccupant with the required OccupantBodyParts
     * and OccupantEntitys. The entity IDs are set to 0.
     * @param {Occupant|string} name_or_occupant Occupant from JSON
     * @param {string} position Occupant position
     * @param {string} side Occupant side
     * @param {string} front_rear Occupant front/rear
     * @returns {WorkflowOccupant}
     * @example
     * let occupant = WorkflowOccupant.CreateWorkflowOccupantFromOccupant(
     *                             WorkflowOccupant.HUMANETICS_HIII_50M_V1_5,
     *                             WorkflowOccupant.DRIVER,
     *                             WorkflowOccupant.LEFT,
     *                             WorkflowOccupant.FRONT);
     */
    static CreateWorkflowOccupantFromOccupant(name_or_occupant, position, side, front_rear) {
        /* Create OccupantEntitys and OccupantBodyParts for each occupant name */

        // TODO Be careful that we do not overwrite original occupant

        let occupant;

        if (typeof name_or_occupant == "string") {
            occupant = OccupantVersion.GetFromName(name_or_occupant);
        } else if (name_or_occupant instanceof Occupant) {
            occupant = name_or_occupant;
        } else {
            throw new Error(`Invalid name_or_occupant passed to the WorkflowOccupant constructor: ${name_or_occupant}`);
        }

        // if (!occupant.body_parts || occupant.body_parts.length < 1) {
        //     return WorkflowOccupant.CreateWorkflowOccupant(occupant.name, position, side, front_rear);
        // }

        /** Body parts
         * @type {OccupantBodyPart[]} */
        let body_parts = [];

        //TODO move this stuff to occupant body_part setter
        for (let body_part of occupant.body_parts) {
            let part = body_part.component_type.toLowerCase(); //todo check validity
            let part_entities = [];
            for (let entity of body_part.entities) {
                part_entities.push(OccupantEntity.FromTagAndType(entity.tag, entity.entity_type, entity.id));
                //Message(`${occupant.name} Add entity, ${entity.entity_type}, ${entity.tag} id: ${entity.id}`);
            }
            body_parts.push(new OccupantBodyPart(part, part_entities));
        }

        if (body_parts.length == 0) WarningMessage(`Occupant ${occupant.name} has no body parts defined.`);

        return new WorkflowOccupant(occupant.name, position, side, front_rear, body_parts);
    }

    /* Instance methods */

    /**
     * String representation
     * @returns {string}
     * @example
     * let s = occupant.toString();
     */
    toString() {
        return `${this.position}-${this.front_rear}-${this.side}`;
    }

    /**
     * JSON representation
     * @returns {object}
     * @example
     * let json = occupant.toJSON();
     */
    toJSON() {
        return {
            name: this.name,
            supplier: this.supplier,
            product: this.product,
            physiology: this.physiology,
            position: this.position,
            side: this.side,
            front_rear: this.front_rear,
            upper_rib_irtracc_length: this.upper_rib_irtracc_length,
            mid_rib_irtracc_length: this.mid_rib_irtracc_length,
            bottom_rib_irtracc_length: this.bottom_rib_irtracc_length,
            upper_abdomen_irtracc_length: this.upper_abdomen_irtracc_length,
            bottom_abdomen_irtracc_length: this.bottom_abdomen_irtracc_length,
            body_parts: this.body_parts
        };
    }

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

        return null;
    }

    /**
     * Get an OccupantEntity type by tag
     * @param {string} tag Entity tag
     * @returns {?string}
     * @example
     * let entity_type = occupant.GetEntityTypeFromTag(OccupantEntity.NECK_LOADCELL);
     */
    GetEntityTypeFromTag(tag) {
        for (let part of this.body_parts) {
            for (let entity of part.entities) {
                if (entity.tag == tag) return entity.entity_type;
            }
        }

        return null;
    }

    /**
     * Get an OccupantBodyPart by body part type
     * @param {string} body_part_type Body part type
     * @returns {?OccupantBodyPart}
     * @example
     * let body_part = occupant.GetBodyPartByType(OccupantBodyPart.HEAD);
     */
    GetBodyPartByType(body_part_type) {
        for (let body_part of this.body_parts) {
            if (body_part.component_type === body_part_type) {
                return body_part;
            }
        }

        return null;
    }

    /**
     * @typedef {Object} TibiaIndexCriticalLoads
     * @property {number} compression Critical compression (kN)
     * @property {number} bending Critical bending (Nm)
     */

    /**
     * Returns the NIJ critical loads for the occupant
     * @returns {TibiaIndexCriticalLoads}
     */
    GetTibiaIndexCriticalLoads() {
        /* Default to values for female occupants */

        /** @type {TibiaIndexCriticalLoads} */
        let critical_loads = {
            compression: 22.9,
            bending: 115.0
        };

        //must be a hybrid 3 or THOR occupant
        let test = this.physiology;
        if (this.product != OccupantProduct.HIII && this.product != OccupantProduct.THOR) test = null;

        switch (test) {
            case OccupantPhysiology.M50:
                critical_loads.compression = 35.9;
                critical_loads.bending = 225.0;

                break;

            case OccupantPhysiology.F5:
                critical_loads.compression = 22.9;
                critical_loads.bending = 115.0;

                break;

            case "95th percentile dummy placeholder - not used anywhere yet":
            case OccupantPhysiology.M95:
                critical_loads.compression = 44.2;
                critical_loads.bending = 307.0;

                break;

            default:
                ErrorMessage(
                    `Unknown occupant ${this.name} in <WorkflowOccupant>.GetTibiaIndexCriticalLoads(). Returning values for a female occupant.`
                );
        }

        return critical_loads;
    }

    /**
     * @typedef {Object} NIJCriticalLoads
     * @property {number} tension Critical tension (kN)
     * @property {number} compression Critical compression (kN)
     * @property {number} flexion Critical flexion (Nm)
     * @property {number} extension Critical extension (Nm)
     */

    /**
     * Returns the NIJ critical loads for the occupant
     * @returns {NIJCriticalLoads}
     */
    GetNIJCriticalLoads() {
        /* Default to values for female occupants */

        /** @type {NIJCriticalLoads} */
        let critical_loads = {
            tension: 4.287,
            compression: 3.88,
            flexion: 155,
            extension: 67
        };

        //must be a hybrid 3 occupant
        let test = this.physiology;
        if (this.product != OccupantProduct.HIII) test = null;

        switch (test) {
            case OccupantPhysiology.M50:
                critical_loads.tension = 6.806;
                critical_loads.compression = 6.16;
                critical_loads.flexion = 310;
                critical_loads.extension = 135;

                break;

            case OccupantPhysiology.F5:
                critical_loads.tension = 4.287;
                critical_loads.compression = 3.88;
                critical_loads.flexion = 155;
                critical_loads.extension = 67;

                break;

            case OccupantPhysiology.M95:
                critical_loads.tension = 0;
                critical_loads.compression = 0;
                critical_loads.flexion = 0;
                critical_loads.extension = 0;
                //TODO update these placeholder values
                WarningMessage("95th percentile critical Nij loads are need to be updated!");

                break;

            default:
                ErrorMessage(
                    `Unknown occupant ${this.name} in <WorkflowOccupant>.GetNIJCriticalLoads(). Returning values for a female occupant.`
                );
        }

        return critical_loads;
    }

    /**
     * Returns the factors to convert the chest rotation values in radian to a deflection in mm
     * @returns {?OccupantChestRotationFactors}
     */
    GetChestRotationFactors() {
        //get the occupant template which may have chest rotation factors stored
        let occupant_template = OccupantVersion.GetFromName(this.name);

        //if the json object has chest rotation factors use them
        if (occupant_template.chest_rotation_factors) {
            Message(`Using chest rotation factors from ${this.name}.json file`);
            return occupant_template.chest_rotation_factors;
        }

        Message(
            `Could not find chest rotation factors in ${this.name}.json file so using supplier and physiology to determine appropriate values.`
        );

        let test = `${this.supplier}${this.physiology}`;

        //TODO add this to occupant definition?

        //must be a hybrid 3 occupant
        if (this.product != OccupantProduct.HIII) test = null;

        Message(`test ${test}`);

        switch (test) {
            case `${OccupantSupplier.ATD}${OccupantPhysiology.M50}`:
                return new OccupantChestRotationFactors(OccupantChestRotationFactors.LINEAR, [-147.0]);

            case `${OccupantSupplier.LSTC}${OccupantPhysiology.M50}`:
                return new OccupantChestRotationFactors(OccupantChestRotationFactors.LINEAR, [-146.0]);

            case `${OccupantSupplier.LSTC}${OccupantPhysiology.F5}`:
                return new OccupantChestRotationFactors(OccupantChestRotationFactors.LINEAR, [-104.0]);

            case `${OccupantSupplier.HUMANETICS}${OccupantPhysiology.M50}`:
                return new OccupantChestRotationFactors(
                    OccupantChestRotationFactors.THIRD_ORDER,
                    [25.13, -35.77, -136.26]
                );

            case `${OccupantSupplier.HUMANETICS}${OccupantPhysiology.F5}`:
                return new OccupantChestRotationFactors(
                    OccupantChestRotationFactors.THIRD_ORDER,
                    [-15.61, 33.84, 81.53]
                );

            default:
                ErrorMessage(
                    `Unknown occupant ${this.name} in <WorkflowOccupant>.GetChestRotationFactors(). Returning null.`
                );
                return null;
        }
    }

    /**
     * Reads the raw body part measurements into T/HIS and returns
     * them in a ComponentMeasurementCurves instance
     * @param {Model} model model to read data from
     * @param {string} body_part_type Body part type to read data for
     * @returns {ComponentMeasurementCurves}
     */
    ReadRawBodyPartMeasurements(model, body_part_type) {
        let entity_tags = OccupantEntity.EntityTags(body_part_type);

        let entities = [];

        for (let tag of entity_tags) {
            entities.push(this.GetEntityByTag(tag));
        }

        let measurement_curves = new ComponentMeasurementCurves();

        for (let entity of entities) {
            if (entity != null) {
                for (let measurement of entity.measurements) {
                    let curve = THisHelper.ReadData(model, entity.entity_type, entity.id, measurement.component, true);

                    if (curve) {
                        measurement_curves.AddCurve(measurement.name, curve);
                    }
                }
            }
        }

        return measurement_curves;
    }
}