modules/shared/vehicle.mjs

import { BaseOccupant } from "./base.mjs";
import { OccupantPhysiology, OccupantProduct } from "./occupant.mjs";
import { OccupantVersion } from "./occupant_version.mjs";
import { WorkflowOccupant } from "./workflow_occupant.mjs";
import { Structure } from "./structure.mjs";

export { ProtocolVehicle, VehicleOccupant };

class ProtocolVehicle {
    /**
     * Class to define a Vehicle with Occupants that can occupy each seat
     * @param {VehicleOccupant[]} vehicle_occupants array of OccupantProduct names in driver seat
     * @param {string[]} structures array of OccupantProduct names in driver seat
     * @example
     * let vehicle = new ProtocolVehicle(driver, front, behind_driver, behind_front, middle);
     */
    constructor(vehicle_occupants, structures) {
        this.vehicle_occupants = vehicle_occupants;
        this.structures = structures;
    }

    /** vehicles occupants array
     * @type {VehicleOccupant[]} */
    get vehicle_occupants() {
        return this._vehicle_occupants;
    }
    set vehicle_occupants(new_vehicle_occupants) {
        this._vehicle_occupants = new_vehicle_occupants;
    }

    /** driver occupant
     * @type {?VehicleOccupant} */
    get driver() {
        return this.GetOccupant(BaseOccupant.DRIVER);
    }
    /** front passenger occupant
     * @type {?VehicleOccupant} */
    get front() {
        return this.GetOccupant(BaseOccupant.FRONT_PASSENGER);
    }
    /** behind driver occupant
     * @type {?VehicleOccupant} */
    get behind_driver() {
        return this.GetOccupant(BaseOccupant.REAR_DRIVER_SIDE);
    }
    /** behind front occupant
     * @type {?VehicleOccupant} */
    get behind_front() {
        return this.GetOccupant(BaseOccupant.REAR_PASSENGER_SIDE);
    }
    /** rear middle passenger occupant names array
     * @type {?VehicleOccupant} */
    get middle() {
        return this.GetOccupant(BaseOccupant.REAR_MIDDLE);
    }

    /**
     * Array of Structure types
     * @type {string[]}
     */
    get structures() {
        return this._structures;
    }
    set structures(new_structures) {
        if (!(new_structures instanceof Array)) {
            throw new Error("structures must be an Array");
        }

        for (let new_structure of new_structures) {
            // @ts-ignore
            if (!Structure.Types().includes(new_structure)) {
                throw new Error(`${new_structure} is not a valid Structure type - check the spelling and case`);
            }
        }

        this._structures = new_structures;
    }

    /** driver occupant names array
     * @param {string} position
     * @return {?VehicleOccupant}
     */
    GetOccupant(position) {
        for (var occupant of this.vehicle_occupants) {
            if (occupant.position == position) {
                return occupant;
            }
        }

        //if we get here then the occupant doesn't yet exist so add an empty occupant
        let empty_occupant = new VehicleOccupant(position);
        this.vehicle_occupants.push(empty_occupant);

        // Message(`Making empty ${position} occupant ${JSON.stringify(empty_occupant.ToJSON())}`);
        return empty_occupant;
    }

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

    /**
     * construct a ProtocolVehicle from JSON
     * @param {ProtocolVehicleJSON} json
     * @return {ProtocolVehicle}
     */
    static FromJSON(json) {
        let expected_keys = ["occupants"];

        for (let key of expected_keys) {
            if (!(key in json)) {
                throw new Error(`${key} is not a key in the json object passed to ProtocolVehicle.FromJSON(json)`);
            }
        }

        /**
         * set undefined optional keys to empty array
         * NOTE: structures property is optional so may not be defined. If it is not defined set as empty array
         */

        let optional_keys = ["structures"];

        for (let key of optional_keys) {
            if (!(key in json)) {
                json[key] = [];
            }
        }

        let occupants = [];

        for (let vo_json of json.occupants) {
            try {
                let vo = VehicleOccupant.FromJSON(vo_json);
                if (vo.Empty()) Message(`Skipping ${vo.position}`);
                occupants.push(vo);
            } catch (error) {
                WarningMessage(
                    `Failed to construct a vehicle occupant. Check the protocol vehicle json was correctly defined`
                );
            }
        }

        return new ProtocolVehicle(occupants, json.structures);
    }

    /**
     * return JSON representation of ProtocolVehicle with protocol data
     * @param {string} regulation Regulation, e.g. Regulation.CNCAP
     * @param {string} crash_test Crash test, e.g. CrashTest.ODB
     * @param {string} version Version, e.g. "2017"
     * @param {?string} [description=null] e.g. "2017"
     * @param {?string} [drive_side=null]
     * @return {Object}
     */
    ToJSON(regulation, crash_test, version, description = null, drive_side = null) {
        let json = {};
        let vehicle = { occupants: [], structures: [] };

        if (description == null) description = `protocol based on... ${version}`;

        for (let o of this.Occupants()) {
            //don't write out empty occupants
            if (o.Empty()) continue;
            let json_occ = o.ToJSON();
            Message(JSON.stringify(json_occ));
            vehicle.occupants.push(json_occ);
        }

        // copy structures
        for (let s of this.structures) {
            vehicle.structures.push(s);
        }

        json.regulation = regulation;
        json.crash_test = crash_test;
        json.version = version.substring(0, 4); //just save year
        json.description = description;
        if (drive_side) json.drive_side = drive_side;
        json.vehicle = vehicle;
        return json;
    }

    /**
     * get array of all the VehicleOccupants in the ProtocolVehicle
     * @returns {VehicleOccupant[]}
     * @example
     * let occupants = this.Occupants();
     */
    Occupants() {
        let vehicle = this;

        return [vehicle.driver, vehicle.behind_driver, vehicle.front, vehicle.behind_front, vehicle.middle];
    }

    /**
     * get array of all the VehicleOccupants in the ProtocolVehicle

     */

    /**
     * returns an array of valid (non-empty) VehicleOccupants that match all the passed criteria for position
     * side and front_rear.
     * Note that each position is unique so passing additonal arguments may result in an empty array being returned
     * Note that null can be passed for any of these criteria and it will not filter on that property
     * Note also that zero length array is returned if no matching occupants found
     * @param {?string} [position = null] Workflowoccupant.DRIVER or WorkflowOccupant.PASSENGER
     * @param {?string} [front_rear = null] front or rear row seat
     * @param {?string} [side = null] WorkflowOccupant.LEFT, WorkflowOccupant.RIGHT or WorkflowOccupant.MIDDLE
     * @param {string} [drive_side = VehicleOccupant.LHD] VehicleOccupant.LHD by default, but can set to VehicleOccupant.RHD. This is only used if side is not null
     * @returns {VehicleOccupant[]}
     * @example
     * let occupants = this.OnlyOccupants(WorkflowOccupant.PASSENGER, WorkflowOccupant.REAR);
     */
    OnlyOccupants(position = null, front_rear = null, side = null, drive_side = VehicleOccupant.LHD) {
        let occupants = this.Occupants();

        let output_occupants = [];

        for (let occupant of occupants) {
            if (!occupant.Empty()) {
                if (position != null) {
                    if (occupant.position != position) {
                        continue;
                    }
                }

                if (front_rear != null) {
                    if (occupant.GetRow() != front_rear) {
                        continue;
                    }
                }

                if (side != null) {
                    if (occupant.GetSide(drive_side) != side) {
                        continue;
                    }
                }

                output_occupants.push(occupant);
            }
        }

        return output_occupants;
    }
}

class VehicleOccupant extends BaseOccupant {
    /**
     * this class holds vehicle occupant data from the protocol json file
     * it also adds meta data e.g. side and position. Positions assume
     * left-hand drive (LHD) by default, but GetPosition method can be used
     * to return the correct positions if vehicle is right-hand drive (RHD).
     */

    /**
     *
     * @param {string} position
     * @param {?string} [product = null] taken from JSON
     * @param {?string} [physiology = null] taken from JSON
     * @example let driver = new VehicleOccupant(
                WorkflowOccupant.DRIVER,
                WorkflowOccupant.LEFT,
                WorkflowOccupant.FRONT,
                json.driver.product,
                json.driver.physiology
            )
     */
    constructor(position, product = null, physiology = null) {
        super(position);
        this.product = product;
        this.physiology = physiology;
    }

    /** Left-hand drive (LHD) constant
     * @type {string} */
    static get LHD() {
        return "LHD";
    }

    /** Right-hand drive (RHD) constant
     * @type {string} */
    static get RHD() {
        return "RHD";
    }

    /**
     * if the hand drive is valid returns the value passed, otherwise prints warning and return VehicleOccupant.LHD
     * @param {string} drive_side
     * @returns {string}
     */
    static GetValidHandDrive(drive_side) {
        if (drive_side == VehicleOccupant.LHD || drive_side == VehicleOccupant.RHD) return drive_side;

        WarningMessage(
            `Vehicle hand drive ${drive_side} was not valid so cannot be set. Defaulting to ${VehicleOccupant.LHD}`
        );

        return VehicleOccupant.LHD;
    }

    /**
     * make the occupant empty by setting product and physiology to null
     */
    SetEmpty() {
        this.product = null;
        this.physiology = null;
    }

    /**
     * occupant may be empty if either product or physiology are null
     * @returns {boolean} empty
     */
    Empty() {
        if (this.position && this.physiology) return false;
        return true;
    }

    /** Occupant product
     * @type {?string|undefined} */
    get product() {
        return this._product;
    }
    set product(new_product) {
        if (!new_product) new_product = null;
        else if (!OccupantProduct.Valid(new_product)) {
            throw new Error(`Invalid product ${new_product} in VehicleOccupant constructor`);
        }
        this._product = new_product;
    }

    /** Occupant physiology
     * @type {?string|undefined} */
    get physiology() {
        return this._physiology;
    }
    set physiology(new_physiology) {
        if (!new_physiology) new_physiology = null;
        else if (!OccupantPhysiology.Valid(new_physiology)) {
            throw new Error(`Invalid physiology ${new_physiology} in VehicleOccupant constructor`);
        }
        this._physiology = new_physiology;
    }

    /**
     * returns a VehicleOccupant from the json
     * @param {VehicleOccupantJSON} json
     * @returns
     */
    static FromJSON(json) {
        return new VehicleOccupant(json.position, json.product, json.physiology);
    }

    /**
     * returns an object representation of VehicleOccupant
     * @returns {Object}
     */
    ToJSON() {
        let json = {};
        json.position = this.position;
        json.product = this.product;
        json.physiology = this.physiology;
        return json;
    }

    /**
     * Get the side of an occupant for a given drive side (LHD or RHD) based on position
     * @param {string} [drive_side = VehicleOccupant.LHD] VehicleOccupant.LHD (default) or VehicleOccupant.RHD
     * @returns {string}
     */
    GetSide(drive_side = VehicleOccupant.LHD) {
        if (this.position == WorkflowOccupant.REAR_MIDDLE)
            // Middle is always middle regardless of drive_side
            return WorkflowOccupant.MIDDLE;

        switch (drive_side) {
            case VehicleOccupant.LHD:
                switch (this.position) {
                    case WorkflowOccupant.DRIVER:
                    case WorkflowOccupant.REAR_DRIVER_SIDE:
                        return WorkflowOccupant.LEFT;
                    case WorkflowOccupant.FRONT_PASSENGER:
                    case WorkflowOccupant.REAR_PASSENGER_SIDE:
                        return WorkflowOccupant.RIGHT;
                }
            case VehicleOccupant.RHD:
                switch (this.position) {
                    case WorkflowOccupant.DRIVER:
                    case WorkflowOccupant.REAR_DRIVER_SIDE:
                        return WorkflowOccupant.RIGHT;
                    case WorkflowOccupant.FRONT_PASSENGER:
                    case WorkflowOccupant.REAR_PASSENGER_SIDE:
                        return WorkflowOccupant.LEFT;
                }
        }
    }

    /**
     * returns an array of valid (supported) occupant names corresponding to
     * the occupant product and physiology properties
     * @returns {string[]} occupant names array
     */
    GetOccupantNames() {
        return OccupantVersion.GetOnly("ALL", this.product, this.physiology);
    }
}