modules/post/this/reporter_check_user_data.mjs

// module: TRUE
import { Regulation } from "../../shared/regulations.mjs";
import { CrashTest } from "../../shared/crash_tests.mjs";
import { Protocol, Protocols } from "../../shared/protocols.mjs";
import { AssessmentType } from "../../shared/assessment_types.mjs";
import { WorkflowOccupant } from "../../shared/workflow_occupant.mjs";
import { VehicleOccupant } from "../../shared/vehicle.mjs";
import { JobControl } from "../../pre/reporter_job_control.mjs";
import { WriteJSON } from "../../shared/file_helper.mjs";
import { Structure } from "../../shared/structure.mjs";

/**
 * The user data object in the workflow file
 * @typedef {Object} UserData
 * @property {string} crash_test Crash test
 * @property {string[]} regulations Regulations
 * @property {string} version Protocol version
 * @property {WorkflowOccupant[]} occupants Array of WorkflowOccupants
 */

/* Lists of all of the available parameters to check against */

let regulations = Regulation.GetAll();
let crash_tests = CrashTest.GetAll();
let occupant_assessment_types = AssessmentType.AllTHisOccupantAssessmentTypes();
let structure_assessment_types = AssessmentType.AllTHisStructureAssessmentTypes();
let positions = WorkflowOccupant.Positions();
let structure_components = Structure.Types();

/** Checks whether image filenames in REPORTER template are valid, and checks whether user data has
 *  required information. Returns job control action (JobControl.RUN, JobControl.SKIP, JobControl.ABORT).
 *  @param {string} output_dir REPORTER output directory for images etc.
 *  @param {string} drive_side VehicleOccupant.LHD or VehicleOccupant.RHD
 *  @returns {string}
 */
export function check_report_contents(output_dir, drive_side) {
    let reporter_data = new ReporterJobs();

    if (drive_side != VehicleOccupant.LHD && drive_side != VehicleOccupant.RHD) {
        ErrorMessage(
            `Unexpected drive side "${drive_side}". Expected "${VehicleOccupant.LHD}" or "${VehicleOccupant.RHD}".`
        );
        return JobControl.ABORT;
    }

    let f_contents_name = `${output_dir}/report_contents.lst`;
    if (!File.Exists(f_contents_name)) {
        ErrorMessage(`Report contents list does not exist: ${f_contents_name}`);
        return JobControl.ABORT;
    }
    if (!File.IsReadable(f_contents_name)) {
        ErrorMessage(`Report contents list could not be opened for reading: ${f_contents_name}`);
        return JobControl.ABORT;
    }
    let f_contents = new File(f_contents_name, File.READ);
    let line;
    let line_count = 0;
    Message(`Reading report contents list: ${f_contents_name}`);
    while ((line = f_contents.ReadLongLine()) != undefined) {
        line_count++;
        Message(`Processing line: ${line}`);

        /* Expect each line to contain a path to an image filename in the form:
         *
         *     ${output_dir}/${regulation}~${crash_test}~${version}~${occ_or_struct_str}~${assessment_type}.png
         *
         * Extract image filename (strip preceding path and trailing ".png"), leaving:
         *
         *     ${regulation}~${crash_test}~${version}~${occ_or_struct_str}~${assessment_type}
         */

        // /* Regex to split the filename into its components */

        let re_filename = /(.*)[\\\/]([^~]*)~([^~]*)~([^~]*)~([^~]*)~([^~]*)\.(.*)/;

        let match;
        if ((match = line.match(re_filename))) {
            var regulation = match[2];
            var crash_test = match[3];
            var version = match[4];
            var occ_or_struct_str = match[5];
            var assessment_type = match[6];
        } else {
            ErrorMessage(`Unexpected filename composition at line ${line_count}. Skipping.`);
            continue;
        }

        /* Check that filename components are valid */

        if (!regulations.includes(regulation)) {
            ErrorMessage(`Unexpected regulation "${regulation}" in filename at line ${line_count}. Skipping.`);
            continue;
        }
        if (!crash_tests.includes(crash_test)) {
            ErrorMessage(`Unexpected crash_test "${crash_test}" in filename at line ${line_count}. Skipping.`);
            continue;
        }
        let versions = Protocol.Versions(regulation, crash_test);
        if (!versions.includes(version)) {
            ErrorMessage(`Unexpected version "${version}" in filename at line ${line_count}. Skipping.`);
            continue;
        }
        if (
            !occupant_assessment_types.includes(assessment_type) &&
            !structure_assessment_types.includes(assessment_type)
        ) {
            ErrorMessage(
                `Unexpected assessment_type "${assessment_type}" in filename at line ${line_count}. Skipping.`
            );
            continue;
        }

        try {
            reporter_data.ProcessString(regulation, crash_test, version, occ_or_struct_str, assessment_type);
        } catch (error) {
            ErrorMessage(`${error}\nat line ${line_count}. Skipping.`);
        }
    } //end while read file lines

    f_contents.Close();

    /* Write the job data as a JSON file that will be picked up by T/HIS */
    let f_out_json_name = `${output_dir}/report_contents.json`;
    WriteJSON(f_out_json_name, { jobs: reporter_data.jobs });
    Message(`Created ${reporter_data.jobs.length} REPORTER job(s).`);

    /* Now check whether user data contains everything we need for our jobs */

    let primer_run_required = false;

    Message(`Reading user data...`);
    // TODO only support one model for now

    /** @type {UserData} */
    let user_data = null;

    try {
        if (Workflow.NumberOfSelectedModels() == 0) throw Error("No user data present");
        user_data = Workflow.ModelUserDataFromIndex(0, "Automotive Assessments");
    } catch (error) {
        //warn user if no user data exists or can be found
        WarningMessage(error);
    }

    /* Whenever we find missing data, we will add it to a list */
    let missing_data = [];
    let reporter_user_data = [];
    let user_occ = null;

    for (let job of reporter_data.jobs) {
        /**
         * check if protocol user_data is missing
         */
        if (user_data) {
            if (user_data.crash_test != job.crash_test) {
                missing_data.push(`Crash test: ${job.crash_test}`);
            }
            if (!user_data.regulations.includes(job.regulation)) {
                missing_data.push(`Regulation: ${job.regulation}`);
            }
            if (!user_data.version.includes(job.version)) {
                missing_data.push(`Version: ${job.version}`);
            }
        }

        /**
         * build list of reqested_occupant_positions and reqested_structural_components for this current protocol
         */

        let reqested_occupant_positions = [];
        let reqested_structural_components = [];

        for (let variant of job.variants) {
            Message(`variant ${JSON.stringify(variant)}`);

            if (variant.type == "occupants") {
                for (let job_occ of variant.data) {
                    //add each unique position requested
                    if (!reqested_occupant_positions.includes(job_occ)) reqested_occupant_positions.push(job_occ);
                }
            } else if (variant.type == "structures") {
                for (let job_structure of variant.data) {
                    //add each unique position requested
                    if (!reqested_structural_components.includes(job_structure))
                        reqested_structural_components.push(job_structure);
                }
            }
        }

        /**
         * get protocol vehicle to check which occupants (TODO and structures) are supported by the protocol
         * if vehicle is null this means that the protocol is invalid.
         */

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

        if (!vehicle) {
            ErrorMessage("Skipping this job as it is not supported.");
            continue;
        }

        /**
         * check if user data contains required structures data
         */

        for (let component_type of reqested_structural_components) {
            let user_structure = null;

            /**
             * todo check if the user data is supported by the protocol (i.e. is included in vehicle.structures property array)
             * if it is not supported then decide if we care or not.
             * an example may be that the user wants to plot the pedal acceleration but the protocol doesn't use this information so
             * we either don't allow this situation or we let them define the appropriate user data and it creates the image
             */

            if (!vehicle.structures.includes(component_type)) {
                /**
                 * remove the invalid component_type if it is not supported by protocol.
                 * Note reqested_structural_components should be a unique array so only need to remove component_type once,
                 * otherwise we would need a while (index !== -1) loop
                 * */
                var index = reqested_structural_components.indexOf(component_type);
                if (index !== -1) {
                    reqested_structural_components.splice(index, 1);
                }

                WarningMessage(
                    `User requested output for ${component_type}, but the protocol does not specify this structure so it will be ignored` +
                        `\nand the image(s) with this component will not be created.`
                );

                continue;
            }

            if ((user_structure = get_user_structure(component_type, user_data)) == null) {
                /**
                 * check if the component type requested by REPORTER image is defined in the user data
                 * for the current job (protocol)
                 * If not, add it to missing data which will trigger the primer GUI
                 */
                // ErrorMessage(`Missing Structure: ${component_type}`);

                missing_data.push(`Missing Structure: ${component_type}`);
            }
        }

        //clear the ProtocolVehicle seats that are not required
        for (let vehicle_occupant of vehicle.Occupants()) {
            if (!reqested_occupant_positions.includes(vehicle_occupant.position)) {
                vehicle_occupant.SetEmpty();
                continue;
            } else if (vehicle_occupant.Empty()) {
                ErrorMessage(
                    `User requested output for ${vehicle_occupant.position}, but the protocol does not specify this occupant.`
                );
                continue;
            } else if ((user_occ = get_user_occupant(vehicle_occupant.position, user_data)) != null) {
                if (user_occ.product != vehicle_occupant.product) {
                    missing_data.push(
                        `Invalid Occupant: Expected ${vehicle_occupant.product} for ${vehicle_occupant.position}, but user data had ${user_occ.product}`
                    );
                }
                if (user_occ.physiology != vehicle_occupant.physiology) {
                    missing_data.push(
                        `Invalid Occupant: Expected ${vehicle_occupant.physiology} for ${vehicle_occupant.position}, but user data had ${user_occ.physiology}`
                    );
                }
            } else {
                missing_data.push(`Missing Occupant: ${vehicle_occupant.position}`);
                // Message(`add missing data for ${regulation} ${crash_test} ${version}`);
            }
        }

        //loop through all the vehicle occupants and set seats to empty if not required

        if (missing_data.length > 0) {
            WarningMessage(
                `Some of the user inputs required for ${job.regulation} ${job.crash_test} ${
                    job.version
                } in this report are missing - \n${missing_data.join(".\n")}`
            );

            //add reqested_structural_components so that we can colour them as latent
            vehicle.structures = reqested_structural_components;

            //add protocol vehicle and missing data to reporter_user_data
            reporter_user_data.push(vehicle.ToJSON(job.regulation, job.crash_test, job.version, missing_data));

            //reset missing data
            missing_data = [];
            primer_run_required = true;
        }
    }

    if (primer_run_required) {
        /* Write the job data as a JSON file that will be picked up by T/HIS */
        let f_out_json_name = `${output_dir}/reporter_user_data.json`;
        WriteJSON(f_out_json_name, reporter_user_data);
        return JobControl.CHECK;
    }

    Message(`Found all necessary user data.`);
    return JobControl.RUN;
}

/**
 *
 * @param {string} position
 * @param {UserData} user_data
 * @returns {?WorkflowOccupant}
 */
function get_user_occupant(position, user_data) {
    if (user_data) {
        for (var user_occ of user_data.occupants) {
            if (user_occ.position == position) return user_occ;
        }
    }
    return null;
}

/**
 *
 * @param {string} component_type
 * @param {UserData} user_data
 * @returns {?WorkflowOccupant}
 */
function get_user_structure(component_type, user_data) {
    if (user_data) {
        for (var user_structure of user_data.structures) {
            if (user_structure.component_type == component_type) return user_structure;
        }
    }
    return null;
}

//PSEUDO CODE
//first check what the protocol requires
//then check what we are askng for (based on the image file names i.e. lst file)
//then define a new protocol that is just the seats we ask for (e.g.) empty the unrequired seats
//then check that the user data has occupants for these seats defined and that their physiology and product types match
//if so then we are happy
//else run PRIMER and pass the new protocol.
//note we don't require drive_side for this check

/* the ReporterJobs class contains REPORTER "jobs", each of which is a unique combination of
 * crash test, regulation and version.
 *
 * Within each job, we create occupant variants and/or struture variants
 * (a variant is a unique combination of one or more occupants or structural components.
 * Note that occupants and structural components cannot be mixed but a job can contain both types of variant)
 *
 * Variants determine which curves are plotted
 * e.g.
 * occupant variant could be:
 * Driver only, or Driver and Front passenger together
 * structural variant could be:
 * Steering column, or Break Pedal and Accelerator Pedal
 *
 * We will have to run through the T/HIS graph plotting process once
 * for each variant within each job.
 *
 * Each occupant variant contains a list of its occupants (positions) and each structural variant contains a list of structural components
 */
class ReporterJobs {
    constructor() {
        this.jobs = [];
    }

    /**
     * @param {string} regulation
     * @param {string} crash_test
     * @param {string} version
     * @param {string} occ_or_struct_str
     * @param {string} assessment_type
     */
    ProcessString(regulation, crash_test, version, occ_or_struct_str, assessment_type) {
        let job = this.AddJobIfMissing(regulation, crash_test, version);
        let variant = this.AddVariantIfMissing(job, occ_or_struct_str);
        this.AddAssessmentType(variant, assessment_type);
    }

    /**
     * this adds a job if a matching one does not exist it then returns the newly created or matching job
     * @param {string} regulation
     * @param {string} crash_test
     * @param {string} version
     * @returns {Object} job
     */
    AddJobIfMissing(regulation, crash_test, version) {
        let job = null;

        if ((job = this.GetJob(regulation, crash_test, version)) == null) {
            job = {
                regulation: regulation,
                crash_test: crash_test,
                version: version,
                variants: []
            };
            this.jobs.push(job);
        }

        return job;
    }

    /**
     * this adds a variant (either occupant or structure) to the job if a matching one does not exist it then returns the newly created or matching variant
     * @param {Object} job
     * @param {string} occ_or_struct_str
     * @returns {?Object} variant
     */
    AddVariantIfMissing(job, occ_or_struct_str) {
        let variant = null;

        // this returns an object of the variant type and the data (i.e. an array of the string split values formatted correctly)
        // not if the occ_or_struct_str is not valid then it will return null
        let occ_or_struct = this.GetVariantType(occ_or_struct_str);

        if (!occ_or_struct) return null;

        if ((variant = this.GetVariant(job, occ_or_struct_str)) == null) {
            variant = {
                string: occ_or_struct_str.toUpperCase(),
                type: occ_or_struct.type,
                data: occ_or_struct.data,
                assessment_types: []
            };
            job.variants.push(variant);
        }

        return variant;
    }

    /**
     * this adds a variant (either occupant or structure) to the job if a matching one does not exist it then returns the newly created or matching variant
     * @param {Object} variant
     * @param {string} assessment_type
     * @returns {?Object} variant
     */
    AddAssessmentType(variant, assessment_type) {
        if (variant.type == "structures") {
            if (!structure_assessment_types.includes(assessment_type)) {
                throw Error(`${assessment_type} is not a valid structural assessment`);
            }
        } else if (variant.type == "occupants") {
        }

        /* If assessment_type already exists for this variant then must be an image duplicate in the report */
        if (variant.assessment_types.includes(assessment_type)) {
            WarningMessage(`Found duplicate image filename at line ${line_count}.`);
            /* Else add to the assessment types processed for this occupant variant */
        } else {
            variant.assessment_types.push(assessment_type);
        }
    }

    /**
     * The <occ_or_struct_str> needs further parsing to detemine if the string is for occupants stuctural components and
     * also to split the string into seperate occupants or components if multiple are given.
     *
     * Multiple occupants and structural components are separated by underscores:
     *
     *     ${occupant_1}_${occupant_2}_${occupant_3}...
     *     or
     *     ${component_1}_${component_2}_${component_3}...
     *
     * Each occupant is simply the position of the occupant where spaces are replaced by hyphens
     * e.g.
     * "Driver", "Front-passenger", "Rear-driver-side", "Rear-middle", "Rear-passenger-side"
     * according to the WorkflowOccupant.toString() method:
     *
     * Each component must be a valid component type contained in the Structre.Types() array
     * @returns {?Object} this is either occupant or structure
     */
    GetVariantType(occ_or_struct_str) {
        let occ_or_struct_split = occ_or_struct_str.split("_");

        let occupants = [];
        let structures = [];
        let bool_occupant = false;
        let bool_structue = false;
        for (let i = 0; i < occ_or_struct_split.length; i++) {
            /* Expected occupant components */

            /* we initially assume that the string represents an occupant position */
            let position = WorkflowOccupant.fromString(occ_or_struct_split[i]);
            let structure = null;

            let valid_position = false;
            let valid_structure = false;

            if (positions.includes(position)) {
                bool_occupant = true;
                valid_position = true;
            } else {
                //only need to parse structure string if assumption was wrong and string
                structure = Structure.fromString(occ_or_struct_split[i]);
                Message(`Structure string: ${occ_or_struct_split[i]} => ${structure}`);

                if (structure_components.includes(structure)) {
                    bool_structue = true;
                    valid_structure = true;
                }
            }

            /* Check that occ_or_struct_str contains only valid structure or valid occupants but not both or neither */

            if (!valid_structure && !valid_position) {
                throw Error(`Unexpected string "${occ_or_struct_split[i]}"`);
            }

            if (bool_occupant && bool_structue) {
                throw Error(`Structure assessment cannot be combined with occupant assessments (${occ_or_struct_str})`);
            }

            if (valid_structure) {
                structures.push(structure);
            } else if (valid_position) {
                occupants.push(position);
            }
        }

        if (bool_occupant) return { data: occupants, type: "occupants" };
        else if (bool_structue) return { data: structures, type: "structures" };
        else return null;
    }

    /**
     *
     * @param {string} regulation
     * @param {string} crash_test
     * @param {string} version
     * @returns {?Object} job
     */
    GetJob(regulation, crash_test, version) {
        for (let job of this.jobs) {
            /* If we find an existing job that matches our requirements, add to it */
            if (regulation == job.regulation && crash_test == job.crash_test && version == job.version) {
                return job;
            }
            return null;
        }
    }

    /**
     *
     * @param {Object} job
     * @param {string} occ_or_struct_str
     * @returns {?Object} job
     */
    GetVariant(job, occ_or_struct_str) {
        for (let variant of job.variants) {
            /* If we find an existing matching occupant/structure variant, add to it */
            if (occ_or_struct_str.toUpperCase() == variant.string) {
                return variant;
            }

            return null;
        }
    }
}