/*- ******************************************************************************* * 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.io.Serializable; import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.eclipse.dawnsci.analysis.dataset.coords.SectorCoords; /** * Class for sector region of interest */ public class SectorROI extends RingROI implements Serializable { protected double ang[]; // angles in radians protected boolean combineSymmetry; // combine symmetry option for profile (where appropriate) protected int symmetry; // symmetry /** * No operation */ public static final int NONE = 0; /** * Full circular sector */ public static final int FULL = 1; /** * Reflect sector in y-axis (left/right) */ public static final int XREFLECT = 2; /** * Reflect sector in x-axis (up/down) */ public static final int YREFLECT = 3; /** * Rotate sector by +90 degrees */ public static final int CNINETY = 4; /** * Rotate sector by -90 degrees */ public static final int ACNINETY = 5; /** * Invert sector through centre */ public static final int INVERT = 6; private static Map<Integer, String> symmetryText = new HashMap<Integer, String>(); static { symmetryText.put(SectorROI.NONE, "None"); symmetryText.put(SectorROI.FULL, "Full"); symmetryText.put(SectorROI.XREFLECT, "L/R"); symmetryText.put(SectorROI.YREFLECT, "U/D"); symmetryText.put(SectorROI.CNINETY, "+90"); symmetryText.put(SectorROI.ACNINETY, "-90"); symmetryText.put(SectorROI.INVERT, "Invert"); } public static Map<Integer, String> getSymmetriesPossible() { return symmetryText; } /** * @param symmetry The symmetry to set. */ public void setSymmetry(int symmetry) { this.symmetry = symmetry; setDirty(); } @Override protected void setDirty() { super.setDirty(); symAng = null; } /** * @param symmetry The symmetry to set. */ public static int getSymmetry(String symmetry) { if (symmetryText.containsValue(symmetry)) for (int key : symmetryText.keySet()) if (symmetry.equals(symmetryText.get(key))) { return key; } return SectorROI.NONE; } /** * @return Returns the symmetry. */ public int getSymmetry() { return symmetry; } /** * */ public SectorROI() { this(30., 120., Math.PI*0.25, Math.PI*2./3.); } private final static double TWO_PI = 2.0 * Math.PI; private final static double HALF_PI = 0.5 * Math.PI; /** * Create an annulus * @param sr * @param er */ public SectorROI(double sr, double er) { this(0., 0., sr, er, 0, TWO_PI, 1.0, false, SectorROI.FULL); } /** * @param sr * @param er * @param sp * @param ep */ public SectorROI(double sr, double er, double sp, double ep) { this(0., 0., sr, er, sp, ep); } /** * @param ptx * @param pty * @param sr * @param er * @param sp * @param ep */ public SectorROI(double ptx, double pty, double sr, double er, double sp, double ep) { this(ptx, pty, sr, er, sp, ep, 1.0, true); } /** * @param ptx * @param pty * @param sr * @param er * @param sp * @param ep * @param dpp * @param clip */ public SectorROI(double ptx, double pty, double sr, double er, double sp, double ep, double dpp, boolean clip) { this(ptx, pty, sr, er, sp, ep, dpp, clip, SectorROI.NONE); } /** * @param ptx * @param pty * @param sr * @param er * @param sp * @param ep * @param dpp * @param clip * @param sym */ public SectorROI(double ptx, double pty, double sr, double er, double sp, double ep, double dpp, boolean clip, int sym) { super(ptx, pty, sr, er, dpp, clip); ang = new double[] {sp, ep}; symmetry = sym; combineSymmetry = false; checkAngles(ang); } /** * Set angles * @param startAngle * @param endAngle */ public void setAngles(double startAngle, double endAngle) { ang[0] = startAngle; ang[1] = endAngle; checkAngles(ang); setDirty(); } /** * @param angles The angles to set */ public void setAngles(double angles[]) { setAngles(angles[0], angles[1]); } /** * @return Returns the angles in degrees */ public double[] getAnglesDegrees() { double[] angles = new double[] { Math.toDegrees(ang[0]), Math.toDegrees(ang[1]) }; return angles; } /** * @param index * @return Returns the angles in degrees */ public double getAngleDegrees(int index) { return Math.toDegrees(ang[index]); } /** * @param angles The angles in degrees to set */ public void setAnglesDegrees(double angles[]) { setAnglesDegrees(angles[0], angles[1]); } /** * For Jython * @param angles The angles in degrees to set */ public void setAnglesdegrees(double[] angles) { setAnglesDegrees(angles); } /** * @param startAngle in degrees * @param endAngle in degrees */ public void setAnglesDegrees(double startAngle, double endAngle) { setAngles(Math.toRadians(startAngle), Math.toRadians(endAngle)); } /** * @return Returns reference to the angles */ public double[] getAngles() { return ang; } /** * @param index * @return Returns the angles */ public double getAngle(int index) { return ang[index]; } /** * Add an offset to both angle * * @param angle */ public void addAngles(double angle) { for (int i = 0; i < 2; i++) { ang[i] += angle; } if (ang[0] > TWO_PI) { ang[0] -= TWO_PI; ang[1] -= TWO_PI; } if (ang[0] < 0) { ang[0] += TWO_PI; ang[1] += TWO_PI; } setDirty(); } /** * Add an offset to an angle * @param index * @param angle */ public void addAngle(int index, double angle) { ang[index] += angle; checkAngles(ang); setDirty(); } /** * Make sure angles lie in permitted ranges: * 0 <= ang0 <= 2*pi, 0 <= ang1 <= 4*pi * 0 <= ang1 - ang0 <= 2*pi */ protected static void checkAngles(double[] angles) { // sort out relative values double a = angles[0]; while (a >= angles[1]) { angles[1] += TWO_PI; } a += TWO_PI; while (a < angles[1]) { angles[1] -= TWO_PI; } // place correctly in absolute terms while (angles[0] < 0) { angles[0] += TWO_PI; angles[1] += TWO_PI; } while (angles[0] > TWO_PI) { angles[0] -= TWO_PI; angles[1] -= TWO_PI; } } private static boolean isAngleInSector(double[] limits, double angle) { if (angle < 0) { angle += TWO_PI; } if (limits[0] <= angle) { return angle <= limits[1]; } return angle + TWO_PI <= limits[1]; } @Override public SectorROI copy() { SectorROI c = new SectorROI(spt[0], spt[1], rad[0], rad[1], ang[0], ang[1], dpp, clippingCompensation, symmetry); c.setCombineSymmetry(combineSymmetry); c.name = name; c.plot = plot; return c; } /** * @param sym * @return true if given symmetry is okay with angles */ public boolean checkSymmetry(int sym) { boolean isOK = false; switch(sym) { case NONE: case FULL: isOK = true; break; case XREFLECT: if (0 <= ang[0] && ang[0] < HALF_PI && ang[0] <= ang[1] && ang[1] <= HALF_PI) isOK = true; else if (HALF_PI <= ang[0] && ang[0] <= 3*HALF_PI && ang[0] <= ang[1] && ang[1] <= 3*HALF_PI) isOK = true; else if (3*HALF_PI <= ang[0] && ang[0] <= TWO_PI && ang[0] <= ang[1] && ang[1] <= 5*HALF_PI) isOK = true; break; case YREFLECT: if (0 <= ang[0] && ang[0] < Math.PI && ang[0] <= ang[1] && ang[1] <= Math.PI) isOK = true; else if (Math.PI <= ang[0] && ang[0] <= TWO_PI && ang[0] <= ang[1] && ang[1] <= TWO_PI) isOK = true; break; case CNINETY: if (ang[1] <= ang[0] + HALF_PI) isOK = true; break; case ACNINETY: if (ang[0] >= ang[1] - HALF_PI) isOK = true; break; case INVERT: if (ang[1] <= ang[0] + HALF_PI) isOK = true; break; } return isOK; } /** * @return angles from symmetry operations */ public double[] getSymmetryAngles() { double[] nang = new double[] {0, TWO_PI}; switch (symmetry) { case XREFLECT: // add in x reflected integral nang[0] = Math.PI - ang[1]; nang[1] = Math.PI - ang[0]; break; case YREFLECT: // add in y reflected integral nang[0] = TWO_PI - ang[1]; nang[1] = TWO_PI - ang[0]; break; case CNINETY: // add in +90 rotated integral nang[0] = ang[0] + HALF_PI; nang[1] = ang[1] + HALF_PI; break; case ACNINETY: // add in -90 rotated integral nang[0] = ang[0] - HALF_PI; nang[1] = ang[1] - HALF_PI; break; case INVERT: // add in inverted integral nang[0] = ang[0] + Math.PI; nang[1] = ang[1] + Math.PI; break; case FULL: break; default: case NONE: return null; } return nang; } /** * @return text for symmetry setting */ public String getSymmetryText() { return getSymmetryText(symmetry); } /** * @return text for symmetry setting */ public static String getSymmetryText(int sym) { if (symmetryText.containsKey(sym)) return symmetryText.get(sym); return "N"; } /** * @param combineSymmetry The combineSymmetry to set. */ public void setCombineSymmetry(boolean combineSymmetry) { this.combineSymmetry = combineSymmetry; } /** * @return Returns the combineSymmetry. */ public boolean isCombineSymmetry() { return combineSymmetry; } /** * @return Returns true if ROI has separate regions (determined by symmetry and combine flag) */ public boolean hasSeparateRegions() { return !(symmetry == NONE || symmetry == FULL || combineSymmetry); } @Override public RectangularROI getBounds() { if (bounds == null) { double[] max = SectorCoords.convertFromPolarRadians(rad[0], ang[0]); double[] min = max.clone(); ROIUtils.updateMaxMin(max, min, SectorCoords.convertFromPolarRadians(rad[0], ang[1])); ROIUtils.updateMaxMin(max, min, SectorCoords.convertFromPolarRadians(rad[1], ang[1])); ROIUtils.updateMaxMin(max, min, SectorCoords.convertFromPolarRadians(rad[1], ang[0])); int beg = (int) Math.ceil(ang[0] / HALF_PI); int end = (int) Math.floor(ang[1] / HALF_PI); for (; beg <= end; beg++) { // angle range spans multiples of pi/2 ROIUtils.updateMaxMin(max, min, SectorCoords.convertFromPolarRadians(rad[1], beg * HALF_PI)); } symAng = getSymmetryAngles(); if (symAng != null) { if (symAng[0] > symAng[1]) { // angles may not be ordered double t = symAng[1]; symAng[1] = symAng[0]; symAng[0] = t; } checkAngles(symAng); // make angles lie in [0, 2pi) ROIUtils.updateMaxMin(max, min, SectorCoords.convertFromPolarRadians(rad[0], symAng[0])); ROIUtils.updateMaxMin(max, min, SectorCoords.convertFromPolarRadians(rad[0], symAng[1])); ROIUtils.updateMaxMin(max, min, SectorCoords.convertFromPolarRadians(rad[1], symAng[1])); ROIUtils.updateMaxMin(max, min, SectorCoords.convertFromPolarRadians(rad[1], symAng[0])); beg = (int) Math.ceil(symAng[0] / HALF_PI); end = (int) Math.floor(symAng[1] / HALF_PI); for (; beg <= end; beg++) { // angle range spans multiples of pi/2 ROIUtils.updateMaxMin(max, min, SectorCoords.convertFromPolarRadians(rad[1], beg * HALF_PI)); } } bounds = new RectangularROI(); bounds.setPoint(min[0] + spt[0], min[1] + spt[1]); bounds.setLengths(max[0] - min[0], max[1] - min[1]); } return bounds; } private transient double[] symAng = null; @Override public boolean containsPoint(double x, double y) { x -= spt[0]; y -= spt[1]; double[] pol = SectorCoords.convertFromCartesianToPolarRadians(x, y); double r = pol[0]; if (r < rad[0] || r > rad[1]) return false; double p = pol[1]; if (p >= ang[0] && p <= ang[1]) return true; if (ang[1] > TWO_PI && p + TWO_PI < ang[1]) // angle domain straddles branch cut return true; if (bounds == null) { // calculate symmetry angles getBounds(); } if (symAng == null) return false; if (symAng[0] > symAng[1]) { // angles may not be ordered double t = symAng[1]; symAng[1] = symAng[0]; symAng[0] = t; } checkAngles(symAng); // make angles lie in [0, 2pi) if (p >= symAng[0] && p <= symAng[1]) return true; return symAng[1] > TWO_PI && p + TWO_PI < symAng[1]; } @Override public boolean isNearOutline(double x, double y, double distance) { x -= spt[0]; y -= spt[1]; double[] pol = SectorCoords.convertFromCartesianToPolarRadians(x, y); double r = pol[0]; double p = pol[1]; if (p >= ang[0] && p <= ang[1]) { // near arcs if (Math.abs(r - rad[0]) <= distance || Math.abs(r - rad[1]) <= distance) return true; } // check radials double[] pt = SectorCoords.convertFromPolarRadians(rad[0], ang[0]); double px = pt[0]; double py = pt[1]; pt = SectorCoords.convertFromPolarRadians(rad[1], ang[0]); if (ROIUtils.isNearSegment(pt[0] - px, pt[1] - py, x - px, y - py, distance)) return true; pt = SectorCoords.convertFromPolarRadians(rad[0], ang[1]); px = pt[0]; py = pt[1]; pt = SectorCoords.convertFromPolarRadians(rad[1], ang[1]); return ROIUtils.isNearSegment(pt[0] - px, pt[1] - py, x - px, y - py, distance); } @Override public String toString() { return super.toString() + String.format("point=%s, radii=%s, angles=[%g, %g], symmetry=%s", Arrays.toString(spt), Arrays.toString(rad), getAngleDegrees(0), getAngleDegrees(1), getSymmetryText()); } protected static double[] calculateArcIntersections(final double[] angles, final double xc, final double yc, final double r) { if (yc < -r || yc > r) return null; if (yc == -r) { return isAngleInSector(angles, 1.5*Math.PI) ? new double[] {xc} : null; } else if (yc == r) { return isAngleInSector(angles, HALF_PI) ? new double[] {xc} : null; } double x = Math.sqrt(r*r - yc*yc); double a = Math.atan2(yc, x); if (isAngleInSector(angles, a)) { return isAngleInSector(angles, Math.PI - a) ? new double[] {xc - x, xc + x} : new double[] {xc + x}; } return isAngleInSector(angles, Math.PI - a) ? new double[] {xc - x} : null; } /** * Get relative point on circle at given angle * @param rad * @param angle in radians * @return point with relative y-values */ double[] getRelativePoint(double rad, double angle) { return new double[] { spt[0] + rad*Math.cos(angle), rad*Math.sin(angle) }; } private void findHorizontalIntersections(Set<Double> values, double[] angles, double y) { double[] xi; xi = calculateArcIntersections(angles, spt[0], y, rad[1]); if (xi != null) { for (double x : xi) values.add(x); } xi = calculateArcIntersections(angles, spt[0], y, rad[0]); if (xi != null) { for (double x : xi) values.add(x); } if (angles[1] - angles[0] != TWO_PI) { xi = ROIUtils.findYIntersection(getRelativePoint(rad[0], angles[0]), getRelativePoint(rad[1], angles[0]), y); if (xi != null) { for (double x : xi) values.add(x); } xi = ROIUtils.findYIntersection(getRelativePoint(rad[0], angles[1]), getRelativePoint(rad[1], angles[1]), y); if (xi != null) { for (double x : xi) values.add(x); } } } @Override public double[] findHorizontalIntersections(double y) { y -= spt[1]; double[] xi; Set<Double> values = new TreeSet<Double>(); if (symmetry != FULL) findHorizontalIntersections(values, ang, y); if (symmetry != NONE) { if (symAng == null) { symAng = getSymmetryAngles(); } findHorizontalIntersections(values, symAng, y); } if (values.size() == 0) return null; xi = new double[values.size()]; int i = 0; for (Double d : values) { xi[i++] = d; } return xi; } @Override public int hashCode() { final int prime = 31; int result = super.hashCode(); result = prime * result + Arrays.hashCode(ang); result = prime * result + (combineSymmetry ? 1231 : 1237); result = prime * result + symmetry; return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!super.equals(obj)) return false; SectorROI other = (SectorROI) obj; if (!Arrays.equals(ang, other.ang)) return false; if (combineSymmetry != other.combineSymmetry) return false; if (symmetry != other.symmetry) return false; return true; } }