ECMM_Mapping.java

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

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
// import java.util.Vector;
import java.util.Random;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.celldynamics.quimp.Nest;
import com.github.celldynamics.quimp.Outline;
import com.github.celldynamics.quimp.OutlineHandler;
import com.github.celldynamics.quimp.QParams;
import com.github.celldynamics.quimp.QParamsQconf;
import com.github.celldynamics.quimp.QuimP;
import com.github.celldynamics.quimp.QuimpException;
import com.github.celldynamics.quimp.QuimpException.MessageSinkTypes;
import com.github.celldynamics.quimp.SnakeHandler;
import com.github.celldynamics.quimp.Vert;
import com.github.celldynamics.quimp.filesystem.DataContainer;
import com.github.celldynamics.quimp.filesystem.FileExtensions;
import com.github.celldynamics.quimp.filesystem.OutlinesCollection;
import com.github.celldynamics.quimp.filesystem.QconfLoader;
import com.github.celldynamics.quimp.filesystem.converter.FormatConverter;
import com.github.celldynamics.quimp.geom.ExtendedVector2d;
import com.github.celldynamics.quimp.plugin.AbstractPluginQconf;
import com.github.celldynamics.quimp.plugin.QuimpPluginException;

import ij.IJ;
import ij.WindowManager;
import ij.gui.YesNoCancelDialog;
import ij.process.ImageProcessor;

/*
 * //!>
 * @startuml doc-files/ECMM_Mapping_1_UML.png
 * start
 * :Check registration;
 * if (input file given) then (no) 
 * :ask user; 
 * endif
 * :Load config file;
 * if (QUIMP_11 file) then (yes)
 * :process it;
 * :scan for other files;
 * repeat 
 * :process other file;
 * repeat while(more files?) else (no)
 * if(BOA data) then (no)
 * stop
 * endif
 * if(ECMM data) then (yes)
 * if(overwrite?) then (no)
 * end
 * endif
 * endif
 * :process it;
 * endif
 * end
 * @enduml
 * 
 * //!< 
 */
/**
 * Main ECMM implementation class.
 * 
 * <p>To disable plotting hide {@link ECMplot#imPlus} by accessing {@link #plot}.
 * 
 * @author Richard Tyson. 23/09/2009. ECM Mapping Systems Biology DTC, Warwick University.
 * @author p.baniukiewicz
 *
 */
public class ECMM_Mapping extends AbstractPluginQconf {

  private static String thisPluginName = "ECMM Mapping";
  /**
   * Required version of IJ1.
   */
  public static final String requiredVersion = "1.52n";

  private File fileToLoad = null; // file to load paQP/QCONF
  /**
   * The Constant LOGGER.
   */
  static final Logger LOGGER = LoggerFactory.getLogger(ECMM_Mapping.class.getName());
  private OutlineHandler oh;
  private OutlineHandler outputH;
  private OutlinesCollection outputOutlineHandlers; // output for new data file

  /**
   * The plot.
   */
  public static ECMplot plot;

  /**
   * Default constructor called on plugin run from IJ GUI.
   */
  public ECMM_Mapping() {
    super(new EcmmOptions(), thisPluginName);
  }

  /**
   * Constructor called by ANA.
   * 
   * @param frames frame
   */
  public ECMM_Mapping(int frames) { // work around. b is nothing
    this();
    if (ECMp.plot) {
      plot = new ECMplot(frames);
    }
  }

  /**
   * Constructor that allows to provide own parameters. Set apiCall to true.
   * 
   * @param paramString it can be null to ask user for file or it can be parameters string like that
   *        passed in macro.
   * @throws QuimpPluginException on any error
   */
  public ECMM_Mapping(String paramString) throws QuimpPluginException {
    super(paramString, new EcmmOptions(), thisPluginName); // will parse and fill options
  }

  /**
   * Create object and fill options with specified file.
   * 
   * <p>Set apiCall to true and errors redirects to console.
   * 
   * @param file file to process.
   */
  public ECMM_Mapping(File file) {
    super(new EcmmOptions(file), thisPluginName);
    apiCall = true;
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.PluginTemplate#run(java.lang.String)
   */
  @Override
  public void run(String arg) {
    super.run(arg);
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginQconf#loadFile(java.lang.String)
   */
  @Override
  protected void loadFile(String paramFile) throws QuimpException {
    if (options.paramFile == null || options.paramFile.isEmpty()) {
      fileToLoad = null;
    } else {
      fileToLoad = new File(options.paramFile);
    }

    qconfLoader = new QconfLoader(fileToLoad); // load file
    if (qconfLoader == null || qconfLoader.getQp() == null) {
      return; // failed to load exit
    }
    if (qconfLoader.isFileLoaded() == QParams.QUIMP_11) { // old path
      QParams qp;
      runFromPaqp();
      // old flow with paQP files - detect other paQP
      File[] otherPaFiles = qconfLoader.getQp().findParamFiles();
      if (otherPaFiles.length > 0) {
        YesNoCancelDialog yncd = new YesNoCancelDialog(IJ.getInstance(), "Batch Process?",
                "\tBatch Process?\n\n" + "Process other " + FileExtensions.configFileExt
                        + " files in the same folder with ECMM?\n"
                        + "[Files already run through ECMM will be skipped!]");
        if (yncd.yesPressed()) {
          ArrayList<String> runOn = new ArrayList<String>(otherPaFiles.length);
          ArrayList<String> skipped = new ArrayList<String>(otherPaFiles.length);

          for (int j = 0; j < otherPaFiles.length; j++) {
            plot.close();
            qconfLoader = new QconfLoader(otherPaFiles[j]); // load file
            if (qconfLoader == null || qconfLoader.getQp() == null) {
              return; // failed to load exit
            }
            qp = qconfLoader.getQp();
            if (!qp.isEcmmHasRun()) {
              IJ.log("Running on " + otherPaFiles[j].getAbsolutePath());
              runFromPaqp();
              runOn.add(otherPaFiles[j].getName());
            } else {
              IJ.log("Skipped " + otherPaFiles[j].getAbsolutePath());
              skipped.add(otherPaFiles[j].getName());
            }

          }
          IJ.log("\n\nBatch - Successfully ran ECMM on:");
          for (int i = 0; i < runOn.size(); i++) {
            IJ.log(runOn.get(i));
          }
          IJ.log("\nSkipped:");
          for (int i = 0; i < skipped.size(); i++) {
            IJ.log(skipped.get(i));
          }
        } else {
          return; // no batch processing
        }
      }
    } else if (qconfLoader.isFileLoaded() == QParams.NEW_QUIMP) { // new path
      // validate in case new format
      validate();
      if (qconfLoader.isECMMPresent() && apiCall == false && errorSink == MessageSinkTypes.GUI) {
        YesNoCancelDialog ync;
        ync = new YesNoCancelDialog(IJ.getInstance(), "Overwrite",
                "You are about to override previous ECMM results. Is it ok?");
        if (!ync.yesPressed()) { // if no or cancel
          IJ.log("No changes done in input file.");
          return; // end}
        }
      }
      runFromQconf();
      IJ.log("The new data file " + qconfLoader.getQp().getParamFile().toString()
              + " has been updated by results of ECMM analysis.");
    } else {
      throw new QuimpPluginException("QconfLoader returned unknown version of QuimP or error: "
              + qconfLoader.isFileLoaded());
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginQconf#executer()
   */
  @Override
  protected void executer() throws QuimpException {
    // we need to use different handling for multiple paQP files, so use own loader
    if (apiCall == true) { // if run from other constructor, override sink
      errorSink = MessageSinkTypes.CONSOLE;
    }
    super.executer();
  }

  /**
   * Called by ANA if no ECCM results are in file (old path).
   * 
   * @param m OutlineHandler
   * @param ipr ImageProcessor
   * @param d cortex?
   * @return Processed outline
   */
  public OutlineHandler runByANA(OutlineHandler m, ImageProcessor ipr, double d) {
    oh = m;
    ECMp.image = ipr;
    ECMp.setParams(oh.maxLength);
    ECMp.startFrame = oh.getStartFrame();
    ECMp.endFrame = oh.getEndFrame();
    ECMp.plot = false;
    ECMp.ANA = true;
    ECMp.anaMigDist = d;
    ECMp.migQ = 1.5E-5; //
    ECMp.tarQ = -1.5E-5; // use same charge

    if (ECMp.plot) {
      plot = new ECMplot(oh.getSize() - 1);
    }

    // ECMp.setParams(m.indexGetOutline(0));

    // *******adjust params for ana***********
    ECMp.h = 0.9;
    ECMp.chargeDensity = 4;
    ECMp.d = 0.4;
    ECMp.maxVertF = 0.7;
    // *************************
    runPlugin();
    // IJ.log("ECM Mapping FINISHED");
    return outputH;
  }

  /**
   * Main executive for ECMM plugin.
   * 
   * @see #runFromQconf()
   * @see #runFromPaqp()
   */
  private void runPlugin() {
    long time = System.currentTimeMillis();
    if (!ECMp.ANA) {
      IJ.log("ECMM resolution: " + ECMp.markerRes + "(av. spacing)\n");
    }

    outputH = new OutlineHandler(oh);
    ECMp.unSnapped = 0;
    // int skippedFrames = 0; // if a frame is skipped need to divide next
    // time point migration by 2, etc...

    Mapping map1;
    int f = ECMp.startFrame; // now in frames
    Outline o1 = oh.getStoredOutline(f);
    // resolution is always as in segmentation - not now
    if (!ECMp.ANA) {
      if (Math.abs(ECMp.markerRes) > 0) {
        o1.setResolution(Math.abs(ECMp.markerRes));
      }
    }
    // LOGGER.trace("Outline o1:head =[" + o1.getHead().getX() + "," + o1.getHead().getY() +
    // "]");
    o1.resetAllCoords();
    o1.clearFluores();
    o1.calcCentroid(); // TODO this should be called as in case of Snakes

    outputH.save(o1, f);
    Outline o2;

    int stopAt = -1; // debug break

    for (; f <= oh.getEndFrame() - 1; f++) {
      if (f == stopAt) {
        ECMp.plot = true;
      }
      if (o1.checkCoordErrors()) {
        IJ.error("There was an error in tracking due to a bug (frame " + (f) + ")"
                + "\nPlease try again");
        break;
      }

      if (!ECMp.ANA) {
        IJ.showStatus("Running ECMM");
        IJ.showProgress(f, oh.getEndFrame());
        IJ.log("Mapping " + f + " to " + (f + 1));
      }

      o2 = oh.getStoredOutline(f + 1);
      // o2 left as seen in the segmentation - i.e. marker res unchanged
      if (!ECMp.ANA && ECMp.markerRes > 0) {
        o2.setResolution(ECMp.markerRes); // must be done b4 intersects are calculated
      }
      o2.resetAllCoords();
      o2.clearFluores();

      if (!ECMp.ANA) {
        this.nudgeOverlaps(o1, o2); // ensure no points/edges lie directly on each other (to 1e-4)/
      }

      if (ECMp.plot) {
        plot.setDrawingFrame(f);
        o1.calcCentroid(); // calc it again as it is broken in loop by migrate() where Outline is
        // initialzied from one vertex
        plot.centre = o1.getCentroid();

        if (ECMp.drawInitialOutlines) {
          plot.setColor(0d, 0d, 1d);
          plot.drawOutline(o1);
          plot.setColor(0d, 1d, 0d);
          plot.drawOutline(o2);
          plot.setSlice(f);
        }
      }

      // OutlineHandler.writeSingle("o2.snQP", o2);
      // OutlineHandler.writeSingle("o1.snQP", o1);

      map1 = new Mapping(o1, o2);

      /*
       * if (map1.invalid) { //Use no sectors IJ.log(" invalid outline intersection,
       * attempting non-intersect mapping..."); plot.writeText("Non-intersect mapping");
       * ECMp.noSectors = true; map1 = new Mapping(o1, o2); ECMp.noSectors = false; }
       */

      o1 = map1.migrate();
      // System.out.println("num nodes: "+o1.getVerts());

      if (!ECMp.ANA) {
        // System.out.println("\n check final intersects");
        if (!ECMp.disableDensityCorrections) {
          if (o1.removeNanoEdges()) {
            // IJ.log(" result had some v.small edges- removed");
          }
          if (o1.cutSelfIntersects()) {
            IJ.log("    result self intersected - fixed");
            if (ECMp.plot) {
              plot.writeText("Fixed self intersection");
            }
          }

          if (ECMp.markerRes == 0) {
            o1.correctDensity(2 * 1.6, 2 / 1.6);
          } else {
            o1.correctDensity(ECMp.markerRes * 1.6, ECMp.markerRes / 1.6);
          }
        }
        if (ECMp.plot && ECMp.drawSolutionOutlines) {
          plot.setColor(1d, 0d, 0d);
          plot.drawOutline(o1);
        }
      }

      if (ECMp.ANA && ECMp.plot) {
        plot.setColor(0d, 0.7d, 0.7d);
        plot.drawOutline(o1);
      }

      // OutlineHandler.writeSingle("o2.snQP", o2);
      // OutlineHandler.writeSingle("o1.snQP", o1);

      o1.coordReset(); // reset the frame Coordinate system

      outputH.save(o1, f + 1);
      if (f == stopAt) {
        break;
      }
    }

    // IJ.log("Total iterations = " + ECMp.its);
    if (ECMp.plot) {
      plot.repaint();
    }

    if (!ECMp.ANA) {
      double timeSec = (System.currentTimeMillis() - time) / 1000d;
      IJ.showStatus("ECMM finished");
      IJ.log("ECMM finished in " + timeSec + " seconds.");
    }
    return;
  }

  private void nudgeOverlaps(Outline o1, Outline o2) {

    int state;
    double[] intersect = new double[2];
    Random rg = new Random();

    Vert na = o1.getHead();
    Vert nb;
    do {
      nb = o2.getHead();
      do {
        // check if points on top of each other
        if (nb.getX() == na.getX() && nb.getY() == na.getY()) {
          // use a minimum nudge of 0.01 (imageJ pixel accurracy
          na.setX(na.getX() + (rg.nextDouble() * 0.5) + 0.01);
          na.setY(na.getY() + (rg.nextDouble() * 0.5) + 0.01);
        }

        // check if lines are parallel
        state = ExtendedVector2d.segmentIntersection(na.getX(), na.getY(), na.getNext().getX(),
                na.getNext().getY(), nb.getX(), nb.getY(), nb.getNext().getX(), nb.getNext().getY(),
                intersect);
        if (state == -1 || state == -2) {
          // IJ.log(" outline parrallel -fixed");
          na.setX(na.getX() + (rg.nextDouble() * 0.5) + 0.01);
          na.setY(na.getY() + (rg.nextDouble() * 0.5) + 0.01);
        }

        nb = nb.getNext();
      } while (!nb.isHead());
      na = na.getNext();

    } while (!na.isHead());

  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.IQuimpPlugin#about()
   */
  @Override
  public String about() {
    return "ECMM plugin.\n" + "Authors: Piotr Baniukiewicz\n"
            + "mail: p.baniukiewicz@warwick.ac.uk\n" + "Richard Tyson";
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginQconf#runFromQconf()
   */
  @Override
  protected void runFromQconf() throws QuimpException {
    LOGGER.debug("Processing from new file format");
    Nest nest = ((QParamsQconf) qconfLoader.getQp()).getNest();
    outputOutlineHandlers = new OutlinesCollection(nest.size());
    for (int i = 0; i < nest.size(); i++) { // go over all snakes
      // For compatibility, all methods have the same syntax (assumes that there is only one
      // handler)
      ((QParamsQconf) qconfLoader.getQp()).setActiveHandler(i); // set current handler number.
      SnakeHandler sh = nest.getHandler(i);
      if (sh == null) {
        continue;
      }
      oh = new OutlineHandler(sh); // convert to outline, oh is global var
      ECMp.setup(qconfLoader.getQp());
      ECMp.setParams(oh.maxLength); // base params on outline in middle of
      // sequence
      if (ECMp.plot) {
        plot = new ECMplot(oh.getSize() - 1);
        plot.imPlus.setTitle(WindowManager.makeUniqueName(ECMplot.ECMM_TITLE + "_" + "cell_" + i));
      }
      runPlugin(); // fills outputH
      outputOutlineHandlers.oHs.add(i, new OutlineHandler(outputH)); // store actual result
    }

    DataContainer dc = ((QParamsQconf) qconfLoader.getQp()).getLoadedDataContainer();
    dc.ECMMState = outputOutlineHandlers; // assign ECMM container to global output
    try {
      qconfLoader.getQp().writeParams();
    } catch (IOException e) {
      throw new QuimpPluginException(e);
    } // save global container
    // generate additional OLD files, disabled #263, enabled GH228
    if (QuimP.newFileFormat.get() == false) {
      FormatConverter foramtConvrter = new FormatConverter(qconfLoader);
      foramtConvrter.doConversion();
    }

  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginQconf#runFromPaqp()
   */
  @Override
  protected void runFromPaqp() throws QuimpException {
    oh = new OutlineHandler(qconfLoader.getQp());
    if (!oh.readSuccess) {
      throw new QuimpException("Could not read OutlineHandler");
    }

    ECMp.setup(qconfLoader.getQp());
    // System.out.println("sf " + ECMp.startFrame + ", ef " +
    // ECMp.endFrame);
    // System.out.println("outfile " + ECMp.OUTFILE.getAbsolutePath());
    ECMp.setParams(oh.maxLength); // base params on outline in middle of sequence
    if (ECMp.plot) {
      plot = new ECMplot(oh.getSize() - 1);
    }
    runPlugin();

    if (ECMp.saveTemp) {
      // ------ save a temporary version instead as to not over write the
      // old version
      File tempFile = new File(ECMp.OUTFILE.getAbsolutePath() + ".temp.txt");
      outputH.writeOutlines(tempFile, true);
      IJ.log("ECMM:137, saving to a temp file instead");
    } else {
      ECMp.INFILE.delete();
      outputH.writeOutlines(ECMp.OUTFILE, true);
    }

  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginQconf#validate()
   */
  @Override
  protected void validate() throws QuimpException {
    super.validate();
    String ver = IJ.getVersion();
    // strip ImageJ2 version from IJ1
    if (ver.indexOf("/") > -1) {
      ver = ver.substring(ver.indexOf("/") + 1);
    }
    LOGGER.debug("IJ1 version " + ver);
    if (ver.compareTo(requiredVersion) < 0) {
      throw new QuimpException("Required ImageJ 1.52n (or above)");
    }
  }

  /*
   * (non-Javadoc)
   * 
   * @see com.github.celldynamics.quimp.plugin.AbstractPluginBase#showUi(boolean)
   */
  @Override
  public void showUi(boolean val) throws Exception {
    // no options given, no UI but load file and execute plugin
    executer();

    // in case user loaded the file
    if (qconfLoader != null && qconfLoader.getQp() != null) {
      options.paramFile = qconfLoader.getQp().getParamFile().getAbsolutePath();
    }
  }
}