modules/pre/pre_automotive_assessment.mjs

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

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

import { WorkflowUnitsCombobox } from "../../../modules/units.mjs";
import { OccupantVersion } from "../shared/occupant_version.mjs";
import { Protocol, Protocols } from "../shared/protocols.mjs";
import { ProtocolVehicle, VehicleOccupant } from "../shared/vehicle.mjs";
import { WorkflowOccupant } from "../shared/workflow_occupant.mjs";
import { ReadJSON } from "../shared/file_helper.mjs";
import { JobControl } from "./reporter_job_control.mjs";
export {
    RunAutomotiveAssessmentTool,
    get_user_data,
    gui,
    add_new_occupant,
    filter_version_drop_down,
    update_occupant_names_combobox,
    set_selected_widget_item,
    GetProtocolVehicle,
    JSPath,
    Protocols
};

/* 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 global_theme;
let workflow_definition_filename;

/**
 * this function runs the PRIMER automotive assessments tool GUI and returns a JobControl status for use in REPORTER
 * @param {string} [reporter_user_data_file="PRIMER"] either "PRIMER" or "REPORTER"
 * @returns {string} job_control
 */
function RunAutomotiveAssessmentTool(reporter_user_data_file = "PRIMER") {
    workflow_definition_filename = Workflow.WorkflowDefinitionFilename("Automotive Assessments");

    Message(`workflow_definition_filename ${workflow_definition_filename}`);

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

    /* Set global theme */
    global_theme = Window.Theme(Window.THEME_CURRENT);

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

    if (gui) {
        try {
            /* set default JobControl return to be "Abort" */

            gui.job_control = JobControl.ABORT;

            /* 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();

            /* handle REPORTER mode*/

            if (reporter_user_data_file != "PRIMER") {
                Message(`looking for reporter_user_data_file: ${reporter_user_data_file}`);

                if (File.Exists(reporter_user_data_file)) {
                    Message(`found reporter_user_data_file: ${reporter_user_data_file}`);
                    Message(JSON.stringify(reporter_user_data_file, null, 4));

                    let reporter_user_data = ReadJSON(reporter_user_data_file);

                    /*if we are in REPORTER mode then we need to update the gui based on the primer data
                    and ensure it never changes so disable regs */

                    for (let protocol_data of reporter_user_data) {
                        // let protocol = Protocol.FromJSON(protocol_data);

                        Message(`protocol_data:`);
                        Message(JSON.stringify(protocol_data, null, 4));

                        protocol_data.vehicle = ProtocolVehicle.FromJSON(protocol_data.vehicle);

                        Window.Information(
                            "REPORTER User Data Required",
                            `Some of the user inputs required for ${protocol_data.regulation} ${protocol_data.crash_test} ${protocol_data.version} ` +
                                `in this report are missing or invalid (see list below):\n${protocol_data.description.join(
                                    "\n"
                                )}`
                        );

                        /*NOTE at the moment only one test/reg/version combo is supported */
                        if (protocol_data.vehicle) SetUpGUIFromREPORTERUserData(protocol_data);
                        else {
                            WarningMessage(`No ProtocolVehicle could not be constructed from reporter_user_data`);
                        }
                    }
                } else {
                    Message(`could not find reporter_user_data_file: ${reporter_user_data_file}`);
                }
            }

            /* Update main window - both occupants and structures */

            update_main_window();

            /* Show the main window */

            gui.wdw_pre_automotive.Show(false);

            return gui.job_control;
        } 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() {
    //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;

    /*add the vehicle - Note this needs to be after drive side is set!*/
    update_vehicle_image();

    /*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 of add, edit, delete (and occupant) 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`];
        let occupant_btn_widget = gui.wdw_pre_automotive[`lbl_${front_rear}_${side}_occupant`];

        //** add onClick callbacks to add, edit, delete */
        add_btn_widget.onClick = add_edit_occupant;
        edit_btn_widget.onClick = add_edit_occupant;
        delete_btn_widget.onClick = delete_occupant;

        //** add onClick callback, front_rear side properties to occupant buttons */
        occupant_btn_widget.onClick = add_edit_occupant;
        occupant_btn_widget.front_rear = front_rear;
        occupant_btn_widget.side = side;

        /*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 image based on the global_theme
 */
function update_vehicle_image() {
    let theme_str = `Light`;

    switch (Window.Theme()) {
        case Window.THEME_DARK:
            theme_str = `Dark`;
            break;
        case Window.THEME_CLASSIC:
            theme_str = `Classic`;
            break;
    }

    let vehicle_image_path = `${JSPath.GetImagesDirectory()}/vehicles/${theme_str}${gui.drive_side}_250px.png`;

    if (!File.Exists(vehicle_image_path)) {
        throw Error(`${vehicle_image_path} does not exist`);
    }

    gui.wdw_pre_automotive.lbl_vehicle.ReadImageFile(vehicle_image_path, Widget.CENTRE | Widget.MIDDLE);
    draw_lines();
    gui.wdw_pre_automotive.Redraw();
}

function draw_lines() {
    let widget = gui.wdw_pre_automotive.lbl_vehicle;
    widget.Clear();

    widget.lineWidth = 3;

    let line_colour = Widget.GREY;

    switch (Window.Theme()) {
        case Window.THEME_DARK:
            line_colour = Widget.GREY;
            break;
        case Window.THEME_LIGHT:
            line_colour = Widget.GREY;
            break;
        case Window.THEME_CLASSIC:
            line_colour = Widget.BLACK;
            break;
    }

    widget.xResolution = 250;
    widget.yResolution = 250;

    let front_diagonals = [75, 75, 105, 107];
    let rear_diagonals = [75, 167, 95, 136];
    let middle_line = [125, 135, 125, 200];

    //front left
    widget.Line(line_colour, front_diagonals[0], front_diagonals[1], front_diagonals[2], front_diagonals[3]);

    //front right
    widget.Line(
        line_colour,
        widget.xResolution - front_diagonals[0],
        front_diagonals[1],
        widget.xResolution - front_diagonals[2],
        front_diagonals[3]
    );

    //rear left
    widget.Line(line_colour, rear_diagonals[0], rear_diagonals[1], rear_diagonals[2], rear_diagonals[3]);

    //rear right
    widget.Line(
        line_colour,
        widget.xResolution - rear_diagonals[0],
        rear_diagonals[1],
        widget.xResolution - rear_diagonals[2],
        rear_diagonals[3]
    );

    //rear middle
    widget.Line(line_colour, middle_line[0], middle_line[1], middle_line[2], middle_line[3]);
}

/**
 * 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;
    }

    let current_theme = Window.Theme();

    if (current_theme != global_theme) {
        global_theme = current_theme;
        update_vehicle_image();
    }

    /*
    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.GetRow();
        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`];
        let occupant_btn_widget = gui.wdw_pre_automotive[`lbl_${front_rear}_${side}_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;

        //reset occupant_btn_widget
        occupant_btn_widget.category = Widget.CATEGORY_LABEL_POPUP; //this gives it a faint border
        // occupant_btn_widget.background = Widget.COLOUR_LATENT;

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

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

        //clear hover text
        selected_occupant_btn_widget.hover = "";

        //clear tick (or any other graphics) onoccupant_btn_widget
        occupant_btn_widget.Clear();

        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 the colour of occupant_btn_widget to latent (meaning the user could/should defined it by pressing Add button)
            occupant_btn_widget.category = Widget.NO_CATEGORY;
            occupant_btn_widget.background = 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_occupant_from_position(vehicle_occupant.position))) {
            let temp_occupant = OccupantVersion.GetFromName(wf_occupant.name);
            selected_occupant_btn_widget.text = `${temp_occupant.product}-${temp_occupant.physiology}`;
            selected_occupant_btn_widget.hover = `${wf_occupant.name}`;

            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;
                occupant_btn_widget.category = Widget.CATEGORY_LABEL_POPUP;
            } else if (selected_occupant_btn_widget.text == expected_occupant_text) {
                /**
                 * colour it green to show that the occupant defined for this seat
                 * is of the same product and physiology as required by the regulation
                 * a tick will be added if the definition is valid (i.e. has no latent values)*/
                occupant_btn_widget.category = Widget.CATEGORY_SAFE_ACTION;
                selected_occupant_btn_widget.category = Widget.CATEGORY_SAFE_ACTION;

                if (are_occupant_entity_ids_valid(wf_occupant)) {
                    /** add a tick to the occupant_btn_widget if the occupant is valid*/
                    occupant_btn_widget.Tick(Widget.BLACK);
                } else {
                    /**
                     * some fields are latent or invalid
                     * put <> around text and add explanation to hover text to show some fields are latent
                     */
                    selected_occupant_btn_widget.text = `<${selected_occupant_btn_widget.text}>`;
                    selected_occupant_btn_widget.hover += " has latent fields.";
                }
            } 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;
                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();
        }

        //duplicate hover text for occupant_btn_widget
        occupant_btn_widget.hover = selected_occupant_btn_widget.hover;

        // 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 positoin or null if not found
 * @param {string} position
 * @returns {?WorkflowOccupant}
 */
function get_occupant_from_position(position) {
    for (let wf_occupant of gui.occupants) {
        if (wf_occupant.position == position) {
            return wf_occupant;
        }
    }
    return null;
}

/**
 * returns the workflow occupant for that seat positoin or null if not found
 * @param {string} side
 * @param {string} front_rear
 * @param {string} drive_side
 * @returns {string}
 */
function get_seat_position(side, front_rear, drive_side) {
    switch (drive_side) {
        case VehicleOccupant.LHD:
            switch (front_rear) {
                case WorkflowOccupant.FRONT:
                    switch (side) {
                        case WorkflowOccupant.LEFT:
                            return WorkflowOccupant.DRIVER;
                        case WorkflowOccupant.RIGHT:
                            return WorkflowOccupant.FRONT_PASSENGER;
                    }
                case WorkflowOccupant.REAR:
                    switch (side) {
                        case WorkflowOccupant.LEFT:
                            return WorkflowOccupant.REAR_DRIVER_SIDE;
                        case WorkflowOccupant.RIGHT:
                            return WorkflowOccupant.REAR_PASSENGER_SIDE;
                        case WorkflowOccupant.MIDDLE:
                            return WorkflowOccupant.REAR_MIDDLE;
                    }
            }
        case VehicleOccupant.RHD:
            switch (front_rear) {
                case WorkflowOccupant.FRONT:
                    switch (side) {
                        case WorkflowOccupant.RIGHT:
                            return WorkflowOccupant.DRIVER;
                        case WorkflowOccupant.LEFT:
                            return WorkflowOccupant.FRONT_PASSENGER;
                    }
                case WorkflowOccupant.REAR:
                    switch (side) {
                        case WorkflowOccupant.RIGHT:
                            return WorkflowOccupant.REAR_DRIVER_SIDE;
                        case WorkflowOccupant.LEFT:
                            return WorkflowOccupant.REAR_PASSENGER_SIDE;
                        case WorkflowOccupant.MIDDLE:
                            return WorkflowOccupant.REAR_MIDDLE;
                    }
            }
        default:
            throw Error(
                `Could not get position for side: ${side}, front_rear: ${front_rear}, drive_side: ${drive_side}`
            );
    }
}

/**
 * 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;

    try {
        // if gui.current_occupant is not null then we are in edit mode
        gui.current_occupant = get_occupant_from_position(get_seat_position(side, front_rear, gui.drive_side));
    } catch (error) {
        ErrorMessage(`${error}\nOccupant window will not be shown.`);
        return;
    }

    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);

        //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 (defined once) */

    gui.structures = [];

    /* Array to store the Structure widgets */

    gui.structure_list_widgets = [];
    gui.structures_page = 1;

    /* Currently selected entity */

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

    /* B-Pillar entity values */

    initialise_b_pillar_entities();

    /* Head Excursion entity values */

    initialise_head_excursion_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.lbl_vehicle.onClick = update_vehicle_image;

    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.lbl_structures_first.onClick = structures_page_switchers_on_click;
    gui.wdw_pre_automotive.lbl_structures_previous.onClick = structures_page_switchers_on_click;
    gui.wdw_pre_automotive.lbl_structures_next.onClick = structures_page_switchers_on_click;
    gui.wdw_pre_automotive.lbl_structures_last.onClick = structures_page_switchers_on_click;

    gui.wdw_structure.btn_update_structure.onClick = update_all_structures_and_close;
    gui.wdw_structure.btn_clear_structure.onClick = clear_the_current_structure;
    gui.wdw_structure.btn_reset_structure.onClick = reset_the_current_structure;
    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 regulation (widget items) supported by currently selected crash test */
        update_regulations();
        /* Set up versions combo box with version (widget items) supported by currently selected crash test and regulation*/
        update_versions();
        /* Update Occupants gui */
        update_occupants_column();
    }
}

/**
 * return the vehicle based on the currently selected version
 * or if in REPORTER mode gui.reporter_protocol_vehicle should be set and its value is returned instead
 * @returns {?ProtocolVehicle}
 */
function GetProtocolVehicle() {
    if (gui.reporter_protocol_vehicle) {
        return gui.reporter_protocol_vehicle;
    }

    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;
}

/**
 * clear the combobox and then add the structure types as widget items to the combobox (in alphabetical order).
 * This is mainly used when the protocol changes
 * @param {string[]} structure_types
 */
function update_structures_combobox(structure_types) {
    let alphabetical_structure_types = structure_types.sort();

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

/**
 * Creates the widgets in the structures window and the structures stored on gui.structures
 */
function create_structures_widgets() {
    /* Get list of possible structures */
    let structure_types = Structure.Types();

    /* 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);

        /**
         * add all the structure to the gui.structures array
         * this means we create them only once and any values changed are set.
         * we should also add logic to reset/clear the structure so user doesn't have to manually set
         * all IDs to 0
         */
        gui.structures.push(structure);

        /* The B-Pillar and Head Excursion are non-standard structures that don't have
         * any StructureEntity instances defined (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 if (structure.component_type == Structure.HEAD_EXCURSION) {
            bottom = create_head_excursion_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;
    gui.b_pillar_widgets.txt_cut_section_node_1.onPopup = b_pillar_on_popup;

    gui.b_pillar_widgets.txt_cut_section_node_1.popupWindow = gui.popup_pick_node;
    gui.b_pillar_widgets.txt_cut_section_node_1.popupDirection = Widget.RIGHT;

    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;
    gui.b_pillar_widgets.txt_cut_section_node_2.onPopup = b_pillar_on_popup;

    gui.b_pillar_widgets.txt_cut_section_node_2.popupWindow = gui.popup_pick_node;
    gui.b_pillar_widgets.txt_cut_section_node_2.popupDirection = Widget.RIGHT;

    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;
    gui.b_pillar_widgets.txt_cut_section_node_3.onPopup = b_pillar_on_popup;

    gui.b_pillar_widgets.txt_cut_section_node_3.popupWindow = gui.popup_pick_node;
    gui.b_pillar_widgets.txt_cut_section_node_3.popupDirection = Widget.RIGHT;

    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;
    gui.b_pillar_widgets.txt_pre_crash_parts.onPopup = b_pillar_on_popup;

    gui.b_pillar_widgets.txt_pre_crash_parts.popupWindow = gui.popup_select_parts;
    gui.b_pillar_widgets.txt_pre_crash_parts.popupDirection = Widget.RIGHT;

    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;
    gui.b_pillar_widgets.txt_post_crash_parts.onPopup = b_pillar_on_popup;

    gui.b_pillar_widgets.txt_post_crash_parts.popupWindow = gui.popup_select_parts;
    gui.b_pillar_widgets.txt_post_crash_parts.popupDirection = Widget.RIGHT;

    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;
    gui.b_pillar_widgets.txt_shift_deform_node_1.onPopup = b_pillar_on_popup;

    gui.b_pillar_widgets.txt_shift_deform_node_1.popupWindow = gui.popup_pick_node;
    gui.b_pillar_widgets.txt_shift_deform_node_1.popupDirection = Widget.RIGHT;

    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;
    gui.b_pillar_widgets.txt_shift_deform_node_2.onPopup = b_pillar_on_popup;

    gui.b_pillar_widgets.txt_shift_deform_node_2.popupWindow = gui.popup_pick_node;
    gui.b_pillar_widgets.txt_shift_deform_node_2.popupDirection = Widget.RIGHT;

    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;
    gui.b_pillar_widgets.txt_shift_deform_node_3.onPopup = b_pillar_on_popup;

    gui.b_pillar_widgets.txt_shift_deform_node_3.popupWindow = gui.popup_pick_node;
    gui.b_pillar_widgets.txt_shift_deform_node_3.popupDirection = Widget.RIGHT;

    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;

    /* Callbacks for buttons on pick/select popups */

    gui.popup_pick_node.btn_pick.onClick = b_pillar_callback;
    gui.popup_select_parts.btn_select.onClick = b_pillar_callback;

    /* Update the values in the widgets */

    update_b_pillar_widgets();

    /* 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;

    gui.b_pillar_popup_widget = null;
}

/**
 * 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;

        case gui.popup_pick_node.btn_pick:
            let node_id = BaseEntity.Pick(BaseEntity.NODE, gui.model);

            if (node_id == null) break;

            if (gui.b_pillar_popup_widget == gui.b_pillar_widgets.txt_cut_section_node_1) {
                gui.b_pillar_cut_section_node_1 = node_id;
            } else if (gui.b_pillar_popup_widget == gui.b_pillar_widgets.txt_cut_section_node_2) {
                gui.b_pillar_cut_section_node_2 = node_id;
            } else if (gui.b_pillar_popup_widget == gui.b_pillar_widgets.txt_cut_section_node_3) {
                gui.b_pillar_cut_section_node_3 = node_id;
            } else if (gui.b_pillar_popup_widget == gui.b_pillar_widgets.txt_shift_deform_node_1) {
                gui.b_pillar_shift_deform_node_1 = node_id;
            } else if (gui.b_pillar_popup_widget == gui.b_pillar_widgets.txt_shift_deform_node_2) {
                gui.b_pillar_shift_deform_node_2 = node_id;
            } else if (gui.b_pillar_popup_widget == gui.b_pillar_widgets.txt_shift_deform_node_3) {
                gui.b_pillar_shift_deform_node_3 = node_id;
            }

            break;

        case gui.popup_select_parts.btn_select:
            let part_ids = BaseEntity.Select(BaseEntity.PART, gui.model, true);

            if (part_ids == null) break;

            if (gui.b_pillar_popup_widget == gui.b_pillar_widgets.txt_pre_crash_parts) {
                gui.b_pillar_pre_crash_parts = part_ids;
            } else if (gui.b_pillar_popup_widget == gui.b_pillar_widgets.txt_post_crash_parts) {
                gui.b_pillar_post_crash_parts = part_ids;
            }

            break;

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

    update_b_pillar_widgets();
}

/**
 * Set which widget the B-Pillar popup was opened from
 */
function b_pillar_on_popup() {
    gui.b_pillar_popup_widget = this;
}

/**
 * Updates the B-Pillar widgets
 */
function update_b_pillar_widgets() {
    /* Update the values in the widgets */

    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;

    /* Update the widgets to indicate if the data is valid or not */

    let nodes = [
        { id: gui.b_pillar_cut_section_node_1, widget: gui.b_pillar_widgets.txt_cut_section_node_1 },
        { id: gui.b_pillar_cut_section_node_2, widget: gui.b_pillar_widgets.txt_cut_section_node_2 },
        { id: gui.b_pillar_cut_section_node_3, widget: gui.b_pillar_widgets.txt_cut_section_node_3 },
        { id: gui.b_pillar_shift_deform_node_1, widget: gui.b_pillar_widgets.txt_shift_deform_node_1 },
        { id: gui.b_pillar_shift_deform_node_2, widget: gui.b_pillar_widgets.txt_shift_deform_node_2 },
        { id: gui.b_pillar_shift_deform_node_3, widget: gui.b_pillar_widgets.txt_shift_deform_node_3 }
    ];

    for (let node of nodes) {
        if (is_entity_id_valid(node.id, BaseEntity.NODE)) {
            node.widget.category = Widget.CATEGORY_TEXT_BOX;
        } else {
            node.widget.category = Widget.CATEGORY_WARNING_ACTION;
        }
    }

    let parts = [
        { ids: gui.b_pillar_pre_crash_parts, widget: gui.b_pillar_widgets.txt_pre_crash_parts },
        { ids: gui.b_pillar_post_crash_parts, widget: gui.b_pillar_widgets.txt_post_crash_parts }
    ];

    for (let part of parts) {
        let valid = true;

        for (let id of part.ids) {
            if (!is_entity_id_valid(id, BaseEntity.PART)) {
                valid = false;
                break;
            }
        }

        if (valid) {
            part.widget.category = Widget.CATEGORY_TEXT_BOX;
        } else {
            part.widget.category = Widget.CATEGORY_WARNING_ACTION;
        }
    }

    gui.wdw_structure.Redraw();
}

/**
 * Creates the Head Excursion 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_head_excursion_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.head_excursion_widgets = {};

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

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

    gui.head_excursion_widgets.lbl_cut_section_thickness = new Widget(
        window,
        Widget.LABEL,
        1,
        82,
        top,
        bottom,
        "Cut Section Thickness"
    );
    gui.head_excursion_widgets.lbl_cut_section_thickness.justify = Widget.LEFT;

    gui.head_excursion_widgets.txt_cut_section_thickness = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.head_excursion_widgets.txt_cut_section_thickness.onChange = head_excursion_callback;

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

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

    gui.head_excursion_widgets.txt_cut_section_node = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.head_excursion_widgets.txt_cut_section_node.onChange = head_excursion_callback;
    gui.head_excursion_widgets.txt_cut_section_node.onPopup = head_excursion_on_popup;

    gui.head_excursion_widgets.txt_cut_section_node.popupWindow = gui.popup_pick_node;
    gui.head_excursion_widgets.txt_cut_section_node.popupDirection = Widget.RIGHT;

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

    gui.head_excursion_widgets.lbl_vehicle_direction = new Widget(
        window,
        Widget.LABEL,
        1,
        82,
        top,
        bottom,
        "Vehicle Direction"
    );
    gui.head_excursion_widgets.lbl_vehicle_direction.justify = Widget.LEFT;

    gui.head_excursion_widgets.cbx_vehicle_direction = new Widget(window, Widget.COMBOBOX, 83, 147, top, bottom);
    gui.head_excursion_widgets.cbx_vehicle_direction.onChange = head_excursion_callback;

    new WidgetItem(gui.head_excursion_widgets.cbx_vehicle_direction, "Positive X");
    new WidgetItem(gui.head_excursion_widgets.cbx_vehicle_direction, "Negative X");

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

    gui.head_excursion_widgets.lbl_head_parts = new Widget(window, Widget.LABEL, 1, 82, top, bottom, "Head Parts");
    gui.head_excursion_widgets.lbl_head_parts.justify = Widget.LEFT;

    gui.head_excursion_widgets.txt_head_parts = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.head_excursion_widgets.txt_head_parts.onChange = head_excursion_callback;
    gui.head_excursion_widgets.txt_head_parts.onPopup = head_excursion_on_popup;

    gui.head_excursion_widgets.txt_head_parts.popupWindow = gui.popup_select_parts;
    gui.head_excursion_widgets.txt_head_parts.popupDirection = Widget.RIGHT;

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

    gui.head_excursion_widgets.lbl_barrier_parts = new Widget(
        window,
        Widget.LABEL,
        1,
        82,
        top,
        bottom,
        "Barrier Parts"
    );
    gui.head_excursion_widgets.lbl_barrier_parts.justify = Widget.LEFT;

    gui.head_excursion_widgets.txt_barrier_parts = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.head_excursion_widgets.txt_barrier_parts.onChange = head_excursion_callback;
    gui.head_excursion_widgets.txt_barrier_parts.onPopup = head_excursion_on_popup;

    gui.head_excursion_widgets.txt_barrier_parts.popupWindow = gui.popup_select_parts;
    gui.head_excursion_widgets.txt_barrier_parts.popupDirection = Widget.RIGHT;

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

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

    gui.head_excursion_widgets.txt_shift_deform_node_1 = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.head_excursion_widgets.txt_shift_deform_node_1.onChange = head_excursion_callback;
    gui.head_excursion_widgets.txt_shift_deform_node_1.onPopup = head_excursion_on_popup;

    gui.head_excursion_widgets.txt_shift_deform_node_1.popupWindow = gui.popup_pick_node;
    gui.head_excursion_widgets.txt_shift_deform_node_1.popupDirection = Widget.RIGHT;

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

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

    gui.head_excursion_widgets.txt_shift_deform_node_2 = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.head_excursion_widgets.txt_shift_deform_node_2.onChange = head_excursion_callback;
    gui.head_excursion_widgets.txt_shift_deform_node_2.onPopup = head_excursion_on_popup;

    gui.head_excursion_widgets.txt_shift_deform_node_2.popupWindow = gui.popup_pick_node;
    gui.head_excursion_widgets.txt_shift_deform_node_2.popupDirection = Widget.RIGHT;

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

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

    gui.head_excursion_widgets.txt_shift_deform_node_3 = new Widget(window, Widget.TEXTBOX, 83, 147, top, bottom);
    gui.head_excursion_widgets.txt_shift_deform_node_3.onChange = head_excursion_callback;
    gui.head_excursion_widgets.txt_shift_deform_node_3.onPopup = head_excursion_on_popup;

    gui.head_excursion_widgets.txt_shift_deform_node_3.popupWindow = gui.popup_pick_node;
    gui.head_excursion_widgets.txt_shift_deform_node_3.popupDirection = Widget.RIGHT;

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

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

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

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

    gui.head_excursion_widgets.lbl_intrusion_from_seat_centre_y = new Widget(
        window,
        Widget.LABEL,
        1,
        82,
        top,
        bottom,
        "Intrusion From Seat Centre Y"
    );
    gui.head_excursion_widgets.lbl_intrusion_from_seat_centre_y.justify = Widget.LEFT;

    gui.head_excursion_widgets.txt_intrusion_from_seat_centre_y = new Widget(
        window,
        Widget.TEXTBOX,
        83,
        147,
        top,
        bottom
    );
    gui.head_excursion_widgets.txt_intrusion_from_seat_centre_y.onChange = head_excursion_callback;

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

    gui.head_excursion_widgets.lbl_countermeasure = new Widget(
        window,
        Widget.LABEL,
        1,
        82,
        top,
        bottom,
        "Countermeasure"
    );
    gui.head_excursion_widgets.lbl_countermeasure.justify = Widget.LEFT;

    gui.head_excursion_widgets.cbx_countermeasure = new Widget(window, Widget.COMBOBOX, 83, 147, top, bottom);
    gui.head_excursion_widgets.cbx_countermeasure.onChange = head_excursion_callback;

    new WidgetItem(gui.head_excursion_widgets.cbx_countermeasure, "Yes");
    new WidgetItem(gui.head_excursion_widgets.cbx_countermeasure, "No");

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

    gui.head_excursion_widgets.lbl_keyword_file = new Widget(window, Widget.LABEL, 1, 82, top, bottom, "Keyword File");
    gui.head_excursion_widgets.lbl_keyword_file.justify = Widget.LEFT;

    gui.head_excursion_widgets.txt_keyword_file = new Widget(window, Widget.TEXTBOX, 83, 136, top, bottom);
    gui.head_excursion_widgets.txt_keyword_file.onChange = head_excursion_callback;

    gui.head_excursion_widgets.btn_keyword_file = new Widget(window, Widget.BUTTON, 137, 147, top, bottom, "");
    gui.head_excursion_widgets.btn_keyword_file.DirectoryIcon(Widget.BLACK, Widget.YELLOW);
    gui.head_excursion_widgets.btn_keyword_file.onClick = head_excursion_callback;

    /* Callbacks for buttons on pick/select popups */

    gui.popup_pick_node.btn_pick.onClick = head_excursion_callback;
    gui.popup_select_parts.btn_select.onClick = head_excursion_callback;

    /* Update the values in the widgets */

    update_head_excursion_widgets();

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

    return bottom;
}

/**
 * Sets the initial values for the Head Excursion entities
 */
function initialise_head_excursion_entities() {
    gui.head_excursion_cut_section_thickness = 10;
    gui.head_excursion_cut_section_node = 0;
    gui.head_excursion_vehicle_direction = "Negative X";
    gui.head_excursion_head_parts = [];
    gui.head_excursion_barrier_parts = [];
    gui.head_excursion_shift_deform_node_1 = 0;
    gui.head_excursion_shift_deform_node_2 = 0;
    gui.head_excursion_shift_deform_node_3 = 0;
    gui.head_excursion_seat_centre_y = 0;
    gui.head_excursion_intrusion_from_seat_centre_y = 0;
    gui.head_excursion_countermeasure = "Yes";
    gui.head_excursion_keyword_file = "";

    gui.head_excursion_popup_widget = null;
}

/**
 * Callback function for B-Pillar widgets
 */
function head_excursion_callback() {
    switch (this) {
        case gui.head_excursion_widgets.txt_cut_section_node:
        case gui.head_excursion_widgets.txt_shift_deform_node_1:
        case gui.head_excursion_widgets.txt_shift_deform_node_2:
        case gui.head_excursion_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.head_excursion_widgets.txt_cut_section_node) {
                    gui.head_excursion_cut_section_node = new_int;
                } else if (this == gui.head_excursion_widgets.txt_shift_deform_node_1) {
                    gui.head_excursion_shift_deform_node_1 = new_int;
                } else if (this == gui.head_excursion_widgets.txt_shift_deform_node_2) {
                    gui.head_excursion_shift_deform_node_2 = new_int;
                } else if (this == gui.head_excursion_widgets.txt_shift_deform_node_3) {
                    gui.head_excursion_shift_deform_node_3 = new_int;
                }
            }

            break;

        case gui.head_excursion_widgets.txt_head_parts:
        case gui.head_excursion_widgets.txt_barrier_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.head_excursion_widgets.txt_head_parts) {
                gui.head_excursion_head_parts = new_parts;
            } else if (this == gui.head_excursion_widgets.txt_barrier_parts) {
                gui.head_excursion_barrier_parts = new_parts;
            }

            break;

        case gui.head_excursion_widgets.txt_cut_section_thickness:
        case gui.head_excursion_widgets.txt_seat_centre_y:
        case gui.head_excursion_widgets.txt_intrusion_from_seat_centre_y:
            let new_float = parseFloat(this.text);

            if (isNaN(new_float)) {
                WarningMessage("Invalid value, must be a number");
            } else {
                if (this == gui.head_excursion_widgets.txt_cut_section_thickness) {
                    gui.head_excursion_cut_section_thickness = new_float;
                } else if (this == gui.head_excursion_widgets.txt_seat_centre_y) {
                    gui.head_excursion_seat_centre_y = new_float;
                } else if (this == gui.head_excursion_widgets.txt_intrusion_from_seat_centre_y) {
                    gui.head_excursion_intrusion_from_seat_centre_y = new_float;
                }
            }

            break;

        case gui.popup_pick_node.btn_pick:
            let node_id = BaseEntity.Pick(BaseEntity.NODE, gui.model);

            if (node_id == null) break;

            if (gui.head_excursion_popup_widget == gui.head_excursion_widgets.txt_cut_section_node) {
                gui.head_excursion_cut_section_node = node_id;
            } else if (gui.head_excursion_popup_widget == gui.head_excursion_widgets.txt_shift_deform_node_1) {
                gui.head_excursion_shift_deform_node_1 = node_id;
            } else if (gui.head_excursion_popup_widget == gui.head_excursion_widgets.txt_shift_deform_node_2) {
                gui.head_excursion_shift_deform_node_2 = node_id;
            } else if (gui.head_excursion_popup_widget == gui.head_excursion_widgets.txt_shift_deform_node_3) {
                gui.head_excursion_shift_deform_node_3 = node_id;
            }

            break;

        case gui.popup_select_parts.btn_select:
            let part_ids = BaseEntity.Select(BaseEntity.PART, gui.model, true);

            if (part_ids == null) break;

            if (gui.head_excursion_popup_widget == gui.head_excursion_widgets.txt_head_parts) {
                gui.head_excursion_head_parts = part_ids;
            } else if (gui.head_excursion_popup_widget == gui.head_excursion_widgets.txt_barrier_parts) {
                gui.head_excursion_barrier_parts = part_ids;
            }

            break;

        case gui.head_excursion_widgets.cbx_vehicle_direction:
            gui.head_excursion_vehicle_direction = this.text;

            break;

        case gui.head_excursion_widgets.cbx_countermeasure:
            gui.head_excursion_countermeasure = this.text;

            break;

        case gui.head_excursion_widgets.txt_keyword_file:
            gui.head_excursion_keyword_file = this.text;
            break;

        case gui.head_excursion_widgets.btn_keyword_file:
            let filename = Window.GetFile("*", true);

            if (filename) gui.head_excursion_keyword_file = filename;

            break;

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

    update_head_excursion_widgets();
}

/**
 * Set which widget the Head Excursion popup was opened from
 */
function head_excursion_on_popup() {
    gui.head_excursion_popup_widget = this;
}

/**
 * Updates the Head Excursion widgets
 */
function update_head_excursion_widgets() {
    /* Update the values in the widgets */

    gui.head_excursion_widgets.txt_cut_section_thickness.text = gui.head_excursion_cut_section_thickness;
    gui.head_excursion_widgets.txt_cut_section_node.text = gui.head_excursion_cut_section_node;

    gui.head_excursion_widgets.txt_head_parts.text = gui.head_excursion_head_parts.join(" ");
    gui.head_excursion_widgets.txt_barrier_parts.text = gui.head_excursion_barrier_parts.join(" ");

    gui.head_excursion_widgets.txt_shift_deform_node_1.text = gui.head_excursion_shift_deform_node_1;
    gui.head_excursion_widgets.txt_shift_deform_node_2.text = gui.head_excursion_shift_deform_node_2;
    gui.head_excursion_widgets.txt_shift_deform_node_3.text = gui.head_excursion_shift_deform_node_3;

    gui.head_excursion_widgets.txt_seat_centre_y.text = gui.head_excursion_seat_centre_y;
    gui.head_excursion_widgets.txt_intrusion_from_seat_centre_y.text = gui.head_excursion_intrusion_from_seat_centre_y;

    gui.head_excursion_widgets.txt_keyword_file.text = gui.head_excursion_keyword_file;

    /* Update the widgets to indicate if the data is valid or not */

    let nodes = [
        { id: gui.head_excursion_cut_section_node, widget: gui.head_excursion_widgets.txt_cut_section_node },
        { id: gui.head_excursion_shift_deform_node_1, widget: gui.head_excursion_widgets.txt_shift_deform_node_1 },
        { id: gui.head_excursion_shift_deform_node_2, widget: gui.head_excursion_widgets.txt_shift_deform_node_2 },
        { id: gui.head_excursion_shift_deform_node_3, widget: gui.head_excursion_widgets.txt_shift_deform_node_3 }
    ];

    for (let node of nodes) {
        if (is_entity_id_valid(node.id, BaseEntity.NODE)) {
            node.widget.category = Widget.CATEGORY_TEXT_BOX;
        } else {
            node.widget.category = Widget.CATEGORY_WARNING_ACTION;
        }
    }

    let parts = [
        { ids: gui.head_excursion_head_parts, widget: gui.head_excursion_widgets.txt_head_parts },
        { ids: gui.head_excursion_barrier_parts, widget: gui.head_excursion_widgets.txt_barrier_parts }
    ];

    for (let part of parts) {
        let valid = true;

        for (let id of part.ids) {
            if (!is_entity_id_valid(id, BaseEntity.PART)) {
                valid = false;
                break;
            }
        }

        if (valid) {
            part.widget.category = Widget.CATEGORY_TEXT_BOX;
        } else {
            part.widget.category = Widget.CATEGORY_WARNING_ACTION;
        }
    }

    for (let wi of gui.head_excursion_widgets.cbx_vehicle_direction.WidgetItems()) {
        if (gui.head_excursion_vehicle_direction == wi.text) {
            wi.selected = true;
        }
    }

    for (let wi of gui.head_excursion_widgets.cbx_countermeasure.WidgetItems()) {
        if (gui.head_excursion_countermeasure == wi.text) {
            wi.selected = true;
        }
    }

    gui.wdw_structure.Redraw();
}

/**
 * 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();

    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].toString());

    /* 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;

        /* create a dummy occupant - defaults to Driver
        TODO - I think this can actually be changed to just use an occupant retrieved with
        let occupant = OccupantVersion.GetFromName(name);
        */
        let occupant = WorkflowOccupant.CreateWorkflowOccupantFromOccupant(name, WorkflowOccupant.DRIVER);

        /** @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) {
        //this is not a warning as it is expected that there will be no user data before the user has defined it
        Message("No Workflow data found for any of the models");
        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, "Automotive Assessments");

        /* 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) {
                WarningMessage(`Invalid Workflow data (crash test:"${user_data.crash_test}") for model \
                                ${i} so it will be ignored and the gui will default to undefined`);
                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) {
                WarningMessage(`Invalid Workflow data (regulation:"${user_data.regulations[0]}") for model \
                                ${i} so it will be ignored and the gui will default to undefined`);
                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) {
                WarningMessage(
                    `Invalid Workflow data (version:"${user_data.version}") for model` +
                        ` ${i} so it will be ignored and the gui will default to undefined`
                );
                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);

                    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 user_data_structure of user_data.structures) {
                    let structure = get_structure_from_type(user_data_structure.component_type);

                    if (structure) {
                        /* Set the entity ids */

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

                            if (se && entity.id) {
                                se.id = entity.id;
                            }
                        }
                    } else {
                        WarningMessage(
                            `user_data contains ${user_data_structure.component_type} structure which is invalid and not supported.`
                        );
                    }
                }
            }

            /* 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 Head Excursion data */
            /** @type {HeadExcursionStructure} */
            let head_excursion = user_data.head_excursion;

            if (head_excursion) {
                gui.head_excursion_cut_section_thickness = head_excursion.cut_section_thickness;
                gui.head_excursion_cut_section_node = head_excursion.cut_section_node;
                gui.head_excursion_vehicle_direction = head_excursion.vehicle_direction;
                gui.head_excursion_head_parts = head_excursion.head_parts;
                gui.head_excursion_barrier_parts = head_excursion.barrier_parts;
                gui.head_excursion_shift_deform_node_1 = head_excursion.shift_deform_nodes[0];
                gui.head_excursion_shift_deform_node_2 = head_excursion.shift_deform_nodes[1];
                gui.head_excursion_shift_deform_node_3 = head_excursion.shift_deform_nodes[2];
                gui.head_excursion_seat_centre_y = head_excursion.seat_centre_y;
                gui.head_excursion_intrusion_from_seat_centre_y = head_excursion.intrusion_from_seat_centre_y;
                gui.head_excursion_countermeasure = head_excursion.countermeasure;
                gui.head_excursion_keyword_file = head_excursion.keyword_file;
            }

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

            let unit_system = Workflow.ModelUnitSystemFromIndex(i, "Automotive Assessments");

            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} */
    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
    };

    /** @type {HeadExcursionStructure} */
    let head_excursion = {
        cut_section_thickness: gui.head_excursion_cut_section_thickness,
        cut_section_node: gui.head_excursion_cut_section_node,
        vehicle_direction: gui.head_excursion_vehicle_direction,
        head_parts: gui.head_excursion_head_parts,
        barrier_parts: gui.head_excursion_barrier_parts,
        shift_deform_nodes: [
            gui.head_excursion_shift_deform_node_1,
            gui.head_excursion_shift_deform_node_2,
            gui.head_excursion_shift_deform_node_3
        ],
        seat_centre_y: gui.head_excursion_seat_centre_y,
        intrusion_from_seat_centre_y: gui.head_excursion_intrusion_from_seat_centre_y,
        countermeasure: gui.head_excursion_countermeasure,
        keyword_file: gui.head_excursion_keyword_file
    };

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

/**
 * returns only the structures which are defined and supported by the current protocol
 * they are then written out as user_data
 * @returns {Structure[]}
 */
function get_defined_structures() {
    /* Structures required for this crash test/regulation/version combination
     * are stored in a list on the vehicle structures property */
    let structures = GetProtocolVehicle().structures;

    let valid_structures = [];
    for (let s of gui.structures) {
        if (are_structure_entity_ids_valid(s)) {
            // @ts-ignore
            if (structures.includes(s.component_type)) valid_structures.push(s);
        }
    }

    return valid_structures;
}

/**
 * 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();
    update_main_window(); //this also calls update_occupants_column()
}

/**
 * callback to trigger changes when regulation changes
 */
function regulation_changed() {
    update_versions();
    update_main_window(); //this also calls update_occupants_column()
}

/**
 * callback to trigger changes when version changes
 */
function version_changed() {
    update_main_window(); //this also calls update_occupants_column()
}

/**
 * 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;

    /**
     * we need to update the vehicle image because the drive side could have changed
     */
    update_vehicle_image();

    update_occupants_column();
}

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

/**
 * 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 edit the current structure (that user clicked)
 */
function edit_structure() {
    initialise_structure_window(this.structure_type);

    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() {
    let position = get_seat_position(this.side, this.front_rear, gui.drive_side);

    for (let i = 0; i < gui.occupants.length; i++) {
        if (gui.occupants[i].position == position) {
            gui.occupants.splice(i, 1);
            gui.current_occupant = null;
            Message(`Deleted ${this.front_rear} ${this.side} occupant (${position})`);
            break;
        }
    }

    update_occupants_column();
}

/**
 * Delete all the occupants
 */
function delete_all_occupants() {
    for (let occupant of gui.occupants) {
        Message(`Deleted ${occupant.position}`);
    }
    gui.occupants = [];
    gui.current_occupant = null;
    Message(`Deleted all occupants`);

    update_occupants_column();
}

/**
 * Flip all the occupant seats right/left all the occupants and correctly set driver
 */
function flip_occupants() {
    for (let occupant of gui.occupants) {
        switch (occupant.position) {
            case WorkflowOccupant.DRIVER:
                occupant.position = WorkflowOccupant.FRONT_PASSENGER;
                break;
            case WorkflowOccupant.FRONT_PASSENGER:
                occupant.position = WorkflowOccupant.DRIVER;
                break;

            case WorkflowOccupant.REAR_DRIVER_SIDE:
                occupant.position = WorkflowOccupant.REAR_PASSENGER_SIDE;
                break;
            case WorkflowOccupant.REAR_PASSENGER_SIDE:
                occupant.position = WorkflowOccupant.REAR_DRIVER_SIDE;
                break;
        }
    }

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

    update_occupants_column();
}

/**
 * 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_occupants_column();
    }
}

/**
 * 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) {
                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;
    }

    /* 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
    );
}

/**
 * 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_all_structures_and_close() {
    update_all_structure();
    gui.wdw_structure.Hide();
    update_structures_column();
}

/**
 * Update the all the structureS with the data selected by the user (i.e. widget text fields)
 */
function update_all_structure() {
    for (let structure of gui.structures) {
        /* Set the entity IDs */
        // Message(`Updating ${structure.component_type} in gui.structures`);
        for (let entity of structure.entities) {
            let id = get_structure_entity_id_from_widget_by_tag(structure.component_type, entity.tag);

            entity.id = id;
        }
    }
}

/** reset the input fields for the current structure (to match those stored in corresponding structure in gui.structures)  */
function reset_the_current_structure() {
    let structure_type = gui.wdw_structure.cbx_structure.selectedItem.text;
    reset_structure_widgets_entity_fields(structure_type);
    update_structure_window();
}

/** clear/remove the input fields for the current structure and reset the values of the corresponding structure in gui.structures (i.e set to zero/undefined)  */
function clear_the_current_structure() {
    let wdw = gui.wdw_structure;
    let structure_type = wdw.cbx_structure.selectedItem.text;

    //we need to create overwrite the corresponding structure in gui.structures to set the values (back to 0)

    for (let i = 0; i < gui.structures.length; i++) {
        if (gui.structures[i].component_type == structure_type) {
            gui.structures[i] = Structure.CreateStructure(structure_type);
            continue;
        }
    }

    /**
     * set structure_widget text fields - this will be defaults as we have overwritten the structure in gui.structures above.
     */

    reset_the_current_structure();
}

/**
 * Store what the occupant entity pick is for so it can process it correctly
 */
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);
}

/**
 * Store what the structure entity pick is for so it can process it correctly
 */
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 {string|number|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;
    }

    /* Take the first id if the entity ids have been passed to this function as an array */

    if (Array.isArray(id)) {
        id = id[0];
    }

    /* 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
 * This updates the structures widget list based on the currently selected protocol
 * The widget category/colours are set to latent if the structure is not (fully) defined (i.e. valid).
 * In REPORTER mode only the required structures are latent if undefined. The other structures appear as not required (as per occupant styling)
 * It also calls an update to the vehicle GUI
 */
function update_main_window() {
    update_structures_column();
    update_occupants_column();
}

/**
 * Update the structures column widgets on the main window
 * This updates the structures widget list based on the currently selected protocol
 * The widget category/colours are set to latent if the structure is not (fully) defined (i.e. valid).
 * In REPORTER mode only the required structures are latent if undefined. The other structures appear as not required (as per occupant styling)
 */
function update_structures_column() {
    Message("update_structures_column");

    let wdw = gui.wdw_pre_automotive;

    /**
     * Recreate widget items in the structures column
     * Only the structures which are supported by the current protocol are shown
     * If they are not defined then colour them as latent
     * If they are not required then colour as grey (this is only for REPORTER mode)
     *
     * Store the structure instance on the widgets to use when the edit/delete
     * buttons are clicked. */

    /**
     * Remove all structures widgets before rebuilding them (also set current structure to null)
     */
    for (let current_structure_widget of gui.structure_list_widgets) {
        current_structure_widget.Delete();
    }
    gui.structure_list_widgets = [];

    let protocol_structures = GetProtocolVehicle().structures;

    update_structures_combobox(protocol_structures);

    /**
     * if the list height exceeds the max_coordinate_of_bottom then we add the widgets from the top again (but they are hidden)
     * and only become active when the user presses a down/up arrow.
     */
    let overflow_list = false;
    let padding = 1;
    let top_coord = wdw.structures_background.top + padding;
    let widget_height = 6;
    let max_coordinate_of_bottom = wdw.lbl_structures_pg.top - padding;
    gui.number_of_structures_widgets_per_pg = Math.floor(
        (max_coordinate_of_bottom - top_coord) / (widget_height + padding)
    );

    for (let structure of gui.structures) {
        /**
         * only add the widgets for this protocol's structures
         */

        // @ts-ignore
        if (!protocol_structures.includes(structure.component_type)) continue;

        let wi = new Widget(
            wdw,
            Widget.LABEL,
            wdw.structures_background.left + padding,
            wdw.structures_background.right - padding,
            top_coord,
            top_coord + widget_height,
            `<${structure.toString()}>`
        );

        // if (overflow_list)
        wi.Hide();

        top_coord += widget_height + padding;

        if (top_coord + widget_height > max_coordinate_of_bottom) {
            overflow_list = true;
            /** reset top coord */
            top_coord = wdw.structures_background.top + padding;
        }

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

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

        /**
         * set default colour - assumed to be latent
         */
        wi.category = Widget.NO_CATEGORY;
        wi.foreground = Widget.COLOUR_LATENT;
        wi.background = Widget.COLOUR_NEUTRAL;

        wi.onClick = edit_structure; /* Opens the edit pane for the structure when clicked */

        if (are_structure_entity_ids_valid(structure)) {
            wi.category = Widget.CATEGORY_SAFE_ACTION;
            wi.text = structure.toString();
        }

        gui.structure_list_widgets.push(wi);
    }

    if (overflow_list) {
        /**
         * set overflow page text
         */

        // gui.structures_page = 1;
        wdw.lbl_structures_first.Show();
        wdw.lbl_structures_previous.Show();
        wdw.lbl_structures_next.Show();
        wdw.lbl_structures_last.Show();
        wdw.lbl_structures_pg.Show();
    } else {
        wdw.lbl_structures_first.Hide();
        wdw.lbl_structures_previous.Hide();
        wdw.lbl_structures_next.Hide();
        wdw.lbl_structures_last.Hide();
        wdw.lbl_structures_pg.Hide();
    }

    set_structures_page(gui.structures_page);
}

/**
 * used to set the page (subset of the list) for the structures widgets
 * @param {number} page
 */
function set_structures_page(page) {
    let wdw = gui.wdw_pre_automotive;
    let out_of_page = Math.ceil(gui.structure_list_widgets.length / gui.number_of_structures_widgets_per_pg);

    //go to last page
    if (page == -1 || page > out_of_page) page = out_of_page;
    else if (page < 1) page = 1;
    wdw.lbl_structures_pg.text = `${page}/${out_of_page}`;

    let index = 1;
    for (let wi of gui.structure_list_widgets) {
        if (Math.ceil(index / gui.number_of_structures_widgets_per_pg) == page) {
            wi.Show();
        } else wi.Hide();

        index++;
    }

    //set the structures page to the new page value one if needed to be updated
    gui.structures_page = page;

    wdw.Redraw();
}

/**
 * onclick callback for switching structures page
 */
function structures_page_switchers_on_click() {
    switch (this.text) {
        case "<<":
            gui.structures_page = 1;
            break;
        case "<":
            gui.structures_page--;
            break;
        case ">":
            gui.structures_page++;
            break;
        case ">>":
            gui.structures_page = -1;
            break;
        default:
            gui.structures_page = 1;
    }

    set_structures_page(gui.structures_page);
}

/**
 * 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 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
 * @param {string} structure_type
 */
function initialise_structure_window(structure_type) {
    let wdw = gui.wdw_structure;

    set_selected_widget_item(wdw.cbx_structure, structure_type);

    /**
     * reset all the widget fields to match entities of Structures stored in gui.structures
     */
    for (let wi of gui.wdw_structure.cbx_structure.WidgetItems()) {
        reset_structure_widgets_entity_fields(wi.text);
    }

    update_structure_window();
}

/**
 * this (re)sets the structure widget entity fields to be those stored on matching structure_type in gui.structures array
 * @param {string} structure_type
 */
function reset_structure_widgets_entity_fields(structure_type) {
    if (structure_type == Structure.B_PILLAR) {
        update_b_pillar_widgets();
    } else if (structure_type == Structure.HEAD_EXCURSION) {
        update_head_excursion_widgets();
    } else {
        /* Set the text on the entity id widgets */

        let structure_widgets = get_structure_widgets(structure_type);

        let structure = get_structure_from_type(structure_type);

        // Message(`Reset Structure = ${structure_type}`);

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

                if (entity) entity_widgets.textbox.text = entity.id.toString();
                else ErrorMessage(`entity not defined.`);
            }
        }
    }
}

/**
 * get structure from gui.structures
 * @param {string} structure_type
 * @returns {?Structure}
 */
function get_structure_from_type(structure_type) {
    for (let structure of gui.structures) {
        if (structure.component_type == structure_type) return structure;
    }

    ErrorMessage(`Could not find ${structure_type} in gui.structures.`);
}

/**
 * Update the structure window - show widgets for current structure type and hide others
 */
function update_structure_window() {
    let wdw = gui.wdw_structure;

    /* 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();
        }
    }

    /* Head Excursion widgets */

    for (let w in gui.head_excursion_widgets) {
        if (wdw.cbx_structure.selectedItem.text == Structure.HEAD_EXCURSION) {
            gui.head_excursion_widgets[w].Show();
        } else {
            gui.head_excursion_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;

            case BaseEntity.PART:
                if (Part.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()) {
        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 FixCombobox(cbx, value) {
    cbx.RemoveAllWidgetItems();
    new WidgetItem(cbx, value);
    cbx.active = false;
}

/**
 * VehicleOccupant-like object
 * @typedef {Object} VehicleOccupantJSON
 * @property {string} position Occupant position
 * @property {string} product Occupant product type
 * @property {string} physiology Occupant physiology
 */

/**
 * an array of VehicleOccupant-like objects
 * @typedef {Object} ProtocolVehicleJSON
 * @property {VehicleOccupantJSON[]} occupants
 * @property {string[]} structures
 */

/**
 * The user data object in the workflow file
 * @typedef {Object} ReporterUserData
 * @property {string} regulation Regulation
 * @property {string} crash_test Crash test
 * @property {string} version Protocol version
 * @property {string[]} description Protocol version
 * @property {ProtocolVehicleJSON} vehicle Protocol version
 */

/**
 *
 * @param {ReporterUserData} reporter_user_data
 */
function SetUpGUIFromREPORTERUserData(reporter_user_data) {
    /*Set the drive side from user data */
    set_vehicle_drive_side(VehicleOccupant.LHD); //reporter_user_data.drive_side);

    /*set the gui reporter_protocol_vehicle property so that it is used instead of a ProtocolVehicle from the Protocols class which
    the reporter_user_data.vehicle has only the positions defined for the positions requested by the REPORTER template */

    gui.reporter_protocol_vehicle = reporter_user_data.vehicle;

    // add_reporter_structures(reporter_user_data.vehicle.structures);

    /*set the crash test to match the REPORTER template crash test */
    FixCombobox(gui.wdw_pre_automotive.cbx_protocol_test, reporter_user_data.crash_test);

    /*set the regulation to match the REPORTER template crash test */
    FixCombobox(gui.wdw_pre_automotive.cbx_protocol_regulation, reporter_user_data.regulation);

    /*set the version to match the REPORTER template crash test */
    FixCombobox(gui.wdw_pre_automotive.cbx_protocol_version, reporter_user_data.version);

    update_occupants_column();

    /*override the save callbacks to set gui state and hide gui */

    gui.wdw_pre_automotive.btn_save_to_file.onClick = save_to_file_and_close;
    gui.wdw_pre_automotive.btn_save_to_model.onClick = save_to_model_and_close;
}

function save_to_file_and_close() {
    save_to_file();
    gui.job_control = JobControl.RUN;
    gui.wdw_pre_automotive.Hide();
}

function save_to_model_and_close() {
    let answer = Window.Question(
        "Save Model",
        `To write Post *END Automotive Assessment Workflow Data the model needs to be saved.\n` +
            `Are you sure you want to save the model? The model Master file will be overwritten so any changes to it will be irreversible`,
        Window.YES | Window.NO
    );

    if (answer == Window.NO) return;

    save_to_model();

    Message(`Writing Master file to ${gui.model.filename}.`);
    gui.model.Write(gui.model.filename, { method: Include.MASTER_ONLY });
    gui.job_control = JobControl.RUN;
    gui.wdw_pre_automotive.Hide();
}

/**
 * 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
 */

/**
 * Object to store data for Head Excursion
 * @typedef {Object} HeadExcursionStructure
 * @property {number} cut_section_thickness Cut section thickness
 * @property {number} cut_section_node Cut section node
 * @property {string} vehicle_direction Vehicle direction (either 'positive X' or 'negative X')
 * @property {number[]} head_parts Head parts
 * @property {number[]} barrier_parts Barrier parts
 * @property {number[]} shift_deform_nodes Shift deform nodes
 * @property {number} seat_centre_y Seat centre Y coordinate
 * @property {number} intrusion_from_seat_centre_y Intrusion From Seat centre Y coordinate
 * @property {string} countermeasure Countermeasure (either 'Yes' or 'No')
 * @property {string} keyword_file Keyword filename
 */

/**
 * 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
 * @property {HeadExcursionStructure} head_excursion Head Excursion structure
 */