TrackVisualisation.java

package com.github.celldynamics.quimp.plugin.protanalysis;

import java.awt.Color;
import java.awt.Point;
import java.awt.Polygon;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.celldynamics.quimp.plugin.protanalysis.ProtAnalysisOptions.OutlinesToImage;
import com.github.celldynamics.quimp.plugin.qanalysis.STmap;
import com.github.celldynamics.quimp.utils.graphics.GraphicsElements;

import ij.ImagePlus;
import ij.ImageStack;
import ij.gui.Overlay;
import ij.gui.PointRoi;
import ij.gui.PolygonRoi;
import ij.plugin.ZProjector;
import ij.process.FloatProcessor;
import ij.process.ImageProcessor;

/**
 * Support various methods of visualising protrusion data.
 * 
 * <p>In general all plots are added to image used to construct this object as overlay layer.
 * 
 * <p>This super class contains methods for creating static plots in [x,y] domain from coordinates
 * in [outline,frame] system (native for maps generated by Qanalysis).
 * 
 * <p><b>Warning</b>
 * 
 * <p>It is assumed that plotted maps have frames on x-axis and indexes on y-axis
 * 
 * @author p.baniukiewicz
 *
 */
public abstract class TrackVisualisation {

  /**
   * The Constant LOGGER.
   */
  static final Logger LOGGER = LoggerFactory.getLogger(TrackVisualisation.class.getName());
  /**
   * Radius of circles plotted.
   */
  public double circleRadius = 7.;
  /**
   * Color for maxima points.
   */
  public static Color MAXIMA_COLOR = Color.MAGENTA;

  /**
   * Definition of colors used to plot tracks.
   * 
   * <p>These are:
   * <ol>
   * <li>index 0 - backtracked position of point
   * <li>index 1 - forwardtracked position of point.
   * <li>index 2 - other
   * </ol>
   */
  public static Color[] color = { Color.YELLOW, Color.GREEN, Color.WHITE };

  /**
   * The original image.
   */
  protected ImagePlus originalImage; // reference of image to be plotted on

  /**
   * The overlay.
   */
  protected Overlay overlay;

  /**
   * Create correct object.
   * 
   * <p>If input image contains any overlay data, they will be extended by new plots.
   * 
   * @param originalImage Image to be plotted on.
   */
  public TrackVisualisation(ImagePlus originalImage) {
    this.originalImage = originalImage;
    LOGGER.trace("Num of slices: " + originalImage.getStackSize());
    overlay = originalImage.getOverlay(); // check for existing overlay
    if (overlay == null) {
      overlay = new Overlay();
    }

  }

  /**
   * Construct object from raw ImageProcessor.
   * 
   * @param name Name of the image
   * @param imp ImageProcessor
   */
  public TrackVisualisation(String name, ImageProcessor imp) {
    this(new ImagePlus(name, imp));
  }

  /**
   * Plot filled circle on overlay.
   * 
   * @param x center
   * @param y center
   * @param color color
   * @param radius radius
   */
  public void plotCircle(double x, double y, Color color, double radius) {
    // create ROI
    PolygonRoi or = GraphicsElements.getCircle(x, y, color, radius);
    overlay.add(or); // add to collection of overlays
  }

  /**
   * getOriginalImage.
   * 
   * @return the originalImage
   */
  public ImagePlus getOriginalImage() {
    return originalImage;
  }

  /**
   * Helper method.
   * 
   * <p>Allows to convert enum to index of array of Colors.
   * 
   * @param track track
   * @return Color from color array
   */
  protected Color getColor(Track track) {
    Color c;
    Track.TrackType type = track.type;
    switch (type) {
      case FORWARD:
        c = color[1];
        break;
      case BACKWARD:
        c = color[0];
        break;
      case OTHER:
        c = color[2];
        break;
      default:
        throw new IllegalArgumentException("Color not supported");
    }
    return c;
  }

  /**
   * Flatten stack according to given type.
   * 
   * <p>Output has the same resolution (x,y,t) as input. For stacks, slices are duplicated. Refer to
   * ij.plugin.ZProjector.setMethod(int)
   * 
   * @param method How to flatten - ZProjector methods.
   * @param preserveStack - if <b>true</b> size of output stack is preserved (slices are
   *        duplicated to form stack with the same number of slices as original one). Otherwise
   *        only one slice is built
   */
  public void flatten(int method, boolean preserveStack) {
    ImageStack is = originalImage.getStack();
    is = is.convertToFloat(); // for better averaging

    ZProjector zp = new ZProjector(new ImagePlus(originalImage.getTitle(), is));
    zp.setStartSlice(1);
    zp.setStopSlice(originalImage.getStackSize());
    zp.setMethod(method);
    zp.doProjection();
    ImagePlus ret = zp.getProjection();
    // recreate stack if needed
    if (originalImage.getStackSize() > 1 && preserveStack) {
      ImageStack imS = new ImageStack(ret.getWidth(), ret.getHeight());
      for (int s = 0; s < originalImage.getStackSize(); s++) {
        imS.addSlice(ret.getProcessor().convertToByte(true));
      }
      originalImage = new ImagePlus(originalImage.getTitle(), imS);
    } else { // return only one slice (due to input format or preserveStack flag status)
      originalImage =
              new ImagePlus(originalImage.getTitle(), ret.getProcessor().convertToByte(true));
    }

  }

  /**
   * Clear overlay.
   */
  public void clear() {
    if (overlay != null) {
      overlay.clear();
    }
  }

  /**
   * Subclass for plotting on single image in coord space [outline,frame].
   * 
   * @author p.baniukiewicz
   *
   */
  static class Map extends TrackVisualisation {

    /**
     * Denote if map is rotated.
     * 
     * <p>QuimP maps returned by
     * {@link STmap#map2ColorImagePlus(String, String, double[][], double, double)} are rotated in
     * relation to raw map returned by {@link STmap#getMotMap()}. If this filed is false it means
     * raw maps, use true for ImagePlus map.
     * 
     * <p>Internally all tracks refer to map that has time on x axis (rotated==false).
     * 
     * @see STmap#map2ImagePlus(String, ImageProcessor)
     */
    private boolean rotated = false;
    // scales if maps are scaled by map2ColorImagePlus
    private double ts = 1.0;
    private double os = 1.0;

    /**
     * Instantiates a new map from ImagePlus.
     *
     * <p>Note that x-coords must be time and y - outline which is reverse to QuimP format.
     * 
     * @param originalImage the original image
     */
    public Map(ImagePlus originalImage) {
      super(originalImage);
    }

    /**
     * Instantiates a new map from ImagePlus.
     *
     * <p>Should be used with output from
     * {@link STmap#map2ColorImagePlus(String, String, double[][], double, double)} after setting
     * correct scales. Typically ts = 400/frames and os = 1
     * 
     * @param originalImage the original image
     * @param rotated true if map is rotated so time is y and outline is x. Typically map is output
     *        from {@link STmap#map2ColorImagePlus(String, String, double[][], double, double)}
     * @param ts time scale if map is scaled as abobe
     * @param os outline scale
     */
    public Map(ImagePlus originalImage, boolean rotated, double ts, double os) {
      super(originalImage);
      this.rotated = rotated;
      this.ts = ts;
      this.os = os;
    }

    /**
     * Instantiates a new map.
     *
     * @param name the name
     * @param imp the imp
     */
    public Map(String name, ImageProcessor imp) {
      super(name, imp);
    }

    /**
     * Create object from raw data like e.g. motility map.
     * 
     * @param name Name of the image
     * @param data 2D data.
     */
    public Map(String name, float[][] data) {
      super(name, new FloatProcessor(data));
    }

    /**
     * Plot unrelated points on image (static).
     * 
     * @param points list of points to plot in coordinates (index,frame)
     * @param color color of point
     * @param radius radius of point
     */
    public void addCirclesToImage(Polygon points, Color color, double radius) {
      Polygon polsc = scale(points); // time scale
      int[] indexes = polsc.ypoints;
      int[] frames = polsc.xpoints;
      for (int n = 0; n < points.npoints; n++) {
        // decode frame,outline to screen coordinates
        if (frames[n] < 0 || indexes[n] < 0) {
          continue;
        }
        plotCircle(frames[n], indexes[n], color, radius);
      }
      originalImage.setOverlay(overlay); // add to image
    }

    /**
     * Plot maxima found by {@link MaximaFinder} on current image.
     * 
     * @param maxF properly initialized {@link MaximaFinder} object.
     */
    public void addMaximaToImage(MaximaFinder maxF) {
      Polygon max = maxF.getMaxima();

      Polygon polsc = scale(max); // time scale
      PointRoi pr = GraphicsElements.getPoint(polsc, TrackVisualisation.MAXIMA_COLOR);
      overlay.add(pr);
      originalImage.setOverlay(overlay);
    }

    private int[] scale(int[] in, int len, double sc) {
      int[] ret = new int[len];
      for (int i = 0; i < len; i++) {
        ret[i] = (int) Math.round(in[i] * sc);
      }
      return ret;
    }

    private Polygon scale(Polygon in) {
      int[] xp;
      int[] yp;
      if (rotated) {
        yp = scale(in.xpoints, in.npoints, ts);
        xp = scale(in.ypoints, in.npoints, os);
      } else {
        xp = scale(in.xpoints, in.npoints, ts);
        yp = scale(in.ypoints, in.npoints, os);
      }
      return new Polygon(xp, yp, xp.length);
    }

    /**
     * Add lines defined as polygons to image.
     * 
     * @param trackCollection initialised TrackCollection object
     * 
     */
    public void addTrackingLinesToImage(TrackCollection trackCollection) {
      Iterator<Pair<Track, Track>> it = trackCollection.iterator();
      while (it.hasNext()) {
        Pair<Track, Track> pair = it.next();

        Polygon pairLeft = scale(pair.getLeft().asPolygon());
        Polygon pairRight = scale(pair.getRight().asPolygon());

        PolygonRoi pr = GraphicsElements.getLine(pairLeft, getColor(pair.getLeft())); // b
        // pr.fitSpline();
        overlay.add(pr);
        pr = GraphicsElements.getLine(pairRight, getColor(pair.getRight())); // fw
        // pr.fitSpline();
        overlay.add(pr);
      }
      originalImage.setOverlay(overlay);
    }

  }

  /**
   * Class for plotting on [x,y] image.
   * 
   * @author p.baniukiewicz
   *
   */
  static class Image extends TrackVisualisation {

    /**
     * Instantiates a new image.
     *
     * @param originalImage the original image
     */
    public Image(ImagePlus originalImage) {
      super(originalImage);
    }

    /**
     * Instantiates a new image.
     *
     * @param name the name
     * @param imp the imp
     */
    public Image(String name, ImageProcessor imp) {
      super(name, imp);
    }

    /**
     * Plot unrelated points on image (static).
     * 
     * @param mapCell source of coordinate maps
     * @param points list of points to plot in coordinates (index,frame)
     * @param color color of point
     * @param radius radius of point
     */
    public void addCirclesToImage(STmap mapCell, Polygon points, Color color, double radius) {
      double[][] x = mapCell.getxMap();
      double[][] y = mapCell.getyMap();
      int[] indexes = points.ypoints;
      int[] frames = points.xpoints;
      for (int n = 0; n < points.npoints; n++) {
        // decode frame,outline to screen coordinates
        if (frames[n] < 0 || indexes[n] < 0) {
          continue;
        }
        double xcoord = x[frames[n]][indexes[n]]; // screen coordinate of
        double ycoord = y[frames[n]][indexes[n]]; // (frame,index) point
        plotCircle(xcoord, ycoord, color, radius);
      }
      originalImage.setOverlay(overlay); // add to image
    }

    /**
     * Plot static elements on image if they are not null.
     * 
     * @param mapCell STmap
     * @param trackCollection initialised TrackCollection object
     * @param mf maxima according to Prot_Analysis.MaximaFinder
     */
    public void addElementsToImage(STmap mapCell, TrackCollection trackCollection,
            MaximaFinder mf) {
      if (mf != null) {
        Polygon max = mf.getMaxima();
        addCirclesToImage(mapCell, max, TrackVisualisation.MAXIMA_COLOR, circleRadius);
      }
      if (trackCollection != null) {
        addTrackingLinesToImage(mapCell, trackCollection);
      }
    }

    /**
     * Plot tracking lines before and after maxima points (static).
     * 
     * @param mapCell map related to given cell.
     * @param trackCollection
     * 
     */
    public void addTrackingLinesToImage(STmap mapCell, TrackCollection trackCollection) {
      double[][] x = mapCell.getxMap(); // temporary x and y coordinates for given cell
      double[][] y = mapCell.getyMap();
      // these are raw coordinates of tracking lines extracted from List<PolygonRoi> pL
      ArrayList<float[]> xcoorda = new ArrayList<>();
      ArrayList<float[]> ycoorda = new ArrayList<>();
      int al = 0;
      // iterate over tracks
      Iterator<Track> it = trackCollection.iteratorTrack();
      while (it.hasNext()) {
        Track track = it.next();
        Polygon pr = track.asPolygon();
        // create store for tracking line coordinates
        xcoorda.add(new float[pr.npoints]);
        ycoorda.add(new float[pr.npoints]);
        // counter of invalid vertexes. According to TrackMap#trackForward last points can
        // be -1 when user provided longer time span than available. (last in term of time)
        int invalidVertex = 0;
        // decode frame,outline to x,y
        for (int f = 0; f < pr.npoints; f++) {
          // -1 stands for points that are outside of range - assured by TrackMap.class
          if (pr.ypoints[f] < 0 || pr.xpoints[f] < 0) {
            invalidVertex++; // count bad points
            continue;
          }
          xcoorda.get(al)[f] = (float) x[pr.xpoints[f]][pr.ypoints[f]];
          ycoorda.get(al)[f] = (float) y[pr.xpoints[f]][pr.ypoints[f]];
        }
        PolygonRoi polyRoi = GraphicsElements.getLine(xcoorda.get(al), ycoorda.get(al),
                pr.npoints - invalidVertex, getColor(track));
        overlay.add(polyRoi);
        al++;
      }
      originalImage.setOverlay(overlay); // add to image
    }
  }

  /**
   * Subclass for plotting on stacks in coord space [x,y,f].
   * 
   * @author p.baniukiewicz
   *
   */
  static class Stack extends TrackVisualisation {

    /**
     * Instantiates a new stack.
     *
     * @param originalImage the original image
     */
    public Stack(ImagePlus originalImage) {
      super(originalImage);
    }

    /**
     * Constructor.
     * 
     * @param name name
     * @param imp base ImageProcessor
     */
    public Stack(String name, ImageProcessor imp) {
      super(name, imp);
    }

    /**
     * Plot unrelated points on image (stack).
     * 
     * @param mapCell source of coordinate maps
     * @param points list of points to plot in coordinates (index,frame)
     * @param color color of point
     * @param radius radius of point
     */
    public void addCirclesToImage(STmap mapCell, Polygon points, Color color, double radius) {
      double[][] x = mapCell.getxMap();
      double[][] y = mapCell.getyMap();
      int[] indexes = points.ypoints;
      int[] frames = points.xpoints;

      // LOGGER.trace("Frames:" + Arrays.toString(frames));
      // LOGGER.trace("Indexe:" + Arrays.toString(indexes));
      for (int n = 0; n < points.npoints; n++) {
        // decode frame,outline to screen coordinates
        if (frames[n] < 0 || indexes[n] < 0) {
          continue;
        }
        double xcoord = x[frames[n]][indexes[n]]; // screen coordinate of
        double ycoord = y[frames[n]][indexes[n]]; // (frame,index) point
        plotCircle(xcoord, ycoord, frames[n] + 1, color, radius);
      }
      originalImage.setOverlay(overlay); // add to image
    }

    /**
     * Plot unrelated points on image (stack). Input compatible with
     * {@link TrackMapAnalyser#getIntersectionParents(List, int)}.
     * 
     * @param mapCell source of coordinate maps
     * @param points list of points to plot in coordinates (index,frame)
     * @param color color of point
     * @param radius radius of point
     */
    public void addCirclesToImage(STmap mapCell, List<Pair<Point, Point>> points, Color color,
            double radius) {
      int[] x = new int[points.size()];
      int[] y = new int[points.size()];
      int l = 0;
      for (Pair<Point, Point> p : points) {
        x[l] = p.getRight().x;
        y[l] = p.getRight().y;
        l++;
      }
      Polygon poly = new Polygon(x, y, points.size());
      addCirclesToImage(mapCell, poly, color, radius);
    }

    /**
     * Plot maxima for each frame.
     * 
     * <p>Work like {@link #addTrackingLinesToImage(STmap, TrackCollection)} but does not remember
     * lines but only points.
     * 
     * @param mapCell map related to given cell.
     * @param trackCollection initialised TrackCollection object TODO This method uses old
     *        approach assuming that back and forward tracks are repeating.
     */
    public void addTrackingMaximaToImage(STmap mapCell, TrackCollection trackCollection) {
      double[][] x = mapCell.getxMap(); // temporary x and y coordinates for given cell
      double[][] y = mapCell.getyMap();
      // these are raw coordinates of tracking lines extracted from List<PolygonRoi> pL
      ArrayList<float[]> xcoorda = new ArrayList<>();
      ArrayList<float[]> ycoorda = new ArrayList<>();
      int al = 0;
      // iterate over tracks
      Iterator<Track> it = trackCollection.iteratorTrack();
      while (it.hasNext()) {
        Track track = it.next();
        Polygon polyR = track.asPolygon();
        // we need to sort tracking line points according to frames where they appear in
        // first convert poygon to list of Point2i object
        List<Point> plR =
                TrackMapAnalyser.polygon2Point2i(new ArrayList<Polygon>(Arrays.asList(polyR)));
        // then sort this list according y-coordinate (frame)
        Collections.sort(plR, new ListPoint2iComparator());
        // convert to polygon again but now it is sorted along frames
        Polygon plRsorted = TrackMapAnalyser.point2i2Polygon(plR);
        // create store for tracking line coordinates
        xcoorda.add(new float[plRsorted.npoints]);
        ycoorda.add(new float[plRsorted.npoints]);
        // counter of invalid vertexes. According to TrackMap#trackForward last points can
        // be -1 when user provided longer time span than available. (last in term of time)
        int invalidVertex = 0;
        // decode frame,outline to x,y
        for (int f = 0; f < plRsorted.npoints; f++) {
          // -1 stands for points that are outside of range - assured by TrackMap.class
          if (plRsorted.ypoints[f] < 0 || plRsorted.xpoints[f] < 0) {
            invalidVertex++; // count bad points
            continue;
          }
          xcoorda.get(al)[f] = (float) x[plRsorted.xpoints[f]][plRsorted.ypoints[f]];
          ycoorda.get(al)[f] = (float) y[plRsorted.xpoints[f]][plRsorted.ypoints[f]];
        }
        // now xcoorda,yccora keep coordinates of aL track, it is time to plot
        // iterate over points in sorted polygon (one track line) even indexes stand for
        // backward tracking, odd for forward tracking lines Some last points can be skipped
        // here (sorting does not influence this because last points means last in term of
        // time)
        for (int f = 0; f < plRsorted.npoints - invalidVertex; f++) {
          // x/ycoorda keep all points of tracking lines but PolygonRoi constructor allow
          // to define how many first of them we take. This allows us to add points
          // together with frames - in result the line grows as frames rise. After
          // sorting, first points are those on lower frames
          // set colors (remember about backward/forward order)
          PolygonRoi polyRoi = GraphicsElements.getCircle(xcoorda.get(al)[f], ycoorda.get(al)[f],
                  color[al % 2], circleRadius);
          // set where we want plot f+1 points from x/ycoorda
          polyRoi.setPosition((int) plRsorted.xpoints[f] + 1);
          overlay.add(polyRoi);
        }
        al++;
      }
      originalImage.setOverlay(overlay); // add to image

    }

    /**
     * Plot tracking lines before and after maxima points (in term of frames).
     * 
     * <p>First backward tracking lines are plotted then forward in two different colors. For given
     * maximum first is plotted backward tracking frame by frame, then forward tracking.
     * Backward tracking is visible as long as forward tracking is plotted. Then both disappear.
     * 
     * @param mapCell map related to given cell.
     * @param trackCollection initialised TrackCollection object TODO This method uses old
     *        approach assuming that back and forward tracks are repeating.
     */
    public void addTrackingLinesToImage(STmap mapCell, TrackCollection trackCollection) {
      double[][] x = mapCell.getxMap(); // temporary x and y coordinates for given cell
      double[][] y = mapCell.getyMap();
      // these are raw coordinates of tracking lines extracted from List<PolygonRoi> pL
      ArrayList<float[]> xcoorda = new ArrayList<>();
      ArrayList<float[]> ycoorda = new ArrayList<>();
      int al = 0;
      // iterate over tracks
      Iterator<Track> it = trackCollection.iteratorTrack();
      while (it.hasNext()) {
        Track track = it.next();
        Polygon polyR = track.asPolygon();
        // we need to sort tracking line points according to frames where they appear in
        // first convert poygon to list of Point2i object
        List<Point> plR =
                TrackMapAnalyser.polygon2Point2i(new ArrayList<Polygon>(Arrays.asList(polyR)));
        // then sort this list according y-coordinate (frame)
        Collections.sort(plR, new ListPoint2iComparator());
        // convert to polygon again but now it is sorted along frames
        Polygon plRsorted = TrackMapAnalyser.point2i2Polygon(plR);
        // create store for tracking line coordinates
        xcoorda.add(new float[plRsorted.npoints]);
        ycoorda.add(new float[plRsorted.npoints]);
        // counter of invalid vertexes. According to TrackMap#trackForward last points can
        // be -1 when user provided longer time span than available. (last in term of time)
        int invalidVertex = 0;
        // decode frame,outline to x,y
        for (int f = 0; f < plRsorted.npoints; f++) {
          // -1 stands for points that are outside of range - assured by TrackMap.class
          if (plRsorted.ypoints[f] < 0 || plRsorted.xpoints[f] < 0) {
            invalidVertex++; // count bad points
            continue;
          }
          xcoorda.get(al)[f] = (float) x[plRsorted.xpoints[f]][plRsorted.ypoints[f]];
          ycoorda.get(al)[f] = (float) y[plRsorted.xpoints[f]][plRsorted.ypoints[f]];
        }
        // now xcoorda,yccora keep coordinates of aL track, it is time to plot
        // iterate over points in sorted polygon (one track line) even indexes stand for
        // backward tracking, odd for forward tracking lines Some last points can be skipped
        // here (sorting does not influence this because last points means last in term of
        // time)
        for (int f = 0; f < plRsorted.npoints - invalidVertex; f++) {
          // x/ycoorda keep all points of tracking lines but PolygonRoi constructor allow
          // to define how many first of them we take. This allows us to add points
          // together with frames - in result the line grows as frames rise. After
          // sorting, first points are those on lower frames
          // set colors (remember about backward/forward order)
          PolygonRoi polyRoi =
                  GraphicsElements.getLine(xcoorda.get(al), ycoorda.get(al), f + 1, color[al % 2]);
          // set where we want plot f+1 points from x/ycoorda
          polyRoi.setPosition((int) plRsorted.xpoints[f] + 1);
          overlay.add(polyRoi);
          // If there is maximum on x frame and we plotted backward line from x-n to x, we
          // wont to keep it during plotting forward tracking from x to x+z frames. So
          // this whole line is plotted on every x-x+z frame
          if (al % 2 == 1) {
            PolygonRoi polyRoi1 = GraphicsElements.getLine(xcoorda.get(al - 1), ycoorda.get(al - 1),
                    xcoorda.get(al - 1).length, color[al % 2 - 1]);
            polyRoi1.setPosition((int) plRsorted.xpoints[f] + 1);
            overlay.add(polyRoi1);
          }
        }
        al++;
      }
      originalImage.setOverlay(overlay); // add to image
    }

    /**
     * Plot maxima found by {@link MaximaFinder} on current image.
     * 
     * @param mapCell map related to given cell.
     * @param mf properly initialized {@link MaximaFinder} object.
     */
    public void addMaximaToImage(STmap mapCell, MaximaFinder mf) {
      Polygon max = mf.getMaxima();
      addCirclesToImage(mapCell, max, TrackVisualisation.MAXIMA_COLOR, 7);
    }

    /**
     * Plot filled circle on overlay on given frame.
     * 
     * @param x center
     * @param y center
     * @param frame frame
     * @param color color
     * @param radius radius
     */
    public void plotCircle(double x, double y, int frame, Color color, double radius) {
      // create ROI
      PolygonRoi or = GraphicsElements.getCircle(x, y, color, radius);
      // set z-position of ROI!!!
      or.setPosition(frame);
      overlay.add(or); // add to collection of overlays
    }

    /**
     * Plot outline around cell on image.
     * 
     * <p>Uses {@link OutlinesToImage} options from {@link ProtAnalysisOptions} through filed
     * {@link ProtAnalysisOptions#selOutlineColoring}
     * 
     * @param mapCell map related to given cell.
     * @param config configuration object defining colors, type of plot, etc.
     * @see ProtAnalysisOptions#selOutlineColoring
     * @see OutlinesToImage
     */
    public void addOutlinesToImage(STmap mapCell, ProtAnalysisOptions config) {
      double[][] mm = mapCell.getMotMap();
      double[][] cm = mapCell.getConvMap();

      switch (config.selOutlineColoring.plotType) {
        case MOTILITY:
          plotOutline(mapCell.getxMap(), mapCell.getyMap(),
                  new Color[] { config.selOutlineColoring.motColor,
                      config.selOutlineColoring.defColor },
                  new double[] { config.selOutlineColoring.motThreshold }, mapCell.getMotMap());
          break;
        case CONVEXITY:
          plotOutline(mapCell.getxMap(), mapCell.getyMap(),
                  new Color[] { config.selOutlineColoring.convColor,
                      config.selOutlineColoring.defColor },
                  new double[] { config.selOutlineColoring.convThreshold }, mapCell.getConvMap());
          break;
        case CONVANDEXP: {
          // prepare fake map
          double[][] tmpMap = new double[mm.length][];
          for (int f = 0; f < tmpMap.length; f++) {
            tmpMap[f] = new double[mm[f].length];
            for (int r = 0; r < mm[f].length; r++) {
              tmpMap[f][r] = ((mm[f][r] > 0 && cm[f][r] > 0)) ? 1.0 : -1.0;
            }
          }
          plotOutline(
                  mapCell.getxMap(), mapCell.getyMap(), new Color[] {
                      config.selOutlineColoring.convColor, config.selOutlineColoring.defColor },
                  new double[] { 0 }, tmpMap);
        }
          break;
        case CONCANDRETR: {
          // prepare fake map
          double[][] tmpMap = new double[mm.length][];
          for (int f = 0; f < tmpMap.length; f++) {
            tmpMap[f] = new double[mm[f].length];
            for (int r = 0; r < mm[f].length; r++) {
              tmpMap[f][r] = (mm[f][r] < 0 && cm[f][r] < 0) ? 1.0 : -1.0;
            }
          }
          plotOutline(
                  mapCell.getxMap(), mapCell.getyMap(), new Color[] {
                      config.selOutlineColoring.motColor, config.selOutlineColoring.defColor },
                  new double[] { 0 }, tmpMap);
        }
          break;
        case BOTH: {
          double[][] tmpMap = new double[mm.length][];
          for (int f = 0; f < tmpMap.length; f++) {
            tmpMap[f] = new double[mm[f].length];
            for (int r = 0; r < mm[f].length; r++) {
              tmpMap[f][r] = (mm[f][r] > 0 && cm[f][r] > 0) ? 1.0 : -1.0;
            }
          }
          double[][] tmpMap1 = new double[mm.length][];
          for (int f = 0; f < tmpMap.length; f++) {
            tmpMap1[f] = new double[mm[f].length];
            for (int r = 0; r < mm[f].length; r++) {
              tmpMap1[f][r] = (mm[f][r] < 0 && cm[f][r] < 0) ? 1.0 : -1.0;
            }
          }

          plotOutline(mapCell.getxMap(), mapCell.getyMap(),
                  new Color[] { config.selOutlineColoring.motColor,
                      config.selOutlineColoring.convColor, config.selOutlineColoring.defColor },
                  new double[] { 0, 0 }, tmpMap, tmpMap1);

        }
          break;
        case UNIFORM: {
          // plot any map with impossible threshold (def color)
          plotOutline(mapCell.getxMap(), mapCell.getyMap(),
                  new Color[] { config.selOutlineColoring.motColor,
                      config.selOutlineColoring.defColor },
                  new double[] { Double.MAX_VALUE }, mapCell.getMotMap());
        }
          break;
        default:
          throw new IllegalArgumentException("Plot Type not supported");
      }

      originalImage.setOverlay(overlay);
    }

    /**
     * Helper method for plotting outlines.
     * 
     * <p>Plot outline according to given maps and thresholds with specified colour. Points that
     * does not meet criterion are plotted in default colour.
     * 
     * @param x x-coordinates map
     * @param y y-coordinates map
     * @param color array of colours used for plotting maps given in <tt>map</tt>. The array
     *        must contain default colour on its last position. Its size is usually greater by 1
     *        than number of maps.
     * @param threshold array of threshold that applies to maps. Must be size of maps.
     * @param map list of maps to be plotted with colour <tt>color</tt> if they meet criterion
     *        <tt>threshold</tt>
     */
    private void plotOutline(double[][] x, double[][] y, Color[] color, double[] threshold,
            double[][]... map) {
      Polygon[] lines = new Polygon[map.length + 1]; // last line is default one
      for (int i = 0; i < lines.length; i++) {
        lines[i] = new Polygon();
      }
      // plot map lines (those points that are consecutive for given map and its criterion)
      // and default lines (those consecutive points that do no belong to any map)
      for (int f = 0; f < x.length; f++) { // over frames
        for (int r = 0; r < x[0].length; r++) { // over indexes of outline
          int l = 0;
          boolean added = false; // indicate whether given point is plotted as map
          for (Object item : map) { // over maps
            double[][] tmpmap = (double[][]) item;
            // check threshold. If point is accepted, any started default line is
            // finished here and new map line is started.
            if (tmpmap[f][r] >= threshold[l]) {
              // store this point as l-th line
              lines[l].addPoint((int) Math.round(x[f][r]), (int) Math.round(y[f][r]));
              added = true; // mark that this point is plotted already within any map
              // plot default no-map points if any stored (stored in else part)
              if (lines[lines.length - 1].npoints > 0) {
                PolygonRoi polyR = GraphicsElements.getLine(lines[lines.length - 1],
                        color[color.length - 1], f + 1);
                overlay.add(polyR);
                lines[lines.length - 1] = new Polygon();
              }
            } else { // if not meet criterion plot all lines and initialise them again
              if (lines[l].npoints > 0) {
                PolygonRoi polyR = GraphicsElements.getLine(lines[l], color[l], f + 1);
                overlay.add(polyR);
                lines[l] = new Polygon();
              }
              // and and add this point to default if not plotted yet
              if (!added) {
                lines[lines.length - 1].addPoint((int) Math.round(x[f][r]),
                        (int) Math.round(y[f][r]));
              }
            }
            l++;
          }
        }
        // plot if else-if above does not fire (if fire all lines are initialized again)
        PolygonRoi polyR;
        for (int i = 0; i < lines.length; i++) {
          if (lines[i].npoints > 0) {
            polyR = GraphicsElements.getLine(lines[i], color[i], f + 1);
            overlay.add(polyR);
            lines[i] = new Polygon();
          }
        }
      }
    }

    /**
     * Compare Point2i objects along frames (x-coordinate).
     * 
     * @author p.baniukiewicz
     *
     */
    class ListPoint2iComparator implements Comparator<Point> {

      /*
       * (non-Javadoc)
       * 
       * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
       */
      @Override
      public int compare(Point o1, Point o2) {
        if (o1.x < o2.x) {
          return -1;
        }
        if (o1.x > o2.x) {
          return 1;
        } else {
          return 0;
        }
      }

    }
  }
}