uccser/cs-field-guide

View on GitHub
csfieldguide/static/interactives/distance-from-stereo-images/js/distance-from-stereo-images.js

Summary

Maintainability
A
55 mins
Test Coverage
/**
 * An object representing the left side of the interactive.
 */
let left = {
  image: null,
  canvas: null,
  x: null,
  y: null,

  toString: function () {
    return "left";
  }
}

/**
 * An object representing the right side of the interactive.
 */
let right = {
  image: null,
  canvas: null,
  x: null,
  y: null,

  toString: function () {
    return "right";
  }
}

const DOT_SIZE = 10;


/**
 * Initialization. Prepares the image select, confirm ('Calculate distance'), and distance between images dropdown
 * buttons. Assigns the values in the left and right objects and adds the click callbacks to the canvases. Updates the
 * canvases. Adds a callback to handle screen size changes as this can change the size of the images.
 */
$(document).ready(function () {
  $('#stereo-left-input').change(function() { loadImageDialog(this, left) });
  $('#stereo-right-input').change(function() { loadImageDialog(this, right) });
  $('#calculate-distance-button').click(displayResult);
  $('#distance-unit-list a').on('click', unitChangeHandler);

  left.image = document.getElementById("img-left");
  left.canvas = document.getElementById("canvas-left");
  left.canvas.onmousedown = (event) => { canvasClickHandler(event, left) };
  right.image = document.getElementById("img-right");
  right.canvas = document.getElementById("canvas-right");
  right.canvas.onmousedown = (event) => { canvasClickHandler(event, right) };

  updateCanvas(left);
  updateCanvas(right);

  window.onresize = reset;
});


/**
 * Handles when the user changes the unit. Updates the displayed unit and converts the unit if necessary.
 */
function unitChangeHandler() {
  let distanceUnit = $('#distance-unit');
  let hasChanged = distanceUnit.html() !== $(this).text();
  let distance = document.getElementById("camera-distance");
  distanceUnit.html($(this).text());
  if (hasChanged) {
    if (distanceUnit.html() === "Inches") {
      distance.value = metresToInches(distance.value)
    } else {
      distance.value = inchesToMetres(distance.value)
    }
  }
}


/**
 * Resets parts of the interactive by resizing the canvases and clearing the click coordinates of images in both the
 * left and right objects, and the displayed values.
 */
function reset() {
  updateCanvas(left);
  updateCanvas(right);
  left.x = left.y = right.x = right.y = null;
  document.getElementById("left-x").innerHTML = null;
  document.getElementById("left-y").innerHTML = null;
  document.getElementById("right-x").innerHTML = null;
  document.getElementById("right-y").innerHTML = null;
}


/**
 * Updates a Canvas such that its dimensions match those of its corresponding image. Also updates the displayed width
 * value under the image.
 * @param side The side to update the canvas of.
 */
function updateCanvas(side) {
  side.canvas.width = side.image.width;
  side.canvas.height = side.image.height;

  document.getElementById(side.toString() + "-width").innerHTML = side.image.width.toString();
}


/**
 * Handles an uploaded image by displaying the image, clearing click coordinates, updating the canvas, and displaying
 * the filename.
 * @param input For obtaining the file data.
 * @param side The side to update.
 */
function loadImageDialog(input, side) {
  if (input.files && input.files[0]) {
    let reader = new FileReader();
    reader.onload = function (e) {
      side.image.crossOrigin = 'anonymous';
      side.image.src = e.target.result;
      side.x = side.y = null;
      document.getElementById(side.toString() + "-x").innerHTML = null;
      document.getElementById(side.toString() + "-y").innerHTML = null;
      // Sometimes the image seemingly is not ready yet, meaning the canvas won't update to the new size. A delay is
      // created to reduce the chances of this occurring, though it still does happen very rarely. Increasing the
      // timeout (or finding a better solution) may fix this.
      setTimeout(() => {  updateCanvas(side); }, 10);
    }
    reader.readAsDataURL(input.files[0]);
    const inputName = "stereo-" + side.toString() + "-input";
    $("label[for=" + inputName + "]").text(input.files[0].name);
  }
}


/**
 * Handles a click in a canvas by updating the coordinates and drawing a dot.
 * @param event The click event.
 * @param side The side to update.
 */
function canvasClickHandler(event, side) {
  const rect = side.canvas.getBoundingClientRect();
  side.x = Math.round(event.clientX - rect.left);
  side.y = Math.round(event.clientY - rect.top);
  document.getElementById(side.toString() + "-x").innerHTML = side.x.toString();
  document.getElementById(side.toString() + "-y").innerHTML = side.y.toString();
  drawDot(side.x, side.y, side.canvas);
}


/**
 * Draws a dot in a canvas.
 * @param x The x-coordinate to place the dot.
 * @param y The y-coordinate to place the dot.
 * @param canvas The canvas to draw on.
 */
function drawDot(x, y, canvas) {
  let ctx = canvas.getContext("2d");
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = "#FF0000";
  ctx.fillRect(x - 0.5 * DOT_SIZE, y - 0.5 * DOT_SIZE, DOT_SIZE, DOT_SIZE);
}


/**
 * Checks all the inputs have been supplied and are valid. If so, displays the calculated result.
 */
function displayResult() {
  if (left.x == null || left.y == null) {
    alert("Select a point in the left camera.");
    return;
  } else if (right.x == null || right.y == null) {
    alert("Select a point in the right camera.");
    return;
  } else if (document.getElementById("angle-of-view").value === "") {
    alert("Provide a valid half angle-of-view.");
    return;
  } else if (document.getElementById("camera-distance").value === "") {
    alert("Provide a valid distance between cameras.");
    return;
  } else if (left.image.width !== right.image.width) {
    alert("The images should be the same width.");
    return;
  }

  document.getElementById("result-title").style.visibility = "visible";
  document.getElementById("result").innerHTML = calculateDistance().toString() + " Meters";
}


/**
 * Calculates the distance to the object based on the equation proposed in the paper
 * http://www.icoci.cms.net.my/proceedings/2017/Pdf_Version_Chap04e/PID105-235-242e.pdf. Also converts the distance from
 * inches to metres if necessary.
 * @returns {number} The distance in metres.
 */
function calculateDistance() {
  let angle_of_view = Number(document.getElementById("angle-of-view").value);
  let angle_of_view_radians = angle_of_view * Math.PI / 180;
  let distance = Number(document.getElementById("camera-distance").value);
  if ($('#distance-unit').html() === "Inches") {
    distance = inchesToMetres(distance);
  }
  return (distance * left.image.width) / (2 * Math.tan(angle_of_view_radians / 2) * (left.x - right.x));
}

/**
 * Converts metres to inches.
 * @param metres Int distance in metres.
 * @returns {number} Int distance in inches.
 */
function metresToInches(metres) {
  return  metres * 39.37007874;
}

/**
 * Converts inches to metres.
 * @param inches Int distance in inches.
 * @returns {number} Int distance in metres.
 */
function inchesToMetres(inches) {
  return inches / 39.37007874;
}