post/reporter/reporter_b_pillar_image.js

/* REPORTER Script to generate an image for the B-Pillar assessment
 *
 * This is done in REPORTER rather than D3PLOT as REPORTER has an Image class
 * that can drawn lines and text, whereas D3PLOT can draw lines, but not text.
 *
 * The script reads the cut-section coordinates from the files generated by
 * D3PLOT (filenames passed to the template as variables) and draws the image
 * saving it to a file that D3PLOT can then pick up and display on a widget
 *
 * The template assumes all coordinates are in mm, i.e. they have been converted
 * from the model units in the D3PLOT script that passes the values to this
 * template.
 *
 * Note: I'm using 'var' instead of 'let' to declare variables so that if the
 * template is generated more than once in a single session it still works.
 * Reporter only has one global instance and if you define things with 'let'
 * it complains the second time the script is run as it thinks you're trying
 * to redeclare and existing variable.
 */

draw_b_pillar_image();

/* TODO - split up the function into separate functions, e.g. calculate_rating, min and max coords */

/**
 * Draw the B-pillar image
 * Also calculates the B-pillar deformation rating
 */
function draw_b_pillar_image() {
    var t = Template.GetCurrent();

    var ground_z = parseFloat(t.GetVariableValue("GROUND_Z"));
    var seat_centre_y = parseFloat(t.GetVariableValue("SEAT_CENTRE_Y"));
    var hpoint_z = parseFloat(t.GetVariableValue("H_POINT_Z"));

    if (isNaN(seat_centre_y) || seat_centre_y == 0) {
        LogError("Invalid SEAT_CENTRE_Y = " + t.GetVariableValue("SEAT_CENTRE_Y"));
        LogPrint("Unable to draw image and calculate B-pillar deformation rating.");
        Exit();
    }

    /* Structural intrusion ratings for B-pillar to seat centerline distance (mm)
     * Note Reporter variable in cm but drawing needs it in mm so x10 */

    var B_PILLAR_DEFORMATION_GOOD = parseFloat(t.GetVariableValue("B_PILLAR_DEFORMATION_GOOD")) * 10; // >=18.0 cm
    var B_PILLAR_DEFORMATION_ACCEPTABLE = parseFloat(t.GetVariableValue("B_PILLAR_DEFORMATION_ACCEPTABLE")) * 10; // >=14.0 cm
    var B_PILLAR_DEFORMATION_MARGINAL = parseFloat(t.GetVariableValue("B_PILLAR_DEFORMATION_MARGINAL")) * 10; // >=10.0 cm

    if (isNaN(B_PILLAR_DEFORMATION_GOOD)) {
        LogError("Invalid B_PILLAR_DEFORMATION_GOOD = " + t.GetVariableValue("B_PILLAR_DEFORMATION_GOOD"));
        LogPrint("Unable to draw image and calculate B-pillar deformation rating.");
        Exit();
    }
    if (isNaN(B_PILLAR_DEFORMATION_ACCEPTABLE)) {
        LogError("Invalid B_PILLAR_DEFORMATION_ACCEPTABLE = " + t.GetVariableValue("B_PILLAR_DEFORMATION_ACCEPTABLE"));
        LogPrint("Unable to draw image and calculate B-pillar deformation rating.");
        Exit();
    }
    if (isNaN(B_PILLAR_DEFORMATION_MARGINAL)) {
        LogError("Invalid B_PILLAR_DEFORMATION_MARGINAL = " + t.GetVariableValue("B_PILLAR_DEFORMATION_MARGINAL"));
        LogPrint("Unable to draw image and calculate B-pillar deformation rating.");
        Exit();
    }

    /* Get max and min coordinates
     * need these so we know how big to make the image to fit the lines
     * and where to reflect as Z is the other way around */

    var max_y = -Number.MAX_VALUE;
    var max_z = -Number.MAX_VALUE;
    var min_y = Number.MAX_VALUE;
    var min_z = Number.MAX_VALUE;

    var y_mid = 0;

    for (var pass = 0; pass < 2; pass++) {
        if (pass == 0) {
            var filename = `${t.GetVariableValue("TEMPLATE_DIR")}/first_state_coords.txt`;
            var f = new File(filename, File.READ);
        }
        if (pass == 1) {
            var filename = `${t.GetVariableValue("TEMPLATE_DIR")}/last_state_coords.txt`;
            var f = new File(filename, File.READ);
        }

        var found = false;
        var line;

        // @ts-ignore - ReadLongLine should have it's return type set to number | string
        while ((line = f.ReadLongLine()) != File.EOF) {
            var data = line.split(",");

            var ncut = parseInt(data[0]);

            if (ncut == 2) {
                found = true;

                y1 = parseFloat(data[2]);
                y2 = parseFloat(data[5]);
                z1 = parseFloat(data[3]);
                z2 = parseFloat(data[6]);

                max_y = Math.max(y1, y2, max_y);
                max_z = Math.max(z1, z2, max_z);

                min_y = Math.min(y1, y2, min_y);
                min_z = Math.min(z1, z2, min_z);
            }
        }

        if (!found) {
            max_y = 1;
            min_y = 0;
            max_z = 1;
            min_z = 0;
        }
    }

    /* Seat centre line */

    max_y = Math.max(seat_centre_y, max_y);
    min_y = Math.min(seat_centre_y, min_y);

    /* Ground Z */

    min_z = Math.min(ground_z, min_z);

    LogPrint("max_y: " + max_y.toFixed(2) + " min_y: " + min_y.toFixed(2));
    LogPrint("max_z: " + max_z.toFixed(2) + " min_z: " + min_z.toFixed(2));

    /* Now create image */

    var buffer = 400;

    var width = max_y - min_y + buffer;
    var height = max_z - min_z + buffer;

    var reflect = height / 2;

    var y_offset = min_y - buffer / 2; // Y offset so the image starts on the left hand side
    var z_offset = min_z - buffer / 2; // Z offset so the image starts at the bottom

    LogPrint("width: " + width.toFixed(2) + " height: " + height.toFixed(2));

    var img = new Image(Math.round(width), Math.round(height));
    img.lineWidth = 3;

    /* Draw colour bands */
    var band_bottom_z, band_top_z;
    [band_bottom_z, band_top_z] = draw_colour_bands();

    /* Draw cross sections of parts
     * Two passes - one for first state, one for the last
     * Format of file is <number of cuts>, <x1>, <y1>, <z1>, .... <xn>, <yn>, <zn> */

    var min_space_remaining = Number.MAX_VALUE;
    var y_at_max_intrusion = 0.0;
    var z_at_max_intrusion = 0.0;

    img.lineStyle = Reporter.LINE_SOLID;

    for (var pass = 0; pass < 2; pass++) {
        if (pass == 0) {
            var filename = `${t.GetVariableValue("TEMPLATE_DIR")}/first_state_coords.txt`;
            var f = new File(filename, File.READ);
            img.lineColour = "#0000FF";
            img.lineWidth = 2;
        }
        if (pass == 1) {
            var filename = `${t.GetVariableValue("TEMPLATE_DIR")}/last_state_coords.txt`;
            var f = new File(filename, File.READ);
            img.lineColour = "#000000";
            img.lineWidth = 6;
        }

        var line;
        var y1, y2, z1, z2;

        // @ts-ignore - ReadLongLine should have it's return type set to number | string
        while ((line = f.ReadLongLine()) != File.EOF) {
            var data = line.split(",");

            var ncut = parseInt(data[0]);

            if (ncut == 2) {
                var y1_model = parseFloat(data[2]);
                var y2_model = parseFloat(data[5]);
                var z1_model = parseFloat(data[3]);
                var z2_model = parseFloat(data[6]);

                y1 = model_y_to_image_y(y1_model);
                y2 = model_y_to_image_y(y2_model);

                z1 = model_z_to_image_z(z1_model);
                z2 = model_z_to_image_z(z2_model);

                img.Line(y1, z1, y2, z2);

                LogPrint(
                    "Line from " + y1.toFixed(2) + ", " + z1.toFixed(2) + " to " + y2.toFixed(2) + ", " + z2.toFixed(2)
                );

                if (pass == 1) {
                    /* Check for intrusion only up to 540mm above H-Point and -100m below H-Point
                     * (and ignore coordinates on the opposite B-pillar) */

                    if (z2 >= band_top_z && z1 <= band_bottom_z && seat_centre_y * y2_model > 0) {
                        if (seat_centre_y > 0) {
                            var space_remaining = y2_model - seat_centre_y;
                        } else {
                            var space_remaining = seat_centre_y - y2_model;
                        }

                        if (space_remaining < min_space_remaining) {
                            min_space_remaining = space_remaining;

                            y_at_max_intrusion = y2;
                            z_at_max_intrusion = z2;
                        }
                    }
                }
            }
        }

        f.Close();
    }

    /* Draw extra gubbins */

    draw_hpoint();
    draw_axis();
    draw_legend();
    draw_max_intrusion_point();

    /* Save image */

    img.Save(`${t.GetVariableValue("TEMPLATE_DIR")}/b_pillar_image_M${t.GetVariableValue("MODEL_ID")}.png`, Image.PNG);

    /* Write intrusion values to Reporter variables - distance from centre line (min_space_remaining) and
     *                                                height above ground */

    var height_above_ground = image_z_to_model_z(z_at_max_intrusion) - ground_z;

    if (min_space_remaining != -Number.MAX_VALUE) {
        new Variable(t, "MAX_INTRUSION", "Maximum intrusion", min_space_remaining.toString(), "Number");
        new Variable(t, "HEIGHT_ABOVE_GROUND", "Height above ground", height_above_ground.toString(), "Number");

        if (min_space_remaining >= B_PILLAR_DEFORMATION_GOOD) {
            new Variable(t, "B_PILLAR_RATING", "B-pillar deformation rating", "GOOD", "General");
        } else if (min_space_remaining >= B_PILLAR_DEFORMATION_ACCEPTABLE) {
            new Variable(t, "B_PILLAR_RATING", "B-pillar deformation rating", "ACCEPTABLE", "General");
        } else if (min_space_remaining >= B_PILLAR_DEFORMATION_MARGINAL) {
            new Variable(t, "B_PILLAR_RATING", "B-pillar deformation rating", "MARGINAL", "General");
        } else {
            new Variable(t, "B_PILLAR_RATING", "B-pillar deformation rating", "POOR", "General");
        }
    } else {
        new Variable(t, "MAX_INTRUSION", "Maximum intrusion", "", "General");
        new Variable(t, "HEIGHT_ABOVE_GROUND", "Height above ground", "", "General");
        new Variable(t, "B_PILLAR_RATING", "B-pillar deformation rating", "POOR", "General");
    }

    /**
     * Draws the colour bands on the image
     * @returns {number[]} The top and bottom Z coords of the colour bands
     */
    function draw_colour_bands() {
        // Draws the colour bands and returns the top of the image top Z coord

        var y1, z1, y2, z2;

        var GREEN_COL = "#52B442"; // Green
        var YELLOW_COL = "#FCE702"; // Yellow
        var ORANGE_COL = "#F79800"; // Orange
        var RED_COL = "#F40000"; // Red

        img.lineWidth = 3;
        img.lineStyle = Reporter.LINE_NONE;

        z1 = model_z_to_image_z(hpoint_z - 100); // bottom is 100m below H-Point
        z2 = model_z_to_image_z(hpoint_z + 540); // Top is 540mm above H-Point

        var band_bottom_z = z1;
        var band_top_z = z2;

        /* GREEN */

        y1 = model_y_to_image_y(min_y);
        y2 = model_y_to_image_y(seat_centre_y - B_PILLAR_DEFORMATION_GOOD);

        img.fillColour = GREEN_COL;
        img.Polygon(y1, z1, y1, z2, y2, z2, y2, z1);

        /* YELLOW */

        y1 = model_y_to_image_y(seat_centre_y - B_PILLAR_DEFORMATION_GOOD);
        y2 = model_y_to_image_y(seat_centre_y - B_PILLAR_DEFORMATION_ACCEPTABLE);

        img.fillColour = YELLOW_COL;
        img.Polygon(y1, z1, y1, z2, y2, z2, y2, z1);

        /* ORANGE */

        y1 = model_y_to_image_y(seat_centre_y - B_PILLAR_DEFORMATION_ACCEPTABLE);
        y2 = model_y_to_image_y(seat_centre_y - B_PILLAR_DEFORMATION_MARGINAL);

        img.fillColour = ORANGE_COL;
        img.Polygon(y1, z1, y1, z2, y2, z2, y2, z1);

        /* RED */

        y1 = model_y_to_image_y(seat_centre_y - B_PILLAR_DEFORMATION_MARGINAL);
        y2 = model_y_to_image_y(y_mid); //draw red zone to middle of car

        img.fillColour = RED_COL;
        img.Polygon(y1, z1, y1, z2, y2, z2, y2, z1);

        /* Return the top of the band in image coords */

        return [band_bottom_z, band_top_z];
    }

    /**
     * Draws the H-Point line on the image
     */
    function draw_hpoint() {
        var y1, z1, y2, z2;

        y1 = model_y_to_image_y(seat_centre_y - B_PILLAR_DEFORMATION_MARGINAL);
        y2 = model_y_to_image_y(y_mid); //draw H-point line zone to middle of car

        z1 = model_z_to_image_z(hpoint_z);
        z2 = z1;

        img.lineWidth = 3;
        img.lineStyle = Reporter.LINE_DOT;
        img.lineColour = "#000000";
        img.Line(y1, z1, y2, z2);

        img.font = "Segoe UI";
        img.fontSize = pixels_to_font_size(36);
        img.fontJustify = Reporter.JUSTIFY_LEFT;
        img.Text(y1, z2 - 4, "H-Point");
    }

    /**
     * Draws the axis on the image
     */
    function draw_axis() {
        var y1, z1, y2, z2;

        img.lineWidth = 3;
        img.lineStyle = Reporter.LINE_SOLID;
        img.lineColour = "#000000";

        // Vertical - Height from ground

        y1 = model_y_to_image_y(min_y);
        y2 = y1;

        z1 = model_z_to_image_z(min_z);
        z2 = model_z_to_image_z(max_z);

        img.Line(y1, z1, y2, z2);

        /* Label */

        img.font = "Segoe UI";
        img.fontSize = pixels_to_font_size(36);
        img.fontJustify = Reporter.JUSTIFY_CENTRE;
        img.fontAngle = 270;

        z1 = Math.floor(0.5 * (z1 + z2));

        img.Text(70, z1, "Height from ground (cm)");

        /* Add marks every 100mm */

        var delta = 10; // 10cm

        img.font = "Segoe UI";
        img.fontSize = pixels_to_font_size(24);
        img.fontJustify = Reporter.JUSTIFY_RIGHT;
        img.fontAngle = 1;

        var height_above_ground;

        var i = 0;
        z1 = model_z_to_image_z(min_z);
        while (z1 > z2) {
            height_above_ground = delta * i; // In cm
            img.Text(y1 - 6, z1 + 12, height_above_ground.toString());

            img.Line(y1, z1, y1 + 10, z1);

            i++;

            z1 = model_z_to_image_z(min_z + delta * 10 * i); // mm
        }

        /* Horizontal - Lateral distance from seat centre line */

        y1 = model_y_to_image_y(min_y);
        y2 = model_y_to_image_y(max_y);

        z1 = model_z_to_image_z(min_z);
        z2 = z1;

        img.Line(y1, z1, y2, z2);

        /* Label */

        img.font = "Segoe UI";
        img.fontSize = pixels_to_font_size(36);
        img.fontJustify = Reporter.JUSTIFY_CENTRE;
        img.fontAngle = 0;

        var centerline_ref = t.GetVariableValue("CENTERLINE");
        draw_x_ticks(centerline_ref, y1, y2, z1);
    }

    /**
     * Draws the x-axis ticks on the image
     * @param {string} centerline_ref Centreline reference - "VEHICLE" or "SEAT"
     * @param {number} y1 Image y coordinate of the left side of the X-Axis
     * @param {number} y2 Image y coordinate of the right side of the X-Axis
     * @param {number} z1 Image z coordinate of the bottom of the Y-Axis
     */
    function draw_x_ticks(centerline_ref, y1, y2, z1) {
        var vehicle_centre_img_y = Math.floor(0.5 * (y1 + y2));

        if (centerline_ref.toUpperCase() == "SEAT") {
            /* Driver seat centreline */
            var centerline_ref_y = seat_centre_y;
            y1 = model_y_to_image_y(seat_centre_y);
            img.Text(vehicle_centre_img_y, height - 70, "Lateral distance from driver centreline (cm)");
        } else {
            /* vehicle centreline */
            var centerline_ref_y = image_y_to_model_y(vehicle_centre_img_y);
            y1 = vehicle_centre_img_y; //use vehicle centerline
            img.Text(vehicle_centre_img_y, height - 70, "Lateral distance from vehicle centreline (cm)");
        }

        /* Add marks every 100mm - starting at the seat centre line */
        var delta = 10; // 10cm

        img.font = "Segoe UI";
        img.fontSize = pixels_to_font_size(24);
        img.fontJustify = Reporter.JUSTIFY_CENTRE;

        var lateral_distance;

        //reference point (zero at either seat centerline or vehicle centerline)

        var i = 0;

        while (y1 < y2) {
            lateral_distance = delta * i; // In cm
            img.Text(y1, z1 + 30, lateral_distance.toString());

            img.Line(y1, z1, y1, z1 - 10);

            i++;

            y1 = model_y_to_image_y(centerline_ref_y + delta * 10 * i); // mm
        }

        i = 1;
        y1 = model_y_to_image_y(centerline_ref_y - delta * 10); // mm
        while (y1 > model_y_to_image_y(min_y)) {
            lateral_distance = -delta * i; // In cm
            img.Text(y1, z1 + 30, lateral_distance.toString());

            img.Line(y1, z1, y1, z1 - 10);

            i++;

            y1 = model_y_to_image_y(centerline_ref_y - delta * 10 * i); // mm
        }
    }

    /**
     * Draws the legend on the image
     */
    function draw_legend() {
        var y1, z1, y2, z2;

        y1 = model_y_to_image_y(min_y);
        y2 = y1 + 100;

        z1 = z2 = 40;

        img.lineWidth = 2;
        img.lineStyle = Reporter.LINE_SOLID;
        img.lineColour = "#0000FF";
        img.Line(y1, z1, y2, z2);

        y1 = y2 + 10;
        z1 = z2 + 18;

        img.font = "Segoe UI";
        img.fontSize = pixels_to_font_size(36);
        img.fontJustify = Reporter.JUSTIFY_LEFT;

        img.Text(y1, z1, "Precrash");

        y1 = model_y_to_image_y(min_y);
        y2 = y1 + 100;

        z1 = z2 = 100;

        img.lineWidth = 6;
        img.lineStyle = Reporter.LINE_SOLID;
        img.lineColour = "#000000";
        img.Line(y1, z1, y2, z2);

        y1 = y2 + 10;
        z1 = z2 + 18;

        img.font = "Segoe UI";
        img.fontSize = pixels_to_font_size(36);
        img.fontJustify = Reporter.JUSTIFY_LEFT;

        img.Text(y1, z1, "Postcrash");
    }

    /**
     * Draws the max intrusion point on the image
     */
    function draw_max_intrusion_point() {
        var y1, z1, y2, z2;

        /* Vertical line */

        img.lineWidth = 1;
        img.lineStyle = Reporter.LINE_DASH;
        img.lineColour = "#000000";

        y1 = y_at_max_intrusion;
        y2 = y_at_max_intrusion;

        z1 = model_z_to_image_z(min_z);
        z2 = z_at_max_intrusion;

        img.Line(y1, z1, y2, z2);

        /* Horizontal line */

        y1 = model_y_to_image_y(min_y);
        y2 = y_at_max_intrusion;

        z1 = z_at_max_intrusion;
        z2 = z_at_max_intrusion;

        img.Line(y1, z1, y2, z2);
    }

    /**
     * Converts the model Y coordinate to an image Y coordinate
     * @param {number} model_y Model Y coordinate
     * @returns {number}
     */
    function model_y_to_image_y(model_y) {
        return Math.floor(model_y - y_offset);
    }

    /**
     * Converts the image Y coordinate to a model Y coordinate
     * @param {number} image_y Image Y coordinate
     * @returns {number}
     */
    function image_y_to_model_y(image_y) {
        return Math.floor(image_y + y_offset);
    }

    /**
     * Converts the model Z coordinate to an image Z coordinate
     * @param {number} model_z Model Z coordinate
     * @returns {number}
     */
    function model_z_to_image_z(model_z) {
        var z = model_z - z_offset;

        z = Math.floor(2 * reflect - z);

        return z;
    }

    /**
     * Converts the image Z coordinate to a model Z coordinate
     * @param {number} image_z Image Z coordinate
     * @returns {number}
     */
    function image_z_to_model_z(image_z) {
        var z = 2 * reflect - image_z;

        z = z + z_offset;

        return z;
    }
}

/**
 * Determines the choice of font point size that will give consistent absolute font height
 * independent of the screen resolution
 * @returns {number}
 */
function determine_appropriate_font_size() {
    var t = Template.GetCurrent();

    //var img_dir = t.GetVariableValue("IMAGES_DIR") + "/text_test/"; //used in testing

    var width = 1;
    var height = 200;
    var test_img = new Image(width, height);
    test_img.font = "Segoe UI";
    test_img.fontColour = "black";
    test_img.fontJustify = Reporter.JUSTIFY_CENTRE;

    var average_pixels_per_pt = 0;
    var samples = 0;
    for (var i = 6; i <= 100; i += 6) {
        test_img.fontSize = i;
        test_img.Text(width, height, "I");
        var nblack = width * height - test_img.PixelCount("white");

        //LogPrint("Font size " + i + "pt generates an 'I' of height " + nblack + " which is " + nblack/i + "pixels per pt");
        //test_img.Save(img_dir + "I_" + i+ "pt_npixels_"+nblack + ".png", Image.PNG);

        //clean with white rectangle
        test_img.lineColour = "white";
        test_img.fillColour = "white";
        test_img.lineWidth = 1;
        test_img.lineStyle = Reporter.LINE_SOLID;
        test_img.Rectangle(0, 0, width, height);

        average_pixels_per_pt += nblack / i;
        samples++;
    }

    average_pixels_per_pt = average_pixels_per_pt / samples;

    return average_pixels_per_pt;
}

/**
 * Converts a desired pixel height of upper case I to a font point size
 * @param {number} desired_pixel_height_upper_case_I Desired height of upper case I in pixels
 * @returns {number}
 */
function pixels_to_font_size(desired_pixel_height_upper_case_I) {
    var average_pixels_per_pt = determine_appropriate_font_size(); //this is based on screen res

    var required_font_point = Math.round(desired_pixel_height_upper_case_I / average_pixels_per_pt);

    LogPrint(
        "For your monitor font size " +
            required_font_point +
            "pt should generate text of height of " +
            desired_pixel_height_upper_case_I +
            "px"
    );

    return required_font_point;
}