modules/post/reporter_check_user_data.mjs

// module: TRUE
import { JSPath } from "../shared/path.mjs";
import { Regulation } from "../shared/regulations.mjs";
import { CrashTest } from "../shared/crash_tests.mjs";
import { Protocol } from "../shared/protocols.mjs";
import { ProtocolAssessment } from "./automotive_assessments.mjs";
import { AssessmentType } from "../shared/assessment_types.mjs";
import { WorkflowOccupant } from "../shared/workflow_occupant.mjs";

/* TODO TODO TODO
 * Questions for Chris Archer:
 * 
 * 1.   At the moment, I don't think we identify which occupant versions are appropriate
 *      for each protocol (e.g. Euro NCAP MPDB needs THOR 50M and HIII 50M), so we're not
 *      filtering in the PRIMER GUI accordingly. Rationale? When it comes to REPORTER, I 
 *      think we'll need some way of checking that correct user data is in place, and I
 *      think that that would start by looking at correct occupant types, and then
 *      correct assessment types. We can check assessment types via
 *      protocolassessment.UniqueOccupantAssessmentTypes, but I can't find an existing way of 
 *      checking occupants are valid. Solution: new protocol.OccupantVersions method.
 * 2.   In PRIMER, there doesn't seem to be a way of selecting the regulation version.
 *      I think we'll need this?
 * /


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

let regulations = Regulation.GetAll();
let crash_tests = CrashTest.GetAll();
let assessment_types = AssessmentType.GetAll();
let positions = WorkflowOccupant.Positions();
let front_rears = WorkflowOccupant.FrontRear();
let sides = WorkflowOccupant.Sides();

/** Checks whether image filenames in REPORTER template are valid
 *  @param {string} output_dir REPORTER output directory for images etc.
 */
export function check_report_contents(f_json_name, output_dir) {
    let reporter_data = {};
    reporter_data.jobs = [];

    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;
    }
    if (!File.IsReadable(f_contents_name)) {
        ErrorMessage(`Report contents list could not be opened for reading: ${f_contents_name}`);
        return;
    }
    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}~${occupants_str}~${assessment_type}.png
         *
         * Extract image filename (strip preceding path and trailing ".png"), leaving:
         *
         *     ${regulation}~${crash_test}~${version}~${occupants_str}~${assessment_type}
         */

        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 occupants_str = match[5];
            var assessment_type = match[6];
        } else {
            ErrorMessage(`Unexpected filename composition at line ${line_count}. Skipping.`);
            continue;
        }

        // let image_filename = line.substring(line.lastIndexOf("/") + 1, line.length - 4);

        // /* Split the filename into its components */

        // let fields = image_filename.split("~");
        // if (fields.length != 5) {
        //     ErrorMessage(`Unexpected filename composition at line ${line_count}. Skipping.`);
        //     continue;
        // }

        // /* Expected filename components */

        // let regulation = fields[0];
        // let crash_test = fields[1];
        // let version = fields[2];
        // let occupants_str = fields[3];
        // let assessment_type = fields[4];

        /* 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 (!assessment_types.includes(assessment_type)) {
            ErrorMessage(
                `Unexpected assessment_type "${assessment_type}" in filename at line ${line_count}. Skipping.`
            );
            continue;
        }

        /* create a protocol with the default datums based on the regulation, crash test and version */
        let protocol = ProtocolAssessment.CreateDefaultProtocol(regulation, crash_test, version);

        /* The <occupants_str> needs further parsing. Multiple occupants are separated by underscores:
         *
         *     ${occupant_1}_${occupant_2}_${occupant_3}
         *
         * And each occupant in turn is separated by hyphens into position, front_rear and side
         * according to the WorkflowOccupant.toString() method:
         *
         *     ${this.position}-${this.front_rear}-${this.side}
         */

        let valid = true;
        let occupants = occupants_str.split("_");
        for (let i = 0; i < occupants.length; i++) {
            let occupant_fields = occupants[i].split("-");
            if (occupant_fields.length != 3) {
                ErrorMessage(`Unexpected occupant composition ("${occupants[i]}") at line ${line_count}. Skipping.`);
                valid = false;
                continue;
            }

            /* Expected occupant components */

            let position = occupant_fields[0];
            let front_rear = occupant_fields[1];
            let side = occupant_fields[2];

            /* Check that occupant components are valid */

            if (!positions.includes(position)) {
                ErrorMessage(`Unexpected position "${position}" in filename at line ${line_count}. Skipping.`);
                valid = false;
                continue;
            }
            if (!front_rears.includes(front_rear)) {
                ErrorMessage(`Unexpected front_rear "${front_rear}" in filename at line ${line_count}. Skipping.`);
                valid = false;
                continue;
            }
            if (!sides.includes(side)) {
                ErrorMessage(`Unexpected side "${side}" in filename at line ${line_count}. Skipping.`);
                valid = false;
                continue;
            }

            /* Create new WorkflowOccupant from parsed data, but use placeholder version and body_parts for now */
            /*occupant = new WorkflowOccupant(WorkflowOccupant.HUMANETICS_HIII_50M_V1_5, position, side, front_rear, [
                OccupantBodyPart.CHEST
            ]);*/

            /* This should really be a class, perhaps a parent class of WorkflowOccupant
             * with just position and front_rear, and a separate array of valid occupant versions */
            occupants[i] = { position: position, front_rear: front_rear };

            //COMMENT RB: Do we want this to be (or look like) user_data?
            //if the occupant null then it is 'latent' and needs to be defined.
            //this should
        }
        if (!valid) {
            ErrorMessage(`Unexpected occupant_str composition at line ${line_count}. Skipping.`);
            continue;
        }

        /* Here, we assemble an object of REPORTER "jobs", each of which is a unique combination of
         * crash test, regulation and version. Within each job, we create occupant variants.
         * Occupant variants determine which curves are plotted e.g. Driver only, or Driver and
         * Passenger together. We will have to run through the T/HIS graph plotting process once
         * for each occupant variant within each job. Each occupant variant contains a list of its
         * occupants. Each occupant has properties for the expected position, front_rear, and a
         * list of the valid occupant versions for that occupant.
         *
         * All of this should probably be handled in a class.
         */
        let found_reg_crash_ver = false;
        for (let job of reporter_data.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) {
                let found_occupants_str = false;
                for (let occ_var of job.occupant_variants) {
                    /* If we find an existing matching occupant variant, add to it */
                    if (occupants_str == occ_var.occupants_str) {
                        /* If everything matches, must be an image duplicate in the report */
                        if (occ_var.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 {
                            occ_var.assessment_types.push(assessment_type);
                        }
                        found_occupants_str = true;
                    }
                }
                /* If a matching occupant variant can't be found, add a new one */
                if (!found_occupants_str) {
                    job.occupant_variants.push({
                        occupants_str: occupants_str,
                        occupants: occupants,
                        assessment_types: [assessment_type]
                    });
                }
                found_reg_crash_ver = true;
            }
        }
        /* If a matching job can't be found, add a new one */
        if (!found_reg_crash_ver) {
            reporter_data.jobs.push({
                regulation: regulation,
                crash_test: crash_test,
                version: version,
                occupant_variants: [
                    {
                        occupants_str: occupants_str,
                        occupants: occupants,
                        assessment_types: [assessment_type]
                    }
                ]
            });
        }
    }
    f_contents.Close();

    /* Write the job data as a JSON file that will be picked up by T/HIS */
    let json_str = JSON.stringify(reporter_data);
    let f_out_json_name = `${output_dir}/report_contents.json`;
    let f_json = new File(f_out_json_name, File.WRITE);
    f_json.Writeln(json_str);
    f_json.Close();

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

    /* Workflow class doesn't work for REPORTER yet so hard-wire JSON.parse for now */
    f_json = new File(f_json_name, File.READ);
    json_str = "";
    while ((line = f_json.ReadLongLine()) != undefined) {
        json_str = json_str + line;
    }
    f_json.Close();
    let user_data = JSON.parse(json_str).workflows[0].data;

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

    for (let job of reporter_data.jobs) {
        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}`);
        }
        /* Loop through all of the job's occupants and check that there is a matching occupant in
         * the user data.
         */
        for (let occ_var of job.occupant_variants) {
            for (let job_occ of occ_var.occupants) {
                let occ_match = false;
                for (let user_occ of user_data.occupants) {
                    if (job_occ.position == user_occ.position && job_occ.front_rear == user_occ.front_rear) {
                        occ_match = true;
                    }
                }
                if (!occ_match) {
                    missing_data.push(
                        `Occupant: ${job_occ.position}-${job_occ.front_rear} [${job_occ.valid_occ_vers.join(", ")}]`
                    );
                }
            }
        }
    }

    if (missing_data.length > 0) {
        ErrorMessage(`Some of the user inputs required for this report are missing (see list below).`);
        for (let msg of missing_data) {
            Message(msg);
        }
    }
}