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. Use the getPolygon() or getFloatPolygon() methods
 * to retrieve the coordinates of the points. 
 * @see <a href="http://wsr.imagej.net/macros/js/PointProperties.js">PointProperties.js</a>
*/
public class PointRoi extends PolygonRoi {
    public static final String[] sizes = {"Tiny", "Small", "Medium", "Large", "Extra Large", "XXL", "XXXL"};
    public static final String[] types = {"Hybrid", "Cross", "Dot", "Circle"};
    public static final int HYBRID=0, CROSS=1, CROSSHAIR=1, DOT=2, CIRCLE=3;
    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, XXL=17, XXXL=25;
    private static final BasicStroke twoPixelsWide = new BasicStroke(2);
    private static final BasicStroke threePixelsWide = new BasicStroke(3);
    private static final BasicStroke fivePixelsWide = new BasicStroke(5);
    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;
    public static PointRoi savedPoints;

    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);
        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);
        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(makeXorYArray(ox, null, false), makeXorYArray(oy, null, true), 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(makeXorYArray(ox, null, false), makeXorYArray(oy, null, true), 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(makeXorYArray(ox, null, false), makeXorYArray(oy, null, true), 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(makeXorYArray(sx, imp, false), makeXorYArray(sy, imp, true), 1, POINT);
        //defaultCounter = 0;
        setImage(imp);
        width=1; height=1;
        type = defaultType;
        size = defaultSize;
        showLabels = !Prefs.noPointLabels;
        if (imp!=null) {
            int r = 10 + size;
            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);
        }
    }

    public 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;
        else if (options.contains("xxxl")) size=XXXL;
        else if (options.contains("xxl")) size=XXL;
        if (options.contains("cross")) type=CROSS;
        else if (options.contains("dot")) type=DOT;
        else if (options.contains("circle")) type=CIRCLE;
        if (options.contains("nolabel")) setShowLabels(false);
        else if (options.contains("label")) setShowLabels(true);
        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; i<n; i++)
            temp[i] = arr[i];
        return temp;
    }

    /** Creates a one-element array with a coordinate; if 'imp' is non-null
     *  converts from a screen coordinate to an image (offscreen) coordinate.
     *  The array can be used for adding to an existing point selection */
    static float[] makeXorYArray(double value, ImagePlus imp, boolean isY) {
        if (imp != null) {
            ImageCanvas canvas = imp.getCanvas();
            if (canvas != null) {           //offset 0.5 converts from area to pixel center coordinates
                value = (isY ? canvas.offScreenYD((int)value) :canvas.offScreenXD((int)value)) - 0.5;
                if (!magnificationForSubPixel(canvas.getMagnification()))
                    value = Math.round(value);
            }
        }
        return new float[] {(float)value};
    }

    void handleMouseMove(int ox, int oy) {
    }

    protected void handleMouseUp(int sx, int sy) {
        super.handleMouseUp(sx, sy);
        modifyRoi(); //adds this point to previous points if shift key down
    }

    /** Draws the points on the image. */
    public void draw(Graphics g) {
        updatePolygon();
        if (showLabels && nPoints>1) {
            fontSize = 8;
            double scale = size>=XXL?2:1.5;
            fontSize += scale*convertSizeToIndex(size);
            fontSize = (int)Math.round(fontSize);
            //IJ.log("fontSize: "+fontSize+" "+scale);
            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; i<nPoints; i++) {
            //IJ.log(i+" "+slice+" "+(positions!=null?positions[i]:-1)+"  "+getPosition());
            if (slice==0 || (positions!=null&&(slice==positions[i]||positions[i]==0)))
                drawPoint(g, xp2[i], yp2[i], i+1);
        }
        if (updateFullWindow) {
            updateFullWindow = false;
            imp.draw();
        }
        PointToolOptions.update();
        flattenScale = 1.0;
    }

    void drawPoint(Graphics g, int x, int y, int n) {
        int size2=size/2;
        boolean colorSet = false;
        Graphics2D g2d = (Graphics2D)g;
        AffineTransform saveXform = null;
        if (flattenScale>1.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>XXL)
                g2d.setStroke(fivePixelsWide);
            else 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 xoffset = 2;
            if (size==LARGE) xoffset=3;
            if (size==EXTRA_LARGE) xoffset=4;
            if (size==XXL) xoffset=5;
            if (size==XXXL) xoffset=7;
            int yoffset = xoffset;
            if (size>=LARGE) yoffset=yoffset-1;
            if (nCounters==1) {
                if (!colorSet)
                    g.setColor(color);
                g.drawString(""+n, x+xoffset, y+yoffset+fontSize);
            } else if (counters!=null) {
                g.setColor(getColor(counters[n-1]));
                g.drawString(""+counters[n-1], x+xoffset, y+yoffset+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);
        double x0 = bounds == null ? x : bounds.x;
        double y0 = bounds == null ? y : bounds.y;
        for (int i=0; i<nPoints; i++) {
            ip.moveTo((int)Math.round(x0+xpf[i]), (int)Math.round(y0+ypf[i]));
            ip.lineTo((int)Math.round(x0+xpf[i]), (int)Math.round(y0+ypf[i]));
        }
    }

    /** Adds a point to this PointRoi. */
    public void addPoint(ImagePlus imp, double ox, double oy) {
        if (nPoints==xpf.length)
            enlargeArrays();
        addPoint2(imp, ox, oy);
        resetBoundingRect();
    }


    public void addUserPoint(ImagePlus imp, double ox, double oy) {
        addPoint(imp, ox, oy);
        nMarkers++;
    }

    private void addPoint2(ImagePlus imp, double ox, double oy) {
        double xbase = getXBase();
        double ybase = getYBase();
        xpf[nPoints] = (float)(ox-xbase);
        ypf[nPoints] = (float)(oy-ybase);
        xp2[nPoints] = (int)ox;
        yp2[nPoints] = (int)oy;
        nPoints++;
        incrementCounter(imp);
        lastPointTime = System.currentTimeMillis();
    }

    /** Adds a point to this PointRoi. */
    public PointRoi addPoint(double x, double y) {
        addPoint(null, x, y);
        return this;
    }

    /** Adds a point at the specified stack position. */
    public void addPoint(double x, double y, int position) {
        if (counters==null) {
            counters = new short[100];
            positions = new int[100];
        }
        addPoint(null, x, y);
        positions[nPoints-1] = position;    
    }

    protected void deletePoint(int index) {
        super.deletePoint(index);
        if (index>=0 && index<=nPoints && counters!=null) {
            counts[counters[index]]--;
            for (int i=index; i<nPoints; i++) {
                counters[i] = counters[i+1];
                positions[i] = positions[i+1];
            }
            if (rt!=null && WindowManager.getFrame(getCountsTitle())!=null)
                displayCounts();
        }
    }

    private synchronized void incrementCounter(ImagePlus imp) {
        //IJ.log("incrementCounter: "+nPoints+" "+counter+" "+(counters!=null?""+counters.length:"null"));
        counts[counter]++;
        boolean isStack = imp!=null && imp.getStackSize()>1;
        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 <a href="http://wsr.imagej.net/macros/js/PointProperties.js">PointProperties.js</a>
     */
    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; counter<nCounters; counter++) {
            if (getCount(counter)>0) 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; i<counts.length; i++)
            counts[i] = 0;
        counters = null;
        positions = null;
        PointToolOptions.update();
    }

    /** Returns the points of this Roi that are not contained in the specified area ROI.
     *  Returns null if there are no resulting points or Roi is not an area roi. */
    public PointRoi subtractPoints(Roi roi) {
        return checkContained(roi, false);
    }

    /** Returns the points of this Roi that are contained in the specified area ROI.
     *  Returns null if there are no resulting points or Roi is not an area roi. */
    public PointRoi containedPoints(Roi roi) {
        return checkContained(roi, true);
    }

    /** Returns the points contained (not contained) in <code>roi</code> if <code>keepContained</code> 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<points.npoints; i++) {
            if (keepContained == roi.containsPoint(points.xpoints[i], points.ypoints[i]))
                points2.addPoint(points.xpoints[i], points.ypoints[i]);
        }
        if (points2.npoints==0)
            return null;
        else {
            PointRoi roi2 = new PointRoi(points2.xpoints, points2.ypoints, points2.npoints);
            roi2.copyAttributes(this);
            return roi2;
        }
    }

    public ImageProcessor getMask() {
        ImageProcessor mask = cachedMask;
        if (mask!=null && mask.getPixels()!=null)
            return mask;
        mask = new ByteProcessor(width, height);
        double deltaX = getXBase() - x;
        double deltaY = getYBase() - y;
        for (int i=0; i<nPoints; i++)
            mask.putPixel((int)Math.round(xpf[i] + deltaX), (int)Math.round(ypf[i] + deltaY), 255);
        cachedMask = mask;
        return mask;
    }

    /** Returns true if (x,y) is one of the points in this collection. */
    public boolean contains(int x, int y) {
        for (int i=0; i<nPoints; i++) {
            if (x==this.x+xpf[i] && y==this.y+ypf[i]) return true;
        }
        return false;
    }

    public void setShowLabels(boolean showLabels) {
        this.showLabels = showLabels;
    }

    public boolean getShowLabels() {
        return showLabels;
    }

    public static void setDefaultType(int type) {
        if (type>=0 && type<types.length) {
            defaultType = type;
            PointRoi instance = getPointRoiInstance();
            if (instance!=null)
                instance.setPointType(defaultType);
            Prefs.set(TYPE_KEY, type);
        }
    }

    public static int getDefaultType() {
        return defaultType;
    }

    /** Sets the point type (0=hybrid, 1=cross, 2=dot, 3=circle). */
    public void setPointType(int type) {
        if (type>=0 && type<types.length)
            this.type = type;
    }

    /** Returns the point type (0=hybrid, 1=cross, 2=dot, 3=circle). */
    public int getPointType() {
        return type;
    }


    /** Sets the default point size, where 'size' is 0-6 (Tiny-XXXL). */
    public static void setDefaultSize(int size) {
        int index = size;
        if (index>=0 && index<sizes.length) {
            defaultSize = convertIndexToSize(index);
            PointRoi instance = getPointRoiInstance();
            if (instance!=null)
                instance.setSize(index);
            Prefs.set(SIZE_KEY, index);
        }
    }

    /** Returns the default point size 0-6 (Tiny-XXXL). */
    public static int getDefaultSize() {
        return convertSizeToIndex(defaultSize);
    }

    /** Sets the point size, where 'size' is 0-6 (Tiny-XXXL). */
    public void setSize(int size) {
        if (size>=0 && size<sizes.length)
            this.size = convertIndexToSize(size);
    }

    /** Returns the current point size 0-6 (Tiny-XXXL). */
    public int getSize() {
        return convertSizeToIndex(size);
    }

    private static int convertSizeToIndex(int size) {
        switch (size) {
            case TINY: return 0;
            case SMALL: return 1;
            case MEDIUM: return 2;
            case LARGE: return 3;
            case EXTRA_LARGE: return 4;
            case XXL: return 5;
            case XXXL: return 6;
        }
        return 1;
    }

    private static int convertIndexToSize(int index) {
        switch (index) {
            case 0: return TINY;
            case 1: return SMALL;
            case 2: return MEDIUM;
            case 3: return LARGE;
            case 4: return EXTRA_LARGE;
            case 5: return XXL;
            case 6: return XXXL;
        }
        return SMALL;
    }

    /** Always returns true. */
    public boolean subPixelResolution() {
        return true;
    }

    private static PointRoi getPointRoiInstance() {
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp!=null) {
            Roi roi  = imp.getRoi();
            if (roi!=null) {
                if (roi instanceof PointRoi)
                    return (PointRoi)roi;
            }
        }
        return null;
    }

    public void setCounter(int counter) {
        this.counter = counter;
        if (counter>nCounters-1 && nCounters<MAX_COUNTERS)
            nCounters = counter + 1;
    }

    public boolean promptBeforeDeleting() {
        if (promptBeforeDeletingCalled && getNCounters()==1)
            return promptBeforeDeleting;
        else
            return (nMarkers>8||getNCounters()>1) && 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<nPoints; i++)
                temp[i] = (counters[i]&0xff) + (positions[i]<<8);
        }
        return temp;
    }

    /** Sets the counter number and slice number for each point from an
     *  array, where the lower 8 bits are the counter number and the
     *  higher 24 bits contain the slice position of each point.
     *  Used when reading a roi fromfile (RoiDecoder).  */
    public void setCounters(int[] counters) {
        if (counters!=null) {
            int n = counters.length;
            this.counters = new short[n*2];
            this.positions = new int[n*2];
            for (int i=0; i<n; i++) {
                int counter = counters[i]&0xff;
                int position = counters[i]>>8;
                //IJ.log(i+" cnt="+counter+" slice="+position);
                this.counters[i] = (short)counter;
                this.positions[i] = position;
                if (counter<counts.length && counter>nCounters-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<nPoints; i++)
            counts[(counters==null || counters[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 && index<nPoints)
            return positions[index];
        else
            return 0;
    }

    public void displayCounts() {
        ImagePlus imp = getImage();
        String firstColumnHdr = "Slice";
        rt = new ResultsTable();
        int row = 0;
        if (imp!=null && imp.getStackSize()>1 && positions!=null) {
            int nChannels = 1;
            int nSlices = 1;
            int nFrames = 1;
            boolean 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; i<nPoints; i++) {
                if (positions[i]>0 && positions[i]<firstSlice)
                    firstSlice = positions[i];
            }
            if (firstSlice==Integer.MAX_VALUE)
                firstSlice = 0;
            int lastSlice = 0;
            if (firstSlice>0) {
                for (int i=0; i<nPoints; i++) {
                    if (positions[i]>lastSlice)
                        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; counter<nCounters; counter++) {
                        int count = 0;
                        for (int i=0; i<nPoints; i++) {
                            if (slice==positions[i] && counter==counters[i])
                                count++;
                        }
                        rt.setValue("Ctr "+counter, row, count);
                    }
                    row++;
                }
            }
        }
        rt.setValue(firstColumnHdr, row, "Total");
        for (int i=0; i<nCounters; i++)
            rt.setValue("Ctr "+i, row, counts[i]);
        rt.show(getCountsTitle());
        if (IJ.debugMode) debug();
    }

    private void debug() {
        FloatPolygon p = getFloatPolygon();
        ResultsTable rt = new ResultsTable();
        for (int i=0; i<nPoints; i++) {
            if (counters!=null) {
                rt.setValue("Counter", i, counters[i]);
                rt.setValue("Position", i, positions[i]);
            }
            rt.setValue("X", i, p.xpoints[i]);
            rt.setValue("Y", i, p.ypoints[i]);
        }
        rt.show(getCountsTitle());
    }

    private String getCountsTitle() {
        return "Counts_"+(imp!=null?imp.getTitle():"");
    }

    public synchronized static String[] getCounterChoices() {
        if (counterChoices==null) {
            counterChoices = new String[MAX_COUNTERS];
            for (int i=0; i<MAX_COUNTERS; i++)
                counterChoices[i] = ""+i;
        }
        return counterChoices;
    }

    private static Color getColor(int index) {
        if (colors==null) {
            colors = new Color[MAX_COUNTERS];
            colors[0]=Color.yellow; colors[1]=Color.magenta; colors[2]=Color.cyan;
            colors[3]=Color.orange; colors[4]=Color.green; colors[5]=Color.blue;
            colors[6]=Color.white; colors[7]=Color.darkGray; colors[8]=Color.pink;
            colors[9]=Color.lightGray;
        }
        if (colors[index]!=null)
            return colors[index];
        else {
            Random ran = new Random();
            float r = (float)ran.nextDouble();
            float g = (float)ran.nextDouble();
            float b = (float)ran.nextDouble();
            Color c = new Color(r, g, b);
            colors[index] = c;
            return c;
        }
    }

    /** Returns a point index if it has been at least one second since
        the last point was added and the specified screen coordinates are
        inside or near a point, otherwise returns -1. */
    public int isHandle(int sx, int sy) {
        if ((System.currentTimeMillis()-lastPointTime)<1000L)
            return -1;
        int size = HANDLE_SIZE+this.size;
        int halfSize = size/2;
        int handle = -1;
        int sx2, sy2;
        int slice = !Prefs.showAllPoints&&positions!=null&&imp!=null&&imp.getStackSize()>1?imp.getCurrentSlice():0;
        for (int i=0; i<nPoints; i++) {
            if (slice!=0 && slice!=positions[i])
                continue;
            sx2 = xp2[i]-halfSize; sy2=yp2[i]-halfSize;
            if (sx>=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<p.npoints; i++)
            points[i] = new Point((int) Math.round(p.xpoints[i] - 0.5f), (int) Math.round(p.ypoints[i] - 0.5f));
        return points;
    }

    /** Returns the points as a FloatPolygon. */
    public FloatPolygon getContainedFloatPoints() {
        return getFloatPolygon();
    }

    /**
     * Custom iterator for points contained in a {@link PointRoi}.
     * Author: W. Burger
    */
    public Iterator<Point> iterator() {
        return new Iterator<Point>() {
            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; i<points.npoints; i++) {
            double dx = points.xpoints[i] - x;
            double dy = points.ypoints[i] - y;
            double distance2 = dx*dx+dy*dy;
            if (distance2<distance && (slice==0||slice==positions[i])) {
                distance = distance2;
                index = i;
            }
        }
        return index;
    }

    /** Returns a copy of this PointRoi. */
    public synchronized Object clone() {
        PointRoi r = (PointRoi)super.clone();
        if (counters!=null) {
            r.counters = new short[counters.length];
            for (int i=0; i<counters.length; i++)
                r.counters[i] = counters[i];
        }
        if (positions!=null) {
            r.positions = new int[positions.length];
            for (int i=0; i<positions.length; i++)
                r.positions[i] = positions[i];
        }
        if (counts!=null) {
            r.counts = new int[counts.length];
            for (int i=0; i<counts.length; i++)
                r.counts[i] = counts[i];
        }
        return r;
    }

    /* Returns a version of this PointRoi that only contains points inside 'roi'. */
    public PointRoi crop(Roi roi) {
        PointRoi points = (PointRoi)clone();
        Polygon p = points.getPolygon();
        for (int i=points.size()-1; i>=0; i--) {
            if (!roi.contains(p.xpoints[i],p.ypoints[i])) {
                points.deletePoint(i);
            }
        }
        return points;
    }

    @Override
    public void copyAttributes(Roi roi2) {
        super.copyAttributes(roi2);
        if (roi2 instanceof PointRoi) {
            PointRoi p2 = (PointRoi)roi2;
            this.type = p2.type;
            this.size = p2.size;
            this.showLabels = p2.showLabels;
            this.fontSize = p2.fontSize;
        }
    }

    public void setCounterInfo(int[] info) {
        counterInfo = info;
    }

    public int[] getCounterInfo() {
        return counterInfo;
    }

    public boolean addToOverlay() {
        return addToOverlay;
    }

    public String toString() {
        if (nPoints>1)
            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;
    }

}