RectangleBox.java

package com.github.celldynamics.quimp.plugin.utils;

import java.util.Collections;
import java.util.Vector;

import org.scijava.vecmath.Matrix3d;
import org.scijava.vecmath.Point3d;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Represents rectangle bounding box.
 * 
 * <p>Bounding box is defined by four corners (in contrary to javafx.geometry.BoundingBox) that can
 * be
 * rotated by any angle.
 * 
 * @author p.baniukiewicz
 */
class RectangleBox {

  static final Logger LOGGER = LoggerFactory.getLogger(RectangleBox.class.getName());

  private Vector<Double> px; // stores px coordinates of bounding box in clockwise order
  private Vector<Double> py; // stores py coordinates of bounding box in clockwise order

  /**
   * Creates bounding box object from px py vectors.
   * 
   * <p>Vectors define corners in clockwise direction. Vectors are referenced only, not copied. They
   * are modified during rotation.
   * 
   * @param x coordinates of bounding box in clockwise order
   * @param y coordinates of bounding box in clockwise order
   * @throws IllegalArgumentException When empty vectors are passed to constructor or input
   *         vectors have different length
   */
  @SuppressWarnings("unchecked")
  public RectangleBox(Vector<Double> x, Vector<Double> y) throws IllegalArgumentException {
    this.px = (Vector<Double>) x.clone();
    this.py = (Vector<Double>) y.clone();
    // get average of px and py
    if (x.isEmpty() || y.isEmpty()) {
      throw new IllegalArgumentException("Input vectors are empty");
    }
    if (x.size() != y.size()) {
      throw new IllegalArgumentException("Input vectors are not equal");
    }
    double centerX = getAverage(x); // centre of mass
    double centerY = getAverage(y); // centre of mass
    // move input points to (0,0)
    for (int i = 0; i < x.size(); i++) {
      this.px.set(i, x.get(i) - centerX);
      this.py.set(i, y.get(i) - centerY);
    }
  }

  /**
   * Specifies bounding box centred at (0,0).
   * 
   * @param width Width of bounding box
   * @param height Height of bounding box
   */
  public RectangleBox(double width, double height) {
    px = new Vector<Double>();
    py = new Vector<Double>();

    // generate artificial rectangle centered at (0,0)
    px.add(-width / 2); // left top
    px.add(width / 2); // right top
    px.add(width / 2); // right down
    px.add(-width / 2); // left down

    py.add(height / 2); // left top
    py.add(height / 2); // right top
    py.add(-height / 2); // right down
    py.add(-height / 2); // left down
  }

  /**
   * Rotates bounding box.
   * 
   * @param angle Rotation angle
   */
  public void rotateBoundingBox(double angle) {

    // assume that image is centered at (0,0)
    // convert to rad
    double angleRad = angle * Math.PI / 180.0;

    // rotation matrix
    Matrix3d rot = new Matrix3d();
    // rotation with - because shear is defined in anti-clockwise and rotZ
    // require counterclockwise (the same)
    rot.rotZ(-angleRad); // generate rotation matrix of angle - bring input image to horizontal
    // position

    // define corner points of image
    Point3d[] cornerTable = new Point3d[4];
    cornerTable[0] = new Point3d(px.get(0), py.get(0), 0); // left up
    cornerTable[1] = new Point3d(px.get(1), py.get(1), 0); // right up
    cornerTable[2] = new Point3d(px.get(2), py.get(2), 0); // right down
    cornerTable[3] = new Point3d(px.get(3), py.get(3), 0); // right up

    int i = 0;
    // rotate virtual image by angle
    for (Point3d p : cornerTable) {
      rot.transform(p); // multiply ROT*P and return result to P
      px.set(i, p.x);
      py.set(i, p.y);
      i++;
    }
  }

  /**
   * Gets width of bounding box as distance over \b px between outermost corners.
   * 
   * @return Width of bounding box
   */
  public double getWidth() {
    return Math.abs(Collections.max(px) - Collections.min(px));
  }

  /**
   * Gets height of bounding box as distance over \b py between outermost corners.
   * 
   * @return Height of bounding box
   */
  public double getHeight() {
    return Math.abs(Collections.max(py) - Collections.min(py));
  }

  /**
   * Gets mean value of input vector.
   * 
   * @param x Vector of to calculate mean
   * @return Mean value of x
   */
  private double getAverage(Vector<Double> x) {
    double sum = 0;
    for (Double val : x) {
      sum += val;
    }
    return sum / x.size();
  }
}