/**
 * Implements ImageJ's Analyze Particles command.
 *
 * <p>
 *
 * <pre>
 * for each line do
 * for each pixel in this line do
 * if the pixel value is "inside" the threshold range then
 * trace the edge to mark the object
 * do the measurement
 * fill the object with a color outside the threshold range
 * else
 * continue the scan
 * </pre>
 */
public class ParticleAnalyzer implements PlugInFilter, Measurements {

  /** Display results in the ImageJ console. */
  public static final int SHOW_RESULTS = 1;

  /** Obsolete */
  public static final int SHOW_SUMMARY = 2;

  /** Display image containing outlines of measured particles. */
  public static final int SHOW_OUTLINES = 4;

  /** Do not measure particles touching edge of image. */
  public static final int EXCLUDE_EDGE_PARTICLES = 8;

  /** Display image containing grayscales masks that identify measured particles. */
  public static final int SHOW_ROI_MASKS = 16;

  /** Display a progress bar. */
  public static final int SHOW_PROGRESS = 32;

  /** Clear ImageJ console before starting. */
  public static final int CLEAR_WORKSHEET = 64;

  /** Record starting coordinates so outline can be recreated later using doWand(x,y). */
  public static final int RECORD_STARTS = 128;

  /** Display a summary. */
  public static final int DISPLAY_SUMMARY = 256;

  /** Do not display particle outline image. */
  public static final int SHOW_NONE = 512;

  /** Flood fill to ignore interior holes. */
  public static final int INCLUDE_HOLES = 1024;

  /** Add particles to ROI Manager. */
  public static final int ADD_TO_MANAGER = 2048;

  /** Display image containing binary masks of measured particles. */
  public static final int SHOW_MASKS = 4096;

  /** Use 4-connected particle tracing. */
  public static final int FOUR_CONNECTED = 8192;

  /** Replace original image with masks. */
  public static final int IN_SITU_SHOW = 16384;

  /** Display particle outlines as an overlay. */
  public static final int SHOW_OVERLAY_OUTLINES = 32768;

  /** Display filled particle as an overlay. */
  public static final int SHOW_OVERLAY_MASKS = 65536;

  static final String OPTIONS = "ap.options";

  static final int BYTE = 0, SHORT = 1, FLOAT = 2, RGB = 3;
  static final double DEFAULT_MIN_SIZE = 0.0;
  static final double DEFAULT_MAX_SIZE = Double.POSITIVE_INFINITY;

  private static double staticMinSize = 0.0;
  private static double staticMaxSize = DEFAULT_MAX_SIZE;
  private static boolean pixelUnits;
  private static int staticOptions = Prefs.getInt(OPTIONS, CLEAR_WORKSHEET);
  private static String[] showStrings = {
    "Nothing",
    "Outlines",
    "Bare Outlines",
    "Ellipses",
    "Masks",
    "Count Masks",
    "Overlay Outlines",
    "Overlay Masks"
  };
  private static double staticMinCircularity = 0.0, staticMaxCircularity = 1.0;
  private static String prevHdr;

  protected static final int NOTHING = 0,
      OUTLINES = 1,
      BARE_OUTLINES = 2,
      ELLIPSES = 3,
      MASKS = 4,
      ROI_MASKS = 5,
      OVERLAY_OUTLINES = 6,
      OVERLAY_MASKS = 7;
  protected static int staticShowChoice;
  protected ImagePlus imp;
  protected ResultsTable rt;
  protected Analyzer analyzer;
  protected int slice;
  protected boolean processStack;
  protected boolean showResults,
      excludeEdgeParticles,
      showSizeDistribution,
      resetCounter,
      showProgress,
      recordStarts,
      displaySummary,
      floodFill,
      addToManager,
      inSituShow;

  private String summaryHdr = "Slice\tCount\tTotal Area\tAverage Size\tArea Fraction";
  private double level1, level2;
  private double minSize, maxSize;
  private double minCircularity, maxCircularity;
  private int showChoice;
  private int options;
  private int measurements;
  private Calibration calibration;
  private String arg;
  private double fillColor;
  private boolean thresholdingLUT;
  private ImageProcessor drawIP;
  private int width, height;
  private boolean canceled;
  private ImageStack outlines;
  private IndexColorModel customLut;
  private int particleCount;
  private int maxParticleCount = 0;
  private int totalCount;
  private TextWindow tw;
  private Wand wand;
  private int imageType, imageType2;
  private boolean roiNeedsImage;
  private int minX, maxX, minY, maxY;
  private ImagePlus redirectImp;
  private ImageProcessor redirectIP;
  private PolygonFiller pf;
  private Roi saveRoi;
  private int beginningCount;
  private Rectangle r;
  private ImageProcessor mask;
  private double totalArea;
  private FloodFiller ff;
  private Polygon polygon;
  private RoiManager roiManager;
  private ImagePlus outputImage;
  private boolean hideOutputImage;
  private int roiType;
  private int wandMode = Wand.LEGACY_MODE;
  private Overlay overlay;
  boolean blackBackground;
  private static int defaultFontSize = 9;
  private static int nextFontSize = defaultFontSize;
  private static int nextLineWidth = 1;
  private int fontSize = nextFontSize;
  private int lineWidth = nextLineWidth;

  /**
   * Constructs a ParticleAnalyzer.
   *
   * @param options a flag word created by Oring SHOW_RESULTS, EXCLUDE_EDGE_PARTICLES, etc.
   * @param measurements a flag word created by ORing constants defined in the Measurements
   *     interface
   * @param rt a ResultsTable where the measurements will be stored
   * @param minSize the smallest particle size in pixels
   * @param maxSize the largest particle size in pixels
   * @param minCirc minimum circularity
   * @param maxCirc maximum circularity
   */
  public ParticleAnalyzer(
      int options,
      int measurements,
      ResultsTable rt,
      double minSize,
      double maxSize,
      double minCirc,
      double maxCirc) {
    this.options = options;
    this.measurements = measurements;
    this.rt = rt;
    if (this.rt == null) this.rt = new ResultsTable();
    this.minSize = minSize;
    this.maxSize = maxSize;
    this.minCircularity = minCirc;
    this.maxCircularity = maxCirc;
    slice = 1;
    if ((options & SHOW_ROI_MASKS) != 0) showChoice = ROI_MASKS;
    if ((options & SHOW_OVERLAY_OUTLINES) != 0) showChoice = OVERLAY_OUTLINES;
    if ((options & SHOW_OVERLAY_MASKS) != 0) showChoice = OVERLAY_MASKS;
    if ((options & SHOW_OUTLINES) != 0) showChoice = OUTLINES;
    if ((options & SHOW_MASKS) != 0) showChoice = MASKS;
    if ((options & SHOW_NONE) != 0) showChoice = NOTHING;
    if ((options & FOUR_CONNECTED) != 0) {
      wandMode = Wand.FOUR_CONNECTED;
      options |= INCLUDE_HOLES;
    }
    nextFontSize = defaultFontSize;
    nextLineWidth = 1;
  }

  /** Constructs a ParticleAnalyzer using the default min and max circularity values (0 and 1). */
  public ParticleAnalyzer(
      int options, int measurements, ResultsTable rt, double minSize, double maxSize) {
    this(options, measurements, rt, minSize, maxSize, 0.0, 1.0);
  }

  /** Default constructor */
  public ParticleAnalyzer() {
    slice = 1;
  }

  public int setup(String arg, ImagePlus imp) {
    this.arg = arg;
    this.imp = imp;
    IJ.register(ParticleAnalyzer.class);
    if (imp == null) {
      IJ.noImage();
      return DONE;
    }
    if (imp.getBitDepth() == 24 && !isThresholdedRGB(imp)) {
      IJ.error(
          "Particle Analyzer",
          "RGB images must be thresholded using\n" + "Image>Adjust>Color Threshold.");
      return DONE;
    }
    if (!showDialog()) return DONE;
    int baseFlags = DOES_ALL + NO_CHANGES + NO_UNDO;
    int flags = IJ.setupDialog(imp, baseFlags);
    processStack = (flags & DOES_STACKS) != 0;
    slice = 0;
    saveRoi = imp.getRoi();
    if (saveRoi != null && saveRoi.getType() != Roi.RECTANGLE && saveRoi.isArea())
      polygon = saveRoi.getPolygon();
    imp.startTiming();
    nextFontSize = defaultFontSize;
    nextLineWidth = 1;
    return flags;
  }

  public void run(ImageProcessor ip) {
    if (canceled) return;
    slice++;
    if (imp.getStackSize() > 1 && processStack) imp.setSlice(slice);
    if (imp.getType() == ImagePlus.COLOR_RGB) {
      ip = (ImageProcessor) imp.getProperty("Mask");
      ip.setThreshold(255, 255, ImageProcessor.NO_LUT_UPDATE);
    }
    if (!analyze(imp, ip)) canceled = true;
    if (slice == imp.getStackSize()) {
      imp.updateAndDraw();
      if (saveRoi != null) imp.setRoi(saveRoi);
    }
  }

  /** Displays a modal options dialog. */
  public boolean showDialog() {
    Calibration cal = imp != null ? imp.getCalibration() : (new Calibration());
    double unitSquared = cal.pixelWidth * cal.pixelHeight;
    if (pixelUnits) unitSquared = 1.0;
    if (Macro.getOptions() != null) {
      boolean oldMacro = updateMacroOptions();
      if (oldMacro) unitSquared = 1.0;
      staticMinSize = 0.0;
      staticMaxSize = DEFAULT_MAX_SIZE;
      staticMinCircularity = 0.0;
      staticMaxCircularity = 1.0;
      staticShowChoice = NOTHING;
    }
    GenericDialog gd = new GenericDialog("Analyze Particles");
    minSize = staticMinSize;
    maxSize = staticMaxSize;
    minCircularity = staticMinCircularity;
    maxCircularity = staticMaxCircularity;
    showChoice = staticShowChoice;
    if (maxSize == 999999) maxSize = DEFAULT_MAX_SIZE;
    options = staticOptions;
    String unit = cal.getUnit();
    boolean scaled = cal.scaled();
    if (unit.equals("inch")) {
      unit = "pixel";
      unitSquared = 1.0;
      scaled = false;
      pixelUnits = true;
    }
    String units = unit + "^2";
    int places = 0;
    double cmin = minSize * unitSquared;
    if ((int) cmin != cmin) places = 2;
    double cmax = maxSize * unitSquared;
    if ((int) cmax != cmax && cmax != DEFAULT_MAX_SIZE) places = 2;
    String minStr = ResultsTable.d2s(cmin, places);
    if (minStr.indexOf("-") != -1) {
      for (int i = places; i <= 6; i++) {
        minStr = ResultsTable.d2s(cmin, i);
        if (minStr.indexOf("-") == -1) break;
      }
    }
    String maxStr = ResultsTable.d2s(cmax, places);
    if (maxStr.indexOf("-") != -1) {
      for (int i = places; i <= 6; i++) {
        maxStr = ResultsTable.d2s(cmax, i);
        if (maxStr.indexOf("-") == -1) break;
      }
    }
    if (scaled) gd.setInsets(5, 0, 0);
    gd.addStringField("Size (" + units + "):", minStr + "-" + maxStr, 12);
    if (scaled) {
      gd.setInsets(0, 40, 5);
      gd.addCheckbox("Pixel units", pixelUnits);
    }
    gd.addStringField("Circularity:", IJ.d2s(minCircularity) + "-" + IJ.d2s(maxCircularity), 12);
    gd.addChoice("Show:", showStrings, showStrings[showChoice]);
    String[] labels = new String[8];
    boolean[] states = new boolean[8];
    labels[0] = "Display results";
    states[0] = (options & SHOW_RESULTS) != 0;
    labels[1] = "Exclude on edges";
    states[1] = (options & EXCLUDE_EDGE_PARTICLES) != 0;
    labels[2] = "Clear results";
    states[2] = (options & CLEAR_WORKSHEET) != 0;
    labels[3] = "Include holes";
    states[3] = (options & INCLUDE_HOLES) != 0;
    labels[4] = "Summarize";
    states[4] = (options & DISPLAY_SUMMARY) != 0;
    labels[5] = "Record starts";
    states[5] = (options & RECORD_STARTS) != 0;
    labels[6] = "Add to Manager";
    states[6] = (options & ADD_TO_MANAGER) != 0;
    labels[7] = "In_situ Show";
    states[7] = (options & IN_SITU_SHOW) != 0;
    gd.addCheckboxGroup(4, 2, labels, states);
    gd.addHelp(IJ.URL + "/docs/menus/analyze.html#ap");
    gd.showDialog();
    if (gd.wasCanceled()) return false;

    String size = gd.getNextString(); // min-max size
    if (scaled) pixelUnits = gd.getNextBoolean();
    if (pixelUnits) unitSquared = 1.0;
    else unitSquared = cal.pixelWidth * cal.pixelHeight;
    String[] minAndMax = Tools.split(size, " -");
    double mins = gd.parseDouble(minAndMax[0]);
    double maxs = minAndMax.length == 2 ? gd.parseDouble(minAndMax[1]) : Double.NaN;
    minSize = Double.isNaN(mins) ? DEFAULT_MIN_SIZE : mins / unitSquared;
    maxSize = Double.isNaN(maxs) ? DEFAULT_MAX_SIZE : maxs / unitSquared;
    if (minSize < DEFAULT_MIN_SIZE) minSize = DEFAULT_MIN_SIZE;
    if (maxSize < minSize) maxSize = DEFAULT_MAX_SIZE;
    staticMinSize = minSize;
    staticMaxSize = maxSize;

    minAndMax = Tools.split(gd.getNextString(), " -"); // min-max circularity
    double minc = gd.parseDouble(minAndMax[0]);
    double maxc = minAndMax.length == 2 ? gd.parseDouble(minAndMax[1]) : Double.NaN;
    minCircularity = Double.isNaN(minc) ? 0.0 : minc;
    maxCircularity = Double.isNaN(maxc) ? 1.0 : maxc;
    if (minCircularity < 0.0 || minCircularity > 1.0) minCircularity = 0.0;
    if (maxCircularity < minCircularity || maxCircularity > 1.0) maxCircularity = 1.0;
    if (minCircularity == 1.0 && maxCircularity == 1.0) minCircularity = 0.0;
    staticMinCircularity = minCircularity;
    staticMaxCircularity = maxCircularity;

    if (gd.invalidNumber()) {
      IJ.error("Bins invalid.");
      canceled = true;
      return false;
    }
    showChoice = gd.getNextChoiceIndex();
    staticShowChoice = showChoice;
    if (gd.getNextBoolean()) options |= SHOW_RESULTS;
    else options &= ~SHOW_RESULTS;
    if (gd.getNextBoolean()) options |= EXCLUDE_EDGE_PARTICLES;
    else options &= ~EXCLUDE_EDGE_PARTICLES;
    if (gd.getNextBoolean()) options |= CLEAR_WORKSHEET;
    else options &= ~CLEAR_WORKSHEET;
    if (gd.getNextBoolean()) options |= INCLUDE_HOLES;
    else options &= ~INCLUDE_HOLES;
    if (gd.getNextBoolean()) options |= DISPLAY_SUMMARY;
    else options &= ~DISPLAY_SUMMARY;
    if (gd.getNextBoolean()) options |= RECORD_STARTS;
    else options &= ~RECORD_STARTS;
    if (gd.getNextBoolean()) options |= ADD_TO_MANAGER;
    else options &= ~ADD_TO_MANAGER;
    if (gd.getNextBoolean()) options |= IN_SITU_SHOW;
    else options &= ~IN_SITU_SHOW;
    staticOptions = options;
    options |= SHOW_PROGRESS;
    if ((options & DISPLAY_SUMMARY) != 0)
      Analyzer.setMeasurements(Analyzer.getMeasurements() | AREA);
    return true;
  }

  private boolean isThresholdedRGB(ImagePlus imp) {
    Object obj = imp.getProperty("Mask");
    if (obj == null || !(obj instanceof ImageProcessor)) return false;
    ImageProcessor mask = (ImageProcessor) obj;
    return mask.getWidth() == imp.getWidth() && mask.getHeight() == imp.getHeight();
  }

  boolean updateMacroOptions() {
    String options = Macro.getOptions();
    int index = options.indexOf("maximum=");
    if (index == -1) return false;
    index += 8;
    int len = options.length();
    while (index < len - 1 && options.charAt(index) != ' ') index++;
    if (index == len - 1) return false;
    int min = (int) Tools.parseDouble(Macro.getValue(options, "minimum", "1"));
    int max = (int) Tools.parseDouble(Macro.getValue(options, "maximum", "999999"));
    options = "size=" + min + "-" + max + options.substring(index, len);
    Macro.setOptions(options);
    return true;
  }

  /** Performs particle analysis on the specified image. Returns false if there is an error. */
  public boolean analyze(ImagePlus imp) {
    return analyze(imp, imp.getProcessor());
  }

  /**
   * Performs particle analysis on the specified ImagePlus and ImageProcessor. Returns false if
   * there is an error.
   */
  public boolean analyze(ImagePlus imp, ImageProcessor ip) {
    if (this.imp == null) this.imp = imp;
    showResults = (options & SHOW_RESULTS) != 0;
    excludeEdgeParticles = (options & EXCLUDE_EDGE_PARTICLES) != 0;
    resetCounter = (options & CLEAR_WORKSHEET) != 0;
    showProgress = (options & SHOW_PROGRESS) != 0;
    floodFill = (options & INCLUDE_HOLES) == 0;
    recordStarts = (options & RECORD_STARTS) != 0;
    addToManager = (options & ADD_TO_MANAGER) != 0;
    displaySummary = (options & DISPLAY_SUMMARY) != 0;
    inSituShow = (options & IN_SITU_SHOW) != 0;
    outputImage = null;
    ip.snapshot();
    ip.setProgressBar(null);
    if (Analyzer.isRedirectImage()) {
      redirectImp = Analyzer.getRedirectImage(imp);
      if (redirectImp == null) return false;
      int depth = redirectImp.getStackSize();
      if (depth > 1 && depth == imp.getStackSize()) {
        ImageStack redirectStack = redirectImp.getStack();
        redirectIP = redirectStack.getProcessor(imp.getCurrentSlice());
      } else redirectIP = redirectImp.getProcessor();
    } else if (imp.getType() == ImagePlus.COLOR_RGB) {
      ImagePlus original = (ImagePlus) imp.getProperty("OriginalImage");
      if (original != null
          && original.getWidth() == imp.getWidth()
          && original.getHeight() == imp.getHeight()) {
        redirectImp = original;
        redirectIP = original.getProcessor();
      }
    }
    if (!setThresholdLevels(imp, ip)) return false;
    width = ip.getWidth();
    height = ip.getHeight();
    if (!(showChoice == NOTHING || showChoice == OVERLAY_OUTLINES || showChoice == OVERLAY_MASKS)) {
      blackBackground = Prefs.blackBackground && inSituShow;
      if (slice == 1) outlines = new ImageStack(width, height);
      if (showChoice == ROI_MASKS) drawIP = new ShortProcessor(width, height);
      else drawIP = new ByteProcessor(width, height);
      drawIP.setLineWidth(lineWidth);
      if (showChoice == ROI_MASKS) {
      } // Place holder for now...
      else if (showChoice == MASKS && !blackBackground) drawIP.invertLut();
      else if (showChoice == OUTLINES) {
        if (!inSituShow) {
          if (customLut == null) makeCustomLut();
          drawIP.setColorModel(customLut);
        }
        drawIP.setFont(new Font("SansSerif", Font.PLAIN, fontSize));
        if (fontSize > 12 && inSituShow) drawIP.setAntialiasedText(true);
      }
      outlines.addSlice(null, drawIP);

      if (showChoice == ROI_MASKS || blackBackground) {
        drawIP.setColor(Color.black);
        drawIP.fill();
        drawIP.setColor(Color.white);
      } else {
        drawIP.setColor(Color.white);
        drawIP.fill();
        drawIP.setColor(Color.black);
      }
    }
    calibration = redirectImp != null ? redirectImp.getCalibration() : imp.getCalibration();

    if (rt == null) {
      rt = Analyzer.getResultsTable();
      analyzer = new Analyzer(imp);
    } else analyzer = new Analyzer(imp, measurements, rt);
    if (resetCounter && slice == 1) {
      if (!Analyzer.resetCounter()) return false;
    }
    beginningCount = Analyzer.getCounter();

    byte[] pixels = null;
    if (ip instanceof ByteProcessor) pixels = (byte[]) ip.getPixels();
    if (r == null) {
      r = ip.getRoi();
      mask = ip.getMask();
      if (displaySummary) {
        if (mask != null) totalArea = ImageStatistics.getStatistics(ip, AREA, calibration).area;
        else totalArea = r.width * calibration.pixelWidth * r.height * calibration.pixelHeight;
      }
    }
    minX = r.x;
    maxX = r.x + r.width;
    minY = r.y;
    maxY = r.y + r.height;
    if (r.width < width || r.height < height || mask != null) {
      if (!eraseOutsideRoi(ip, r, mask)) return false;
    }
    int offset;
    double value;
    int inc = Math.max(r.height / 25, 1);
    int mi = 0;
    ImageWindow win = imp.getWindow();
    if (win != null) win.running = true;
    if (measurements == 0) measurements = Analyzer.getMeasurements();
    if (showChoice == ELLIPSES) measurements |= ELLIPSE;
    measurements &= ~LIMIT; // ignore "Limit to Threshold"
    roiNeedsImage =
        (measurements & PERIMETER) != 0
            || (measurements & SHAPE_DESCRIPTORS) != 0
            || (measurements & FERET) != 0;
    particleCount = 0;
    wand = new Wand(ip);
    pf = new PolygonFiller();
    if (floodFill) {
      ImageProcessor ipf = ip.duplicate();
      ipf.setValue(fillColor);
      ff = new FloodFiller(ipf);
    }
    roiType = Wand.allPoints() ? Roi.FREEROI : Roi.TRACED_ROI;

    for (int y = r.y; y < (r.y + r.height); y++) {
      offset = y * width;
      for (int x = r.x; x < (r.x + r.width); x++) {
        if (pixels != null) value = pixels[offset + x] & 255;
        else if (imageType == SHORT) value = ip.getPixel(x, y);
        else value = ip.getPixelValue(x, y);
        if (value >= level1 && value <= level2) analyzeParticle(x, y, imp, ip);
      }
      if (showProgress && ((y % inc) == 0)) IJ.showProgress((double) (y - r.y) / r.height);
      if (win != null) canceled = !win.running;
      if (canceled) {
        Macro.abort();
        break;
      }
    }
    if (showProgress) IJ.showProgress(1.0);
    if (showResults) rt.updateResults();
    imp.killRoi();
    ip.resetRoi();
    ip.reset();
    if (displaySummary && IJ.getInstance() != null) updateSliceSummary();
    if (addToManager && roiManager != null) roiManager.setEditMode(imp, true);
    maxParticleCount = (particleCount > maxParticleCount) ? particleCount : maxParticleCount;
    totalCount += particleCount;
    if (!canceled) showResults();
    return true;
  }

  void updateSliceSummary() {
    int slices = imp.getStackSize();
    float[] areas = rt.getColumn(ResultsTable.AREA);
    if (areas == null) areas = new float[0];
    String label = imp.getTitle();
    if (slices > 1) {
      label = imp.getStack().getShortSliceLabel(slice);
      label = label != null && !label.equals("") ? label : "" + slice;
    }
    String aLine = null;
    double sum = 0.0;
    int start = areas.length - particleCount;
    if (start < 0) return;
    for (int i = start; i < areas.length; i++) sum += areas[i];
    int places = Analyzer.getPrecision();
    Calibration cal = imp.getCalibration();
    String total = "\t" + ResultsTable.d2s(sum, places);
    String average = "\t" + ResultsTable.d2s(sum / particleCount, places);
    String fraction = "\t" + ResultsTable.d2s(sum * 100.0 / totalArea, 1);
    aLine = label + "\t" + particleCount + total + average + fraction;
    aLine = addMeans(aLine, areas.length > 0 ? start : -1);
    if (slices == 1) {
      Frame frame = WindowManager.getFrame("Summary");
      if (frame != null && (frame instanceof TextWindow) && summaryHdr.equals(prevHdr))
        tw = (TextWindow) frame;
    }
    if (tw == null) {
      String title = slices == 1 ? "Summary" : "Summary of " + imp.getTitle();
      tw = new TextWindow(title, summaryHdr, aLine, 450, 300);
      prevHdr = summaryHdr;
    } else tw.append(aLine);
  }

  String addMeans(String line, int start) {
    if ((measurements & MEAN) != 0) line = addMean(ResultsTable.MEAN, line, start);
    if ((measurements & MODE) != 0) line = addMean(ResultsTable.MODE, line, start);
    if ((measurements & PERIMETER) != 0) line = addMean(ResultsTable.PERIMETER, line, start);
    if ((measurements & ELLIPSE) != 0) {
      line = addMean(ResultsTable.MAJOR, line, start);
      line = addMean(ResultsTable.MINOR, line, start);
      line = addMean(ResultsTable.ANGLE, line, start);
    }
    if ((measurements & SHAPE_DESCRIPTORS) != 0) {
      line = addMean(ResultsTable.CIRCULARITY, line, start);
      line = addMean(ResultsTable.SOLIDITY, line, start);
    }
    if ((measurements & FERET) != 0) {
      line = addMean(ResultsTable.FERET, line, start);
      line = addMean(ResultsTable.FERET_X, line, start);
      line = addMean(ResultsTable.FERET_Y, line, start);
      line = addMean(ResultsTable.FERET_ANGLE, line, start);
      line = addMean(ResultsTable.MIN_FERET, line, start);
    }
    if ((measurements & INTEGRATED_DENSITY) != 0)
      line = addMean(ResultsTable.INTEGRATED_DENSITY, line, start);
    if ((measurements & MEDIAN) != 0) line = addMean(ResultsTable.MEDIAN, line, start);
    if ((measurements & SKEWNESS) != 0) line = addMean(ResultsTable.SKEWNESS, line, start);
    if ((measurements & KURTOSIS) != 0) line = addMean(ResultsTable.KURTOSIS, line, start);
    return line;
  }

  private String addMean(int column, String line, int start) {
    if (start == -1) {
      line += "\tNaN";
      summaryHdr += "\t" + ResultsTable.getDefaultHeading(column);
    } else {
      float[] c = column >= 0 ? rt.getColumn(column) : null;
      if (c != null) {
        ImageProcessor ip = new FloatProcessor(c.length, 1, c, null);
        if (ip == null) return line;
        ip.setRoi(start, 0, ip.getWidth() - start, 1);
        ip = ip.crop();
        ImageStatistics stats = new FloatStatistics(ip);
        if (stats == null) return line;
        line += n(stats.mean);
      } else line += "\tNaN";
      summaryHdr += "\t" + rt.getColumnHeading(column);
    }
    return line;
  }

  String n(double n) {
    String s;
    if (Math.round(n) == n) s = ResultsTable.d2s(n, 0);
    else s = ResultsTable.d2s(n, Analyzer.getPrecision());
    return "\t" + s;
  }

  boolean eraseOutsideRoi(ImageProcessor ip, Rectangle r, ImageProcessor mask) {
    int width = ip.getWidth();
    int height = ip.getHeight();
    ip.setRoi(r);
    if (excludeEdgeParticles && polygon != null) {
      ImageStatistics stats = ImageStatistics.getStatistics(ip, MIN_MAX, null);
      if (fillColor >= stats.min && fillColor <= stats.max) {
        double replaceColor = level1 - 1.0;
        if (replaceColor < 0.0 || replaceColor == fillColor) {
          replaceColor = level2 + 1.0;
          int maxColor = imageType == BYTE ? 255 : 65535;
          if (replaceColor > maxColor || replaceColor == fillColor) {
            IJ.error("Particle Analyzer", "Unable to remove edge particles");
            return false;
          }
        }
        for (int y = minY; y < maxY; y++) {
          for (int x = minX; x < maxX; x++) {
            int v = ip.getPixel(x, y);
            if (v == fillColor) ip.putPixel(x, y, (int) replaceColor);
          }
        }
      }
    }
    ip.setValue(fillColor);
    if (mask != null) {
      mask = mask.duplicate();
      mask.invert();
      ip.fill(mask);
    }
    ip.setRoi(0, 0, r.x, height);
    ip.fill();
    ip.setRoi(r.x, 0, r.width, r.y);
    ip.fill();
    ip.setRoi(r.x, r.y + r.height, r.width, height - (r.y + r.height));
    ip.fill();
    ip.setRoi(r.x + r.width, 0, width - (r.x + r.width), height);
    ip.fill();
    ip.resetRoi();
    // IJ.log("erase: "+fillColor+"	"+level1+"	"+level2+"	"+excludeEdgeParticles);
    // (new ImagePlus("ip2", ip.duplicate())).show();
    return true;
  }

  boolean setThresholdLevels(ImagePlus imp, ImageProcessor ip) {
    double t1 = ip.getMinThreshold();
    double t2 = ip.getMaxThreshold();
    boolean invertedLut = imp.isInvertedLut();
    boolean byteImage = ip instanceof ByteProcessor;
    if (ip instanceof ShortProcessor) imageType = SHORT;
    else if (ip instanceof FloatProcessor) imageType = FLOAT;
    else imageType = BYTE;
    if (t1 == ImageProcessor.NO_THRESHOLD) {
      ImageStatistics stats = imp.getStatistics();
      if (imageType != BYTE || (stats.histogram[0] + stats.histogram[255] != stats.pixelCount)) {
        IJ.error(
            "Particle Analyzer",
            "A thresholded image or 8-bit binary image is\n"
                + "required. Threshold levels can be set using\n"
                + "the Image->Adjust->Threshold tool.");
        canceled = true;
        return false;
      }
      boolean threshold255 = invertedLut;
      if (Prefs.blackBackground) threshold255 = !threshold255;
      if (threshold255) {
        level1 = 255;
        level2 = 255;
        fillColor = 64;
      } else {
        level1 = 0;
        level2 = 0;
        fillColor = 192;
      }
    } else {
      level1 = t1;
      level2 = t2;
      if (imageType == BYTE) {
        if (level1 > 0) fillColor = 0;
        else if (level2 < 255) fillColor = 255;
      } else if (imageType == SHORT) {
        if (level1 > 0) fillColor = 0;
        else if (level2 < 65535) fillColor = 65535;
      } else if (imageType == FLOAT) fillColor = -Float.MAX_VALUE;
      else return false;
    }
    imageType2 = imageType;
    if (redirectIP != null) {
      if (redirectIP instanceof ShortProcessor) imageType2 = SHORT;
      else if (redirectIP instanceof FloatProcessor) imageType2 = FLOAT;
      else if (redirectIP instanceof ColorProcessor) imageType2 = RGB;
      else imageType2 = BYTE;
    }
    return true;
  }

  int counter = 0;

  void analyzeParticle(int x, int y, ImagePlus imp, ImageProcessor ip) {
    // Wand wand = new Wand(ip);
    ImageProcessor ip2 = redirectIP != null ? redirectIP : ip;
    wand.autoOutline(x, y, level1, level2, wandMode);
    if (wand.npoints == 0) {
      IJ.log("wand error: " + x + " " + y);
      return;
    }
    Roi roi = new PolygonRoi(wand.xpoints, wand.ypoints, wand.npoints, roiType);
    Rectangle r = roi.getBounds();
    if (r.width > 1 && r.height > 1) {
      PolygonRoi proi = (PolygonRoi) roi;
      pf.setPolygon(proi.getXCoordinates(), proi.getYCoordinates(), proi.getNCoordinates());
      ip2.setMask(pf.getMask(r.width, r.height));
      if (floodFill) ff.particleAnalyzerFill(x, y, level1, level2, ip2.getMask(), r);
    }
    ip2.setRoi(r);
    ip.setValue(fillColor);
    ImageStatistics stats = getStatistics(ip2, measurements, calibration);
    boolean include = true;
    if (excludeEdgeParticles) {
      if (r.x == minX || r.y == minY || r.x + r.width == maxX || r.y + r.height == maxY)
        include = false;
      if (polygon != null) {
        Rectangle bounds = roi.getBounds();
        int x1 = bounds.x + wand.xpoints[wand.npoints - 1];
        int y1 = bounds.y + wand.ypoints[wand.npoints - 1];
        int x2, y2;
        for (int i = 0; i < wand.npoints; i++) {
          x2 = bounds.x + wand.xpoints[i];
          y2 = bounds.y + wand.ypoints[i];
          if (!polygon.contains(x2, y2)) {
            include = false;
            break;
          }
          if ((x1 == x2 && ip.getPixel(x1, y1 - 1) == fillColor)
              || (y1 == y2 && ip.getPixel(x1 - 1, y1) == fillColor)) {
            include = false;
            break;
          }
          x1 = x2;
          y1 = y2;
        }
      }
    }
    ImageProcessor mask = ip2.getMask();
    if (minCircularity > 0.0 || maxCircularity < 1.0) {
      double perimeter = roi.getLength();
      double circularity =
          perimeter == 0.0 ? 0.0 : 4.0 * Math.PI * (stats.pixelCount / (perimeter * perimeter));
      if (circularity > 1.0) circularity = 1.0;
      // IJ.log(circularity+"	"+perimeter+"  "+stats.area);
      if (circularity < minCircularity || circularity > maxCircularity) include = false;
    }
    if (stats.pixelCount >= minSize && stats.pixelCount <= maxSize && include) {
      particleCount++;
      if (roiNeedsImage) roi.setImage(imp);
      stats.xstart = x;
      stats.ystart = y;
      saveResults(stats, roi);
      if (showChoice != NOTHING) drawParticle(drawIP, roi, stats, mask);
    }
    if (redirectIP != null) ip.setRoi(r);
    ip.fill(mask);
  }

  ImageStatistics getStatistics(ImageProcessor ip, int mOptions, Calibration cal) {
    switch (imageType2) {
      case BYTE:
        return new ByteStatistics(ip, mOptions, cal);
      case SHORT:
        return new ShortStatistics(ip, mOptions, cal);
      case FLOAT:
        return new FloatStatistics(ip, mOptions, cal);
      case RGB:
        return new ColorStatistics(ip, mOptions, cal);
      default:
        return null;
    }
  }

  /**
   * Saves statistics for one particle in a results table. This is a method subclasses may want to
   * override.
   */
  protected void saveResults(ImageStatistics stats, Roi roi) {
    analyzer.saveResults(stats, roi);
    if (recordStarts) {
      rt.addValue("XStart", stats.xstart);
      rt.addValue("YStart", stats.ystart);
    }
    if (addToManager) {
      if (roiManager == null) {
        if (Macro.getOptions() != null && Interpreter.isBatchMode())
          roiManager = Interpreter.getBatchModeRoiManager();
        if (roiManager == null) {
          Frame frame = WindowManager.getFrame("ROI Manager");
          if (frame == null) IJ.run("ROI Manager...");
          frame = WindowManager.getFrame("ROI Manager");
          if (frame == null || !(frame instanceof RoiManager)) {
            addToManager = false;
            return;
          }
          roiManager = (RoiManager) frame;
        }
        if (resetCounter) roiManager.runCommand("reset");
      }
      if (imp.getStackSize() > 1) roi.setPosition(imp.getCurrentSlice());
      if (lineWidth != 1) roi.setStrokeWidth(lineWidth);
      roiManager.add(imp, roi, rt.getCounter());
    }
    if (showResults) rt.addResults();
  }

  /**
   * Draws a selected particle in a separate image. This is another method subclasses may want to
   * override.
   */
  protected void drawParticle(
      ImageProcessor drawIP, Roi roi, ImageStatistics stats, ImageProcessor mask) {
    switch (showChoice) {
      case MASKS:
        drawFilledParticle(drawIP, roi, mask);
        break;
      case OUTLINES:
      case BARE_OUTLINES:
      case OVERLAY_OUTLINES:
      case OVERLAY_MASKS:
        drawOutline(drawIP, roi, rt.getCounter());
        break;
      case ELLIPSES:
        drawEllipse(drawIP, stats, rt.getCounter());
        break;
      case ROI_MASKS:
        drawRoiFilledParticle(drawIP, roi, mask, rt.getCounter());
        break;
      default:
    }
  }

  void drawFilledParticle(ImageProcessor ip, Roi roi, ImageProcessor mask) {
    // IJ.write(roi.getBounds()+" "+mask.length);
    ip.setRoi(roi.getBounds());
    ip.fill(mask);
  }

  void drawOutline(ImageProcessor ip, Roi roi, int count) {
    if (showChoice == OVERLAY_OUTLINES || showChoice == OVERLAY_MASKS) {
      if (overlay == null) {
        overlay = new Overlay();
        overlay.drawLabels(true);
        overlay.setLabelFont(new Font("SansSerif", Font.PLAIN, fontSize));
      }
      Roi roi2 = (Roi) roi.clone();
      roi2.setStrokeColor(Color.cyan);
      if (lineWidth != 1) roi2.setStrokeWidth(lineWidth);
      if (showChoice == OVERLAY_MASKS) roi2.setFillColor(Color.cyan);
      overlay.add(roi2);
    } else {
      Rectangle r = roi.getBounds();
      int nPoints = ((PolygonRoi) roi).getNCoordinates();
      int[] xp = ((PolygonRoi) roi).getXCoordinates();
      int[] yp = ((PolygonRoi) roi).getYCoordinates();
      int x = r.x, y = r.y;
      if (!inSituShow) ip.setValue(0.0);
      ip.moveTo(x + xp[0], y + yp[0]);
      for (int i = 1; i < nPoints; i++) ip.lineTo(x + xp[i], y + yp[i]);
      ip.lineTo(x + xp[0], y + yp[0]);
      if (showChoice != BARE_OUTLINES) {
        String s = ResultsTable.d2s(count, 0);
        ip.moveTo(r.x + r.width / 2 - ip.getStringWidth(s) / 2, r.y + r.height / 2 + fontSize / 2);
        if (!inSituShow) ip.setValue(1.0);
        ip.drawString(s);
      }
    }
  }

  void drawEllipse(ImageProcessor ip, ImageStatistics stats, int count) {
    stats.drawEllipse(ip);
  }

  void drawRoiFilledParticle(ImageProcessor ip, Roi roi, ImageProcessor mask, int count) {
    int grayLevel = (count < 65535) ? count : 65535;
    ip.setValue((double) grayLevel);
    ip.setRoi(roi.getBounds());
    ip.fill(mask);
  }

  void showResults() {
    int count = rt.getCounter();
    // if (count==0) return;
    boolean lastSlice = !processStack || slice == imp.getStackSize();
    if ((showChoice == OVERLAY_OUTLINES || showChoice == OVERLAY_MASKS) && slice == 1 && count > 0)
      imp.setOverlay(overlay);
    else if (outlines != null && lastSlice) {
      String title = imp != null ? imp.getTitle() : "Outlines";
      String prefix;
      if (showChoice == MASKS) prefix = "Mask of ";
      else if (showChoice == ROI_MASKS) prefix = "Count Masks of ";
      else prefix = "Drawing of ";
      outlines.update(drawIP);
      outputImage = new ImagePlus(prefix + title, outlines);
      if (inSituShow) {
        if (imp.getStackSize() == 1) Undo.setup(Undo.TRANSFORM, imp);
        imp.setStack(null, outputImage.getStack());
      } else if (!hideOutputImage) outputImage.show();
    }
    if (showResults && !processStack) {
      TextPanel tp = IJ.getTextPanel();
      if (beginningCount > 0 && tp != null && tp.getLineCount() != count) rt.show("Results");
      Analyzer.firstParticle = beginningCount;
      Analyzer.lastParticle = Analyzer.getCounter() - 1;
    } else Analyzer.firstParticle = Analyzer.lastParticle = 0;
  }

  /**
   * Returns the "Outlines", "Masks", "Elipses" or "Count Masks" image, or null if "Nothing" is
   * selected in the "Show:" menu.
   */
  public ImagePlus getOutputImage() {
    return outputImage;
  }

  /** Set 'hideOutputImage' true to not display the "Show:" image. */
  public void setHideOutputImage(boolean hideOutputImage) {
    this.hideOutputImage = hideOutputImage;
  }

  /** Sets the size of the font used to label outlines in the next particle analyzer instance. */
  public static void setFontSize(int size) {
    nextFontSize = size;
  }

  /** Sets the outline line width for the next particle analyzer instance. */
  public static void setLineWidth(int width) {
    nextLineWidth = width;
  }

  int getColumnID(String name) {
    int id = rt.getFreeColumn(name);
    if (id == ResultsTable.COLUMN_IN_USE) id = rt.getColumnIndex(name);
    return id;
  }

  void makeCustomLut() {
    IndexColorModel cm = (IndexColorModel) LookUpTable.createGrayscaleColorModel(false);
    byte[] reds = new byte[256];
    byte[] greens = new byte[256];
    byte[] blues = new byte[256];
    cm.getReds(reds);
    cm.getGreens(greens);
    cm.getBlues(blues);
    reds[1] = (byte) 255;
    greens[1] = (byte) 0;
    blues[1] = (byte) 0;
    customLut = new IndexColorModel(8, 256, reds, greens, blues);
  }

  /** Called once when ImageJ quits. */
  public static void savePreferences(Properties prefs) {
    prefs.put(OPTIONS, Integer.toString(staticOptions));
  }
}
  public void run(ImageProcessor ip) {
    String[] imageNames = getOpenImageNames();
    if (imageNames[0] == "None") {
      IJ.error("need at least 2 binary open images");
      return;
    }
    double previousMinOverlap = Prefs.get("BVTB.BinaryFeatureExtractor.minOverlap", 0);
    boolean previousCombine = Prefs.get("BVTB.BinaryFeatureExtractor.combine", false);

    GenericDialog gd = new GenericDialog("Binary Feature Extractor");
    gd.addChoice("Objects image", imageNames, imageNames[0]);
    gd.addChoice("Selector image", imageNames, imageNames[1]);
    gd.addNumericField("Object_overlap in % (0=off)", previousMinOverlap, 0, 9, "");
    gd.addCheckbox("Combine objects and selectors", previousCombine);
    gd.addCheckbox("Count output", true);
    gd.addCheckbox("Analysis tables", false);
    gd.showDialog();
    if (gd.wasCanceled()) {
      return;
    }
    String objectsImgTitle = gd.getNextChoice();
    String selectorsImgTitle = gd.getNextChoice();
    double minOverlap = gd.getNextNumber();
    boolean combineImages = gd.getNextBoolean();
    boolean showCountOutput = gd.getNextBoolean();
    boolean showAnalysis = gd.getNextBoolean();
    if (gd.invalidNumber() || minOverlap < 0 || minOverlap > 100) {
      IJ.error("invalid number");
      return;
    }
    Prefs.set("BVTB.BinaryFeatureExtractor.minOverlap", minOverlap);
    Prefs.set("BVTB.BinaryFeatureExtractor.combine", combineImages);

    if (objectsImgTitle.equals(selectorsImgTitle)) {
      IJ.error("images need to be different");
      return;
    }

    ImagePlus objectsImp = WindowManager.getImage(objectsImgTitle);
    ImageProcessor objectsIP = objectsImp.getProcessor();
    ImagePlus selectorsImp = WindowManager.getImage(selectorsImgTitle);
    ImageProcessor selectorsIP = selectorsImp.getProcessor();

    if (!objectsIP.isBinary() || !selectorsIP.isBinary()) {
      IJ.error("works with 8-bit binary images only");
      return;
    }

    if ((objectsImp.getWidth() != selectorsImp.getWidth())
        || objectsImp.getHeight() != selectorsImp.getHeight()) {
      IJ.error("images need to be of the same size");
      return;
    }

    // close any existing RoiManager before instantiating a new one for this analysis
    RoiManager oldRM = RoiManager.getInstance2();
    if (oldRM != null) {
      oldRM.close();
    }

    RoiManager objectsRM = new RoiManager(true);
    ResultsTable objectsRT = new ResultsTable();
    ParticleAnalyzer analyzeObjects =
        new ParticleAnalyzer(analyzerOptions, measurementFlags, objectsRT, 0.0, 999999999.9);
    analyzeObjects.setRoiManager(objectsRM);

    analyzeObjects.analyze(objectsImp);
    objectsRM.runCommand("Show None");
    int objectNumber = objectsRT.getCounter();

    Roi[] objectRoi = objectsRM.getRoisAsArray();

    ResultsTable measureSelectorsRT = new ResultsTable();
    Analyzer overlapAnalyzer = new Analyzer(selectorsImp, measurementFlags, measureSelectorsRT);

    ImagePlus outputImp =
        IJ.createImage("output", "8-bit black", objectsImp.getWidth(), objectsImp.getHeight(), 1);
    ImageProcessor outputIP = outputImp.getProcessor();

    double[] measuredOverlap = new double[objectNumber];

    outputIP.setValue(255.0);
    for (int o = 0; o < objectNumber; o++) {
      selectorsImp.killRoi();
      selectorsImp.setRoi(objectRoi[o]);
      overlapAnalyzer.measure();
      measuredOverlap[o] = measureSelectorsRT.getValue("%Area", o);
      if (minOverlap != 0.0 && measuredOverlap[o] >= minOverlap) {
        outputIP.fill(objectRoi[o]);
        finalCount++;
      } else if (minOverlap == 0.0 && measuredOverlap[o] > 0.0) {
        outputIP.fill(objectRoi[o]);
        finalCount++;
      }
    }
    // measureSelectorsRT.show("Objects");

    selectorsImp.killRoi();
    RoiManager selectorRM = new RoiManager(true);
    ResultsTable selectorRT = new ResultsTable();
    ParticleAnalyzer.setRoiManager(selectorRM);
    ParticleAnalyzer analyzeSelectors =
        new ParticleAnalyzer(analyzerOptions, measurementFlags, selectorRT, 0.0, 999999999.9);
    analyzeSelectors.analyze(selectorsImp);
    selectorRM.runCommand("Show None");
    int selectorNumber = selectorRT.getCounter();

    if (combineImages) {
      outputImp.updateAndDraw();
      Roi[] selectorRoi = selectorRM.getRoisAsArray();

      ResultsTable measureObjectsRT = new ResultsTable();
      Analyzer selectorAnalyzer = new Analyzer(outputImp, measurementFlags, measureObjectsRT);

      double[] selectorOverlap = new double[selectorNumber];
      outputIP.setValue(255.0);
      for (int s = 0; s < selectorNumber; s++) {
        outputImp.killRoi();
        outputImp.setRoi(selectorRoi[s]);
        selectorAnalyzer.measure();
        selectorOverlap[s] = measureObjectsRT.getValue("%Area", s);
        if (selectorOverlap[s] > 0.0d) {
          outputIP.fill(selectorRoi[s]);
        }
      }
      selectorRoi = null;
      selectorAnalyzer = null;
      measureObjectsRT = null;
    }
    // selectorRT.show("Selectors");
    outputImp.killRoi();
    String outputImageTitle = WindowManager.getUniqueName("Extracted_" + objectsImgTitle);
    outputImp.setTitle(outputImageTitle);
    outputImp.show();
    outputImp.changes = true;

    if (showCountOutput) {
      String[] openTextWindows = WindowManager.getNonImageTitles();
      boolean makeNewTable = true;
      for (int w = 0; w < openTextWindows.length; w++) {
        if (openTextWindows[w].equals("BFE_Results")) {
          makeNewTable = false;
        }
      }

      TextWindow existingCountTable = ResultsTable.getResultsWindow();
      if (makeNewTable) {
        countTable = new ResultsTable();
        countTable.setPrecision(0);
        countTable.setValue("Image", 0, outputImageTitle);
        countTable.setValue("Objects", 0, objectNumber);
        countTable.setValue("Selectors", 0, selectorNumber);
        countTable.setValue("Extracted", 0, finalCount);
        countTable.show("BFE_Results");
      } else {
        IJ.renameResults("BFE_Results", "Results");
        countTable = ResultsTable.getResultsTable();
        countTable.setPrecision(0);
        countTable.incrementCounter();
        countTable.addValue("Image", outputImageTitle);
        countTable.addValue("Objects", objectNumber);
        countTable.addValue("Selectors", selectorNumber);
        countTable.addValue("Extracted", finalCount);
        IJ.renameResults("Results", "BFE_Results");
        countTable.show("BFE_Results");
      }
    }

    if (showAnalysis) {
      ResultsTable extractedRT = new ResultsTable();
      ParticleAnalyzer analyzeExtracted =
          new ParticleAnalyzer(
              ParticleAnalyzer.CLEAR_WORKSHEET | ParticleAnalyzer.RECORD_STARTS,
              measurementFlags,
              extractedRT,
              0.0,
              999999999.9);
      analyzeExtracted.analyze(outputImp);
      objectsRT.show("Objects");
      selectorRT.show("Selectors");
      extractedRT.show("Extracted");
    } else {
      objectsRT = null;
      selectorRT = null;
    }

    objectsRM = null;
    measureSelectorsRT = null;
    analyzeObjects = null;
    overlapAnalyzer = null;
    objectRoi = null;
    selectorRM = null;

    objectsImp.killRoi();
    objectsImp.changes = false;
    selectorsImp.changes = false;
  }
示例#3
0
/** This is a Canvas used to display images in a Window. */
public class ImageCanvas extends Canvas implements MouseListener, MouseMotionListener, Cloneable {

  protected static Cursor defaultCursor = new Cursor(Cursor.DEFAULT_CURSOR);
  protected static Cursor handCursor = new Cursor(Cursor.HAND_CURSOR);
  protected static Cursor moveCursor = new Cursor(Cursor.MOVE_CURSOR);
  protected static Cursor crosshairCursor = new Cursor(Cursor.CROSSHAIR_CURSOR);

  public static boolean usePointer = Prefs.usePointerCursor;

  protected ImagePlus imp;
  protected boolean imageUpdated;
  protected Rectangle srcRect;
  protected int imageWidth, imageHeight;
  protected int xMouse; // current cursor offscreen x location
  protected int yMouse; // current cursor offscreen y location

  private boolean showCursorStatus = true;
  private int sx2, sy2;
  private boolean disablePopupMenu;
  private boolean showAllROIs;
  private static Color zoomIndicatorColor;
  private static Font smallFont, largeFont;
  private Font font;
  private Rectangle[] labelRects;
  private boolean maxBoundsReset;
  private Overlay overlay, showAllList;
  private static final int LIST_OFFSET = 100000;
  private static Color showAllColor = Prefs.getColor(Prefs.SHOW_ALL_COLOR, new Color(0, 255, 255));
  private Color defaultColor = showAllColor;
  private static Color labelColor, bgColor;
  private int resetMaxBoundsCount;
  private Roi currentRoi;

  protected ImageJ ij;
  protected double magnification;
  protected int dstWidth, dstHeight;

  protected int xMouseStart;
  protected int yMouseStart;
  protected int xSrcStart;
  protected int ySrcStart;
  protected int flags;

  private Image offScreenImage;
  private int offScreenWidth = 0;
  private int offScreenHeight = 0;
  private boolean mouseExited = true;
  private boolean customRoi;
  private boolean drawNames;

  public ImageCanvas(ImagePlus imp) {
    this.imp = imp;
    ij = IJ.getInstance();
    int width = imp.getWidth();
    int height = imp.getHeight();
    imageWidth = width;
    imageHeight = height;
    srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
    setDrawingSize(imageWidth, (int) (imageHeight));
    magnification = 1.0;
    addMouseListener(this);
    addMouseMotionListener(this);
    addKeyListener(ij); // ImageJ handles keyboard shortcuts
    setFocusTraversalKeysEnabled(false);
  }

  void updateImage(ImagePlus imp) {
    this.imp = imp;
    int width = imp.getWidth();
    int height = imp.getHeight();
    imageWidth = width;
    imageHeight = height;
    srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
    setDrawingSize(imageWidth, (int) imageHeight);
    magnification = 1.0;
  }

  /** Update this ImageCanvas to have the same zoom and scale settings as the one specified. */
  void update(ImageCanvas ic) {
    if (ic == null || ic == this || ic.imp == null) return;
    if (ic.imp.getWidth() != imageWidth || ic.imp.getHeight() != imageHeight) return;
    srcRect = new Rectangle(ic.srcRect.x, ic.srcRect.y, ic.srcRect.width, ic.srcRect.height);
    setMagnification(ic.magnification);
    setDrawingSize(ic.dstWidth, ic.dstHeight);
  }

  public void setSourceRect(Rectangle r) {
    srcRect = r;
  }

  void setSrcRect(Rectangle srcRect) {
    this.srcRect = srcRect;
  }

  public Rectangle getSrcRect() {
    return srcRect;
  }

  public void setDrawingSize(int width, int height) {
    dstWidth = width;
    dstHeight = height;
    setSize(dstWidth, dstHeight);
  }

  /**
   * ImagePlus.updateAndDraw calls this method to force the paint() method to update the image from
   * the ImageProcessor.
   */
  public void setImageUpdated() {
    imageUpdated = true;
  }

  public void update(Graphics g) {
    paint(g);
  }

  public void paint(Graphics g) {
    Roi roi = imp.getRoi();
    if (roi != null || showAllROIs || overlay != null) {
      if (roi != null) roi.updatePaste();
      if (!IJ.isMacOSX() && imageWidth != 0) {
        paintDoubleBuffered(g);
        return;
      }
    }
    try {
      if (imageUpdated) {
        imageUpdated = false;
        imp.updateImage();
      }
      Java2.setBilinearInterpolation(g, Prefs.interpolateScaledImages);
      Image img = imp.getImage();
      if (img != null)
        g.drawImage(
            img,
            0,
            0,
            (int) (srcRect.width * magnification),
            (int) (srcRect.height * magnification),
            srcRect.x,
            srcRect.y,
            srcRect.x + srcRect.width,
            srcRect.y + srcRect.height,
            null);
      if (overlay != null) drawOverlay(g);
      if (showAllROIs) drawAllROIs(g);
      if (roi != null) drawRoi(roi, g);
      if (srcRect.width < imageWidth || srcRect.height < imageHeight) drawZoomIndicator(g);
      if (IJ.debugMode) showFrameRate(g);
    } catch (OutOfMemoryError e) {
      IJ.outOfMemory("Paint");
    }
  }

  private void drawRoi(Roi roi, Graphics g) {
    if (roi == currentRoi) {
      Color lineColor = roi.getStrokeColor();
      Color fillColor = roi.getFillColor();
      float lineWidth = roi.getStrokeWidth();
      roi.setStrokeColor(null);
      roi.setFillColor(null);
      boolean strokeSet = roi.getStroke() != null;
      if (strokeSet) roi.setStrokeWidth(1);
      roi.draw(g);
      roi.setStrokeColor(lineColor);
      if (strokeSet) roi.setStrokeWidth(lineWidth);
      roi.setFillColor(fillColor);
      currentRoi = null;
    } else roi.draw(g);
  }

  void drawAllROIs(Graphics g) {
    RoiManager rm = RoiManager.getInstance();
    if (rm == null) {
      rm = Interpreter.getBatchModeRoiManager();
      if (rm != null && rm.getList().getItemCount() == 0) rm = null;
    }
    if (rm == null) {
      // if (showAllList!=null)
      //	overlay = showAllList;
      showAllROIs = false;
      repaint();
      return;
    }
    initGraphics(g, null, showAllColor);
    Hashtable rois = rm.getROIs();
    java.awt.List list = rm.getList();
    boolean drawLabels = rm.getDrawLabels();
    currentRoi = null;
    int n = list.getItemCount();
    if (IJ.debugMode) IJ.log("paint: drawing " + n + " \"Show All\" ROIs");
    if (labelRects == null || labelRects.length != n) labelRects = new Rectangle[n];
    if (!drawLabels) showAllList = new Overlay();
    else showAllList = null;
    if (imp == null) return;
    int currentImage = imp.getCurrentSlice();
    int channel = 0, slice = 0, frame = 0;
    boolean hyperstack = imp.isHyperStack();
    if (hyperstack) {
      channel = imp.getChannel();
      slice = imp.getSlice();
      frame = imp.getFrame();
    }
    drawNames = Prefs.useNamesAsLabels;
    for (int i = 0; i < n; i++) {
      String label = list.getItem(i);
      Roi roi = (Roi) rois.get(label);
      if (roi == null) continue;
      if (showAllList != null) showAllList.add(roi);
      if (i < 200 && drawLabels && roi == imp.getRoi()) currentRoi = roi;
      if (Prefs.showAllSliceOnly && imp.getStackSize() > 1) {
        if (hyperstack && roi.getPosition() == 0) {
          int c = roi.getCPosition();
          int z = roi.getZPosition();
          int t = roi.getTPosition();
          if ((c == 0 || c == channel) && (z == 0 || z == slice) && (t == 0 || t == frame))
            drawRoi(g, roi, drawLabels ? i : -1);
        } else {
          int position = roi.getPosition();
          if (position == 0) position = getSliceNumber(roi.getName());
          if (position == 0 || position == currentImage) drawRoi(g, roi, drawLabels ? i : -1);
        }
      } else drawRoi(g, roi, drawLabels ? i : -1);
    }
    ((Graphics2D) g).setStroke(Roi.onePixelWide);
    drawNames = false;
  }

  public int getSliceNumber(String label) {
    int slice = 0;
    if (label.length() >= 14 && label.charAt(4) == '-' && label.charAt(9) == '-')
      slice = (int) Tools.parseDouble(label.substring(0, 4), -1);
    else if (label.length() >= 17 && label.charAt(5) == '-' && label.charAt(11) == '-')
      slice = (int) Tools.parseDouble(label.substring(0, 5), -1);
    else if (label.length() >= 20 && label.charAt(6) == '-' && label.charAt(13) == '-')
      slice = (int) Tools.parseDouble(label.substring(0, 6), -1);
    return slice;
  }

  void drawOverlay(Graphics g) {
    if (imp != null && imp.getHideOverlay()) return;
    Color labelColor = overlay.getLabelColor();
    if (labelColor == null) labelColor = Color.white;
    initGraphics(g, labelColor, Roi.getColor());
    int n = overlay.size();
    if (IJ.debugMode) IJ.log("paint: drawing " + n + " ROI display list");
    int currentImage = imp != null ? imp.getCurrentSlice() : -1;
    if (imp.getStackSize() == 1) currentImage = -1;
    int channel = 0, slice = 0, frame = 0;
    boolean hyperstack = imp.isHyperStack();
    if (hyperstack) {
      channel = imp.getChannel();
      slice = imp.getSlice();
      frame = imp.getFrame();
    }
    drawNames = overlay.getDrawNames();
    boolean drawLabels = drawNames || overlay.getDrawLabels();
    for (int i = 0; i < n; i++) {
      if (overlay == null) break;
      Roi roi = overlay.get(i);
      if (hyperstack && roi.getPosition() == 0) {
        int c = roi.getCPosition();
        int z = roi.getZPosition();
        int t = roi.getTPosition();
        if ((c == 0 || c == channel) && (z == 0 || z == slice) && (t == 0 || t == frame))
          drawRoi(g, roi, drawLabels ? i + LIST_OFFSET : -1);
      } else {
        int position = roi.getPosition();
        if (position == 0 || position == currentImage)
          drawRoi(g, roi, drawLabels ? i + LIST_OFFSET : -1);
      }
    }
    ((Graphics2D) g).setStroke(Roi.onePixelWide);
    drawNames = false;
  }

  void initGraphics(Graphics g, Color textColor, Color defaultColor) {
    if (smallFont == null) {
      smallFont = new Font("SansSerif", Font.PLAIN, 9);
      largeFont = new Font("SansSerif", Font.PLAIN, 12);
    }
    if (textColor != null) {
      labelColor = textColor;
      if (overlay != null && overlay.getDrawBackgrounds())
        bgColor =
            new Color(
                255 - labelColor.getRed(), 255 - labelColor.getGreen(), 255 - labelColor.getBlue());
      else bgColor = null;
    } else {
      int red = defaultColor.getRed();
      int green = defaultColor.getGreen();
      int blue = defaultColor.getBlue();
      if ((red + green + blue) / 3 < 128) labelColor = Color.white;
      else labelColor = Color.black;
      bgColor = defaultColor;
    }
    this.defaultColor = defaultColor;
    g.setColor(defaultColor);
  }

  void drawRoi(Graphics g, Roi roi, int index) {
    int type = roi.getType();
    ImagePlus imp2 = roi.getImage();
    roi.setImage(imp);
    Color saveColor = roi.getStrokeColor();
    if (saveColor == null) roi.setStrokeColor(defaultColor);
    if (roi instanceof TextRoi) ((TextRoi) roi).drawText(g);
    else roi.drawOverlay(g);
    roi.setStrokeColor(saveColor);
    if (index >= 0) {
      if (roi == currentRoi) g.setColor(Roi.getColor());
      else g.setColor(defaultColor);
      drawRoiLabel(g, index, roi);
    }
    if (imp2 != null) roi.setImage(imp2);
    else roi.setImage(null);
  }

  void drawRoiLabel(Graphics g, int index, Roi roi) {
    Rectangle r = roi.getBounds();
    int x = screenX(r.x);
    int y = screenY(r.y);
    double mag = getMagnification();
    int width = (int) (r.width * mag);
    int height = (int) (r.height * mag);
    int size = width > 40 && height > 40 ? 12 : 9;
    if (font != null) {
      g.setFont(font);
      size = font.getSize();
    } else if (size == 12) g.setFont(largeFont);
    else g.setFont(smallFont);
    boolean drawingList = index >= LIST_OFFSET;
    if (drawingList) index -= LIST_OFFSET;
    String label = "" + (index + 1);
    if (drawNames && roi.getName() != null) label = roi.getName();
    FontMetrics metrics = g.getFontMetrics();
    int w = metrics.stringWidth(label);
    x = x + width / 2 - w / 2;
    y = y + height / 2 + Math.max(size / 2, 6);
    int h = metrics.getAscent() + metrics.getDescent();
    if (bgColor != null) {
      g.setColor(bgColor);
      g.fillRoundRect(x - 1, y - h + 2, w + 1, h - 3, 5, 5);
    }
    if (!drawingList && labelRects != null && index < labelRects.length)
      labelRects[index] = new Rectangle(x - 1, y - h + 2, w + 1, h);
    g.setColor(labelColor);
    g.drawString(label, x, y - 2);
    g.setColor(defaultColor);
  }

  void drawZoomIndicator(Graphics g) {
    int x1 = 10;
    int y1 = 10;
    double aspectRatio = (double) imageHeight / imageWidth;
    int w1 = 64;
    if (aspectRatio > 1.0) w1 = (int) (w1 / aspectRatio);
    int h1 = (int) (w1 * aspectRatio);
    if (w1 < 4) w1 = 4;
    if (h1 < 4) h1 = 4;
    int w2 = (int) (w1 * ((double) srcRect.width / imageWidth));
    int h2 = (int) (h1 * ((double) srcRect.height / imageHeight));
    if (w2 < 1) w2 = 1;
    if (h2 < 1) h2 = 1;
    int x2 = (int) (w1 * ((double) srcRect.x / imageWidth));
    int y2 = (int) (h1 * ((double) srcRect.y / imageHeight));
    if (zoomIndicatorColor == null) zoomIndicatorColor = new Color(128, 128, 255);
    g.setColor(zoomIndicatorColor);
    ((Graphics2D) g).setStroke(Roi.onePixelWide);
    g.drawRect(x1, y1, w1, h1);
    if (w2 * h2 <= 200 || w2 < 10 || h2 < 10) g.fillRect(x1 + x2, y1 + y2, w2, h2);
    else g.drawRect(x1 + x2, y1 + y2, w2, h2);
  }

  // Use double buffer to reduce flicker when drawing complex ROIs.
  // Author: Erik Meijering
  void paintDoubleBuffered(Graphics g) {
    final int srcRectWidthMag = (int) (srcRect.width * magnification);
    final int srcRectHeightMag = (int) (srcRect.height * magnification);
    if (offScreenImage == null
        || offScreenWidth != srcRectWidthMag
        || offScreenHeight != srcRectHeightMag) {
      offScreenImage = createImage(srcRectWidthMag, srcRectHeightMag);
      offScreenWidth = srcRectWidthMag;
      offScreenHeight = srcRectHeightMag;
    }
    Roi roi = imp.getRoi();
    try {
      if (imageUpdated) {
        imageUpdated = false;
        imp.updateImage();
      }
      Graphics offScreenGraphics = offScreenImage.getGraphics();
      Java2.setBilinearInterpolation(offScreenGraphics, Prefs.interpolateScaledImages);
      Image img = imp.getImage();
      if (img != null)
        offScreenGraphics.drawImage(
            img,
            0,
            0,
            srcRectWidthMag,
            srcRectHeightMag,
            srcRect.x,
            srcRect.y,
            srcRect.x + srcRect.width,
            srcRect.y + srcRect.height,
            null);
      if (overlay != null) drawOverlay(offScreenGraphics);
      if (showAllROIs) drawAllROIs(offScreenGraphics);
      if (roi != null) drawRoi(roi, offScreenGraphics);
      if (srcRect.width < imageWidth || srcRect.height < imageHeight)
        drawZoomIndicator(offScreenGraphics);
      if (IJ.debugMode) showFrameRate(offScreenGraphics);
      g.drawImage(offScreenImage, 0, 0, null);
    } catch (OutOfMemoryError e) {
      IJ.outOfMemory("Paint");
    }
  }

  public void resetDoubleBuffer() {
    offScreenImage = null;
  }

  long firstFrame;
  int frames, fps;

  void showFrameRate(Graphics g) {
    frames++;
    if (System.currentTimeMillis() > firstFrame + 1000) {
      firstFrame = System.currentTimeMillis();
      fps = frames;
      frames = 0;
    }
    g.setColor(Color.white);
    g.fillRect(10, 12, 50, 15);
    g.setColor(Color.black);
    g.drawString((int) (fps + 0.5) + " fps", 10, 25);
  }

  public Dimension getPreferredSize() {
    return new Dimension(dstWidth, dstHeight);
  }

  int count;

  /*
    public Graphics getGraphics() {
     	Graphics g = super.getGraphics();
  IJ.write("getGraphics: "+count++);
  if (IJ.altKeyDown())
  	throw new IllegalArgumentException("");
    	return g;
    }
    */

  /** Returns the current cursor location in image coordinates. */
  public Point getCursorLoc() {
    return new Point(xMouse, yMouse);
  }

  /** Returns 'true' if the cursor is over this image. */
  public boolean cursorOverImage() {
    return !mouseExited;
  }

  /** Returns the mouse event modifiers. */
  public int getModifiers() {
    return flags;
  }

  /** Returns the ImagePlus object that is associated with this ImageCanvas. */
  public ImagePlus getImage() {
    return imp;
  }

  /** Sets the cursor based on the current tool and cursor location. */
  public void setCursor(int sx, int sy, int ox, int oy) {
    xMouse = ox;
    yMouse = oy;
    mouseExited = false;
    Roi roi = imp.getRoi();
    ImageWindow win = imp.getWindow();
    if (win == null) return;
    if (IJ.spaceBarDown()) {
      setCursor(handCursor);
      return;
    }
    int id = Toolbar.getToolId();
    switch (Toolbar.getToolId()) {
      case Toolbar.MAGNIFIER:
        setCursor(moveCursor);
        break;
      case Toolbar.HAND:
        setCursor(handCursor);
        break;
      default: // selection tool
        if (id == Toolbar.SPARE1 || id >= Toolbar.SPARE2) {
          if (Prefs.usePointerCursor) setCursor(defaultCursor);
          else setCursor(crosshairCursor);
        } else if (roi != null && roi.getState() != roi.CONSTRUCTING && roi.isHandle(sx, sy) >= 0)
          setCursor(handCursor);
        else if (Prefs.usePointerCursor
            || (roi != null && roi.getState() != roi.CONSTRUCTING && roi.contains(ox, oy)))
          setCursor(defaultCursor);
        else setCursor(crosshairCursor);
    }
  }

  /** Converts a screen x-coordinate to an offscreen x-coordinate. */
  public int offScreenX(int sx) {
    return srcRect.x + (int) (sx / magnification);
  }

  /** Converts a screen y-coordinate to an offscreen y-coordinate. */
  public int offScreenY(int sy) {
    return srcRect.y + (int) (sy / magnification);
  }

  /** Converts a screen x-coordinate to a floating-point offscreen x-coordinate. */
  public double offScreenXD(int sx) {
    return srcRect.x + sx / magnification;
  }

  /** Converts a screen y-coordinate to a floating-point offscreen y-coordinate. */
  public double offScreenYD(int sy) {
    return srcRect.y + sy / magnification;
  }

  /** Converts an offscreen x-coordinate to a screen x-coordinate. */
  public int screenX(int ox) {
    return (int) ((ox - srcRect.x) * magnification);
  }

  /** Converts an offscreen y-coordinate to a screen y-coordinate. */
  public int screenY(int oy) {
    return (int) ((oy - srcRect.y) * magnification);
  }

  /** Converts a floating-point offscreen x-coordinate to a screen x-coordinate. */
  public int screenXD(double ox) {
    return (int) ((ox - srcRect.x) * magnification);
  }

  /** Converts a floating-point offscreen x-coordinate to a screen x-coordinate. */
  public int screenYD(double oy) {
    return (int) ((oy - srcRect.y) * magnification);
  }

  public double getMagnification() {
    return magnification;
  }

  public void setMagnification(double magnification) {
    setMagnification2(magnification);
  }

  void setMagnification2(double magnification) {
    if (magnification > 32.0) magnification = 32.0;
    if (magnification < 0.03125) magnification = 0.03125;
    this.magnification = magnification;
    imp.setTitle(imp.getTitle());
  }

  /** Enlarge the canvas if the user enlarges the window. */
  void resizeCanvas(int width, int height) {
    ImageWindow win = imp.getWindow();
    // IJ.log("resizeCanvas: "+srcRect+" "+imageWidth+"  "+imageHeight+" "+width+"  "+height+"
    // "+dstWidth+"  "+dstHeight+" "+win.maxBounds);
    if (!maxBoundsReset
        && (width > dstWidth || height > dstHeight)
        && win != null
        && win.maxBounds != null
        && width != win.maxBounds.width - 10) {
      if (resetMaxBoundsCount != 0)
        resetMaxBounds(); // Works around problem that prevented window from being larger than
                          // maximized size
      resetMaxBoundsCount++;
    }
    if (IJ.altKeyDown()) {
      fitToWindow();
      return;
    }
    if (srcRect.width < imageWidth || srcRect.height < imageHeight) {
      if (width > imageWidth * magnification) width = (int) (imageWidth * magnification);
      if (height > imageHeight * magnification) height = (int) (imageHeight * magnification);
      setDrawingSize(width, height);
      srcRect.width = (int) (dstWidth / magnification);
      srcRect.height = (int) (dstHeight / magnification);
      if ((srcRect.x + srcRect.width) > imageWidth) srcRect.x = imageWidth - srcRect.width;
      if ((srcRect.y + srcRect.height) > imageHeight) srcRect.y = imageHeight - srcRect.height;
      repaint();
    }
    // IJ.log("resizeCanvas2: "+srcRect+" "+dstWidth+"  "+dstHeight+" "+width+"  "+height);
  }

  public void fitToWindow() {
    ImageWindow win = imp.getWindow();
    if (win == null) return;
    Rectangle bounds = win.getBounds();
    Insets insets = win.getInsets();
    int sliderHeight = (win instanceof StackWindow) ? 20 : 0;
    double xmag = (double) (bounds.width - 10) / srcRect.width;
    double ymag = (double) (bounds.height - (10 + insets.top + sliderHeight)) / srcRect.height;
    setMagnification(Math.min(xmag, ymag));
    int width = (int) (imageWidth * magnification);
    int height = (int) (imageHeight * magnification);
    if (width == dstWidth && height == dstHeight) return;
    srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
    setDrawingSize(width, height);
    getParent().doLayout();
  }

  void setMaxBounds() {
    if (maxBoundsReset) {
      maxBoundsReset = false;
      ImageWindow win = imp.getWindow();
      if (win != null && !IJ.isLinux() && win.maxBounds != null) {
        win.setMaximizedBounds(win.maxBounds);
        win.setMaxBoundsTime = System.currentTimeMillis();
      }
    }
  }

  void resetMaxBounds() {
    ImageWindow win = imp.getWindow();
    if (win != null && (System.currentTimeMillis() - win.setMaxBoundsTime) > 500L) {
      win.setMaximizedBounds(win.maxWindowBounds);
      maxBoundsReset = true;
    }
  }

  private static final double[] zoomLevels = {
    1 / 72.0, 1 / 48.0, 1 / 32.0, 1 / 24.0, 1 / 16.0, 1 / 12.0, 1 / 8.0, 1 / 6.0, 1 / 4.0, 1 / 3.0,
    1 / 2.0, 0.75, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, 8.0, 12.0, 16.0, 24.0, 32.0
  };

  public static double getLowerZoomLevel(double currentMag) {
    double newMag = zoomLevels[0];
    for (int i = 0; i < zoomLevels.length; i++) {
      if (zoomLevels[i] < currentMag) newMag = zoomLevels[i];
      else break;
    }
    return newMag;
  }

  public static double getHigherZoomLevel(double currentMag) {
    double newMag = 32.0;
    for (int i = zoomLevels.length - 1; i >= 0; i--) {
      if (zoomLevels[i] > currentMag) newMag = zoomLevels[i];
      else break;
    }
    return newMag;
  }

  /**
   * Zooms in by making the window bigger. If it can't be made bigger, then make the source
   * rectangle (srcRect) smaller and center it at (sx,sy). Note that sx and sy are screen
   * coordinates.
   */
  public void zoomIn(int sx, int sy) {
    if (magnification >= 32) return;
    double newMag = getHigherZoomLevel(magnification);
    int newWidth = (int) (imageWidth * newMag);
    int newHeight = (int) (imageHeight * newMag);
    Dimension newSize = canEnlarge(newWidth, newHeight);
    if (newSize != null) {
      setDrawingSize(newSize.width, newSize.height);
      if (newSize.width != newWidth || newSize.height != newHeight)
        adjustSourceRect(newMag, sx, sy);
      else setMagnification(newMag);
      imp.getWindow().pack();
    } else adjustSourceRect(newMag, sx, sy);
    repaint();
    if (srcRect.width < imageWidth || srcRect.height < imageHeight) resetMaxBounds();
  }

  void adjustSourceRect(double newMag, int x, int y) {
    // IJ.log("adjustSourceRect1: "+newMag+" "+dstWidth+"  "+dstHeight);
    int w = (int) Math.round(dstWidth / newMag);
    if (w * newMag < dstWidth) w++;
    int h = (int) Math.round(dstHeight / newMag);
    if (h * newMag < dstHeight) h++;
    x = offScreenX(x);
    y = offScreenY(y);
    Rectangle r = new Rectangle(x - w / 2, y - h / 2, w, h);
    if (r.x < 0) r.x = 0;
    if (r.y < 0) r.y = 0;
    if (r.x + w > imageWidth) r.x = imageWidth - w;
    if (r.y + h > imageHeight) r.y = imageHeight - h;
    srcRect = r;
    setMagnification(newMag);
    // IJ.log("adjustSourceRect2: "+srcRect+" "+dstWidth+"  "+dstHeight);
  }

  protected Dimension canEnlarge(int newWidth, int newHeight) {
    // if ((flags&Event.CTRL_MASK)!=0 || IJ.controlKeyDown()) return null;
    ImageWindow win = imp.getWindow();
    if (win == null) return null;
    Rectangle r1 = win.getBounds();
    Insets insets = win.getInsets();
    Point loc = getLocation();
    if (loc.x > insets.left + 5 || loc.y > insets.top + 5) {
      r1.width = newWidth + insets.left + insets.right + 10;
      r1.height = newHeight + insets.top + insets.bottom + 10;
      if (win instanceof StackWindow) r1.height += 20;
    } else {
      r1.width = r1.width - dstWidth + newWidth + 10;
      r1.height = r1.height - dstHeight + newHeight + 10;
    }
    Rectangle max = win.getMaxWindow(r1.x, r1.y);
    boolean fitsHorizontally = r1.x + r1.width < max.x + max.width;
    boolean fitsVertically = r1.y + r1.height < max.y + max.height;
    if (fitsHorizontally && fitsVertically) return new Dimension(newWidth, newHeight);
    else if (fitsVertically && newHeight < dstWidth) return new Dimension(dstWidth, newHeight);
    else if (fitsHorizontally && newWidth < dstHeight) return new Dimension(newWidth, dstHeight);
    else return null;
  }

  /**
   * Zooms out by making the source rectangle (srcRect) larger and centering it on (x,y). If we
   * can't make it larger, then make the window smaller.
   */
  public void zoomOut(int x, int y) {
    if (magnification <= 0.03125) return;
    double oldMag = magnification;
    double newMag = getLowerZoomLevel(magnification);
    double srcRatio = (double) srcRect.width / srcRect.height;
    double imageRatio = (double) imageWidth / imageHeight;
    double initialMag = imp.getWindow().getInitialMagnification();
    if (Math.abs(srcRatio - imageRatio) > 0.05) {
      double scale = oldMag / newMag;
      int newSrcWidth = (int) Math.round(srcRect.width * scale);
      int newSrcHeight = (int) Math.round(srcRect.height * scale);
      if (newSrcWidth > imageWidth) newSrcWidth = imageWidth;
      if (newSrcHeight > imageHeight) newSrcHeight = imageHeight;
      int newSrcX = srcRect.x - (newSrcWidth - srcRect.width) / 2;
      int newSrcY = srcRect.y - (newSrcHeight - srcRect.height) / 2;
      if (newSrcX < 0) newSrcX = 0;
      if (newSrcY < 0) newSrcY = 0;
      srcRect = new Rectangle(newSrcX, newSrcY, newSrcWidth, newSrcHeight);
      // IJ.log(newMag+" "+srcRect+" "+dstWidth+" "+dstHeight);
      int newDstWidth = (int) (srcRect.width * newMag);
      int newDstHeight = (int) (srcRect.height * newMag);
      setMagnification(newMag);
      setMaxBounds();
      // IJ.log(newDstWidth+" "+dstWidth+" "+newDstHeight+" "+dstHeight);
      if (newDstWidth < dstWidth || newDstHeight < dstHeight) {
        // IJ.log("pack");
        setDrawingSize(newDstWidth, newDstHeight);
        imp.getWindow().pack();
      } else repaint();
      return;
    }
    if (imageWidth * newMag > dstWidth) {
      int w = (int) Math.round(dstWidth / newMag);
      if (w * newMag < dstWidth) w++;
      int h = (int) Math.round(dstHeight / newMag);
      if (h * newMag < dstHeight) h++;
      x = offScreenX(x);
      y = offScreenY(y);
      Rectangle r = new Rectangle(x - w / 2, y - h / 2, w, h);
      if (r.x < 0) r.x = 0;
      if (r.y < 0) r.y = 0;
      if (r.x + w > imageWidth) r.x = imageWidth - w;
      if (r.y + h > imageHeight) r.y = imageHeight - h;
      srcRect = r;
    } else {
      srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
      setDrawingSize((int) (imageWidth * newMag), (int) (imageHeight * newMag));
      // setDrawingSize(dstWidth/2, dstHeight/2);
      imp.getWindow().pack();
    }
    // IJ.write(newMag + " " + srcRect.x+" "+srcRect.y+" "+srcRect.width+" "+srcRect.height+"
    // "+dstWidth + " " + dstHeight);
    setMagnification(newMag);
    // IJ.write(srcRect.x + " " + srcRect.width + " " + dstWidth);
    setMaxBounds();
    repaint();
  }

  /** Implements the Image/Zoom/Original Scale command. */
  public void unzoom() {
    double imag = imp.getWindow().getInitialMagnification();
    if (magnification == imag) return;
    srcRect = new Rectangle(0, 0, imageWidth, imageHeight);
    ImageWindow win = imp.getWindow();
    setDrawingSize((int) (imageWidth * imag), (int) (imageHeight * imag));
    setMagnification(imag);
    setMaxBounds();
    win.pack();
    setMaxBounds();
    repaint();
  }

  /** Implements the Image/Zoom/View 100% command. */
  public void zoom100Percent() {
    if (magnification == 1.0) return;
    double imag = imp.getWindow().getInitialMagnification();
    if (magnification != imag) unzoom();
    if (magnification == 1.0) return;
    if (magnification < 1.0) {
      while (magnification < 1.0) zoomIn(imageWidth / 2, imageHeight / 2);
    } else if (magnification > 1.0) {
      while (magnification > 1.0) zoomOut(imageWidth / 2, imageHeight / 2);
    } else return;
    int x = xMouse, y = yMouse;
    if (mouseExited) {
      x = imageWidth / 2;
      y = imageHeight / 2;
    }
    int sx = screenX(x);
    int sy = screenY(y);
    adjustSourceRect(1.0, sx, sy);
    repaint();
  }

  protected void scroll(int sx, int sy) {
    int ox = xSrcStart + (int) (sx / magnification); // convert to offscreen coordinates
    int oy = ySrcStart + (int) (sy / magnification);
    // IJ.log("scroll: "+ox+" "+oy+" "+xMouseStart+" "+yMouseStart);
    int newx = xSrcStart + (xMouseStart - ox);
    int newy = ySrcStart + (yMouseStart - oy);
    if (newx < 0) newx = 0;
    if (newy < 0) newy = 0;
    if ((newx + srcRect.width) > imageWidth) newx = imageWidth - srcRect.width;
    if ((newy + srcRect.height) > imageHeight) newy = imageHeight - srcRect.height;
    srcRect.x = newx;
    srcRect.y = newy;
    // IJ.log(sx+"  "+sy+"  "+newx+"  "+newy+"  "+srcRect);
    imp.draw();
    Thread.yield();
  }

  Color getColor(int index) {
    IndexColorModel cm = (IndexColorModel) imp.getProcessor().getColorModel();
    // IJ.write(""+index+" "+(new Color(cm.getRGB(index))));
    return new Color(cm.getRGB(index));
  }

  protected void setDrawingColor(int ox, int oy, boolean setBackground) {
    // IJ.log("setDrawingColor: "+setBackground+this);
    int type = imp.getType();
    int[] v = imp.getPixel(ox, oy);
    switch (type) {
      case ImagePlus.GRAY8:
        {
          if (setBackground) setBackgroundColor(getColor(v[0]));
          else setForegroundColor(getColor(v[0]));
          break;
        }
      case ImagePlus.GRAY16:
      case ImagePlus.GRAY32:
        {
          double min = imp.getProcessor().getMin();
          double max = imp.getProcessor().getMax();
          double value = (type == ImagePlus.GRAY32) ? Float.intBitsToFloat(v[0]) : v[0];
          int index = (int) (255.0 * ((value - min) / (max - min)));
          if (index < 0) index = 0;
          if (index > 255) index = 255;
          if (setBackground) setBackgroundColor(getColor(index));
          else setForegroundColor(getColor(index));
          break;
        }
      case ImagePlus.COLOR_RGB:
      case ImagePlus.COLOR_256:
        {
          Color c = new Color(v[0], v[1], v[2]);
          if (setBackground) setBackgroundColor(c);
          else setForegroundColor(c);
          break;
        }
    }
    Color c;
    if (setBackground) c = Toolbar.getBackgroundColor();
    else {
      c = Toolbar.getForegroundColor();
      imp.setColor(c);
    }
    IJ.showStatus("(" + c.getRed() + ", " + c.getGreen() + ", " + c.getBlue() + ")");
  }

  private void setForegroundColor(Color c) {
    Toolbar.setForegroundColor(c);
    if (Recorder.record)
      Recorder.record("setForegroundColor", c.getRed(), c.getGreen(), c.getBlue());
  }

  private void setBackgroundColor(Color c) {
    Toolbar.setBackgroundColor(c);
    if (Recorder.record)
      Recorder.record("setBackgroundColor", c.getRed(), c.getGreen(), c.getBlue());
  }

  public void mousePressed(MouseEvent e) {
    // if (ij==null) return;
    showCursorStatus = true;
    int toolID = Toolbar.getToolId();
    ImageWindow win = imp.getWindow();
    if (win != null && win.running2 && toolID != Toolbar.MAGNIFIER) {
      if (win instanceof StackWindow) ((StackWindow) win).setAnimate(false);
      else win.running2 = false;
      return;
    }

    int x = e.getX();
    int y = e.getY();
    flags = e.getModifiers();
    // IJ.log("Mouse pressed: " + e.isPopupTrigger() + "  " + ij.modifiers(flags));
    // if (toolID!=Toolbar.MAGNIFIER && e.isPopupTrigger()) {
    if (toolID != Toolbar.MAGNIFIER
        && (e.isPopupTrigger() || (!IJ.isMacintosh() && (flags & Event.META_MASK) != 0))) {
      handlePopupMenu(e);
      return;
    }

    int ox = offScreenX(x);
    int oy = offScreenY(y);
    xMouse = ox;
    yMouse = oy;
    if (IJ.spaceBarDown()) {
      // temporarily switch to "hand" tool of space bar down
      setupScroll(ox, oy);
      return;
    }
    if (showAllROIs) {
      Roi roi = imp.getRoi();
      if (!(roi != null && (roi.contains(ox, oy) || roi.isHandle(x, y) >= 0))
          && roiManagerSelect(x, y)) return;
    }
    if (customRoi && overlay != null) return;

    switch (toolID) {
      case Toolbar.MAGNIFIER:
        if (IJ.shiftKeyDown()) zoomToSelection(ox, oy);
        else if ((flags & (Event.ALT_MASK | Event.META_MASK | Event.CTRL_MASK)) != 0) {
          // IJ.run("Out");
          zoomOut(x, y);
          if (getMagnification() < 1.0) imp.repaintWindow();
        } else {
          // IJ.run("In");
          zoomIn(x, y);
          if (getMagnification() <= 1.0) imp.repaintWindow();
        }
        break;
      case Toolbar.HAND:
        setupScroll(ox, oy);
        break;
      case Toolbar.DROPPER:
        setDrawingColor(ox, oy, IJ.altKeyDown());
        break;
      case Toolbar.WAND:
        Roi roi = imp.getRoi();
        if (roi != null && roi.contains(ox, oy)) {
          Rectangle r = roi.getBounds();
          if (r.width == imageWidth && r.height == imageHeight) imp.killRoi();
          else if (!e.isAltDown()) {
            handleRoiMouseDown(e);
            return;
          }
        }
        if (roi != null) {
          int handle = roi.isHandle(x, y);
          if (handle >= 0) {
            roi.mouseDownInHandle(handle, x, y);
            return;
          }
        }
        setRoiModState(e, roi, -1);
        String mode = WandToolOptions.getMode();
        double tolerance = WandToolOptions.getTolerance();
        int npoints = IJ.doWand(ox, oy, tolerance, mode);
        if (Recorder.record && npoints > 0) {
          if (tolerance == 0.0 && mode.equals("Legacy")) Recorder.record("doWand", ox, oy);
          else
            Recorder.recordString(
                "doWand(" + ox + ", " + oy + ", " + tolerance + ", \"" + mode + "\");\n");
        }
        break;
      case Toolbar.OVAL:
        if (Toolbar.getBrushSize() > 0) new RoiBrush();
        else handleRoiMouseDown(e);
        break;
      case Toolbar.SPARE1:
      case Toolbar.SPARE2:
      case Toolbar.SPARE3:
      case Toolbar.SPARE4:
      case Toolbar.SPARE5:
      case Toolbar.SPARE6:
      case Toolbar.SPARE7:
      case Toolbar.SPARE8:
      case Toolbar.SPARE9:
        Toolbar.getInstance().runMacroTool(toolID);
        break;
      default: // selection tool
        handleRoiMouseDown(e);
    }
  }

  boolean roiManagerSelect(int x, int y) {
    RoiManager rm = RoiManager.getInstance();
    if (rm == null) return false;
    Hashtable rois = rm.getROIs();
    java.awt.List list = rm.getList();
    int n = list.getItemCount();
    if (labelRects == null || labelRects.length != n) return false;
    boolean stackMode = imp != null && imp.getStackSize() > 1 && Prefs.showAllSliceOnly;
    for (int i = 0; i < n; i++) {
      if (labelRects[i] != null && labelRects[i].contains(x, y)) {
        if (stackMode) {
          int slice = getSliceNumber(list.getItem(i));
          if (slice != imp.getCurrentSlice()) continue;
        }
        // rm.select(i);
        // this needs to run on a separate thread, at least on OS X
        // "update2" does not clone the ROI so the "Show All"
        // outline moves as the user moves the RO.
        new ij.macro.MacroRunner("roiManager('select', " + i + "); roiManager('update2');");
        return true;
      }
    }
    return false;
  }

  void zoomToSelection(int x, int y) {
    IJ.setKeyUp(IJ.ALL_KEYS);
    String macro =
        "args = split(getArgument);\n"
            + "x1=parseInt(args[0]); y1=parseInt(args[1]); flags=20;\n"
            + "while (flags&20!=0) {\n"
            + "getCursorLoc(x2, y2, z, flags);\n"
            + "if (x2>=x1) x=x1; else x=x2;\n"
            + "if (y2>=y1) y=y1; else y=y2;\n"
            + "makeRectangle(x, y, abs(x2-x1), abs(y2-y1));\n"
            + "wait(10);\n"
            + "}\n"
            + "run('To Selection');\n";
    new MacroRunner(macro, x + " " + y);
  }

  protected void setupScroll(int ox, int oy) {
    xMouseStart = ox;
    yMouseStart = oy;
    xSrcStart = srcRect.x;
    ySrcStart = srcRect.y;
  }

  protected void handlePopupMenu(MouseEvent e) {
    if (disablePopupMenu) return;
    if (IJ.debugMode) IJ.log("show popup: " + (e.isPopupTrigger() ? "true" : "false"));
    int x = e.getX();
    int y = e.getY();
    Roi roi = imp.getRoi();
    if (roi != null
        && (roi.getType() == Roi.POLYGON
            || roi.getType() == Roi.POLYLINE
            || roi.getType() == Roi.ANGLE)
        && roi.getState() == roi.CONSTRUCTING) {
      roi.handleMouseUp(x, y); // simulate double-click to finalize
      roi.handleMouseUp(x, y); // polygon or polyline selection
      return;
    }
    PopupMenu popup = Menus.getPopupMenu();
    if (popup != null) {
      add(popup);
      if (IJ.isMacOSX()) IJ.wait(10);
      popup.show(this, x, y);
    }
  }

  public void mouseExited(MouseEvent e) {
    // autoScroll(e);
    ImageWindow win = imp.getWindow();
    if (win != null) setCursor(defaultCursor);
    IJ.showStatus("");
    mouseExited = true;
  }

  /*
  public void autoScroll(MouseEvent e) {
  	Roi roi = imp.getRoi();
  	if (roi==null || roi.getState()!=roi.CONSTRUCTING || srcRect.width>=imageWidth || srcRect.height>=imageHeight
  	|| !(roi.getType()==Roi.POLYGON || roi.getType()==Roi.POLYLINE || roi.getType()==Roi.ANGLE))
  		return;
  	int sx = e.getX();
  	int sy = e.getY();
  	xMouseStart = srcRect.x+srcRect.width/2;
  	yMouseStart = srcRect.y+srcRect.height/2;
  	Rectangle r = roi.getBounds();
  	Dimension size = getSize();
  	int deltax=0, deltay=0;
  	if (sx<0)
  		deltax = srcRect.width/4;
  	else if (sx>size.width)
  		deltax = -srcRect.width/4;
  	if (sy<0)
  		deltay = srcRect.height/4;
  	else if (sy>size.height)
  		deltay = -srcRect.height/4;
  	//IJ.log("autoscroll: "+sx+" "+sy+" "+deltax+" "+deltay+" "+r);
  	scroll(screenX(xMouseStart+deltax), screenY(yMouseStart+deltay));
  }
  */

  public void mouseDragged(MouseEvent e) {
    int x = e.getX();
    int y = e.getY();
    xMouse = offScreenX(x);
    yMouse = offScreenY(y);
    flags = e.getModifiers();
    // IJ.log("mouseDragged: "+flags);
    if (flags == 0) // workaround for Mac OS 9 bug
    flags = InputEvent.BUTTON1_MASK;
    if (Toolbar.getToolId() == Toolbar.HAND || IJ.spaceBarDown()) scroll(x, y);
    else {
      IJ.setInputEvent(e);
      Roi roi = imp.getRoi();
      if (roi != null) roi.handleMouseDrag(x, y, flags);
    }
  }

  protected void handleRoiMouseDown(MouseEvent e) {
    int sx = e.getX();
    int sy = e.getY();
    int ox = offScreenX(sx);
    int oy = offScreenY(sy);
    Roi roi = imp.getRoi();
    int handle = roi != null ? roi.isHandle(sx, sy) : -1;
    boolean multiPointMode =
        roi != null
            && (roi instanceof PointRoi)
            && handle == -1
            && Toolbar.getToolId() == Toolbar.POINT
            && Toolbar.getMultiPointMode();
    if (multiPointMode) {
      imp.setRoi(((PointRoi) roi).addPoint(ox, oy));
      return;
    }
    setRoiModState(e, roi, handle);
    if (roi != null) {
      if (handle >= 0) {
        roi.mouseDownInHandle(handle, sx, sy);
        return;
      }
      Rectangle r = roi.getBounds();
      int type = roi.getType();
      if (type == Roi.RECTANGLE
          && r.width == imp.getWidth()
          && r.height == imp.getHeight()
          && roi.getPasteMode() == Roi.NOT_PASTING
          && !(roi instanceof ImageRoi)) {
        imp.killRoi();
        return;
      }
      if (roi.contains(ox, oy)) {
        if (roi.modState == Roi.NO_MODS) roi.handleMouseDown(sx, sy);
        else {
          imp.killRoi();
          imp.createNewRoi(sx, sy);
        }
        return;
      }
      if ((type == Roi.POLYGON || type == Roi.POLYLINE || type == Roi.ANGLE)
          && roi.getState() == roi.CONSTRUCTING) return;
      int tool = Toolbar.getToolId();
      if ((tool == Toolbar.POLYGON || tool == Toolbar.POLYLINE || tool == Toolbar.ANGLE)
          && !(IJ.shiftKeyDown() || IJ.altKeyDown())) {
        imp.killRoi();
        return;
      }
    }
    imp.createNewRoi(sx, sy);
  }

  void setRoiModState(MouseEvent e, Roi roi, int handle) {
    if (roi == null || (handle >= 0 && roi.modState == Roi.NO_MODS)) return;
    if (roi.state == Roi.CONSTRUCTING) return;
    int tool = Toolbar.getToolId();
    if (tool > Toolbar.FREEROI && tool != Toolbar.WAND && tool != Toolbar.POINT) {
      roi.modState = Roi.NO_MODS;
      return;
    }
    if (e.isShiftDown()) roi.modState = Roi.ADD_TO_ROI;
    else if (e.isAltDown()) roi.modState = Roi.SUBTRACT_FROM_ROI;
    else roi.modState = Roi.NO_MODS;
    // IJ.log("setRoiModState: "+roi.modState+" "+ roi.state);
  }

  /** Disable/enable popup menu. */
  public void disablePopupMenu(boolean status) {
    disablePopupMenu = status;
  }

  /** Enables/disables the ROI Manager "Show All" mode. */
  public void setShowAllROIs(boolean showAllROIs) {
    this.showAllROIs = showAllROIs;
  }

  /** Returns the state of the ROI Manager "Show All" flag. */
  public boolean getShowAllROIs() {
    return showAllROIs;
  }

  /** Return the ROI Manager "Show All" list as an overlay. */
  public Overlay getShowAllList() {
    if (!showAllROIs) return null;
    if (showAllList != null) return showAllList;
    RoiManager rm = RoiManager.getInstance();
    if (rm == null) return null;
    Roi[] rois = rm.getRoisAsArray();
    if (rois.length == 0) return null;
    Overlay overlay = new Overlay();
    for (int i = 0; i < rois.length; i++) overlay.add((Roi) rois[i].clone());
    return overlay;
  }

  /** Returns the color used for "Show All" mode. */
  public static Color getShowAllColor() {
    if (showAllColor != null && showAllColor.getRGB() == 0xff80ffff) showAllColor = Color.cyan;
    return showAllColor;
  }

  /** Sets the color used used for the ROI Manager "Show All" mode. */
  public static void setShowAllColor(Color c) {
    if (c == null) return;
    showAllColor = c;
    labelColor = null;
    ImagePlus img = WindowManager.getCurrentImage();
    if (img != null) {
      ImageCanvas ic = img.getCanvas();
      if (ic != null && ic.getShowAllROIs()) img.draw();
    }
  }

  /** Use ImagePlus.setOverlay(ij.gui.Overlay). */
  public void setOverlay(Overlay overlay) {
    this.overlay = overlay;
    if (overlay != null) font = overlay.getLabelsFont();
    repaint();
  }

  /** Use ImagePlus.getOverlay(). */
  public Overlay getOverlay() {
    return overlay;
  }

  /** @deprecated replaced by ImagePlus.setOverlay(ij.gui.Overlay) */
  public void setDisplayList(Vector list) {
    if (list != null) {
      Overlay list2 = new Overlay();
      list2.setVector(list);
      setOverlay(list2);
    } else setOverlay(null);
    if (overlay != null)
      overlay.drawLabels(overlay.size() > 0 && overlay.get(0).getStrokeColor() == null);
    else customRoi = false;
    repaint();
  }

  /** @deprecated replaced by ImagePlus.setOverlay(Shape, Color, BasicStroke) */
  public void setDisplayList(Shape shape, Color color, BasicStroke stroke) {
    if (shape == null) {
      setOverlay(null);
      return;
    }
    Roi roi = new ShapeRoi(shape);
    roi.setStrokeColor(color);
    roi.setStroke(stroke);
    Overlay list = new Overlay();
    list.add(roi);
    setOverlay(list);
  }

  /** @deprecated replaced by ImagePlus.setOverlay(Roi, Color, int, Color) */
  public void setDisplayList(Roi roi, Color color) {
    roi.setStrokeColor(color);
    Overlay list = new Overlay();
    list.add(roi);
    setOverlay(list);
  }

  /** @deprecated replaced by ImagePlus.getOverlay() */
  public Vector getDisplayList() {
    if (overlay == null) return null;
    Vector displayList = new Vector();
    for (int i = 0; i < overlay.size(); i++) displayList.add(overlay.get(i));
    return displayList;
  }

  /** Allows plugins (e.g., Orthogonal_Views) to create a custom ROI using a display list. */
  public void setCustomRoi(boolean customRoi) {
    this.customRoi = customRoi;
  }

  public boolean getCustomRoi() {
    return customRoi;
  }

  /**
   * Called by IJ.showStatus() to prevent status bar text from being overwritten until the cursor
   * moves at least 12 pixels.
   */
  public void setShowCursorStatus(boolean status) {
    showCursorStatus = status;
    if (status == true) sx2 = sy2 = -1000;
    else {
      sx2 = screenX(xMouse);
      sy2 = screenY(yMouse);
    }
  }

  public void mouseReleased(MouseEvent e) {
    flags = e.getModifiers();
    flags &= ~InputEvent.BUTTON1_MASK; // make sure button 1 bit is not set
    flags &= ~InputEvent.BUTTON2_MASK; // make sure button 2 bit is not set
    flags &= ~InputEvent.BUTTON3_MASK; // make sure button 3 bit is not set
    Roi roi = imp.getRoi();
    if (roi != null) {
      Rectangle r = roi.getBounds();
      int type = roi.getType();
      if ((r.width == 0 || r.height == 0)
          && !(type == Roi.POLYGON || type == Roi.POLYLINE || type == Roi.ANGLE || type == Roi.LINE)
          && !(roi instanceof TextRoi)
          && roi.getState() == roi.CONSTRUCTING
          && type != roi.POINT) imp.killRoi();
      else {
        roi.handleMouseUp(e.getX(), e.getY());
        if (roi.getType() == Roi.LINE && roi.getLength() == 0.0) imp.killRoi();
      }
    }
  }

  public void mouseMoved(MouseEvent e) {
    // if (ij==null) return;
    int sx = e.getX();
    int sy = e.getY();
    int ox = offScreenX(sx);
    int oy = offScreenY(sy);
    flags = e.getModifiers();
    setCursor(sx, sy, ox, oy);
    IJ.setInputEvent(e);
    Roi roi = imp.getRoi();
    if (roi != null
        && (roi.getType() == Roi.POLYGON
            || roi.getType() == Roi.POLYLINE
            || roi.getType() == Roi.ANGLE)
        && roi.getState() == roi.CONSTRUCTING) {
      PolygonRoi pRoi = (PolygonRoi) roi;
      pRoi.handleMouseMove(ox, oy);
    } else {
      if (ox < imageWidth && oy < imageHeight) {
        ImageWindow win = imp.getWindow();
        // Cursor must move at least 12 pixels before text
        // displayed using IJ.showStatus() is overwritten.
        if ((sx - sx2) * (sx - sx2) + (sy - sy2) * (sy - sy2) > 144) showCursorStatus = true;
        if (win != null && showCursorStatus) win.mouseMoved(ox, oy);
      } else IJ.showStatus("");
    }
  }

  public void mouseClicked(MouseEvent e) {}

  public void mouseEntered(MouseEvent e) {}
}