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