pre/pre_automotive_assessment.js

// module: TRUE
// @ts-ignore
import { gui } from "./pre_automotive_assessment_gui.jsi";
import { BaseEntity, BaseEntityWidgets } from "../modules/shared/base.mjs";
import {
    BodyPartWidgets,
    Occupant,
    OccupantEntity,
    OccupantEntityWidgets,
    OccupantWidgets
} from "../modules/shared/occupant.mjs";
import { JSPath } from "../modules/shared/path.mjs";

import { Structure, StructureEntityWidgets, StructureWidgets } from "../modules/shared/structure.mjs";

import { WorkflowUnitsCombobox } from "../../modules/units.mjs";
import { OccupantVersion } from "../modules/shared/occupant_version.mjs";
import { Protocol, Protocols } from "../modules/shared/protocols.mjs";
import { ProtocolVehicle, VehicleOccupant } from "../modules/shared/vehicle.mjs";
import { WorkflowOccupant } from "../modules/shared/workflow_occupant.mjs";

export {
    get_user_data,
    gui,
    add_new_occupant,
    filter_version_drop_down,
    update_occupant_names_combobox,
    set_selected_widget_item,
    GetProtocolVehicle
};

// var OccupantVersion = OccupantVersion.GetInstance();

/* Workflow definition filename is passed as an argument to this script from the Workflow menu.
 * It's required when writing the user data to a file or model. */

let workflow_definition_filename = Workflow.WorkflowDefinitionFilename();

/* Models can contain many DATABASE HISTORY cards, so increase the max number of widgets */
Options.max_widgets = 10000;

/* New UI */
Window.Theme(Window.THEME_CURRENT);

/* Set the workflows directory */
JSPath.SetWorkflowsDirectory(JSPath.PRE);

/* Initialise the Protocols class */

Protocols.Initialise();

/* Setup the GUI and show the main window */

if (gui) {
    try {
        /* Ask the user which model to use */

        gui.model = Model.Select("Select the model to use");

        if (gui.model == null) {
            Window.Information("", "You need to select a model before you can use this workflow");
            Exit();
        }

        /* Set up the gui */

        setup_gui();

        /* Update main window */

        update_main_window();

        /* Show the main window */

        gui.wdw_pre_automotive.Show(false);
    } catch (e) {
        ErrorMessage(`Something went wrong: ${e}.\n${e.stack}`);
    }
}

/**
 * this is used to set up the initial display of the vehicle occupants selection area adding onClicks
 * and hiding initially disabled buttons and moving 'Add' buttons to the correct location
 */
function setup_vehicle_occupants() {
    /*add the vehicle*/

    //TODO update image based on THEME (dark or not)

    //default set the vehicle hand drive to left when opened in PRIMER (or use user data to set it if present)
    gui.drive_side = VehicleOccupant.LHD;
    gui.wdw_pre_automotive.radio_hand_drive.ItemAt(0).selected = true; // = VehicleOccupant.LHD;

    /*assign onClick callbacks for all buttons and add front_rear adn side properties*/
    for (let front_rear_side of [
        [WorkflowOccupant.FRONT, WorkflowOccupant.LEFT],
        [WorkflowOccupant.FRONT, WorkflowOccupant.RIGHT],
        [WorkflowOccupant.REAR, WorkflowOccupant.LEFT],
        [WorkflowOccupant.REAR, WorkflowOccupant.RIGHT],
        [WorkflowOccupant.REAR, WorkflowOccupant.MIDDLE]
    ]) {
        /*store front_rear and side value in seperate temporary variables*/
        let front_rear = front_rear_side[0];
        let side = front_rear_side[1];

        /*get hold hod add, edit and delete buttons for the seat position*/
        let add_btn_widget = gui.wdw_pre_automotive[`btn_${front_rear}_${side}_add`];
        let edit_btn_widget = gui.wdw_pre_automotive[`btn_${front_rear}_${side}_edit`];
        let delete_btn_widget = gui.wdw_pre_automotive[`btn_${front_rear}_${side}_delete`];

        add_btn_widget.onClick = add_edit_occupant;
        edit_btn_widget.onClick = add_edit_occupant;
        delete_btn_widget.onClick = delete_occupant;

        /*add front_rear and side properties and onClick to the buttons and hide them to start with*/
        for (let button of [add_btn_widget, edit_btn_widget, delete_btn_widget]) {
            button.front_rear = front_rear;
            button.side = side;
            button.Hide();
        }

        /*Move occupant 'Add' button up to make it lie on top of edit and delete buttons and Show it so it is initially visible
        note they are origionally 6 units below to make it easier to work with them in the gui builder */
        add_btn_widget.top = edit_btn_widget.top;
        add_btn_widget.bottom = edit_btn_widget.bottom;
        add_btn_widget.Show();

        /* update occupant button text*/
        let expected_occupant_btn_widget = gui.wdw_pre_automotive[`lbl_${front_rear}_${side}_expected_occupant`];
        let selected_occupant_btn_widget = gui.wdw_pre_automotive[`lbl_${front_rear}_${side}_selected_occupant`];

        expected_occupant_btn_widget.text = "not required";
        selected_occupant_btn_widget.text = "<empty>";
        selected_occupant_btn_widget.category = Widget.NO_CATEGORY;
        selected_occupant_btn_widget.foreground = Widget.BLACK;
        selected_occupant_btn_widget.background = Widget.COLOUR_NEUTRAL;
    }
}

/**
 * set the gui.drive_side and the radio button to the specified hand drive (if valid)
 * defaults to LHD if not valid and prints a warning message
 * @param {string} drive_side
 */
function set_vehicle_drive_side(drive_side) {
    if ((drive_side = VehicleOccupant.GetValidHandDrive(drive_side))) {
        gui.drive_side = drive_side;
        for (let radio_wi of gui.wdw_pre_automotive.radio_hand_drive.WidgetItems()) {
            if (radio_wi.text == drive_side) {
                radio_wi.selected = true;
                break;
            }
        }
    }
}

/**
 * update the vehicle occupant gui based on the passed vehicle and hand drive
 * @param {ProtocolVehicle} vehicle
 * @param {string} drive_side
 */
function update_vehicle_occupants(vehicle, drive_side) {
    set_vehicle_drive_side(drive_side);

    if (!vehicle) {
        WarningMessage("Vehicle is null");
        return;
    }

    /*
    do it like this as drive_side could be invalid and 
    set_vehicle_drive_side also sets gui.drive_side to be valid
    i.e. defaults to VehicleOccupant.LHD*/
    drive_side = gui.drive_side;

    for (let vehicle_occupant of vehicle.Occupants()) {
        /*store front_rear and side value in seperate temporary variables*/
        let front_rear = vehicle_occupant.front_rear;
        let side = vehicle_occupant.GetSide(drive_side);

        /* get widgets associated with occupant */
        let add_btn_widget = gui.wdw_pre_automotive[`btn_${front_rear}_${side}_add`];
        let edit_btn_widget = gui.wdw_pre_automotive[`btn_${front_rear}_${side}_edit`];
        let delete_btn_widget = gui.wdw_pre_automotive[`btn_${front_rear}_${side}_delete`];
        let selected_occupant_btn_widget = gui.wdw_pre_automotive[`lbl_${front_rear}_${side}_selected_occupant`];
        let expected_occupant_btn_widget = gui.wdw_pre_automotive[`lbl_${front_rear}_${side}_expected_occupant`];

        /* get add button and make it generic colour (or perhaps disabled) */
        add_btn_widget.category = Widget.CATEGORY_GENERIC;

        //reset selected_occupant_btn_widget
        selected_occupant_btn_widget.text = "<empty>";
        selected_occupant_btn_widget.category = Widget.NO_CATEGORY;
        selected_occupant_btn_widget.foreground = Widget.BLACK;
        selected_occupant_btn_widget.background = Widget.COLOUR_NEUTRAL;

        /* update expected occupant button text*/
        let expected_occupant_text = "not required";

        //clear vehicle_occupant property
        // @ts-ignore
        expected_occupant_btn_widget.vehicle_occupant = null;

        if (!vehicle_occupant.Empty()) {
            expected_occupant_text = `${vehicle_occupant.product}-${vehicle_occupant.physiology}`;
            // Ignore squiggles below because Widget objects can have user defined properties added
            // @ts-ignore
            expected_occupant_btn_widget.vehicle_occupant = vehicle_occupant;

            //set the colour of text to latent (meaning the user could/should defined it by pressing Add button)
            selected_occupant_btn_widget.foreground = Widget.COLOUR_LATENT;

            /* set add button category to the Widget.CATEGORY_APPLY because we want to encourage the user to press it*/
            add_btn_widget.category = Widget.CATEGORY_APPLY;
        }

        /* set the expected occupant text */
        expected_occupant_btn_widget.text = expected_occupant_text;

        let wf_occupant = null;
        if ((wf_occupant = get_seat_occupant(side, front_rear))) {
            let temp_occupant = OccupantVersion.GetFromName(wf_occupant.name);
            selected_occupant_btn_widget.text = `${temp_occupant.product}-${temp_occupant.physiology}`;

            if (expected_occupant_text == "not required") {
                /**
                 * colour it neutral if the expected_occupant_text is "not required" as we do not care if an occupant is defined*/
                selected_occupant_btn_widget.category = Widget.NO_CATEGORY;
                // selected_occupant_btn_widget.foreground = Widget.BLACK;
                // selected_occupant_btn_widget.background = Widget.COLOUR_NEUTRAL;
            } else if (selected_occupant_btn_widget.text == expected_occupant_text) {
                /**
                 * colour it green if the texts matche as this means that the occupant defined for this seat
                 * is of the same product and physiology as required by the regulation*/
                selected_occupant_btn_widget.category = Widget.CATEGORY_SAFE_ACTION;
            } else {
                /**
                 * colour it red if the text does not match as this means that the occupant defined for this seat
                 * is of a different product and physiology as required by the regulation*/
                selected_occupant_btn_widget.category = Widget.CATEGORY_WARNING_ACTION;
            }

            add_btn_widget.Hide();
            edit_btn_widget.Show();
            delete_btn_widget.Show();
        } else {
            add_btn_widget.active = expected_occupant_text != "not required";
            add_btn_widget.Show();
            edit_btn_widget.Hide();
            delete_btn_widget.Hide();
        }

        // Message(`wf_occupant = ${wf_occupant} ${front_rear} ${side}`);
    }

    /* change activity of delete button */

    gui.wdw_pre_automotive.btn_delete_all_occupants.active = gui.occupants.length != 0;

    /*redraw the window so that changes appear immediately*/
    gui.wdw_pre_automotive.Redraw();
}

/**
 * returns the workflow occupant for that seat or null if not found
 * @param {string} side
 * @param {string} front_rear
 * @returns {?WorkflowOccupant}
 */
function get_seat_occupant(side, front_rear) {
    for (let wf_occupant of gui.occupants) {
        // Message(`${wf_occupant} ${front_rear} ${side} len gui.occupants = ${gui.occupants.length}`);
        if (
            //wf_occupant.position == vehicle_occupant.position &&
            wf_occupant.side == side &&
            wf_occupant.front_rear == front_rear
        ) {
            return wf_occupant;
        }
    }
    return null;
}

/**
 * update the filters on the occupant window to only allow valid occupants
 */
function set_up_occupant_window(side, front_rear) {
    let wdw = gui.wdw_occupant;

    // if gui.current_occupant is not null then we are in edit mode
    gui.current_occupant = get_seat_occupant(side, front_rear);

    if (gui.current_occupant) {
        Message(`Edit occupant ${front_rear} ${side}`);
        edit_occupant();
        return;
    }

    /*add an occupant, but help the user out by pre-filtering dropdowns

    /* get expected occupant button for current seat */
    let expected_occupant_btn_widget = gui.wdw_pre_automotive[`lbl_${front_rear}_${side}_expected_occupant`];

    //alternatively could get this from protocol, but do it this way so that REPORTER works but passing a SpecifiedVehicle class json
    let vehicle_occupant = expected_occupant_btn_widget.vehicle_occupant;

    if (vehicle_occupant && !vehicle_occupant.Empty()) {
        wdw.cbx_occupant_name.active = true;
        wdw.cbx_occupant_supplier.active = true;
        wdw.cbx_occupant_product.active = true;
        wdw.cbx_occupant_physiology.active = true;
        set_selected_widget_item(wdw.cbx_occupant_supplier, "all");
        set_selected_widget_item(wdw.cbx_occupant_product, vehicle_occupant.product);
        set_selected_widget_item(wdw.cbx_occupant_physiology, vehicle_occupant.physiology);

        set_selected_widget_item(wdw.cbx_occupant_position, vehicle_occupant.position);
        set_selected_widget_item(wdw.cbx_occupant_side, side);
        set_selected_widget_item(wdw.cbx_occupant_front_rear, vehicle_occupant.front_rear);

        //once all the widget items have been set call update filters to update the drop-down
        //list of occupant names
        let occupant_names = filter_version_drop_down();
        update_occupant_names_combobox(occupant_names);

        Message(`Add occupant ${front_rear} ${side}`);
        add_new_occupant();
    }
}

/**
 * Sets the GUI up with things not set in the GUI Builder,
 * e.g. combobox items are added dynamically here using the list
 * of possible values from the WorkflowOccupant class.
 */
function setup_gui() {
    /* Initialise GUI global variables */

    /* Entity ID offset */

    gui.offset = 0;

    /* Array to store the WorkflowOccupant instances */

    gui.occupants = [];

    /* Currently selected occupant */

    gui.current_occupant = null;

    /* Array to store the Structure instances */

    gui.structures = [];

    /* Currently selected structure */

    gui.current_structure = null;

    /* Currently selected entity */

    gui.current_entity_tag = "";
    gui.current_entity_type = "";

    /* B-Pillar entity values */

    initialise_b_pillar_entities();

    /* Assign callbacks */

    /* Regulation and crash test on change callbacks */

    gui.wdw_pre_automotive.cbx_protocol_test.onChange = crash_test_changed;
    gui.wdw_pre_automotive.cbx_protocol_regulation.onChange = regulation_changed;
    gui.wdw_pre_automotive.cbx_protocol_version.onChange = version_changed;

    /* Set up initial state of occupants region and add occupant button callbacks */
    setup_vehicle_occupants();

    /* Occupant callbacks */

    gui.wdw_pre_automotive.radio_hand_drive.onClick = update_vehicle_hand_drive;
    gui.wdw_pre_automotive.btn_flip_occupants.onClick = flip_occupants;
    gui.wdw_pre_automotive.btn_delete_all_occupants.onClick = delete_all_occupants;

    gui.wdw_occupant.btn_use_id_num.onClick = update_and_toggle_ids_between_num_and_dbhistitle;
    gui.wdw_occupant.btn_use_dbhistitle.onClick = update_and_toggle_ids_between_num_and_dbhistitle;

    gui.wdw_occupant.txt_entity_offset.onChange = offset_changed;
    gui.wdw_occupant.btn_update_occupant.onClick = update_current_occupant_and_close;
    gui.wdw_occupant.btn_cancel_occupant.onClick = close_occupant_window;
    gui.wdw_occupant.cbx_occupant_name.onChange = update_occupant_window;
    gui.wdw_occupant.cbx_occupant_supplier.onChange = update_filters;
    gui.wdw_occupant.cbx_occupant_product.onChange = update_filters;
    gui.wdw_occupant.cbx_occupant_physiology.onChange = update_filters;

    gui.wdw_occupant.cbx_occupant_supplier.onClick = disable_invalid_filter_options;
    gui.wdw_occupant.cbx_occupant_product.onClick = disable_invalid_filter_options;
    gui.wdw_occupant.cbx_occupant_physiology.onClick = disable_invalid_filter_options;

    gui.wdw_occupant.btn_save_occupant.onClick = save_occupant_to_file;
    gui.wdw_occupant.btn_save_occupant.Hide(); /* This is for internal use only, so hide from users */

    /* Structure callbacks */

    gui.wdw_pre_automotive.btn_add_structure.onClick = add_new_structure;
    gui.wdw_pre_automotive.btn_add_default_structures.onClick = add_default_structures;

    gui.wdw_pre_automotive.lbx_structures.onChange = update_structure_edit_and_delete_buttons;
    gui.wdw_pre_automotive.lbx_structures.onClick = update_structure_edit_and_delete_buttons;

    gui.wdw_pre_automotive.btn_edit_structure.onClick = edit_structure;
    gui.wdw_pre_automotive.btn_delete_structure.onClick = delete_selected_structure;

    gui.wdw_structure.btn_update_structure.onClick = update_current_structure_and_close;
    gui.wdw_structure.btn_cancel_structure.onClick = close_structure_window;

    gui.wdw_structure.cbx_structure.onChange = update_structure_window;

    /* Picking and selecting callbacks */

    gui.popup_select_entities.btn_pick_entity.onClick = pick_entity;
    gui.popup_select_entities.btn_select_entity.onClick = select_entity;

    /* Save data callbacks */

    gui.wdw_pre_automotive.btn_save_to_file.onClick = save_to_file;
    gui.wdw_pre_automotive.btn_save_to_model.onClick = save_to_model;

    /* Add widget items to the units combobox */

    gui.wdw_pre_automotive.cbx_unit_system = new WorkflowUnitsCombobox(gui.wdw_pre_automotive.cbx_unit_system);

    /* Get list of possible options for test combobox and add widget items */

    let tests = Protocols.GetAllCrashTests();

    for (let test of tests) {
        new WidgetItem(gui.wdw_pre_automotive.cbx_protocol_test, test);
    }

    /* Add line to label to separate save buttons from selection widgets */

    gui.wdw_pre_automotive.lbl_line.Line(Widget.DARKGREY, 0, 50, 100, 50);

    /* Create the widgets in the structures window */

    create_structures_widgets();

    /* Create the widgets in the occupant window */

    create_occupant_widgets();

    /* Add buttons for selecting database history nodes/beams/discrete and cross sections */

    add_buttons_to_select_entities_popup();

    /* Get any data that already exists for the model */

    let success = read_model_data();

    if (!success) {
        /* Set up regulations combo box with regulations (widget items) supported by currently selected crash test */

        update_regulations();
        update_versions();

        /* Update Occupants gui */

        UpdateVehicleGUI();
    }
}

/**
 * update the vehicle based on the currently selected version
 * @returns {?ProtocolVehicle}
 */
function GetProtocolVehicle() {
    let regulation = get_selected_regulation();
    let crash_test = get_selected_crash_test_protocol();

    let version = gui.wdw_pre_automotive.cbx_protocol_version.text;

    let vehicle = Protocols.GetProtocolVehicle(regulation, crash_test, version);

    if (vehicle) {
        return vehicle;
    } else {
        ErrorMessage(`Could not find protocol vehicle for ${regulation} ${crash_test} ${version}`);
    }

    return null;
}

/**
 * Creates the widgets in the structures window
 */
function create_structures_widgets() {
    /* Get list of possible options for comboxes in the structures window and add widget items */

    let structure_types = Structure.Types();

    for (let i = 0; i < structure_types.length; i++)
        new WidgetItem(gui.wdw_structure.cbx_structure, structure_types[i]);

    /* Create widgets for selecting entity IDs */

    gui.structure_widgets = [];

    let max_bottom = 1;

    for (let structure_type of structure_types) {
        let top = gui.wdw_structure.cbx_structure.bottom + 1;
        let bottom = top + 6;

        let structure = Structure.CreateStructure(structure_type);

        /* The B-Pillar is a non-standard structure, that doesn't have any StructureEntity
         * instances defined for it (see CreateBPillarEntities() for an explanation).
         *
         * Bespoke logic is required here to create the required widgets.
         *
         * For other strutures create the widgets from the StructureEntity instances.
         */
        if (structure.component_type == Structure.B_PILLAR) {
            bottom = create_b_pillar_widgets(gui.wdw_structure, top, bottom);
        } else {
            /* No entities for this structure */

            if (structure.entities.length == 0) continue;

            /* Structure header */

            let structure_label = new Widget(
                gui.wdw_structure,
                Widget.LABEL,
                1,
                147,
                top,
                bottom,
                structure.component_type.toUpperCase()
            );
            structure_label.category = Widget.CATEGORY_TITLE;

            top = bottom + 1;
            bottom = top + 6;

            /* Entity widgets */

            /** @type {StructureEntityWidgets[]} */
            let entity_widgets = [];

            for (let structure_entity of structure.entities) {
                let label = new Widget(gui.wdw_structure, Widget.LABEL, 1, 82, top, bottom, structure_entity.name);
                label.justify = Widget.LEFT;

                let textbox = new Widget(
                    gui.wdw_structure,
                    Widget.TEXTBOX,
                    83,
                    147,
                    top,
                    bottom,
                    structure_entity.id.toString()
                );
                textbox.popupWindow = gui.popup_select_entities;
                textbox.popupDirection = Widget.RIGHT;
                textbox.onPopup = structure_entity_on_popup;
                textbox.onChange = update_structure_window;

                /* Tag and entity type used in popup from textbox for selecting the entity */
                // @ts-ignore
                textbox.entity_tag = structure_entity.tag;
                // @ts-ignore
                textbox.entity_type = structure_entity.entity_type;

                top = bottom + 1;
                bottom = top + 6;

                entity_widgets.push(
                    new StructureEntityWidgets(structure_entity.entity_type, label, textbox, structure_entity.tag)
                );
            }

            let structure_widgets = new StructureWidgets(structure.component_type, structure_label, entity_widgets);

            gui.structure_widgets.push(structure_widgets);
        }

        max_bottom = Math.max(max_bottom, bottom);
    }

    /* Create a  label widget at the bottom of the window so it maps to the correct size to fit all the entity widgets on */

    gui.wdw_structure.lbl_occupant_bottom = new Widget(
        gui.wdw_structure,
        Widget.LABEL,
        1,
        147,
        max_bottom,
        max_bottom + 1,
        ""
    );
}

/**
 * Creates the B-Pillar widgets, returning the bottom position of the last widget created
 * @param {Window} window Window to create widgets in
 * @param {number} start_top Starting top position of widgets
 * @param {number} start_bottom Starting bottom position of widgets
 * @returns {number}
 */
function create_b_pillar_widgets(window, start_top, start_bottom) {
    let top = start_top;
    let bottom = start_bottom;

    /* All the widgets are added to an object on the gui object */
    gui.b_pillar_widgets = {};

    gui.b_pillar_widgets.lbl_structure = new Widget(
        window,
        Widget.LABEL,
        1,
        147,
        top,
        bottom,
        Structure.B_PILLAR.toUpperCase()
    );
    gui.b_pillar_widgets.lbl_structure.category = Widget.CATEGORY_TITLE;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_cut_section = new Widget(
        window,
        Widget.LABEL,
        1,
        82,
        top,
        bottom,
        "Cut Section Definition Method"
    );
    gui.b_pillar_widgets.lbl_cut_section.justify = Widget.LEFT;

    gui.b_pillar_widgets.cbx_cut_section = new Widget(window, Widget.COMBOBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.cbx_cut_section.onChange = b_pillar_callback;

    new WidgetItem(gui.b_pillar_widgets.cbx_cut_section, "Constant X");
    new WidgetItem(gui.b_pillar_widgets.cbx_cut_section, "Constant Y");
    new WidgetItem(gui.b_pillar_widgets.cbx_cut_section, "Constant Z");
    new WidgetItem(gui.b_pillar_widgets.cbx_cut_section, "Three Nodes");

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_cut_section_node_1 = new Widget(
        window,
        Widget.LABEL,
        1,
        82,
        top,
        bottom,
        "Cut Section Node 1"
    );
    gui.b_pillar_widgets.lbl_cut_section_node_1.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_cut_section_node_1 = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_cut_section_node_1.onChange = b_pillar_callback;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_cut_section_node_2 = new Widget(
        window,
        Widget.LABEL,
        1,
        82,
        top,
        bottom,
        "Cut Section Node 2"
    );
    gui.b_pillar_widgets.lbl_cut_section_node_2.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_cut_section_node_2 = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_cut_section_node_2.onChange = b_pillar_callback;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_cut_section_node_3 = new Widget(
        window,
        Widget.LABEL,
        1,
        82,
        top,
        bottom,
        "Cut Section Node 3"
    );
    gui.b_pillar_widgets.lbl_cut_section_node_3.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_cut_section_node_3 = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_cut_section_node_3.onChange = b_pillar_callback;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_pre_crash_parts = new Widget(window, Widget.LABEL, 1, 82, top, bottom, "Pre-Crash Parts");
    gui.b_pillar_widgets.lbl_pre_crash_parts.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_pre_crash_parts = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_pre_crash_parts.onChange = b_pillar_callback;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_post_crash_parts = new Widget(
        window,
        Widget.LABEL,
        1,
        82,
        top,
        bottom,
        "Post-Crash Parts"
    );
    gui.b_pillar_widgets.lbl_post_crash_parts.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_post_crash_parts = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_post_crash_parts.onChange = b_pillar_callback;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_shift_deform_node_1 = new Widget(window, Widget.LABEL, 1, 82, top, bottom, "Shift Node 1");
    gui.b_pillar_widgets.lbl_shift_deform_node_1.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_shift_deform_node_1 = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_shift_deform_node_1.onChange = b_pillar_callback;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_shift_deform_node_2 = new Widget(window, Widget.LABEL, 1, 82, top, bottom, "Shift Node 2");
    gui.b_pillar_widgets.lbl_shift_deform_node_2.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_shift_deform_node_2 = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_shift_deform_node_2.onChange = b_pillar_callback;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_shift_deform_node_3 = new Widget(window, Widget.LABEL, 1, 82, top, bottom, "Shift Node 3");
    gui.b_pillar_widgets.lbl_shift_deform_node_3.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_shift_deform_node_3 = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_shift_deform_node_3.onChange = b_pillar_callback;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_ground_z = new Widget(window, Widget.LABEL, 1, 82, top, bottom, "Ground Z");
    gui.b_pillar_widgets.lbl_ground_z.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_ground_z = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_ground_z.onChange = b_pillar_callback;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_seat_centre_y = new Widget(window, Widget.LABEL, 1, 82, top, bottom, "Seat Centre Y");
    gui.b_pillar_widgets.lbl_seat_centre_y.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_seat_centre_y = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_seat_centre_y.onChange = b_pillar_callback;

    top = bottom + 1;
    bottom = top + 6;

    gui.b_pillar_widgets.lbl_h_point_z = new Widget(window, Widget.LABEL, 1, 82, top, bottom, "H-Point Z");
    gui.b_pillar_widgets.lbl_h_point_z.justify = Widget.LEFT;

    gui.b_pillar_widgets.txt_h_point_z = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.b_pillar_widgets.txt_h_point_z.onChange = b_pillar_callback;

    /* Update the values in the widgets */

    update_b_pillar_widget_values();

    /* Return the bottom coordinate of the last widget created */

    return bottom;
}

/**
 * Sets the initial values for the B-Pillar entities
 */
function initialise_b_pillar_entities() {
    gui.b_pillar_cut_section_method = "Constant X";
    gui.b_pillar_cut_section_node_1 = 0;
    gui.b_pillar_cut_section_node_2 = 0;
    gui.b_pillar_cut_section_node_3 = 0;
    gui.b_pillar_pre_crash_parts = [];
    gui.b_pillar_post_crash_parts = [];
    gui.b_pillar_shift_deform_node_1 = 0;
    gui.b_pillar_shift_deform_node_2 = 0;
    gui.b_pillar_shift_deform_node_3 = 0;
    gui.b_pillar_ground_z = 0;
    gui.b_pillar_seat_centre_y = 0;
    gui.b_pillar_h_point_z = 0;
}

/**
 * Callback function for B-Pillar widgets
 */
function b_pillar_callback() {
    switch (this) {
        case gui.b_pillar_widgets.cbx_cut_section:
            gui.b_pillar_cut_section_method = this.text;

            break;

        case gui.b_pillar_widgets.txt_cut_section_node_1:
        case gui.b_pillar_widgets.txt_cut_section_node_2:
        case gui.b_pillar_widgets.txt_cut_section_node_3:
        case gui.b_pillar_widgets.txt_shift_deform_node_1:
        case gui.b_pillar_widgets.txt_shift_deform_node_2:
        case gui.b_pillar_widgets.txt_shift_deform_node_3:
            let new_int = parseInt(this.text);

            if (isNaN(new_int)) {
                WarningMessage("Invalid value, must be an integer");
            } else if (new_int < 0) {
                WarningMessage("Invalid value, must be greater than or equal to 0");
            } else {
                if (this == gui.b_pillar_widgets.txt_cut_section_node_1) {
                    gui.b_pillar_cut_section_node_1 = new_int;
                } else if (this == gui.b_pillar_widgets.txt_cut_section_node_2) {
                    gui.b_pillar_cut_section_node_2 = new_int;
                } else if (this == gui.b_pillar_widgets.txt_cut_section_node_3) {
                    gui.b_pillar_cut_section_node_3 = new_int;
                } else if (this == gui.b_pillar_widgets.txt_shift_deform_node_1) {
                    gui.b_pillar_shift_deform_node_1 = new_int;
                } else if (this == gui.b_pillar_widgets.txt_shift_deform_node_2) {
                    gui.b_pillar_shift_deform_node_2 = new_int;
                } else if (this == gui.b_pillar_widgets.txt_shift_deform_node_3) {
                    gui.b_pillar_shift_deform_node_3 = new_int;
                }
            }

            break;

        case gui.b_pillar_widgets.txt_pre_crash_parts:
        case gui.b_pillar_widgets.txt_post_crash_parts:
            let valid = true;
            let parts = this.text.trim().split(/\s+/);
            let new_parts = [];

            for (let p of parts) {
                let part = parseInt(p);

                if (isNaN(part)) {
                    WarningMessage("Invalid value, parts must be a space-separated list of integers");
                    valid = false;
                    break;
                } else if (part < 0) {
                    WarningMessage("Invalid value, parts must be greater than or equal to 0");
                    valid = false;
                    break;
                }

                new_parts.push(part);
            }

            if (!valid) break;

            if (this == gui.b_pillar_widgets.txt_pre_crash_parts) {
                gui.b_pillar_pre_crash_parts = new_parts;
            } else if (this == gui.b_pillar_widgets.txt_post_crash_parts) {
                gui.b_pillar_post_crash_parts = new_parts;
            }

            break;

        case gui.b_pillar_widgets.txt_ground_z:
        case gui.b_pillar_widgets.txt_seat_centre_y:
        case gui.b_pillar_widgets.txt_h_point_z:
            let new_float = parseFloat(this.text);

            if (isNaN(new_float)) {
                WarningMessage("Invalid value, must be a number");
            } else {
                if (this == gui.b_pillar_widgets.txt_ground_z) {
                    gui.b_pillar_ground_z = new_float;
                } else if (this == gui.b_pillar_widgets.txt_seat_centre_y) {
                    gui.b_pillar_seat_centre_y = new_float;
                } else if (this == gui.b_pillar_widgets.txt_h_point_z) {
                    gui.b_pillar_h_point_z = new_float;
                }
            }

            break;

        default:
            ErrorMessage("Unknown widget in b_pillar_callback()");
    }

    update_b_pillar_widget_values();
}

/**
 * Updates the values in the widgets
 */
function update_b_pillar_widget_values() {
    for (let wi of gui.b_pillar_widgets.cbx_cut_section.WidgetItems()) {
        if (gui.b_pillar_cut_section_method == wi.text) {
            wi.selected = true;
        }
    }

    if (gui.b_pillar_cut_section_method == "Three Nodes") {
        gui.b_pillar_widgets.txt_cut_section_node_2.active = true;
        gui.b_pillar_widgets.txt_cut_section_node_3.active = true;
    } else {
        gui.b_pillar_widgets.txt_cut_section_node_2.active = false;
        gui.b_pillar_widgets.txt_cut_section_node_3.active = false;
    }

    gui.b_pillar_widgets.txt_cut_section_node_1.text = gui.b_pillar_cut_section_node_1;
    gui.b_pillar_widgets.txt_cut_section_node_2.text = gui.b_pillar_cut_section_node_2;
    gui.b_pillar_widgets.txt_cut_section_node_3.text = gui.b_pillar_cut_section_node_3;

    gui.b_pillar_widgets.txt_pre_crash_parts.text = gui.b_pillar_pre_crash_parts.join(" ");
    gui.b_pillar_widgets.txt_post_crash_parts.text = gui.b_pillar_post_crash_parts.join(" ");

    gui.b_pillar_widgets.txt_shift_deform_node_1.text = gui.b_pillar_shift_deform_node_1;
    gui.b_pillar_widgets.txt_shift_deform_node_2.text = gui.b_pillar_shift_deform_node_2;
    gui.b_pillar_widgets.txt_shift_deform_node_3.text = gui.b_pillar_shift_deform_node_3;

    gui.b_pillar_widgets.txt_ground_z.text = gui.b_pillar_ground_z;
    gui.b_pillar_widgets.txt_seat_centre_y.text = gui.b_pillar_seat_centre_y;
    gui.b_pillar_widgets.txt_h_point_z.text = gui.b_pillar_h_point_z;
}

/**
 * Creates the widgets in the occupant window
 */
function create_occupant_widgets() {
    /* Get list of possible options for comboxes in the occupant window and add widget items */

    let names = WorkflowOccupant.Versions();

    // prepend "all" to the array of each filter so that it is the default
    let ALL = ["all"];
    let suppliers = ALL.concat(WorkflowOccupant.Suppliers());
    let products = ALL.concat(WorkflowOccupant.Products());
    let physiologies = ALL.concat(WorkflowOccupant.Physiologies());

    let positions = WorkflowOccupant.Positions();
    let sides = WorkflowOccupant.Sides();
    let front_rear = WorkflowOccupant.FrontRear();

    update_occupant_names_combobox(names);
    for (let i = 0; i < suppliers.length; i++) new WidgetItem(gui.wdw_occupant.cbx_occupant_supplier, suppliers[i]);
    for (let i = 0; i < products.length; i++) new WidgetItem(gui.wdw_occupant.cbx_occupant_product, products[i]);
    for (let i = 0; i < physiologies.length; i++)
        new WidgetItem(gui.wdw_occupant.cbx_occupant_physiology, physiologies[i]);
    for (let i = 0; i < positions.length; i++) new WidgetItem(gui.wdw_occupant.cbx_occupant_position, positions[i]);
    for (let i = 0; i < sides.length; i++) new WidgetItem(gui.wdw_occupant.cbx_occupant_side, sides[i]);
    for (let i = 0; i < front_rear.length; i++) new WidgetItem(gui.wdw_occupant.cbx_occupant_front_rear, front_rear[i]);

    /* Create widgets for selecting entity IDs */

    gui.occupant_widgets = [];

    let max_bottom = 1;

    for (let name of names) {
        let top = gui.wdw_occupant.btn_use_id_num.bottom + 1;
        let bottom = top + 6;

        let occupant = WorkflowOccupant.CreateWorkflowOccupantFromOccupant(
            name,
            WorkflowOccupant.DRIVER,
            WorkflowOccupant.LEFT,
            WorkflowOccupant.FRONT
        );

        /** @type {BodyPartWidgets[]} */
        let body_part_widgets = [];

        for (let occupant_body_part of occupant.body_parts) {
            /* No entities for this body part */

            if (occupant_body_part.entities.length == 0) continue;

            /* Body part header */

            let body_part_label = new Widget(
                gui.wdw_occupant,
                Widget.LABEL,
                1,
                147,
                top,
                bottom,
                occupant_body_part.component_type.toUpperCase()
            );
            body_part_label.category = Widget.CATEGORY_TITLE;

            top = bottom + 1;
            bottom = top + 6;

            /* Entity widgets */

            /** @type {OccupantEntityWidgets[]} */
            let entity_widgets = [];

            for (let occupant_entity of occupant_body_part.entities) {
                let label = new Widget(gui.wdw_occupant, Widget.LABEL, 1, 82, top, bottom, occupant_entity.name);
                label.justify = Widget.LEFT;

                let textbox = new Widget(
                    gui.wdw_occupant,
                    Widget.TEXTBOX,
                    83,
                    147,
                    top,
                    bottom,
                    occupant_entity.id.toString()
                );
                textbox.popupWindow = gui.popup_select_entities;
                textbox.popupDirection = Widget.RIGHT;
                textbox.onPopup = occupant_entity_on_popup;
                textbox.onChange = update_occupant_window;

                /* Tag and entity type used in popup from textbox for selecting the entity */
                // @ts-ignore
                textbox.entity_tag = occupant_entity.tag;
                // @ts-ignore
                textbox.entity_type = occupant_entity.entity_type;

                top = bottom + 1;
                bottom = top + 6;

                entity_widgets.push(
                    new OccupantEntityWidgets(occupant_entity.entity_type, label, textbox, occupant_entity.tag)
                );
            }

            body_part_widgets.push(new BodyPartWidgets(body_part_label, entity_widgets));
        }

        let occupant_widgets = new OccupantWidgets(name, body_part_widgets);

        gui.occupant_widgets.push(occupant_widgets);

        max_bottom = Math.max(max_bottom, bottom);
    }

    /* Create a  label widget at the bottom of the window so it maps to the correct size to fit all the entity widgets on */

    gui.wdw_occupant.lbl_occupant_bottom = new Widget(
        gui.wdw_occupant,
        Widget.LABEL,
        1,
        147,
        max_bottom,
        max_bottom + 1,
        ""
    );
}

/**
 * Reads user data from the selected model and stores it on the gui object
 * and populates the widgets with the data
 @returns {boolean} successfully read data or not
 */
function read_model_data() {
    let success = false;

    /* Number of models loaded in PRIMER that have data for this workflow */

    let num_models = Workflow.NumberOfModels();
    if (num_models == 0) return false;

    /* If the model selected by the user has data read it in */

    for (let i = 0; i < num_models; i++) {
        let model_id = Workflow.ModelIdFromIndex(i);

        if (model_id != gui.model.number) continue;

        /* Get the user data */

        /** @type {UserData} */
        // @ts-ignore
        let user_data = Workflow.ModelUserDataFromIndex(i);

        /* Put in try-catch in case any of the saved data is invalid */

        try {
            /*Set the drive side from user data */
            set_vehicle_drive_side(user_data.drive_side);

            /* Get the crash test type */
            success = set_selected_widget_item(gui.wdw_pre_automotive.cbx_protocol_test, user_data.crash_test);
            if (!success) return false;

            /* update the regulation combobox so that the selection can be set (otherwise it may not exist in the list)*/
            update_regulations();

            /* Get the regulations */
            success = set_selected_widget_item(
                gui.wdw_pre_automotive.cbx_protocol_regulation,
                user_data.regulations[0]
            );
            if (!success) return false;

            /* update the version comboboxe so that the selection can be set (otherwise it may not exist in the list)*/
            update_versions();

            /* Set the version */
            success = set_selected_widget_item(gui.wdw_pre_automotive.cbx_protocol_version, user_data.version);
            if (!success) return false;

            /* Get the occupants */
            let occupants = user_data.occupants;

            if (occupants) {
                for (let occupant of occupants) {
                    let o = WorkflowOccupant.CreateWorkflowOccupantFromOccupant(
                        occupant.name,
                        occupant.position,
                        occupant.side,
                        occupant.front_rear
                    );

                    gui.occupants.push(o);

                    /* Set the entity ids */

                    for (let body_part of occupant.body_parts) {
                        for (let entity of body_part.entities) {
                            let oe = o.GetEntityByTag(entity.tag);

                            if (oe && entity.id) {
                                oe.id = entity.id;
                            }
                        }
                    }
                }
            }

            /* Get the structures */
            let structures = user_data.structures;

            if (structures) {
                for (let structure of structures) {
                    let s = Structure.CreateStructure(structure.component_type);

                    gui.structures.push(s);

                    /* Set the entity ids */

                    for (let entity of structure.entities) {
                        let se = s.GetEntityByTag(entity.tag);

                        if (se && entity.id) {
                            se.id = entity.id;
                        }
                    }
                }
            }

            /* Get the B-Pillar data */
            /** @type {BPillarStructure} */
            let b_pillar = user_data.b_pillar;

            if (b_pillar) {
                gui.b_pillar_cut_section_method = b_pillar.cut_section_method;
                gui.b_pillar_cut_section_node_1 = b_pillar.cut_section_nodes[0];
                gui.b_pillar_cut_section_node_2 = b_pillar.cut_section_nodes[1];
                gui.b_pillar_cut_section_node_3 = b_pillar.cut_section_nodes[2];
                gui.b_pillar_pre_crash_parts = b_pillar.pre_crash_parts;
                gui.b_pillar_post_crash_parts = b_pillar.post_crash_parts;
                gui.b_pillar_shift_deform_node_1 = b_pillar.shift_deform_nodes[0];
                gui.b_pillar_shift_deform_node_2 = b_pillar.shift_deform_nodes[1];
                gui.b_pillar_shift_deform_node_3 = b_pillar.shift_deform_nodes[2];
                gui.b_pillar_ground_z = b_pillar.ground_z;
                gui.b_pillar_seat_centre_y = b_pillar.seat_centre_y;
                gui.b_pillar_h_point_z = b_pillar.h_point_z;
            }

            /* Get the model unit system and set the combobox selected item */

            let unit_system = Workflow.ModelUnitSystemFromIndex(i);

            gui.wdw_pre_automotive.cbx_unit_system.SetSelectedUnitSystem(unit_system);
        } catch (e) {
            ErrorMessage(`Error reading saved data: ${e}`);
            return false;
        }

        /* No need to check any other models */

        return true;
    }
}

/**
 * get the currently selected crash test protocol
 * @returns {string}
 */
function get_selected_crash_test_protocol() {
    return gui.wdw_pre_automotive.cbx_protocol_test.text;
}

/**
 * get the currently selected regulation
 * @returns {string}
 */
function get_selected_regulation() {
    return gui.wdw_pre_automotive.cbx_protocol_regulation.text;
}

/**
 * get the currently selected version
 * @returns {string}
 */
function get_selected_version() {
    return gui.wdw_pre_automotive.cbx_protocol_version.text;
}

/**
 * Get the user data object
 * @returns {?UserData} User data object
 */
function get_user_data() {
    let regulation = get_selected_regulation();

    /* Calculate the rib irtracc lengths */

    for (let occupant of gui.occupants) {
        /* Chest */

        occupant.upper_rib_irtracc_length = calculate_irtracc_length(
            occupant,
            OccupantEntity.CHEST_UPPER_RIB_SPRING_TRANS
        );

        occupant.mid_rib_irtracc_length = calculate_irtracc_length(
            occupant,
            OccupantEntity.CHEST_MIDDLE_RIB_SPRING_TRANS
        );

        occupant.bottom_rib_irtracc_length = calculate_irtracc_length(
            occupant,
            OccupantEntity.CHEST_BOTTOM_RIB_SPRING_TRANS
        );

        /* Abdomen */

        occupant.upper_abdomen_irtracc_length = calculate_irtracc_length(
            occupant,
            OccupantEntity.ABDOMEN_UPPER_SPRING_TRANS
        );

        occupant.bottom_abdomen_irtracc_length = calculate_irtracc_length(
            occupant,
            OccupantEntity.ABDOMEN_LOWER_SPRING_TRANS
        );
    }

    /** @type {BPillarStructure} B-Pillar structure */
    let b_pillar = {
        cut_section_method: gui.b_pillar_cut_section_method,
        cut_section_nodes: [
            gui.b_pillar_cut_section_node_1,
            gui.b_pillar_cut_section_node_2,
            gui.b_pillar_cut_section_node_3
        ],
        pre_crash_parts: gui.b_pillar_pre_crash_parts,
        post_crash_parts: gui.b_pillar_post_crash_parts,
        shift_deform_nodes: [
            gui.b_pillar_shift_deform_node_1,
            gui.b_pillar_shift_deform_node_2,
            gui.b_pillar_shift_deform_node_3
        ],
        ground_z: gui.b_pillar_ground_z,
        seat_centre_y: gui.b_pillar_seat_centre_y,
        h_point_z: gui.b_pillar_h_point_z
    };

    return {
        regulations: [regulation],
        crash_test: get_selected_crash_test_protocol(),
        version: get_selected_version(),
        drive_side: gui.drive_side,
        occupants: gui.occupants,
        structures: gui.structures,
        b_pillar: b_pillar
    };
}

/**
 * Calculate the irtracc length for a given occupant and rib spring
 * If it doesn't exist in the occupant, returns 0.0
 * @param {WorkflowOccupant} occupant Occupant
 * @param {string} entity_tag Rib spring tag, e.g. OccupantEntity.CHEST_UPPER_RIB_SPRING_TRANS
 * @returns {number}
 * @example
 * let length = calculate_irtracc_length(occupant, OccupantEntity.CHEST_UPPER_RIB_SPRING_TRANS);
 */
function calculate_irtracc_length(occupant, entity_tag) {
    let irtracc_trans = occupant.GetEntityByTag(entity_tag);

    /* Enitity not defined/not in occupant, so just set length to zero */

    if (!irtracc_trans) return 0.0;

    /* If the ID is a database history string, convert it to a number
     * so it can be used in the GetFromID function */

    let id = -1;

    if (typeof irtracc_trans.id == "string") {
        let history = History.First(gui.model, History.DISCRETE);

        while (history) {
            if (history.heading == irtracc_trans.id) {
                id = history.id;
                break;
            }

            history = history.Next();
        }
    } else {
        id = irtracc_trans.id;
    }

    if (id == -1) {
        ErrorMessage(`Could not find numerical ID for ${irtracc_trans.name}. Setting IR-TRACC length to 0.0.`);
        return 0.0;
    }

    /* Calculate length */

    let el = Discrete.GetFromID(gui.model, id);

    /* If it's not in the model, return 0.0 */

    if (!el) {
        return 0.0;
    }

    let n1 = Node.GetFromID(gui.model, el.n1);
    let n2 = Node.GetFromID(gui.model, el.n2);

    let x1 = n1.x;
    let x2 = n2.x;

    let y1 = n1.y;
    let y2 = n2.y;

    let z1 = n1.z;
    let z2 = n2.z;

    let dx = x2 - x1;
    let dy = y2 - y1;
    let dz = z2 - z1;

    return Math.sqrt(dx * dx + dy * dy + dz * dz);
}

/**
 * Get the extra data object
 * @returns {WriteToFileArgument_extra} extra data object
 */
function get_extra_data() {
    return {
        model_unit_system: gui.wdw_pre_automotive.cbx_unit_system.GetSelectedUnitSystem()
    };
}

/**
 * Add buttons to the 'popup_select_entities' popup for selecting database
 * history nodes/beams/discretes and database cross sections directly, rather
 * than having to pick/select them in the graphics window
 */
function add_buttons_to_select_entities_popup() {
    /* Create buttons for each node/beam/discrete database history item in the
     * model, storing them on the gui object so they can be (un)mapped as
     * appropriate depending on the entity type that the popup is being mapped for. */

    gui.database_buttons = [];

    let dy = 6;
    let y1 = gui.popup_select_entities.lbl_database_history.bottom;
    let y2 = y1 + dy;
    let x1 = 1;
    let x2 = 81;

    let history = History.First(gui.model);

    while (history) {
        switch (history.type) {
            case History.NODE:
            case History.BEAM:
            case History.DISCRETE:
                let history_label = "";

                if (history.heading == "") {
                    history_label = history.id.toString();
                } else {
                    history_label = history.heading;
                }

                let button = new Widget(gui.popup_select_entities, Widget.BUTTON, x1, x2, y1, y2, history_label);
                button.justify = Widget.LEFT;
                button.onClick = select_database_item;

                /* Store the entity type and label on the widget object to make it easy
                 * to (un)map the correct buttons depending on what entity type is being selected
                 * and when processing the button click */

                if (history.type == History.NODE) {
                    // @ts-ignore
                    button.entity_type = BaseEntity.NODE;
                } else if (history.type == History.BEAM) {
                    // @ts-ignore
                    button.entity_type = BaseEntity.BEAM_BASIC;
                } else if (history.type == History.DISCRETE) {
                    // @ts-ignore
                    button.entity_type = BaseEntity.SPRING_TRANSLATIONAL;
                }
                // @ts-ignore
                button.entity_label = history_label;

                /* Store the button widget so we can loop over them in the update function */

                gui.database_buttons.push(button);

                break;
        }

        history = history.Next();
    }

    /* Database cross sections */

    let xsec = CrossSection.First(gui.model);

    while (xsec) {
        let xsec_label = "";

        if (xsec.heading == "") {
            xsec_label = xsec.label.toString();
        } else {
            xsec_label = xsec.heading;
        }

        let button = new Widget(gui.popup_select_entities, Widget.BUTTON, x1, x2, y1, y2, xsec_label);
        button.justify = Widget.LEFT;
        button.onClick = select_database_item;

        /* Store the history type and label on the widget object to make it easy
         * to (un)map the correct buttons depending on what entity type is being selected
         * and when processing the button click */

        // @ts-ignore
        button.entity_type = BaseEntity.XSECTION;
        // @ts-ignore
        button.entity_label = xsec_label;

        /* Store the button widget so we can loop over them in the update function */

        gui.database_buttons.push(button);

        xsec = xsec.Next();
    }

    /* Sort into alphabetical then numerical order */

    gui.database_buttons.sort(function (a, b) {
        // Sort array of buttons into alphabetical then numerical order

        var ai = parseInt(a.text);
        var bi = parseInt(b.text);

        /* Both text - case insensitive */

        if (isNaN(ai) && isNaN(bi)) {
            if (a.text.toLowerCase() > b.text.toLowerCase()) {
                return 1;
            } else if (a.text.toLowerCase() < b.text.toLowerCase()) {
                return -1;
            } else {
                return 0;
            }

            /* Both numbers */
        } else if (!isNaN(ai) && !isNaN(bi)) {
            return ai - bi;

            /* One text, one number -> text should come first */
        } else {
            if (isNaN(ai)) {
                return -1;
            } else {
                return 1;
            }
        }
    });
}

/* Callback functions */

/**
 * Open the occupant window to add a new occupant
 */
function add_new_occupant() {
    gui.current_occupant = null;
    update_occupant_window();
    update_entity_ids();
    gui.wdw_occupant.Show(false);
}

/**
 * Open the occupant window to edit the current occupant
 */
function edit_occupant() {
    if (!gui.current_occupant) return;

    initialise_occupant_window();

    gui.wdw_occupant.Show(false);
}

/**
 * update the ids in the gui to use the numbers
 */
function update_entity_ids() {
    let use_db_history = false;

    if (!gui.wdw_occupant.btn_use_dbhistitle.active) use_db_history = true;

    let occupant_name = gui.wdw_occupant.cbx_occupant_name.selectedItem.text;

    let occupant_widgets = get_occupant_widgets(occupant_name);

    let occupant = OccupantVersion.GetFromName(occupant_name);

    //var workflow_occupant = gui.current_occupant;

    /* Find the the entity widgets with the same tag and set the text to the ID + the offset */

    for (let body_part_widgets of occupant_widgets.body_part_widgets) {
        for (let entity_widgets of body_part_widgets.entity_widgets) {
            let entity = occupant.GetEntityByTag(entity_widgets.tag);

            if (use_db_history) {
                if (entity.history_title != "") {
                    entity_widgets.textbox.text = entity.history_title;
                } else if (entity.id) {
                    entity_widgets.textbox.text = entity.id + gui.offset;
                }
            } else {
                if (entity.id) {
                    entity_widgets.textbox.text = entity.id + gui.offset;
                }
            }
        }
    }

    // update_occupant_window();
}

/**
 * callback to trigger changes crash test changes
 */
function crash_test_changed() {
    update_regulations();
    update_versions();
    UpdateVehicleGUI();
}

/**
 * callback to trigger changes when regulation changes
 */
function regulation_changed() {
    update_versions();
    UpdateVehicleGUI();
}

/**
 * callback to trigger changes when version changes
 */
function version_changed() {
    UpdateVehicleGUI();
}

/**
 * update regulation combobox based on current crash test selection - try to keep regulation the same if it exists in new list
 */
function update_regulations() {
    Message(`update_regulations`);

    let crash_test = get_selected_crash_test_protocol();
    let current_regulation = get_selected_regulation();

    let protocols = Protocols.GetOnly("ALL", crash_test, "ALL");

    let cbx = gui.wdw_pre_automotive.cbx_protocol_regulation;

    /* clears all versions in the combobox and adds the versions for the new regulation */
    cbx.RemoveAllWidgetItems();

    let regulations = [];
    for (let protocol of protocols) {
        if (regulations.indexOf(protocol.regulation) == -1) regulations.push(protocol.regulation);
    }

    /* sort regulation strings alphabetically and select the same one as is currently selected if possible */
    for (let regulation of regulations.sort()) {
        let wi = new WidgetItem(cbx, regulation.toString());
        if (regulation == current_regulation) wi.selected = true;
    }
}

/**
 * update version combobox based on current crash test selection - try to keep version the same if it exists in new list
 */
function update_versions() {
    Message(`update_versions`);

    let crash_test = get_selected_crash_test_protocol();
    let regulation = get_selected_regulation();
    let current_version = get_selected_version();

    let cbx = gui.wdw_pre_automotive.cbx_protocol_version;

    /* clears all versions in the combobox and adds the versions for the new regulation */
    cbx.RemoveAllWidgetItems();

    let versions = Protocol.Versions(regulation, crash_test);

    for (let version of versions) {
        let wi = new WidgetItem(cbx, version);
        if (version == current_version) wi.selected = true;
    }
}

/**
 * callback to toggle between using id numbers and database history titles (if they exisit in JSON or can be extracted from model using IDs)
 */
function update_and_toggle_ids_between_num_and_dbhistitle() {
    //make sure to toggle buttons before updating so that the correct state is used
    gui.wdw_occupant.btn_use_dbhistitle.active = !gui.wdw_occupant.btn_use_dbhistitle.active;
    gui.wdw_occupant.btn_use_id_num.active = !gui.wdw_occupant.btn_use_id_num.active;
    update_entity_ids();
}

/**
 * callback to toggle between using id numbers and database history titles (if they exisit in JSON or can be extracted from model using IDs)
 */
function update_vehicle_hand_drive() {
    //make sure to toggle buttons before updating so that the correct state is used
    Message(`Hand drive = ${this.text}`);
    gui.drive_side = this.text;

    UpdateVehicleGUI();
}

/**
 * function to update vehicle gui when regulation/crash_test/version change
 */
function UpdateVehicleGUI() {
    Message("Updating vehicle gui");
    update_vehicle_occupants(GetProtocolVehicle(), gui.drive_side);
    // gui.vehicle.Update(GetProtocolVehicle());
}

/**
 * When the offset is changed in the textbox, check it's an integer
 * If it's not, then reset it to the original value
 */
function offset_changed() {
    //if empty string then set offset to 0
    if (gui.wdw_occupant.txt_entity_offset.text == "") gui.offset = 0;

    let offset = parseInt(gui.wdw_occupant.txt_entity_offset.text);

    if (!isNaN(offset)) {
        gui.offset = offset;
    }

    gui.wdw_occupant.txt_entity_offset.text = gui.offset;

    update_entity_ids();
}

/**
 * Open the structure window to add a new structure
 */
function add_new_structure() {
    gui.current_structure = null;
    update_structure_window();

    gui.wdw_structure.Show(false);
}

/**
 * Adds the structures required for the selected crash test and regulation(s)
 */
function add_default_structures() {
    /* Structures required for this crash test/regulation combination */

    let structures = Protocol.StructureTypes(get_selected_regulation(), get_selected_crash_test_protocol());

    /* Add any structures if they're not already present in the gui.structures array */

    for (let structure of structures) {
        /* Ignore if the structure is already present */

        let present = false;

        for (let present_structure of gui.structures) {
            if (present_structure.component_type == structure) {
                present = true;
            }
        }

        if (present) continue;

        /* Add it in */

        gui.structures.push(Structure.CreateStructure(structure));
    }

    Message("Required structures added for the selected crash test and regulation");

    update_main_window();
}

/**
 * Open the structure window to edit the current structure
 */
function edit_structure() {
    if (!gui.current_structure) return;

    initialise_structure_window();

    gui.wdw_structure.Show(false);
}

/**
 * Saves the selected data to a workflow JSON file
 */
function save_occupant_to_file() {
    /* Get data to write to file */

    let occupant = GetOccupantFromGUI();

    if (occupant) {
        update_occupant_entity_ids_from_gui(occupant);

        // if (occupant.version) var current_version = occupant.version;

        let match = occupant.name.match(/(.* v)(.*)/i);
        if (match != null) {
            var root_name = match[1]; //first capture group
            var current_version = match[2]; //first capture group
        } else {
            Message(`failed to extract version from ${occupant.name}`);
            return;
        }

        let version = Window.GetString(
            "Occupant Version",
            `Input the occupant version for:\n${occupant.name}`,
            current_version
        );

        if (!version) return;

        if (Unix()) {
            var occupants_directory = `${GetInstallDirectory()}/workflows/scripts/automotive_assessments/occupants`;
            var sub_folders = ["", `/${occupant.supplier}`, `/${occupant.product}`, `/${occupant.physiology}`];
            var slash = "/";
        } else {
            var occupants_directory = `${GetInstallDirectory()}\\workflows\\scripts\\automotive_assessments\\occupants`;
            var sub_folders = ["", `\\${occupant.supplier}`, `\\${occupant.product}`, `\\${occupant.physiology}`];
            var slash = "\\";
        }

        //check each subfolder exists and make it if it does not.
        for (let sub_folder of sub_folders) {
            occupants_directory += sub_folder;
            if (!File.Exists(occupants_directory)) {
                //make it if it doesn't exist
                if (File.Mkdir(occupants_directory)) {
                    Message(`Successfully made ${occupants_directory} directory.`);
                } else {
                    Message(`Failed to make ${occupants_directory} directory.`);
                    return;
                }
            }
        }

        occupants_directory += slash;

        let output_filename = `${occupants_directory}${root_name}${version}.json`;

        if (!output_filename) return;

        if (File.Exists(output_filename)) {
            var answer = Window.Warning(
                "Warning",
                `${output_filename} already exists. Are you sure you want to overwrite it?`,
                Window.YES | Window.CANCEL
            );
            if (answer == Window.CANCEL) return;
            Message(`Overwritting ${output_filename} with new data`);
        }

        //construct an Occupant so that we can call toJSON to write it out in a consistent format
        let occupant_json = new Occupant(
            occupant.supplier,
            occupant.product,
            occupant.physiology,
            version,
            occupant.body_parts,
            occupant.GetChestRotationFactors()
        ); //make a copy of occupant

        /* Write occupant json file */

        let f = new File(output_filename, File.WRITE);

        let json = occupant_json.toJSON();

        for (let p of json.body_parts) {
            for (let e of p.entities) {
                //try and add a history title if it can be found and is not blank

                if (!e.history_title) {
                    let history_title = BaseEntity.GetHistoryTitleForId(e.entity_type, e.id);
                    if (history_title != "") {
                        e.history_title = history_title;
                    }
                }
            }
        }

        f.Write(JSON.stringify(json, null, 4)); //4 spaces
        f.Close();

        Message("Written occupant to " + output_filename);
    }
}

/**
 * Saves the selected data to a workflow JSON file
 */
function save_to_file() {
    /* Get data to write to file */

    let user_data = get_user_data();

    //do not save the workflow if some required user data is missing
    if (!user_data) return;

    let extra = get_extra_data();

    /* Ask the user where to write it */

    var output_filename = Window.GetFile(".json", true);

    if (output_filename == null) return;

    /* API call to write workflow file */

    Workflow.WriteToFile(user_data, output_filename, workflow_definition_filename, extra);

    Message("Written workflow file to " + output_filename);
}

/**
 * Saves the selected data to the keyword file
 */
function save_to_model() {
    /* Get data to write to file */

    let user_data = get_user_data();

    //do not save the workflow if some required user data is missing
    if (!user_data) return;

    let extra = get_extra_data();

    /* Ask the user which model to write it to */

    var model = Model.Select("Select the model to write to");

    if (model == null) return;

    /* API call to write workflow to a model */

    Workflow.WriteToModel(user_data, model, workflow_definition_filename, extra);

    Message(
        "Workflow data added to post *END data. You need to write out the model from the main Model->Write menu to save the additions."
    );
}

/**
 * onclick callback for add and edit buttons
 */
function add_edit_occupant() {
    set_up_occupant_window(this.side, this.front_rear);
}

/**
 * Delete the associated occupant
 */
function delete_occupant() {
    for (let i = 0; i < gui.occupants.length; i++) {
        if (gui.occupants[i].side == this.side && gui.occupants[i].front_rear == this.front_rear) {
            gui.occupants.splice(i, 1);
            gui.current_occupant = null;
            Message(`Deleted ${this.front_rear} ${this.side} occupant`);
            break;
        }
    }

    update_main_window();
}

/**
 * Delete all the occupants
 */
function delete_all_occupants() {
    gui.occupants = [];
    gui.current_occupant = null;
    Message(`Deleted all occupants`);

    update_main_window();
}

/**
 * Flip all the occupant seats right/left all the occupants and correctly set driver
 */
function flip_occupants() {
    for (let occupant of gui.occupants) {
        Message(`Flipping ${occupant.name}`);
        if (occupant.side == WorkflowOccupant.LEFT) occupant.side = WorkflowOccupant.RIGHT;
        else if (occupant.side == WorkflowOccupant.RIGHT) occupant.side = WorkflowOccupant.LEFT;

        if (occupant.front_rear == WorkflowOccupant.FRONT) {
            if (occupant.position == WorkflowOccupant.DRIVER) occupant.position = WorkflowOccupant.PASSENGER;
            else occupant.position = WorkflowOccupant.DRIVER;
        }
    }

    Message(`swapped occupant seats (right/left)`);

    update_main_window();
}

/**
 * Delete the selected structures
 */
function delete_selected_structure() {
    for (let widget_item of gui.wdw_pre_automotive.lbx_structures.WidgetItems()) {
        if (widget_item.selected) {
            for (let i = 0; i < gui.structures.length; i++) {
                if (gui.structures[i] == widget_item.structure) {
                    gui.structures.splice(i, 1);
                    gui.current_structure = null;
                    break;
                }
            }
        }
    }

    update_main_window();
}

/**
 * Close the occupant window
 */
function close_occupant_window() {
    gui.wdw_occupant.Hide();
}

/**
 * Update the current occupant with the data selected by the user
 * and then close the occupant window
 */
function update_current_occupant_and_close() {
    let updated = update_current_occupant();

    if (updated) {
        gui.wdw_occupant.Hide();
        update_main_window();
    }
}

/**
 * Update the current occupant with the data selected by the user
 * Returns true if the occupant was updated, false otherwise
 * @return {boolean}
 */
function update_current_occupant() {
    let wdw = gui.wdw_occupant;

    /* A null current occupant means a new one is being added.
     * Create a new occupant with the selected values. */

    if (gui.current_occupant == null) {
        /* If an occupant in this position/side/front_rear already exists ask the user if
         * they want to overwrite it and if they say yes set the current occupant to it.
         *
         * If it doesn't already exist create a new one and add it to the gui.occupants list */

        /** @type {?WorkflowOccupant} */
        let existing_occupant = null;
        for (let occupant of gui.occupants) {
            if (
                occupant.position == wdw.cbx_occupant_position.selectedItem.text &&
                occupant.side == wdw.cbx_occupant_side.selectedItem.text &&
                occupant.front_rear == wdw.cbx_occupant_front_rear.selectedItem.text
            ) {
                existing_occupant = occupant;
                break;
            }
        }

        if (existing_occupant) {
            let answer = Window.Message(
                "Occupant already defined",
                `'${existing_occupant.toString()}' occupant already exists. Update with new values?`,
                Window.YES | Window.NO
            );

            if (answer == Window.NO) {
                return false;
            } else {
                gui.current_occupant = existing_occupant;
            }
        } else {
            gui.current_occupant = GetOccupantFromGUI();

            gui.occupants.push(gui.current_occupant);
        }
    } else {
        /* Update the current occupant with the selected values */
        gui.current_occupant.SetOccupantFields = wdw.cbx_occupant_name.selectedItem.text;
        gui.current_occupant.position = wdw.cbx_occupant_position.selectedItem.text;
        gui.current_occupant.side = wdw.cbx_occupant_side.selectedItem.text;
        gui.current_occupant.front_rear = wdw.cbx_occupant_front_rear.selectedItem.text;
    }

    /* Set the entity IDs */
    update_occupant_entity_ids_from_gui(gui.current_occupant);

    return true;
}

/**
 * update the ids for the occupant
 * @param {WorkflowOccupant|Occupant} occupant
 */
function update_occupant_entity_ids_from_gui(occupant) {
    for (let body_part of occupant.body_parts) {
        for (let entity of body_part.entities) {
            let id = get_occupant_entity_id_from_widget_by_tag(occupant.name, entity.tag);
            entity.id = id;
        }
    }
}

/**
 * get occupant from the current gui inputs
 * @returns {WorkflowOccupant}
 */
function GetOccupantFromGUI() {
    let wdw = gui.wdw_occupant;

    return WorkflowOccupant.CreateWorkflowOccupantFromOccupant(
        wdw.cbx_occupant_name.selectedItem.text,
        wdw.cbx_occupant_position.selectedItem.text,
        wdw.cbx_occupant_side.selectedItem.text,
        wdw.cbx_occupant_front_rear.selectedItem.text
    );
}

/**
 * Close the structure window
 */
function close_structure_window() {
    gui.wdw_structure.Hide();
}

/**
 * Update the current structure with the data selected by the user
 * and then close the structure window
 */
function update_current_structure_and_close() {
    let updated = update_current_structure();

    if (updated) {
        gui.wdw_structure.Hide();
        update_main_window();
    }
}

/**
 * Update the current structure with the data selected by the user
 * Returns true if the structure was updated, false otherwise
 * @return {boolean}
 */
function update_current_structure() {
    let wdw = gui.wdw_structure;

    /* A null current structure means a new one is being added.
     * Create a new structure with the selected values. */

    if (gui.current_structure == null) {
        /* If this structure type already exists ask the user if they want to overwrite it
         * and if they say yes set the current structure to it.
         *
         * If it doesn't already exist create a new one and add it to the gui.structures list
         */

        /** @type {?Structure} */
        let existing_structure = null;
        for (let structure of gui.structures) {
            if (structure.component_type == wdw.cbx_structure.selectedItem.text) {
                existing_structure = structure;
                break;
            }
        }

        if (existing_structure) {
            let answer = Window.Message(
                "Structure already defined",
                `'${existing_structure.component_type}' structure already exists. Update with new values?`,
                Window.YES | Window.NO
            );

            if (answer == Window.NO) {
                return false;
            } else {
                gui.current_structure = existing_structure;
            }
        } else {
            gui.current_structure = Structure.CreateStructure(wdw.cbx_structure.selectedItem.text);
            gui.structures.push(gui.current_structure);
        }
    } else {
        /* Update the current structure with the selected values */
        gui.current_structure.component_type = wdw.cbx_structure.selectedItem.text;
    }

    /* Set the entity IDs */

    for (let entity of gui.current_structure.entities) {
        let id = get_structure_entity_id_from_widget_by_tag(gui.current_structure.component_type, entity.tag);

        entity.id = id;
    }

    return true;
}

/**
 * Set the currently selected occupant when the occupant widget item is clicked
 */
function occupant_on_click() {
    if (this.selected) {
        gui.current_occupant = this.occupant;
    } else {
        gui.current_occupant = null;
    }
}

/**
 * Set the currently selected structure when the structure widget item is clicked
 */
function structure_on_click() {
    if (this.selected) {
        gui.current_structure = this.structure;
    } else {
        gui.current_structure = null;
    }

    update_structure_edit_and_delete_buttons();
}

function occupant_entity_on_popup() {
    /* Store what the pick is for so it can process it correctly  */
    gui.current_entity_popup = "occupant";

    /* Current entity tag and type */

    gui.current_entity_tag = this.entity_tag;
    gui.current_entity_type = this.entity_type;

    entity_on_popup(this.entity_type);
}

function structure_entity_on_popup() {
    /* Store what the pick is for so it can process it correctly  */
    gui.current_entity_popup = "structure";

    /* Current entity tag and type */

    gui.current_entity_tag = this.entity_tag;
    gui.current_entity_type = this.entity_type;

    entity_on_popup(this.entity_type);
}

/**
 * Set the currently selected entity when the entity popup is mapped from the textbox
 * and map the appropriate database history items for the entity type
 * @param {string} entity_type The entity type, e.g. BaseEntity.NODE
 */
function entity_on_popup(entity_type) {
    /* Change the text on the label to say what the type is */

    if (entity_type == BaseEntity.NODE) {
        gui.popup_select_entities.lbl_database_history.text = "DATABASE HISTORY NODE";
    } else if (entity_type == BaseEntity.BEAM_BASIC) {
        gui.popup_select_entities.lbl_database_history.text = "DATABASE HISTORY BEAM";
    } else if (entity_type == BaseEntity.SPRING_ROTATIONAL || entity_type == BaseEntity.SPRING_TRANSLATIONAL) {
        gui.popup_select_entities.lbl_database_history.text = "DATABASE HISTORY DISCRETE";
    } else if (entity_type == BaseEntity.XSECTION) {
        gui.popup_select_entities.lbl_database_history.text = "DATABASE XSECTION";
    }

    /* Get location to start mapping the database history buttons */

    let dy = 6;
    let y1 = gui.popup_select_entities.lbl_database_history.bottom;
    let y2 = y1 + dy;

    /* (Un)map and position the appropriate database history buttons for this entity type */

    for (let button of gui.database_buttons) {
        if (
            entity_type == button.entity_type ||
            (entity_type == BaseEntity.SPRING_ROTATIONAL && button.entity_type == BaseEntity.SPRING_TRANSLATIONAL)
        ) {
            button.top = y1;
            button.bottom = y2;

            button.Show();

            y1 = y2;
            y2 += dy;
        } else {
            button.Hide();
        }
    }
}

/**
 * Pick an entity
 */
function pick_entity() {
    let id = BaseEntity.Pick(gui.current_entity_type, gui.model);

    if (id == null) return;

    set_widget_entity_id_by_tag(gui.current_entity_popup, id);
}

/**
 * Select an entity
 */
function select_entity() {
    let id = BaseEntity.Select(gui.current_entity_type, gui.model);

    if (id == null) return;

    set_widget_entity_id_by_tag(gui.current_entity_popup, id);
}

/**
 * Select a database item
 */
function select_database_item() {
    let id = this.entity_label;

    set_widget_entity_id_by_tag(gui.current_entity_popup, id);
}

/**
 * Set the id on the structure or occupant entity widget by entity tag
 * @param {string} type Either "occupant" or "structure"
 * @param {number} id Enitity id
 */
function set_widget_entity_id_by_tag(type, id) {
    if (type != "occupant" && type != "structure") {
        ErrorMessage(`Invalid type '${type}' in set_widget_entity_id_by_tag`);
        return;
    }

    /* Call the appropriate function to set the id on the entity widget */

    if (type == "occupant") {
        set_occupant_widget_entity_id_by_tag(
            gui.wdw_occupant.cbx_occupant_name.selectedItem.text,
            gui.current_entity_tag,
            id
        );
    } else if (type == "structure") {
        set_structure_widget_entity_id_by_tag(
            gui.wdw_structure.cbx_structure.selectedItem.text,
            gui.current_entity_tag,
            id
        );
    }
}

/**
 * Update the main window
 */
function update_main_window() {
    let wdw = gui.wdw_pre_automotive;

    /* Recreate widget items in the structures listbox for each currently defined structure
     * Store the structure instance on the widget items to use when the edit/delete
     * buttons are clicked. */

    wdw.lbx_structures.RemoveAllWidgetItems();

    for (let structure of gui.structures) {
        let wi = new WidgetItem(wdw.lbx_structures, structure.toString());

        /* Store the structure on the widget item (to use when the edit or delete button are pressed) */

        // @ts-ignore
        wi.structure = structure;

        wi.onClick = structure_on_click; /* Updates the current occupant when clicked */

        /* Select the widget item if it's the current structure */

        if (gui.current_structure && wi.text == gui.current_structure.toString()) {
            wi.selected = true;
        }

        /* Colour the widget item red if any of the entity IDs are invalid for this structure */
        /* This doesn't work very well especially if you try it in different themes. Commented
         * out for now 
        if (are_structure_entity_ids_valid(occupant)) {
            wi.background = Widget.WHITE;
            wi.foreground = Widget.BLACK;
        } else {
            wi.background = Widget.DARKRED;
            wi.foreground = Widget.WHITE;
        }
        */
    }

    /* (De)activate the edit and delete buttons depending on whether there is a current structure */

    update_structure_edit_and_delete_buttons();

    /* If the number of widget items in a list box has decreased you can end up
     * with a grey row, so redraw the window */

    /* update vehicle gui */
    UpdateVehicleGUI();
    wdw.Redraw();
}

/**
 * Make the structure edit and delete buttons active or inactive
 * depending on whether an structure is selected.
 */
function update_structure_edit_and_delete_buttons() {
    let wdw = gui.wdw_pre_automotive;

    if (gui.current_structure) {
        wdw.btn_edit_structure.active = true;
        wdw.btn_delete_structure.active = true;
    } else {
        wdw.btn_edit_structure.active = false;
        wdw.btn_delete_structure.active = false;
    }
}

/**
 * Initialise the occupant window with the data from the current occupant
 */
function initialise_occupant_window() {
    let wdw = gui.wdw_occupant;

    if (gui.current_occupant) {
        /**we need to rebuild the name combo-box as it may only contain a filtered subset of name
         * from previous selections so first empty the combobox then add just the current name.
         * it doesn't matter that other name widget items are missing as the combo-box will be inactive.
         */
        wdw.cbx_occupant_name.RemoveAllWidgetItems();
        new WidgetItem(gui.wdw_occupant.cbx_occupant_name, gui.current_occupant.name);
        set_selected_widget_item(wdw.cbx_occupant_name, gui.current_occupant.name);
        set_selected_widget_item(wdw.cbx_occupant_position, gui.current_occupant.position);
        set_selected_widget_item(wdw.cbx_occupant_side, gui.current_occupant.side);
        set_selected_widget_item(wdw.cbx_occupant_front_rear, gui.current_occupant.front_rear);

        /* Set the text on the entity id widgets */

        let occupant_widgets = get_occupant_widgets(gui.wdw_occupant.cbx_occupant_name.selectedItem.text);

        for (let body_part_widgets of occupant_widgets.body_part_widgets) {
            for (let entity_widgets of body_part_widgets.entity_widgets) {
                let entity = gui.current_occupant.GetEntityByTag(entity_widgets.tag);

                if (entity) entity_widgets.textbox.text = entity.id;
            }
        }
    }

    update_occupant_window();
}

/**
 * filter the name drop-down based on the selected supplier, model and physiology filters.
 * All versions will be returned if none are selected already. i.e. their value is 'ALL'
 * @returns {string[]} occupant name names
 */
function filter_version_drop_down() {
    let wdw = gui.wdw_occupant;

    let selected_supplier = wdw.cbx_occupant_supplier.text;
    let selected_product = wdw.cbx_occupant_product.text;
    let selected_physiology = wdw.cbx_occupant_physiology.text;

    Message(`Selected supplier: ${selected_supplier}`);

    let versions = OccupantVersion.GetOnly(selected_supplier, selected_product, selected_physiology);

    if (versions.length == 0) {
        let unsupported_str = "";
        if (selected_supplier.toLowerCase() != "all") unsupported_str += selected_supplier + " ";
        if (selected_product.toLowerCase() != "all") unsupported_str += selected_product + " ";
        if (selected_physiology.toLowerCase() != "all") unsupported_str += selected_physiology + " ";

        Window.Warning("Unsupported Occupant", `No ${unsupported_str} occupants supported so removing all filters.`);

        //set filters to 'ALL'
        wdw.cbx_occupant_supplier.ItemAt(0).selected = true;
        wdw.cbx_occupant_product.ItemAt(0).selected = true;
        wdw.cbx_occupant_physiology.ItemAt(0).selected = true;

        //return all supported versions
        return WorkflowOccupant.Versions();
    }

    return versions;
}

/**
 * on change callback for supplier, model and physiology callbacks
 * to filter the name drop-down to make it easier to pick a name from a long list
 */
function update_filters() {
    /*update occupant_names based on current selection*/
    let occupant_names = filter_version_drop_down();
    if (occupant_names.length == 0) {
        Message('No currently supported occupants match the selected filter. Filter defaulting to "all"');
        set_selected_widget_item(this, "all");
        occupant_names = filter_version_drop_down();
    }

    update_occupant_names_combobox(occupant_names);
    update_occupant_window();
}

/**
 * occupant_names is an array of strings of occupant names
 *
 * @param {string[]} occupant_names
 */
function update_occupant_names_combobox(occupant_names) {
    gui.wdw_occupant.cbx_occupant_name.RemoveAllWidgetItems();
    for (let i = 0; i < occupant_names.length; i++)
        new WidgetItem(gui.wdw_occupant.cbx_occupant_name, occupant_names[i]);
}

/**
 * on click callback for filters to grey out invalid options
 */
function disable_invalid_filter_options() {
    //deactivate invalid filters (i.e those that will return no versions)
    let wdw = gui.wdw_occupant;

    let selected_supplier = wdw.cbx_occupant_supplier.text;
    let selected_product = wdw.cbx_occupant_product.text;
    let selected_physiology = wdw.cbx_occupant_physiology.text;
    let filter;
    /** the current drop down should not affect the filtered versions so assume it is 'all'*/
    if (this == wdw.cbx_occupant_supplier) {
        selected_supplier = "all";
        filter = "suppliers";
    } else if (this == wdw.cbx_occupant_product) {
        selected_product = "all";
        filter = "products";
    } else if (this == wdw.cbx_occupant_physiology) {
        selected_physiology = "all";
        filter = "physiologies";
    }

    let versions = OccupantVersion.GetOnly(selected_supplier, selected_product, selected_physiology);

    let filters = OccupantVersion.GetValidFilters(versions);

    let filter_options = filters[filter];
    for (let wi of this.WidgetItems()) {
        if (wi.selected) continue;
        //if not selected assume it is unselectable unless it is an option in filter_options
        wi.selectable = false;
        for (let option of filter_options) {
            if (wi.text == option) {
                wi.selectable = true;
                break;
            }
        }
    }
}

/**
 * Update the occupant window
 */
function update_occupant_window() {
    let wdw = gui.wdw_occupant;
    Message(`update_occupant_window`);
    /* get selected name name */
    let name = wdw.cbx_occupant_name.selectedItem.text;

    /* Populate the combobox widgets with data from the currently selected occupant */

    if (gui.current_occupant) {
        wdw.btn_update_occupant.text = "Update";

        /* Grey out name combobox - can't change the occupant name once it's been set
         * as the gui.current_occupant instance may not have the same body parts
         * as the one it's being changed to, so would require deleting the current occupant
         * and then recreating another one, which is a bit more complicated to manage.  */

        wdw.cbx_occupant_name.active = false;

        Message(`Update! ${gui.current_occupant.supplier}`);
        // update the values of the name meta data fields then make them inactive
        let occupant = OccupantVersion.GetFromName(name);
        set_selected_widget_item(wdw.cbx_occupant_supplier, occupant.supplier);
        set_selected_widget_item(wdw.cbx_occupant_product, occupant.product);
        set_selected_widget_item(wdw.cbx_occupant_physiology, occupant.physiology);

        wdw.cbx_occupant_supplier.active = false;
        wdw.cbx_occupant_product.active = false;
        wdw.cbx_occupant_physiology.active = false;
    } else {
        wdw.btn_update_occupant.text = "Add";
        wdw.cbx_occupant_name.active = true;
        wdw.cbx_occupant_supplier.active = true;
        wdw.cbx_occupant_product.active = true;
        wdw.cbx_occupant_physiology.active = true;
    }

    /* Reposition and show the relevant entity widgets */

    let top = wdw.btn_use_id_num.bottom + 1;
    let bottom = top + 6;

    for (let occupant_widgets of gui.occupant_widgets) {
        if (occupant_widgets.name == wdw.cbx_occupant_name.selectedItem.text) {
            occupant_widgets.Show();

            for (let body_part_widgets of occupant_widgets.body_part_widgets) {
                body_part_widgets.label.top = top;
                body_part_widgets.label.bottom = bottom;

                top = bottom + 1;
                bottom = top + 6;

                for (let entity_widgets of body_part_widgets.entity_widgets) {
                    entity_widgets.label.top = top;
                    entity_widgets.label.bottom = bottom;

                    entity_widgets.textbox.top = top;
                    entity_widgets.textbox.bottom = bottom;

                    /* Colour the textbox red if the entity ID is invalid */
                    if (is_entity_widget_id_valid(entity_widgets)) {
                        entity_widgets.textbox.category = Widget.CATEGORY_TEXT_BOX;
                    } else {
                        entity_widgets.textbox.category = Widget.CATEGORY_WARNING_ACTION;
                    }

                    top = bottom + 1;
                    bottom = top + 6;
                }
            }
        } else {
            /* Not relevant to this occupant name, so hide the widgets */
            occupant_widgets.Hide();
        }
    }

    wdw.Redraw();
}

/**
 * Initialise the structure window with the data from the current structure
 */
function initialise_structure_window() {
    let wdw = gui.wdw_structure;

    if (gui.current_structure) {
        set_selected_widget_item(wdw.cbx_structure, gui.current_structure.component_type);

        if (gui.current_structure.component_type == Structure.B_PILLAR) {
            update_b_pillar_widget_values();
        } else {
            /* Set the text on the entity id widgets */

            let structure_widgets = get_structure_widgets(gui.wdw_structure.cbx_structure.selectedItem.text);

            if (structure_widgets) {
                for (let entity_widgets of structure_widgets.entity_widgets) {
                    let entity = gui.current_structure.GetEntityByTag(entity_widgets.tag);

                    if (entity) entity_widgets.textbox.text = entity.id;
                }
            }
        }
    }

    update_structure_window();
}

/**
 * Update the structure window
 */
function update_structure_window() {
    let wdw = gui.wdw_structure;

    if (gui.current_structure) {
        wdw.btn_update_structure.text = "Update";

        /* Grey out the structure type combobox - can't change the structure type once it's been set
         * as the gui.current_structure instance won't have the same entities as the one it's
         * being changed to, so would require deleting the current structure and then recreating
         * another one, which is a bit more complicated to manage.  */

        wdw.cbx_structure.active = false;
    } else {
        wdw.btn_update_structure.text = "Add";
        wdw.cbx_structure.active = true;
    }

    /* Show the relevant entity widgets */

    for (let structure_widgets of gui.structure_widgets) {
        if (structure_widgets.structure_type == wdw.cbx_structure.selectedItem.text) {
            structure_widgets.Show();

            for (let entity_widgets of structure_widgets.entity_widgets) {
                /* Colour the textbox red if the entity ID is invalid */
                if (is_entity_widget_id_valid(entity_widgets)) {
                    entity_widgets.textbox.category = Widget.CATEGORY_TEXT_BOX;
                } else {
                    entity_widgets.textbox.category = Widget.CATEGORY_WARNING_ACTION;
                }
            }
        } else {
            /* Not relevant to this structure type, so hide the widgets */
            structure_widgets.Hide();
        }
    }

    /* B-Pillar widgets */

    for (let w in gui.b_pillar_widgets) {
        if (wdw.cbx_structure.selectedItem.text == Structure.B_PILLAR) {
            gui.b_pillar_widgets[w].Show();
        } else {
            gui.b_pillar_widgets[w].Hide();
        }
    }

    wdw.Redraw();
}

/**
 * Returns whether the value in the textbox is a valid entity ID,
 * i.e. does it exists in any of the models currently loaded
 * @param {BaseEntityWidgets} entity_widgets
 * @returns {boolean}
 */
function is_entity_widget_id_valid(entity_widgets) {
    return is_entity_id_valid(entity_widgets.textbox.text, entity_widgets.entity_type);
}

/**
 * Checks if an entity ID is valid (exists in the model)
 * @param {string} entity_id Entity ID to check
 * @param {string} entity_type Entity type
 * @returns {boolean}
 */
function is_entity_id_valid(entity_id, entity_type) {
    let id_number = parseInt(entity_id);

    if (isNaN(id_number)) {
        let history = History.First(gui.model);
        let xsec = CrossSection.First(gui.model);

        /* 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.heading == entity_id) return true;
                    history = history.Next();
                }
                break;

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

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

            case BaseEntity.XSECTION:
                while (xsec) {
                    if (xsec.heading == entity_id) return true;
                    xsec = xsec.Next();
                }

                break;
        }
    } else {
        /* It's a number, so check if the item exists */

        switch (entity_type) {
            case BaseEntity.NODE:
                if (Node.GetFromID(gui.model, id_number)) return true;
                break;

            case BaseEntity.BEAM_BASIC:
                if (Beam.GetFromID(gui.model, id_number)) return true;
                break;

            case BaseEntity.SPRING_TRANSLATIONAL:
            case BaseEntity.SPRING_ROTATIONAL:
                if (Discrete.GetFromID(gui.model, id_number)) return true;
                break;

            case BaseEntity.XSECTION:
                if (CrossSection.GetFromID(gui.model, id_number)) return true;
                break;

            case BaseEntity.JOINT:
                if (Joint.GetFromID(gui.model, id_number)) return true;
                break;
        }
    }

    /* Get here and it's not been found */

    return false;
}

/**
 * Checks if all the entity IDs in the occupant are valid
 * @param {WorkflowOccupant} occupant WorkflowOccupant instance to check
 * @returns {boolean}
 */
function are_occupant_entity_ids_valid(occupant) {
    for (let body_part of occupant.body_parts) {
        for (let entity of body_part.entities) {
            if (!is_entity_id_valid(entity.id.toString(), entity.entity_type)) return false;
        }
    }

    /* Get here and all the entity IDs are valid */

    return true;
}

/**
 * Checks if all the entity IDs in the structure are valid
 * @param {Structure} structure Structure instance to check
 * @returns {boolean}
 */
function are_structure_entity_ids_valid(structure) {
    for (let entity of structure.entities) {
        if (!is_entity_id_valid(entity.id.toString(), entity.entity_type)) return false;
    }

    /* Get here and all the entity IDs are valid */

    return true;
}

/**
 * Returns the entity widgets for an occupant name
 * @param {string} name Occupant name
 * @returns {OccupantWidgets}
 */
function get_occupant_widgets(name) {
    for (let occupant_widgets of gui.occupant_widgets) {
        if (occupant_widgets.name == name) {
            return occupant_widgets;
        }
    }

    return null;
}

/**
 * Returns the id on the entity widget by occupant name and entity tag
 * @param {string} name Occupant name
 * @param {string} tag Entity tag
 * @returns {string|number}
 */
function get_occupant_entity_id_from_widget_by_tag(name, tag) {
    let occupant_widgets = get_occupant_widgets(name);

    if (!occupant_widgets) return "";

    for (let body_part_widgets of occupant_widgets.body_part_widgets) {
        for (let entity_widgets of body_part_widgets.entity_widgets) {
            if (entity_widgets.tag == tag) {
                // @ts-ignore
                if (!isNaN(entity_widgets.textbox.text)) {
                    return parseInt(entity_widgets.textbox.text);
                } else {
                    return entity_widgets.textbox.text;
                }
            }
        }
    }

    return "";
}

/**
 * Set the id on the occupant entity widget by entity tag
 * @param {string} name Occupant name
 * @param {string} tag Entity tag
 * @param {number|string} id Entity id
 */
function set_occupant_widget_entity_id_by_tag(name, tag, id) {
    let occupant_widgets = get_occupant_widgets(name);

    if (!occupant_widgets) return;

    for (let body_part_widgets of occupant_widgets.body_part_widgets) {
        for (let entity_widgets of body_part_widgets.entity_widgets) {
            if (entity_widgets.tag == tag) {
                entity_widgets.textbox.text = id.toString();
            }
        }
    }
    update_occupant_window();
}

/**
 * Returns the entity widgets for an structure name
 * @param {string} structure_type Structure type
 * @returns {StructureWidgets}
 */
function get_structure_widgets(structure_type) {
    for (let structure_widgets of gui.structure_widgets) {
        if (structure_widgets.structure_type == structure_type) {
            return structure_widgets;
        }
    }

    return null;
}

/**
 * Returns the id on the entity widget by structure type and entity tag
 * @param {string} structure_type Structure type
 * @param {string} tag Entity tag
 * @returns {string|number}
 */
function get_structure_entity_id_from_widget_by_tag(structure_type, tag) {
    let structure_widgets = get_structure_widgets(structure_type);

    if (!structure_widgets) return "";

    for (let entity_widgets of structure_widgets.entity_widgets) {
        if (entity_widgets.tag == tag) {
            // @ts-ignore
            if (!isNaN(entity_widgets.textbox.text)) {
                return parseInt(entity_widgets.textbox.text);
            } else {
                return entity_widgets.textbox.text;
            }
        }
    }

    return "";
}

/**
 * Set the id on the structure entity widget by entity tag
 * @param {string} structure_type Structure type
 * @param {string} tag Entity tag
 * @param {number|string} id Entity id
 */
function set_structure_widget_entity_id_by_tag(structure_type, tag, id) {
    let structure_widgets = get_structure_widgets(structure_type);

    if (!structure_widgets) return;

    for (let entity_widgets of structure_widgets.entity_widgets) {
        if (entity_widgets.tag == tag) {
            entity_widgets.textbox.text = id.toString();
        }
    }

    update_structure_window();
}

/**
 * Set the combobox/listbox selected item from a value
 * @param {Widget} cbx Combobox
 * @param {string} value Value of the selected item
 * @returns {boolean} true if the widget was successfully set
 */
function set_selected_widget_item(cbx, value) {
    cbx.selectedItem = null;

    for (let widget_item of cbx.WidgetItems()) {
        Message(`widget_item.text ${widget_item.text}, value ${value}`);
        if (widget_item.text == value) {
            cbx.selectedItem = widget_item;
            return true;
        }
    }

    if (cbx.selectedItem == null) {
        WarningMessage(`Could not select ${value} as it did not match one of the combobox widget items`);

        return false;
    }
}

/**
 * Set the listbox selected items from a list of values
 * @param {Widget} lbx listbox
 * @param {string[]} values Values of the selected items
 */
function set_selected_widget_items(lbx, values) {
    lbx.selectedItem = null;

    for (let widget_item of lbx.WidgetItems()) {
        for (let value of values) {
            if (widget_item.text == value) {
                widget_item.selected = true;
                lbx.selectedItem = widget_item;
            }
        }
    }
}

function SetUpGUIFromUserData(user_data, unit_system) {
    //first set the regulation(s)
    /* Get the crash test type */
    set_selected_widget_item(gui.wdw_pre_automotive.cbx_protocol_test, user_data.crash_test);

    /* Get the regulations */
    set_selected_widget_items(gui.wdw_pre_automotive.lbx_protocol_regulations, user_data.regulations);

    /* Get the occupants */
    let occupants = user_data.occupants;

    if (occupants) {
        for (let occupant of occupants) {
            let o = WorkflowOccupant.CreateWorkflowOccupantFromOccupant(
                occupant.name,
                occupant.position,
                occupant.side,
                occupant.front_rear
            );

            gui.occupants.push(o);

            /* Set the entity ids */

            for (let body_part of occupant.body_parts) {
                for (let entity of body_part.entities) {
                    let oe = o.GetEntityByTag(entity.tag);

                    if (oe && entity.id) {
                        oe.id = entity.id;
                    }
                }
            }
        }
    }

    /* Get the structures */
    let structures = user_data.structures;

    if (structures) {
        for (let structure of structures) {
            let s = Structure.CreateStructure(structure.component_type);

            gui.structures.push(s);

            /* Set the entity ids */

            for (let entity of structure.entities) {
                let se = s.GetEntityByTag(entity.tag);

                if (se && entity.id) {
                    se.id = entity.id;
                }
            }
        }
    }

    /* Get the model unit system and set the combobox selected item */

    //let unit_system = Workflow.ModelUnitSystemFromIndex(i);

    gui.wdw_pre_automotive.cbx_unit_system.SetSelectedUnitSystem(unit_system);
}

/**
 * Object to store data for B-Pillar
 * @typedef {Object} BPillarStructure
 * @property {string} cut_section_method Cut section method
 * @property {number[]} cut_section_nodes Cut section nodes
 * @property {number[]} pre_crash_parts Pre-crash parts
 * @property {number[]} post_crash_parts Post-crash parts
 * @property {number[]} shift_deform_nodes Shift deform nodes
 * @property {number} ground_z Ground Z coordinate
 * @property {number} seat_centre_y Seat centre Y coordinate
 * @property {number} h_point_z H-Point Z coordinate
 */

/**
 * The user data object in the workflow file
 * @typedef {Object} UserData
 * @property {string} crash_test Crash test
 * @property {string[]} regulations Regulations
 * @property {string} version Version
 * @property {string} drive_side either 'LHD' or 'RHD' vehicle
 * @property {WorkflowOccupant[]} occupants Array of WorkflowOccupants
 * @property {Structure[]} structures Array of Structures
 * @property {BPillarStructure} b_pillar B-Pillar structure
 */