/*- ******************************************************************************* * Copyright (c) 2011, 2014 Diamond Light Source Ltd. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Peter Chang - initial API and implementation and/or initial documentation *******************************************************************************/ package org.eclipse.dawnsci.analysis.dataset.roi; import java.util.Arrays; import org.eclipse.dawnsci.analysis.api.roi.IParametricROI; /** * An elliptical region of interest with the start point as the centre */ public class EllipticalROI extends OrientableROIBase implements IParametricROI { private double[] saxis; // semi-axes /** * No argument constructor need for serialization */ public EllipticalROI() { this(1, 1, 0, 0, 0); } /** * Create a circular ROI * @param croi */ public EllipticalROI(CircularROI croi) { this(croi.getRadius(), croi.getRadius(), 0, croi.getPointX() , croi.getPointY()); } /** * Create a circular ROI * @param radius * @param ptx centre point x value * @param pty centre point y value */ public EllipticalROI(double radius, double ptx, double pty) { this(radius, radius, 0, ptx, pty); } /** * Create an elliptical ROI * @param major semi-axis * @param minor semi-axis * @param angle major axis angle * @param ptx centre point x value * @param pty centre point y value */ public EllipticalROI(double major, double minor, double angle, double ptx, double pty) { spt = new double[] { ptx, pty }; saxis = new double[] { major, minor }; ang = angle; checkAngle(); } @Override public void downsample(double subFactor) { super.downsample(subFactor); saxis[0] /= subFactor; saxis[1] /= subFactor; setDirty(); } @Override public EllipticalROI copy() { EllipticalROI c = new EllipticalROI(saxis[0], saxis[1], ang, spt[0], spt[1]); c.name = name; c.plot = plot; return c; } /** * @return Returns reference to the semi-axes */ public double[] getSemiAxes() { return saxis; } /** * @param index (should be 0 or 1 for major or minor axis) * @return Returns the semi-axis value */ public double getSemiAxis(int index) { if (index < 0 || index > 1) throw new IllegalArgumentException("Index should be 0 or 1"); return saxis[index]; } /** * Set semi-axis values * @param semiaxis */ public void setSemiAxes(double[] semiaxis) { if (saxis.length < 2) throw new IllegalArgumentException("Need at least two semi-axis values"); saxis[0] = semiaxis[0]; saxis[1] = semiaxis[1]; setDirty(); } /** * Set semi-axis values * @param semiaxis */ public void setSemiaxes(double[] semiaxis) { setSemiAxes(semiaxis); } /** * Set semi-axis value * @param index (should be 0 or 1 for major or minor axis) * @param semiaxis */ public void setSemiAxis(int index, double semiaxis) { if (index < 0 || index > 1) throw new IllegalArgumentException("Index should be 0 or 1"); saxis[index] = semiaxis; setDirty(); } /** * For Jython * @param angle The angle in degrees to set */ public void setAngledegrees(double angle) { setAngleDegrees(angle); } /** * @return true if ellipse is circular (i.e. its axes have the same length) */ public boolean isCircular() { return saxis[0] == saxis[1]; } /** * @return aspect ratio, i.e. ratio of major to minor axes */ public double getAspectRatio() { return saxis[0] / saxis[1]; } /** * Get point on ellipse at given angle * @param angle in radians * @return point */ @Override public double[] getPoint(double angle) { double[] pt = getRelativePoint(angle); pt[0] += spt[0]; pt[1] += spt[1]; return pt; } /** * Get point on ellipse at given angle relative to centre * @param angle in radians * @return point */ public double[] getRelativePoint(double angle) { double cb = Math.cos(angle); double sb = Math.sin(angle); return new double[] { saxis[0] * cang * cb - saxis[1] * sang * sb, saxis[0] * sang * cb + saxis[1] * cang * sb }; } /** * Get point on ellipse at given angle * @param angle in degrees * @return point */ public double[] getPointDegrees(double angle) { return getPoint(Math.toRadians(angle)); } /** * Get distance from centre to point on ellipse at given angle * @param angle in radians * @return distance */ public double getDistance(double angle) { double[] p = getRelativePoint(angle); return Math.hypot(p[0], p[1]); } @Override public RectangularROI getBounds() { if (bounds == null) { // angles which produce stationary points in x and y double[] angles = new double[] { Math.atan2(-saxis[1] * sang, saxis[0] * cang), Math.atan2(saxis[1] * cang, saxis[0] * sang) }; double[] max = getRelativePoint(angles[0]); double[] min = max.clone(); ROIUtils.updateMaxMin(max, min, getRelativePoint(angles[0] + Math.PI)); ROIUtils.updateMaxMin(max, min, getRelativePoint(angles[1])); ROIUtils.updateMaxMin(max, min, getRelativePoint(angles[1] + Math.PI)); bounds = new RectangularROI(); bounds.setLengths(max[0] - min[0], max[1] - min[1]); bounds.setPoint(spt[0] + min[0], spt[1] + min[1]); } return bounds; } protected double getAngleRelative(double x, double y) { return Math.atan2(saxis[0]*(cang*y - sang*x), saxis[1]*(cang*x + sang*y)); } @Override public boolean containsPoint(double x, double y) { x -= spt[0]; y -= spt[1]; double a = getAngleRelative(x, y); return Math.hypot(x, y) <= getDistance(a); } @Override public boolean isNearOutline(double x, double y, double distance) { x -= spt[0]; y -= spt[1]; double a = getAngleRelative(x, y); return Math.abs(getDistance(a) - Math.hypot(x, y)) <= distance; } /** * Determine if ellipse is within an axis-aligned rectangle * @param rect * @return true if ellipse lies wholly within rectangle */ public boolean isContainedBy(RectangularROI rect) { double as = saxis[0]*saxis[0]; double bs = saxis[1]*saxis[1]; double dx = Math.sqrt(as*cang*cang + bs*sang*sang); double dy = Math.sqrt(as*sang*sang + bs*cang*cang); double ang = Math.abs(rect.getAngle()); double[] a = rect.getPointRef(); double[] b = rect.getEndPoint(); if (ang == 0 || ang == Math.PI) { // do nothing } else if (ang == 0.5*Math.PI || ang == 1.5*Math.PI) { double t = dx; dx = dy; dy = t; } else { throw new UnsupportedOperationException("Non-axis-aligned rectangles are not implemented yet"); } double x = a[0] - spt[0]; if (x > -dx) return false; x = b[0] - spt[0]; if (x < dx) return false; double y = a[1] - spt[1]; if (y > -dy) return false; y = b[1] - spt[1]; if (y < dy) return false; return true; } @Override protected void setDirty() { super.setDirty(); qhB = Double.NaN; } /** * Calculate values for angle at which ellipse will intersect vertical line of given x * @param x * @return possible angles */ @Override public double[] getVerticalIntersectionParameters(double x) { double tx = saxis[0]*cang; double ty = saxis[1]*sang; double r = Math.hypot(tx, ty); x -= spt[0]; if (x < -r || x > r) { return null; } x /= r; double t = Math.atan2(ty, tx); if (x == -1 || x == 1) { // touching case return sanifyAngles(Math.acos(x) - t); } x = Math.acos(x); return sanifyAngles(x - t, 2 * Math.PI - x - t); } /** * Calculate values for angle at which ellipse will intersect horizontal line of given y * @param y * @return possible angles */ @Override public double[] getHorizontalIntersectionParameters(double y) { double tx = saxis[0] * sang; double ty = saxis[1] * cang; double r = Math.hypot(tx, ty); y -= spt[1]; if (y < -r || y > r) { return null; } y /= r; double t = Math.atan2(tx, ty); if (y == -1 || y == 1) { // touching case return sanifyAngles(Math.asin(y) - t); } y = Math.asin(y); return sanifyAngles(y - t, Math.PI - y - t); } @Override public double getStartParameter(double d) { return 0; } @Override public double getEndParameter(double d) { return Math.PI * 2; } @Override public String toString() { if (isCircular()) { return super.toString() + String.format("point=%s, radius=%g, angle=%g", Arrays.toString(spt), saxis[0], getAngleDegrees()); } return super.toString() + String.format("point=%s, semiaxes=%s, angle=%g", Arrays.toString(spt), Arrays.toString(saxis), getAngleDegrees()); } private transient double qhB = Double.NaN; // coefficients of quadratic equation of ellipse private transient double qBC = -1; private transient double qC2 = -1; private void updateQValues() { double a = saxis[0]; double b = saxis[1]; double a2 = a * a; double b2 = b * b; double f = a2 * sang * sang + b2 * cang * cang; double asbs = (a2 + b2) / f; qhB = - asbs * sang * cang / 2; qBC = qhB * qhB - asbs + 1; qC2 = - a2 * b2 / f; } @Override public double[] findHorizontalIntersections(double y) { if (Double.isNaN(qhB)) { updateQValues(); } double disc = qBC * y * y - qC2; double[] xi = null; double hb = qhB * y; if (Math.abs(disc) < Math.ulp(hb*hb + 1e-15)) { xi = new double[] { -hb }; } else if (disc > 0) { disc = Math.sqrt(disc); xi = new double[] { -hb - disc, -hb + disc }; } return xi; } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + Arrays.hashCode(saxis); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!super.equals(obj)) return false; EllipticalROI other = (EllipticalROI) obj; if (!Arrays.equals(saxis, other.saxis)) return false; return true; } }