package ij.gui;
import ij.*;
import ij.process.*;
import ij.measure.*;
import ij.plugin.Colors;
import ij.plugin.PointToolOptions;
import ij.plugin.filter.Analyzer;
import ij.plugin.frame.Recorder;
import ij.util.Java2;
import java.awt.*;
import java.awt.image.*;
import java.awt.event.KeyEvent;
import java.util.*;
import java.awt.geom.*;
/** This class represents a collection of points that can be associated with counters.
* @see PointProperties.js
*/
public class PointRoi extends PolygonRoi {
public static final String[] sizes = {"Tiny", "Small", "Medium", "Large", "Extra Large"};
public static final String[] types = {"Hybrid", "Cross", "Dot", "Circle"};
private static final String TYPE_KEY = "point.type";
private static final String SIZE_KEY = "point.size";
private static final String CROSS_COLOR_KEY = "point.cross.color";
private static final int TINY=1, SMALL=3, MEDIUM=5, LARGE=7, EXTRA_LARGE=11;
private static final int HYBRID=0, CROSS=1, CROSSHAIR=1, DOT=2, CIRCLE=3;
private static final BasicStroke twoPixelsWide = new BasicStroke(2);
private static final BasicStroke threePixelsWide = new BasicStroke(3);
private static int defaultType = HYBRID;
private static int defaultSize = SMALL;
private static Font font;
private static Color defaultCrossColor = Color.white;
private static int fontSize = 9;
public static final int MAX_COUNTERS = 100;
private static String[] counterChoices;
private static Color[] colors;
private boolean showLabels;
private int type = HYBRID;
private int size = SMALL;
private static int defaultCounter;
private int counter;
private int nCounters = 1;
private short[] counters; //for each point, 0-100 for counter (=category that can be defined by the user)
private int[] positions; //for each point, the stack slice, or 0 for 'show on all'
private int[] counts = new int[MAX_COUNTERS];
private ResultsTable rt;
private long lastPointTime;
private int[] counterInfo;
private boolean promptBeforeDeleting;
private boolean promptBeforeDeletingCalled;
private int nMarkers;
private boolean addToOverlay;
static {
setDefaultType((int)Prefs.get(TYPE_KEY, HYBRID));
setDefaultSize((int)Prefs.get(SIZE_KEY, 1));
}
public PointRoi() {
this(0.0, 0.0);
deletePoint(0);
}
/** Creates a new PointRoi using the specified int arrays of offscreen coordinates. */
public PointRoi(int[] ox, int[] oy, int points) {
super(itof(ox), itof(oy), points, POINT);
width+=1; height+=1;
updateCounts();
}
/** Creates a new PointRoi using the specified float arrays of offscreen coordinates. */
public PointRoi(float[] ox, float[] oy, int points) {
super(ox, oy, points, POINT);
width+=1; height+=1;
updateCounts();
}
/** Creates a new PointRoi using the specified float arrays of offscreen coordinates. */
public PointRoi(float[] ox, float[] oy) {
this(ox, oy, ox.length);
}
/** Creates a new PointRoi using the specified coordinate arrays and options. */
public PointRoi(float[] ox, float[] oy, String options) {
this(ox, oy, ox.length);
setOptions(options);
}
/** Creates a new PointRoi from a FloatPolygon. */
public PointRoi(FloatPolygon poly) {
this(poly.xpoints, poly.ypoints, poly.npoints);
}
/** Creates a new PointRoi from a Polygon. */
public PointRoi(Polygon poly) {
this(itof(poly.xpoints), itof(poly.ypoints), poly.npoints);
}
/** Creates a new PointRoi using the specified coordinates and options. */
public PointRoi(double ox, double oy, String options) {
super(makeXArray(ox, null), makeYArray(oy, null), 1, POINT);
width=1; height=1;
incrementCounter(null);
setOptions(options);
}
/** Creates a new PointRoi using the specified offscreen int coordinates. */
public PointRoi(int ox, int oy) {
super(makeXArray(ox, null), makeYArray(oy, null), 1, POINT);
width=1; height=1;
incrementCounter(null);
}
/** Creates a new PointRoi using the specified offscreen double coordinates. */
public PointRoi(double ox, double oy) {
super(makeXArray(ox, null), makeYArray(oy, null), 1, POINT);
width=1; height=1;
incrementCounter(null);
}
/** Creates a new PointRoi using the specified screen coordinates. */
public PointRoi(int sx, int sy, ImagePlus imp) {
super(makeXArray(sx, imp), makeYArray(sy, imp), 1, POINT);
defaultCounter = 0;
setImage(imp);
width=1; height=1;
type = defaultType;
size = defaultSize;
showLabels = !Prefs.noPointLabels;
if (imp!=null) {
int r = 10;
double mag = ic!=null?ic.getMagnification():1;
if (mag<1)
r = (int)(r/mag);
imp.draw(x-r, y-r, 2*r, 2*r);
}
setCounter(Toolbar.getMultiPointMode()?defaultCounter:0);
incrementCounter(imp);
enlargeArrays(50);
if (Recorder.record) {
String add = Prefs.pointAddToOverlay?" add":"";
String options = sizes[convertSizeToIndex(size)]+" "+Colors.colorToString(getColor())+" "+types[type]+add;
options = options.toLowerCase();
if (Recorder.scriptMode())
Recorder.recordCall("imp.setRoi(new PointRoi("+x+","+y+",\""+options+"\"));");
else
Recorder.record("makePoint", x, y, options);
}
}
private void setOptions(String options) {
if (options==null)
return;
if (options.contains("tiny")) size=TINY;
else if (options.contains("medium")) size=MEDIUM;
else if (options.contains("extra")) size=EXTRA_LARGE;
else if (options.contains("large")) size=LARGE;
if (options.contains("cross")) type=CROSS;
else if (options.contains("dot")) type=DOT;
else if (options.contains("circle")) type=CIRCLE;
setStrokeColor(Colors.getColor(options,Roi.getColor()));
addToOverlay = options.contains("add");
}
static float[] itof(int[] arr) {
if (arr==null)
return null;
int n = arr.length;
float[] temp = new float[n];
for (int i=0; i1) {
fontSize = 8;
fontSize += convertSizeToIndex(size);
if (fontSize>18)
fontSize = 18;
fontSize = (int)Math.round(fontSize);
font = new Font("SansSerif", Font.PLAIN, fontSize);
g.setFont(font);
if (fontSize>9)
Java2.setAntialiasedText(g, true);
}
int slice = imp!=null&&positions!=null&&imp.getStackSize()>1?imp.getCurrentSlice():0;
ImageCanvas ic = imp!=null?imp.getCanvas():null;
if (ic!=null && overlay && ic.getShowAllList()!=null && ic.getShowAllList().contains(this) && !Prefs.showAllSliceOnly)
slice = 0; // draw point irrespective of currently selected slice
if (Prefs.showAllPoints)
slice = 0;
//IJ.log("draw: "+positions+" "+imp.getCurrentSlice());
for (int i=0; i1.0) {
saveXform = g2d.getTransform();
g2d.translate(x, y);
g2d.scale(flattenScale, flattenScale);
x = y = 0;
}
Color color = strokeColor!=null?strokeColor:ROIColor;
if (!overlay && isActiveOverlayRoi()) {
if (color==Color.cyan)
color = Color.magenta;
else
color = Color.cyan;
}
if (nCounters>1 && counters!=null && n<=counters.length)
color = getColor(counters[n-1]);
if (type==HYBRID || type==CROSS) {
if (type==HYBRID)
g.setColor(Color.white);
else {
g.setColor(color);
colorSet = true;
}
if (size>LARGE)
g2d.setStroke(threePixelsWide);
g.drawLine(x-(size+2), y, x+size+2, y);
g.drawLine(x, y-(size+2), x, y+size+2);
}
if (type!=CROSS && size>SMALL)
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
if (type==HYBRID || type==DOT) {
if (!colorSet) {
g.setColor(color);
colorSet = true;
}
if (size>LARGE)
g2d.setStroke(onePixelWide);
if (size>LARGE && type==DOT)
g.fillOval(x-size2, y-size2, size, size);
else if (size>LARGE && type==HYBRID)
g.fillRect(x-(size2-2), y-(size2-2), size-4, size-4);
else if (size>SMALL && type==HYBRID)
g.fillRect(x-(size2-1), y-(size2-1), size-2, size-2);
else
g.fillRect(x-size2, y-size2, size, size);
}
if (showLabels && nPoints>1) {
int offset = (int)Math.round(0.4);
if (offset<1) offset=1;
offset++;
if (nCounters==1) {
if (!colorSet)
g.setColor(color);
g.drawString(""+n, x+offset, y+offset+fontSize);
} else if (counters!=null) {
g.setColor(getColor(counters[n-1]));
g.drawString(""+counters[n-1], x+offset, y+offset+fontSize);
}
}
if ((size>TINY||type==DOT) && (type==HYBRID||type==DOT)) {
g.setColor(Color.black);
if (size>LARGE && type==HYBRID)
g.drawOval(x-(size2-1), y-(size2-1), size-3, size-3);
else if (size>SMALL && type==HYBRID)
g.drawOval(x-size2, y-size2, size-1, size-1);
else
g.drawOval(x-(size2+1), y-(size2+1), size+1, size+1);
}
if (type==CIRCLE) {
int scaledSize = (int)Math.round(size+1);
g.setColor(color);
if (size>LARGE)
g2d.setStroke(twoPixelsWide);
g.drawOval(x-scaledSize/2, y-scaledSize/2, scaledSize, scaledSize);
}
if (saveXform!=null)
g2d.setTransform(saveXform);
}
public void drawPixels(ImageProcessor ip) {
ip.setLineWidth(Analyzer.markWidth);
for (int i=0; i=0 && index<=nPoints && counters!=null) {
counts[counters[index]]--;
for (int i=index; i1;
if (counter!=0 || isStack || counters!=null) {
if (counters==null) {
counters = new short[nPoints*2];
positions = new int[nPoints*2];
}
counters[nPoints-1] = (short)counter;
if (imp!=null)
positions[nPoints-1] = imp.getStackSize()>1 ? imp.getCurrentSlice() : 0;
//if (positions[nPoints-1]==0 || positions[nPoints-1]==1 || counters[nPoints-1]==0)
// IJ.log("incrementCounter: "+nPoints+" "+" "+positions[nPoints-1]+" "+counters[nPoints-1]+" "+imp);
if (nPoints+1==counters.length) {
short[] temp = new short[counters.length*2];
System.arraycopy(counters, 0, temp, 0, counters.length);
counters = temp;
int[] temp1 = new int[counters.length*2];
System.arraycopy(positions, 0, temp1, 0, positions.length);
positions = temp1;
}
}
if (rt!=null && WindowManager.getFrame(getCountsTitle())!=null)
displayCounts();
}
/** Returns the index of the current counter. */
public int getCounter() {
return counter;
}
/** Returns the count associated with the specified counter index.
* @see #getLastCounter
* @see PointProperties.js
*/
public int getCount(int counter) {
if (counter==0 && counters==null)
return nPoints;
else
return counts[counter];
}
/** Returns the index of the last counter. */
public int getLastCounter() {
return nCounters - 1;
}
/** Returns the number of counters. */
public int getNCounters() {
int n = 0;
for (int counter=0; counter0) n++;
}
return n;
}
/** Returns the counter assocated with the specified point. */
public int getCounter(int index) {
if (counters==null || index>=counters.length)
return 0;
else
return counters[index];
}
public void resetCounters() {
for (int i=0; iroi if keepContained
is true (false). */
PointRoi checkContained(Roi roi, boolean keepContained) {
if (!roi.isArea()) return null;
FloatPolygon points = getFloatPolygon();
FloatPolygon points2 = new FloatPolygon();
for (int i=0; i=0 && type=0 && type=0 && index=0 && sizenCounters-1 && nCounters10 && imp!=null && imp.getWindow()!=null;
}
public void promptBeforeDeleting(Boolean prompt) {
promptBeforeDeleting = prompt;
promptBeforeDeletingCalled = true;
}
public static void setDefaultCounter(int counter) {
defaultCounter = counter;
}
/** Returns an array containing for each point:
* The counter number (0-100) in the lower 8 bits, and the slice number
* (or 0, if the point appears on all slices) in the higher 24 bits.
* Used when writing a Roi to file (RoiEncoder) */
public int[] getCounters() {
if (nPoints>65535)
return null;
int[] temp = new int[nPoints];
if (counters!=null) {
for (int i=0; i>8;
//IJ.log(i+" cnt="+counter+" slice="+position);
this.counters[i] = (short)counter;
this.positions[i] = position;
if (counternCounters-1)
nCounters = counter + 1;
}
updateCounts();
}
}
/** Updates the counts for each category in 'counters' */
public void updateCounts() {
Arrays.fill(counts, 0);
for (int i=0; i=counts.length) ? 0 : counters[i]] ++;
}
/** Returns the stack slice of the point with the given index, or 0 if no slice defined for this point */
public int getPointPosition(int index) {
if (positions!=null && index1 && positions!=null) {
int nChannels = 1;
int nSlices = 1;
int nFrames = 1;
boolean isHyperstack = false; isHyperstack = true;
if (imp.isComposite() || imp.isHyperStack()) {
nChannels = imp.getNChannels();
nSlices = imp.getNSlices();
nFrames = imp.getNFrames();
int nDimensions = 2;
if (nChannels>1) nDimensions++;
if (nSlices>1) nDimensions++;
if (nFrames>1) nDimensions++;
if (nDimensions==3) {
isHyperstack = false;
if (nChannels>1)
firstColumnHdr = "Channel";
} else
firstColumnHdr = "Image";
}
int firstSlice = Integer.MAX_VALUE;
for (int i=0; i0 && positions[i]0) {
for (int i=0; ilastSlice)
lastSlice = positions[i];
}
}
if (firstSlice>0) {
for (int slice=firstSlice; slice<=lastSlice; slice++) {
rt.setValue(firstColumnHdr, row, slice);
if (isHyperstack) {
int[] position = imp.convertIndexToPosition(slice);
if (nChannels>1)
rt.setValue("Channel", row, position[0]);
if (nSlices>1)
rt.setValue("Slice", row, position[1]);
if (nFrames>1)
rt.setValue("Frame", row, position[2]);
}
for (int counter=0; counter1?imp.getCurrentSlice():0;
for (int i=0; i=sx2 && sx<=sx2+size && sy>=sy2 && sy<=sy2+size) {
handle = i;
break;
}
}
return handle;
}
/** Returns the points as an array of Points.
* Wilhelm Burger: modified to use FloatPolygon for correct point positions.
*/
public Point[] getContainedPoints() {
FloatPolygon p = getFloatPolygon();
Point[] points = new Point[p.npoints];
for (int i=0; i iterator() {
return new Iterator() {
final Point[] pnts = getContainedPoints();
final int n = pnts.length;
int next = (n == 0) ? 1 : 0;
@Override
public boolean hasNext() {
return next < n;
}
@Override
public Point next() {
if (next >= n) {
throw new NoSuchElementException();
}
Point pnt = pnts[next];
next = next + 1;
return pnt;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
};
}
@Override
protected int getClosestPoint(double x, double y, FloatPolygon points) {
int index = -1;
double distance = Double.MAX_VALUE;
int slice = imp!=null&&positions!=null&&imp.getStackSize()>1?imp.getCurrentSlice():0;
if (Prefs.showAllPoints)
slice = 0;
for (int i=0; i1)
return ("Roi[Points, count="+nPoints+"]");
else
return ("Roi[Point, x="+x+", y="+y+"]");
}
/** @deprecated */
public void setHideLabels(boolean hideLabels) {
this.showLabels = !hideLabels;
}
/** @deprecated */
public static void setDefaultMarkerSize(String size) {
}
/** @deprecated */
public static String getDefaultMarkerSize() {
return sizes[defaultSize];
}
/** Deprecated */
public static void setDefaultCrossColor(Color color) {
}
/** Deprecated */
public static Color getDefaultCrossColor() {
return null;
}
}