<commit_before>/*
 * Copyright 2014, <NAME>, Inc.
 * Contributed as open source to the Micro-Manager
 * code base.
 * Code written by <NAME>.
 * 
 */

package org.micromanager.projector;

import ij.IJ;
import ij.ImagePlus;
import ij.gui.ImageWindow;
import ij.gui.PolygonRoi;
import ij.gui.Roi;
import ij.plugin.frame.RoiManager;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Window;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JComboBox;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableModel;
import mmcorej.CMMCore;
import mmcorej.DeviceType;
import mmcorej.StrVector;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.micromanager.api.ScriptInterface;
import org.micromanager.utils.FileDialogs;
import org.micromanager.utils.GUIUtils;
import org.micromanager.utils.ReportingUtils;
import org.micromanager.utils.TextUtils;

/**
 * This code is partly generated by the Netbeans Form Editor,
 * and those parts should not be edited by hand.
 */
public class MosaicSequencingFrame extends javax.swing.JFrame {
   private class Sequence {
      Vector<Vector<String>> sequenceTableDataVector;
      int repeatCount;
   }

   private final CMMCore core_;
   private final ScriptInterface gui_;
   private final String mosaic_;
   private final int mosaicWidth_;
   private final int mosaicHeight_;
   private final ProjectorController projectorController_;
   private final DefaultTableModel sequenceTableModel_;
   private final Vector<String> headerNames = new Vector<String>(Arrays.asList(         
                           new String[] {"Time Slot", "Roi Indices", "On Duration (ms)",
                              "Off Duration (ms)", "Loop count"}));
   
   // The triggerProperties_ allow us to convert GUI names for triggering
   // to the Mosaic3 device adapter names for triggering.
   final Map<String, String> triggerProperties_ = new HashMap<String, String>() {{
         put("External Frame Bulb", "ExternalBulb");
         put("Sequence Start", "InternalExpose");
         put("External Advance", "ExternalExpose");
         put("External Start", "ExternalSequenceStart");
      }};

   // Returns all active Mosaic devices reported by the MMCore.
   public static ArrayList<String> getMosaicDevices(CMMCore core) {
      StrVector slmDevices = core.getLoadedDevicesOfType(DeviceType.SLMDevice);
      final ArrayList<String> mosaicDevices = new ArrayList<String>();
      for (final String slmDevice : slmDevices) {
         try {
            if (core.getDeviceLibrary(slmDevice).contentEquals("Mosaic3"))
               mosaicDevices.add(slmDevice);
         } catch (Exception ex) {
            ReportingUtils.logError(ex);
         }
      }
      return mosaicDevices;
   }
   
   // Alter the Time Slot indices such that the first row is
   // column 1, the second column 2, and so on.
   private void updateTimeSlotIndices(final DefaultTableModel sequenceTableModel) {
      for (int i = 0; i < sequenceTableModel.getRowCount(); ++i) {
         sequenceTableModel.setValueAt(i + 1, i, 0);
      }
   }
   
   // Ensures that the "Time Slot" column such that the first
   // row is column 1, the second is 2, and so on.
   private void keepTimeSlotIndicesUpToDate(final DefaultTableModel sequenceTableModel) {
      sequenceTableModel.addTableModelListener(new TableModelListener() {
         @Override
         public void tableChanged(TableModelEvent e) {
            sequenceTableModel.removeTableModelListener(this);
            updateTimeSlotIndices(sequenceTableModel);
            sequenceTableModel.addTableModelListener(this);
         }
      });
   }
   
   // Adds a row to the sequence table, including ROIs, an
   // on-duration period, an off-duration period, and a loop 
   // repeat count. Returns the index of the added row.
   private int addRow(DefaultTableModel sequenceTableModel, String roiIndicesString, int onDurationMs, int offDurationMs, int loopCount) {
      ArrayList<String> rows = new ArrayList<String>();
      rows.add("");
      rows.add(roiIndicesString);
      rows.add(String.valueOf(onDurationMs).trim());
      rows.add(String.valueOf(offDurationMs).trim());
      rows.add(String.valueOf(loopCount).trim());
      sequenceTableModel.addRow(rows.toArray());
      sequenceTable_.invalidate();
      return sequenceTableModel.getRowCount() - 1;
   }
   
   // Implements the Add Time Slot button. Adds the default 
   // integer values. Sends the user to edit the Frame ROIs.
   private void addTimeSlot() {
      int row = addRow(
            sequenceTableModel_,
            "",
            GUIUtils.getIntValue(onDurationTextField_),
            GUIUtils.getIntValue(offDurationTextField_),
            GUIUtils.getIntValue(loopCountTextField_));
      sequenceTable_.setRowSelectionInterval(row, row);
      GUIUtils.startEditingAtCell(sequenceTable_, row, 1);
   }
   
   // Generate a new sequence of phototargeting events.
   private void generateNewSequence(int roiCount) {
      int onDurationMs = GUIUtils.getIntValue(onDurationTextField_);
      int offDurationMs = GUIUtils.getIntValue(offDurationTextField_);
      int loopCount = GUIUtils.getIntValue(loopCountTextField_);
      String sequenceType = (String) sequenceTypeComboBox.getSelectedItem();
      sequenceTable_.clearSelection();
      sequenceTableModel_.getDataVector().clear();
      if (sequenceType.contentEquals("Simultaneous")) {
         addRow(sequenceTableModel_, "1-" + String.valueOf(roiCount).trim(), onDurationMs, offDurationMs, loopCount);
      }
      if (sequenceType.contentEquals("Sequential")) {
         for (int i = 0; i < roiCount; ++i) {
            addRow(sequenceTableModel_, String.valueOf(i + 1).trim(), onDurationMs, offDurationMs, loopCount);      
         }
      }
      if (sequenceType.contentEquals("Cumulative")) {
         for (int i = 0; i < roiCount; ++i) {
            addRow(sequenceTableModel_, i == 0 ? "1" : "1-" + String.valueOf(i + 1).trim(), onDurationMs, offDurationMs, loopCount);      
         }
      }
      updateTimeSlotIndices(sequenceTableModel_);
   }
   
   // Convert a textual representation of a set of one-based roi indices to a
   // vector of zero-based integers also representing those indices. For example,
   // "1-3,5,7,9-11" gets converted to [0,1,2,4,6,8,9,10].
   private ArrayList<Integer> frameStringToRoiIndexList(String sequenceString) {
      ArrayList<Integer> sequence = new ArrayList<Integer>();
      String[] pieces = sequenceString.split(",");
      try {
         for (String piece : pieces) {
            // First parse a range fragment, such as "2-4".
            if (piece.contains("-")) {
               String[] limits = piece.split("-");
               int min = Integer.parseInt(limits[0]);
               int max = Integer.parseInt(limits[1]);
               // If the limits are specified in reverse order, fix that.
               if (min > max) {
                  int temp = max;
                  max = min;
                  min = temp;
               }
               // Add each integer in range (inclusive) to the sequence.
               for (int i = min; i <= max; ++i) {
                  sequence.add(i - 1);
               }
            } else {
               // Parse a standalone integer.
               int value = Integer.parseInt(piece);
               sequence.add(value - 1);
            }
         }
      // Throw an exception in case we have a parse error. We'll use
      // the absence (presence) of this exception to (in)validate
      // user entries.
      } catch (NumberFormatException e) {
         throw new RuntimeException("Unable to parse frame specifier");
      }
      return sequence;
   }

   // The SequenceEvent object defines a Sequence event by a number of
   // simple parameters, including the on and off duration, the loopCount,
   // and the pixels belonging to a particular frame.
   private class SequenceEvent {
      public byte[] framePixels;
      public int onDurationMs;
      public int offDurationMs;
      public int loopCount;
   }
   
   // Read an integer from a sequenceTable_ cell.
   private int integerAt(int rowIndex, int columnIndex) {
      return Integer.parseInt(sequenceTableModel_.getValueAt(rowIndex, columnIndex).toString());
   }
   
   
   // Returns the current Rois on the live window.
   private Roi[] getRois() {
      ImageWindow win;
      try {
         win = gui_.getSnapLiveWin();
      } catch (Exception e) {
         return new Roi[0]; // empty array
      }
      return ProjectorController.getRois(win);  
   }
   
   // Get the total number of ROIs available in the Snap/Live window.
   private int getRoiCount() {
      Roi[] rois = getRois();
      return rois.length;
   }
   
   // Convert a row from the sequenceTable_ to a SequenceEvent object.
   private SequenceEvent sequenceRowToSequenceEvent(int rowIndex, Polygon[] availableRoiPolygons) {
      final ArrayList<Integer> roiIndexList = frameStringToRoiIndexList(sequenceTableModel_.getValueAt(rowIndex, 1).toString());
      SequenceEvent sequenceEvent = new SequenceEvent();
      ArrayList<Polygon> selectedRoiPolygons = new ArrayList<Polygon>();
      for (int roiIndex : roiIndexList) {
         selectedRoiPolygons.add(availableRoiPolygons[roiIndex]);
      }
      sequenceEvent.framePixels = SLM.roisToPixels(mosaicWidth_, mosaicHeight_,
                                                   selectedRoiPolygons.toArray(new Polygon[0]));
      sequenceEvent.onDurationMs = integerAt(rowIndex, 2);
      sequenceEvent.offDurationMs = integerAt(rowIndex, 3);
      sequenceEvent.loopCount = integerAt(rowIndex, 4);
      return sequenceEvent;
   }
   
   // Get a list of SequenceEvents by extracting information from the sequenceTable_.
   private ArrayList<SequenceEvent> getSequenceEvents() {
      final ImageWindow snapLiveWin = gui_.getSnapLiveWin();
      final ImagePlus snapLiveImage = snapLiveWin.getImagePlus();
      Polygon[] availableRoiPolygons = projectorController_.getTransformedRois(snapLiveImage, getRois());
      ArrayList<SequenceEvent> events = new ArrayList<SequenceEvent>();
      for (int i = 0; i < sequenceTableModel_.getRowCount(); ++i) {
         events.add(sequenceRowToSequenceEvent(i, availableRoiPolygons));
      }
      return events;
   }
   
   // Convert a series of sequence events to a string.
   private String sequenceEventsToString(final ArrayList<SequenceEvent> events) {
      String eventString = "";
      int i = 0;
      for (SequenceEvent event:events) {
         eventString += i
                       + " " + event.onDurationMs
                       + " " + event.offDurationMs
                       + " " + event.loopCount + ";";
         ++i;
      }
      return eventString;
   }
   
   // Convert a list of SequenceEvents to a list of byte arrays, each
   // corresponding to a frame in the sequence.
   private List<byte[]> imageListFromSequenceEvents(final ArrayList<SequenceEvent> events) {
      List<byte[]> imageList = new ArrayList<byte[]>();
      for (SequenceEvent event:events) {
         imageList.add(event.framePixels);
      }
      return imageList;
   }
   
   // Upload the provided SequenceEvents to the Mosaic, by using the
   // "SequenceSettings" property.
   private void uploadSequence(final ArrayList<SequenceEvent> events) {
      try {
         core_.loadSLMSequence(mosaic_, imageListFromSequenceEvents(events));      
         core_.setProperty(mosaic_, "SequenceSettings", sequenceEventsToString(events));
      } catch (Exception ex) {
         ReportingUtils.showError(ex);
      }
   }
   
   // Upload the sequence from the sequence table to the Mosaic.
   private void uploadSequence() {
      uploadSequence(getSequenceEvents());
   }

   
   private RoiManager showRoiManager() {
      IJ.run("ROI Manager...");
      return RoiManager.getInstance();
   }
   
   // Generate the ROI grid from a single ROI. The initial ROI will be located
   // in the upper left corner of the grid and the remaining ROIs will be
   // clones of that ROI, with offsets specified.
   private void generateRoiGrid() {
      RoiManager roiManager = showRoiManager();
      Roi selectedRoi = null;
      if (roiManager.getCount() == 0) {
         selectedRoi = IJ.getImage().getRoi();
      } else {
         int firstSelectedRoi = Math.max(0, roiManager.getSelectedIndex());
         selectedRoi = roiManager.getRoisAsArray()[firstSelectedRoi];
      }
      if (selectedRoi == null) {
         ReportingUtils.showError("Please draw a single ROI for duplication in a grid.");
         return;
      }
      roiManager.runCommand("reset");
      int numX = GUIUtils.getIntValue(this.numRoisAcrossField_);
      int numY = GUIUtils.getIntValue(this.numRoisDownField_);
      int spacingX = GUIUtils.getIntValue(this.roiSpacingAcrossField_);
      int spacingY = GUIUtils.getIntValue(this.roiSpacingDownField_);
      for (int j = 0; j < numY; ++j) {
         for (int i = 0; i < numX; ++i) {
            Roi newRoi = (Roi) selectedRoi.clone();
            Rectangle bounds = selectedRoi.getBounds();
            newRoi.setLocation(bounds.x + i * spacingX, bounds.y + j * spacingY);
            roiManager.addRoi(newRoi);
         }
      }
   }

   // Create the Sequence from the sequenceTable_.
   private void generateSequence() {
      int roiCount = getRoiCount();
      if (roiCount == 0) {
         ReportingUtils.showMessage("Please draw ROIs for phototargeting.");
      }
      generateNewSequence(getRoiCount());
   }
   
   // Run the Mosaic Sequence. Called by Run button.
   private void runSequence() {
      try {
         final String selectedItem = sequenceTriggerComboBox.getSelectedItem().toString();
         core_.setProperty(mosaic_, "SequenceLoopCount", GUIUtils.getIntValue(sequenceLoopCountTextField_));
         core_.setProperty(mosaic_, "TriggerMode", triggerProperties_.get(selectedItem));
         core_.setProperty(mosaic_, "OperationMode", "FrameSequence");
         core_.startSLMSequence(mosaic_);
      } catch (Exception ex) {
         ReportingUtils.showError(ex);
      }
   }
   
   // Abort the Mosaic sequence. Called by Stop button.
   private void stopSequence() {
      try {
         core_.stopSLMSequence(mosaic_);
      } catch (Exception ex) {
         ReportingUtils.showError(ex);
      }
   }
   
   // Reads the current ROIs and returns their vertices in JSON Array.
   private JSONArray roisToJSON(Roi[] rois) {
      JSONArray allPolygons = new JSONArray();
      for (Roi roi : rois) {
         JSONArray polyPoints = new JSONArray();
         Polygon polygon = roi.getPolygon();
         for (int i = 0; i < polygon.npoints; ++i) {
            JSONArray xy = new JSONArray();
            xy.put(polygon.xpoints[i]);
            xy.put(polygon.ypoints[i]);
            polyPoints.put(xy);
         }
         allPolygons.put(polyPoints);
      }
      return allPolygons;
   }
   
   // Returns ROI objects generated by interpreting vertices from JSON.
   private Roi[] roisFromJSON(JSONArray data) {
      try {
         Vector<Roi> rois = new Vector<Roi>();
         for (int i = 0; i < data.length(); ++i) {
            Polygon poly = new Polygon();
            JSONArray points = data.getJSONArray(i);
            for (int j = 0 ; j < points.length(); ++j) {
               int x = points.getJSONArray(j).getInt(0);
               int y = points.getJSONArray(j).getInt(1);
               poly.addPoint(x,y);
            }
            Roi roi = new PolygonRoi(poly, Roi.POLYGON);
            rois.add(roi);
         }
         return rois.toArray(new Roi[0]);
      } catch (JSONException ex) {
         ReportingUtils.logError(ex);
         return new Roi[0];
      }      
   }
   
   // Convert current ROI sequence data to a JSON string.
   private String serializeToJSONString() {
      JSONObject toSave = new JSONObject();
      int columnCount = sequenceTableModel_.getColumnCount();
      int rowCount = sequenceTableModel_.getRowCount();
      JSONArray tableData = new JSONArray();
      for (int row = 0; row < rowCount; ++row) {
         JSONObject rowData = new JSONObject();
         for (int column = 0; column < columnCount; ++column) {
            try {
               headerNames.get(column);
               rowData.put(headerNames.get(column), sequenceTableModel_.getValueAt(row, column));
            } catch (JSONException ex) {
               ReportingUtils.logError(ex);
            }
         }
         tableData.put(rowData);
      }

      try {
         toSave.put("Rois", roisToJSON(getRois()));
         toSave.put("SequenceData", tableData);
         toSave.put("SequenceRepeats", GUIUtils.getIntValue(this.sequenceLoopCountTextField_));
      } catch (JSONException ex) {
         ReportingUtils.logError(ex);
      }
      return toSave.toString();
   }

   // Read ROI sequence data from a JSON string and populate window.
   private void deserializeFromJSONString(String jsonString) {
      try {
         JSONObject jsonData = new JSONObject(jsonString);
         JSONArray tableData = jsonData.getJSONArray("SequenceData");
         for (int i = sequenceTableModel_.getRowCount() - 1; i >= 0; --i) {
            sequenceTableModel_.removeRow(i);
         }
         for (int row = 0; row < tableData.length(); ++row) {
            JSONObject rowData = tableData.getJSONObject(row);
            int col = 0;
            Vector<String> newRow = new Vector<String>();
            for (String headerName : headerNames) {
               String value = rowData.getString(headerName);
               newRow.add(value);
               ++col;
            }
            sequenceTableModel_.addRow(newRow);
         }
         sequenceLoopCountTextField_.setText(String.valueOf(jsonData.getInt("SequenceRepeats")));
         Roi[] rois = roisFromJSON(jsonData.getJSONArray("Rois"));
         RoiManager roiManager = showRoiManager();
         roiManager.runCommand("reset");
         for (Roi roi:rois) {
            roiManager.addRoi(roi);
         }
      } catch (JSONException ex) {
         ReportingUtils.logError(ex);
      }
   }
   
   private  FileDialogs.FileType fileType = new FileDialogs.FileType("Mosaic", "Mosaic Sequence Description", "MosaicSequence.txt", true, "txt");   
   
   // Save the sequence settings to a file.
   private void save() {
      try {
         File theFile = FileDialogs.save(null, "Please save the Mosaic sequence file.", fileType);
         String content = serializeToJSONString();
         TextUtils.writeTextFile(theFile.getAbsolutePath(), content);
      } catch (IOException ex) {
         ReportingUtils.logError(ex);
      }
   }
   
   // Load the sequence settings from a file.
   private void load() {
      File theFile = FileDialogs.openFile(null, "Please choose a Mosaic sequence file to load.", fileType);
      try {
         deserializeFromJSONString(TextUtils.readTextFile(theFile.getAbsolutePath()));
      } catch (IOException ex) {
         ReportingUtils.showError(ex);
      }
   }
   
   // Starts the sequence when the acquisition engine starts.
   public void attachToAcquisition() {
      gui_.attachRunnable(0, 0, 0, 0, new Runnable() {
         public void run() {
            new Thread() {
               public void run() {
                  runSequence();
               }
            }.start();
         }
      });
   }
   
   public void detachFromAcquisition() {
      gui_.clearRunnables();
   }
   
   // Creates a new window, the MosaicSequencingFrame. This frame allows the
   // user to generate sequences of ROIs, and optionally, generate ROIs in
   // a grid pattern.
   public MosaicSequencingFrame(ScriptInterface gui, CMMCore core, ProjectorController projectorController) {
      initComponents();
      gui_ = gui;
      core_ = core;
      projectorController_ = projectorController;
      // Get the first available Mosaic device for now.
      mosaic_ = getMosaicDevices(core_).get(0);
      mosaicWidth_ = (int) core_.getSLMWidth(mosaic_);
      mosaicHeight_ = (int) core.getSLMHeight(mosaic_);
      sequenceTableModel_ = (DefaultTableModel) sequenceTable_.getModel();
      
      GUIUtils.recallPosition(this);
      GUIUtils.enforceIntegerTextField(onDurationTextField_, 0, 200000);
      GUIUtils.enforceIntegerTextField(offDurationTextField_, 0, 200000);
      GUIUtils.enforceIntegerTextField(loopCountTextField_, 0, 65535);
      GUIUtils.enforceIntegerTextField(sequenceLoopCountTextField_, 0, 65535);
      
      GUIUtils.enforceIntegerTextField(numRoisAcrossField_, 1, 30);
      GUIUtils.enforceIntegerTextField(numRoisDownField_, 1, 30);
      GUIUtils.enforceIntegerTextField(this.roiSpacingAcrossField_, 10, 1000);
      GUIUtils.enforceIntegerTextField(this.roiSpacingDownField_, 10, 1000);
      
      GUIUtils.makeIntoMoveRowUpButton(sequenceTable_, upButton);<commit_msg>helpful comment<commit_after>/*
 * Copyright 2014, Andor Technology, Inc.
 * Contributed as open source to the Micro-Manager
 * code base.
 * Code written by <NAME>.
 * 
 */

package org.micromanager.projector;

import ij.IJ;
import ij.ImagePlus;
import ij.gui.ImageWindow;
import ij.gui.PolygonRoi;
import ij.gui.Roi;
import ij.plugin.frame.RoiManager;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Window;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JComboBox;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableModel;
import mmcorej.CMMCore;
import mmcorej.DeviceType;
import mmcorej.StrVector;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.micromanager.api.ScriptInterface;
import org.micromanager.utils.FileDialogs;
import org.micromanager.utils.GUIUtils;
import org.micromanager.utils.ReportingUtils;
import org.micromanager.utils.TextUtils;

/**
 * This code is partly generated by the Netbeans Form Editor,
 * and those parts should not be edited by hand.
 */
public class MosaicSequencingFrame extends javax.swing.JFrame {
   private class Sequence {
      Vector<Vector<String>> sequenceTableDataVector;
      int repeatCount;
   }

   private final CMMCore core_;
   private final ScriptInterface gui_;
   private final String mosaic_;
   private final int mosaicWidth_;
   private final int mosaicHeight_;
   private final ProjectorController projectorController_;
   private final DefaultTableModel sequenceTableModel_;
   private final Vector<String> headerNames = new Vector<String>(Arrays.asList(         
                           new String[] {"Time Slot", "Roi Indices", "On Duration (ms)",
                              "Off Duration (ms)", "Loop count"}));
   
   // The triggerProperties_ allow us to convert GUI names for triggering
   // to the Mosaic3 device adapter names for triggering.
   final Map<String, String> triggerProperties_ = new HashMap<String, String>() {{
         put("External Frame Bulb", "ExternalBulb");
         put("Sequence Start", "InternalExpose");
         put("External Advance", "ExternalExpose");
         put("External Start", "ExternalSequenceStart");
      }};

   // Returns all active Mosaic devices reported by the MMCore.
   public static ArrayList<String> getMosaicDevices(CMMCore core) {
      StrVector slmDevices = core.getLoadedDevicesOfType(DeviceType.SLMDevice);
      final ArrayList<String> mosaicDevices = new ArrayList<String>();
      for (final String slmDevice : slmDevices) {
         try {
            if (core.getDeviceLibrary(slmDevice).contentEquals("Mosaic3"))
               mosaicDevices.add(slmDevice);
         } catch (Exception ex) {
            ReportingUtils.logError(ex);
         }
      }
      return mosaicDevices;
   }
   
   // Alter the Time Slot indices such that the first row is
   // column 1, the second column 2, and so on.
   private void updateTimeSlotIndices(final DefaultTableModel sequenceTableModel) {
      for (int i = 0; i < sequenceTableModel.getRowCount(); ++i) {
         sequenceTableModel.setValueAt(i + 1, i, 0);
      }
   }
   
   // Ensures that the "Time Slot" column such that the first
   // row is column 1, the second is 2, and so on.
   private void keepTimeSlotIndicesUpToDate(final DefaultTableModel sequenceTableModel) {
      sequenceTableModel.addTableModelListener(new TableModelListener() {
         @Override
         public void tableChanged(TableModelEvent e) {
            sequenceTableModel.removeTableModelListener(this);
            updateTimeSlotIndices(sequenceTableModel);
            sequenceTableModel.addTableModelListener(this);
         }
      });
   }
   
   // Adds a row to the sequence table, including ROIs, an
   // on-duration period, an off-duration period, and a loop 
   // repeat count. Returns the index of the added row.
   private int addRow(DefaultTableModel sequenceTableModel, String roiIndicesString, int onDurationMs, int offDurationMs, int loopCount) {
      ArrayList<String> rows = new ArrayList<String>();
      rows.add("");
      rows.add(roiIndicesString);
      rows.add(String.valueOf(onDurationMs).trim());
      rows.add(String.valueOf(offDurationMs).trim());
      rows.add(String.valueOf(loopCount).trim());
      sequenceTableModel.addRow(rows.toArray());
      sequenceTable_.invalidate();
      return sequenceTableModel.getRowCount() - 1;
   }
   
   // Implements the Add Time Slot button. Adds the default 
   // integer values. Sends the user to edit the Frame ROIs.
   private void addTimeSlot() {
      int row = addRow(
            sequenceTableModel_,
            "",
            GUIUtils.getIntValue(onDurationTextField_),
            GUIUtils.getIntValue(offDurationTextField_),
            GUIUtils.getIntValue(loopCountTextField_));
      sequenceTable_.setRowSelectionInterval(row, row);
      GUIUtils.startEditingAtCell(sequenceTable_, row, 1);
   }
   
   // Generate a new sequence of phototargeting events.
   private void generateNewSequence(int roiCount) {
      int onDurationMs = GUIUtils.getIntValue(onDurationTextField_);
      int offDurationMs = GUIUtils.getIntValue(offDurationTextField_);
      int loopCount = GUIUtils.getIntValue(loopCountTextField_);
      String sequenceType = (String) sequenceTypeComboBox.getSelectedItem();
      sequenceTable_.clearSelection();
      sequenceTableModel_.getDataVector().clear();
      if (sequenceType.contentEquals("Simultaneous")) {
         addRow(sequenceTableModel_, "1-" + String.valueOf(roiCount).trim(), onDurationMs, offDurationMs, loopCount);
      }
      if (sequenceType.contentEquals("Sequential")) {
         for (int i = 0; i < roiCount; ++i) {
            addRow(sequenceTableModel_, String.valueOf(i + 1).trim(), onDurationMs, offDurationMs, loopCount);      
         }
      }
      if (sequenceType.contentEquals("Cumulative")) {
         for (int i = 0; i < roiCount; ++i) {
            addRow(sequenceTableModel_, i == 0 ? "1" : "1-" + String.valueOf(i + 1).trim(), onDurationMs, offDurationMs, loopCount);      
         }
      }
      updateTimeSlotIndices(sequenceTableModel_);
   }
   
   // Convert a textual representation of a set of one-based roi indices to a
   // vector of zero-based integers also representing those indices. For example,
   // "1-3,5,7,9-11" gets converted to [0,1,2,4,6,8,9,10].
   private ArrayList<Integer> frameStringToRoiIndexList(String sequenceString) {
      ArrayList<Integer> sequence = new ArrayList<Integer>();
      String[] pieces = sequenceString.split(",");
      try {
         for (String piece : pieces) {
            // First parse a range fragment, such as "2-4".
            if (piece.contains("-")) {
               String[] limits = piece.split("-");
               int min = Integer.parseInt(limits[0]);
               int max = Integer.parseInt(limits[1]);
               // If the limits are specified in reverse order, fix that.
               if (min > max) {
                  int temp = max;
                  max = min;
                  min = temp;
               }
               // Add each integer in range (inclusive) to the sequence.
               for (int i = min; i <= max; ++i) {
                  sequence.add(i - 1);
               }
            } else {
               // Parse a standalone integer.
               int value = Integer.parseInt(piece);
               sequence.add(value - 1);
            }
         }
      // Throw an exception in case we have a parse error. We'll use
      // the absence (presence) of this exception to (in)validate
      // user entries.
      } catch (NumberFormatException e) {
         throw new RuntimeException("Unable to parse frame specifier");
      }
      return sequence;
   }

   // The SequenceEvent object defines a Sequence event by a number of
   // simple parameters, including the on and off duration, the loopCount,
   // and the pixels belonging to a particular frame.
   private class SequenceEvent {
      public byte[] framePixels;
      public int onDurationMs;
      public int offDurationMs;
      public int loopCount;
   }
   
   // Read an integer from a sequenceTable_ cell.
   private int integerAt(int rowIndex, int columnIndex) {
      return Integer.parseInt(sequenceTableModel_.getValueAt(rowIndex, columnIndex).toString());
   }
   
   
   // Returns the current Rois on the live window.
   private Roi[] getRois() {
      ImageWindow win;
      try {
         win = gui_.getSnapLiveWin();
      } catch (Exception e) {
         return new Roi[0]; // empty array
      }
      return ProjectorController.getRois(win);  
   }
   
   // Get the total number of ROIs available in the Snap/Live window.
   private int getRoiCount() {
      Roi[] rois = getRois();
      return rois.length;
   }
   
   // Convert a row from the sequenceTable_ to a SequenceEvent object.
   private SequenceEvent sequenceRowToSequenceEvent(int rowIndex, Polygon[] availableRoiPolygons) {
      final ArrayList<Integer> roiIndexList = frameStringToRoiIndexList(sequenceTableModel_.getValueAt(rowIndex, 1).toString());
      SequenceEvent sequenceEvent = new SequenceEvent();
      ArrayList<Polygon> selectedRoiPolygons = new ArrayList<Polygon>();
      for (int roiIndex : roiIndexList) {
         selectedRoiPolygons.add(availableRoiPolygons[roiIndex]);
      }
      sequenceEvent.framePixels = SLM.roisToPixels(mosaicWidth_, mosaicHeight_,
                                                   selectedRoiPolygons.toArray(new Polygon[0]));
      sequenceEvent.onDurationMs = integerAt(rowIndex, 2);
      sequenceEvent.offDurationMs = integerAt(rowIndex, 3);
      sequenceEvent.loopCount = integerAt(rowIndex, 4);
      return sequenceEvent;
   }
   
   // Get a list of SequenceEvents by extracting information from the sequenceTable_.
   private ArrayList<SequenceEvent> getSequenceEvents() {
      final ImageWindow snapLiveWin = gui_.getSnapLiveWin();
      final ImagePlus snapLiveImage = snapLiveWin.getImagePlus();
      Polygon[] availableRoiPolygons = projectorController_.getTransformedRois(snapLiveImage, getRois());
      ArrayList<SequenceEvent> events = new ArrayList<SequenceEvent>();
      for (int i = 0; i < sequenceTableModel_.getRowCount(); ++i) {
         events.add(sequenceRowToSequenceEvent(i, availableRoiPolygons));
      }
      return events;
   }
   
   // Convert a series of sequence events to a string.
   private String sequenceEventsToString(final ArrayList<SequenceEvent> events) {
      String eventString = "";
      int i = 0;
      for (SequenceEvent event:events) {
         eventString += i
                       + " " + event.onDurationMs
                       + " " + event.offDurationMs
                       + " " + event.loopCount + ";";
         ++i;
      }
      return eventString;
   }
   
   // Convert a list of SequenceEvents to a list of byte arrays, each
   // corresponding to a frame in the sequence.
   private List<byte[]> imageListFromSequenceEvents(final ArrayList<SequenceEvent> events) {
      List<byte[]> imageList = new ArrayList<byte[]>();
      for (SequenceEvent event:events) {
         imageList.add(event.framePixels);
      }
      return imageList;
   }
   
   // Upload the provided SequenceEvents to the Mosaic, by using the
   // "SequenceSettings" property.
   private void uploadSequence(final ArrayList<SequenceEvent> events) {
      try {
         core_.loadSLMSequence(mosaic_, imageListFromSequenceEvents(events));      
         core_.setProperty(mosaic_, "SequenceSettings", sequenceEventsToString(events));
      } catch (Exception ex) {
         ReportingUtils.showError(ex);
      }
   }
   
   // Upload the sequence from the sequence table to the Mosaic.
   private void uploadSequence() {
      uploadSequence(getSequenceEvents());
   }

   // Show the ImageJ Roi Manager and return a reference to it.   
   private RoiManager showRoiManager() {
      IJ.run("ROI Manager...");
      return RoiManager.getInstance();
   }
   
   // Generate the ROI grid from a single ROI. The initial ROI will be located
   // in the upper left corner of the grid and the remaining ROIs will be
   // clones of that ROI, with offsets specified.
   private void generateRoiGrid() {
      RoiManager roiManager = showRoiManager();
      Roi selectedRoi = null;
      if (roiManager.getCount() == 0) {
         selectedRoi = IJ.getImage().getRoi();
      } else {
         int firstSelectedRoi = Math.max(0, roiManager.getSelectedIndex());
         selectedRoi = roiManager.getRoisAsArray()[firstSelectedRoi];
      }
      if (selectedRoi == null) {
         ReportingUtils.showError("Please draw a single ROI for duplication in a grid.");
         return;
      }
      roiManager.runCommand("reset");
      int numX = GUIUtils.getIntValue(this.numRoisAcrossField_);
      int numY = GUIUtils.getIntValue(this.numRoisDownField_);
      int spacingX = GUIUtils.getIntValue(this.roiSpacingAcrossField_);
      int spacingY = GUIUtils.getIntValue(this.roiSpacingDownField_);
      for (int j = 0; j < numY; ++j) {
         for (int i = 0; i < numX; ++i) {
            Roi newRoi = (Roi) selectedRoi.clone();
            Rectangle bounds = selectedRoi.getBounds();
            newRoi.setLocation(bounds.x + i * spacingX, bounds.y + j * spacingY);
            roiManager.addRoi(newRoi);
         }
      }     
   }

   // Create the Sequence from the sequenceTable_.
   private void generateSequence() {
      int roiCount = getRoiCount();
      if (roiCount == 0) {
         ReportingUtils.showMessage("Please draw ROIs for phototargeting.");
      }
      generateNewSequence(getRoiCount());
   }
   
   // Run the Mosaic Sequence. Called by Run button.
   private void runSequence() {
      try {
         final String selectedItem = sequenceTriggerComboBox.getSelectedItem().toString();
         core_.setProperty(mosaic_, "SequenceLoopCount", GUIUtils.getIntValue(sequenceLoopCountTextField_));
         core_.setProperty(mosaic_, "TriggerMode", triggerProperties_.get(selectedItem));
         core_.setProperty(mosaic_, "OperationMode", "FrameSequence");
         core_.startSLMSequence(mosaic_);
      } catch (Exception ex) {
         ReportingUtils.showError(ex);
      }
   }
   
   // Abort the Mosaic sequence. Called by Stop button.
   private void stopSequence() {
      try {
         core_.stopSLMSequence(mosaic_);
      } catch (Exception ex) {
         ReportingUtils.showError(ex);
      }
   }
   
   // Reads the current ROIs and returns their vertices in JSON Array.
   private JSONArray roisToJSON(Roi[] rois) {
      JSONArray allPolygons = new JSONArray();
      for (Roi roi : rois) {
         JSONArray polyPoints = new JSONArray();
         Polygon polygon = roi.getPolygon();
         for (int i = 0; i < polygon.npoints; ++i) {
            JSONArray xy = new JSONArray();
            xy.put(polygon.xpoints[i]);
            xy.put(polygon.ypoints[i]);
            polyPoints.put(xy);
         }
         allPolygons.put(polyPoints);
      }
      return allPolygons;
   }
   
   // Returns ROI objects generated by interpreting vertices from JSON.
   private Roi[] roisFromJSON(JSONArray data) {
      try {
         Vector<Roi> rois = new Vector<Roi>();
         for (int i = 0; i < data.length(); ++i) {
            Polygon poly = new Polygon();
            JSONArray points = data.getJSONArray(i);
            for (int j = 0 ; j < points.length(); ++j) {
               int x = points.getJSONArray(j).getInt(0);
               int y = points.getJSONArray(j).getInt(1);
               poly.addPoint(x,y);
            }
            Roi roi = new PolygonRoi(poly, Roi.POLYGON);
            rois.add(roi);
         }
         return rois.toArray(new Roi[0]);
      } catch (JSONException ex) {
         ReportingUtils.logError(ex);
         return new Roi[0];
      }      
   }
   
   // Convert current ROI sequence data to a JSON string.
   private String serializeToJSONString() {
      JSONObject toSave = new JSONObject();
      int columnCount = sequenceTableModel_.getColumnCount();
      int rowCount = sequenceTableModel_.getRowCount();
      JSONArray tableData = new JSONArray();
      for (int row = 0; row < rowCount; ++row) {
         JSONObject rowData = new JSONObject();
         for (int column = 0; column < columnCount; ++column) {
            try {
               headerNames.get(column);
               rowData.put(headerNames.get(column), sequenceTableModel_.getValueAt(row, column));
            } catch (JSONException ex) {
               ReportingUtils.logError(ex);
            }
         }
         tableData.put(rowData);
      }

      try {
         toSave.put("Rois", roisToJSON(getRois()));
         toSave.put("SequenceData", tableData);
         toSave.put("SequenceRepeats", GUIUtils.getIntValue(this.sequenceLoopCountTextField_));
      } catch (JSONException ex) {
         ReportingUtils.logError(ex);
      }
      return toSave.toString();
   }

   // Read ROI sequence data from a JSON string and populate window.
   private void deserializeFromJSONString(String jsonString) {
      try {
         JSONObject jsonData = new JSONObject(jsonString);
         JSONArray tableData = jsonData.getJSONArray("SequenceData");
         for (int i = sequenceTableModel_.getRowCount() - 1; i >= 0; --i) {
            sequenceTableModel_.removeRow(i);
         }
         for (int row = 0; row < tableData.length(); ++row) {
            JSONObject rowData = tableData.getJSONObject(row);
            int col = 0;
            Vector<String> newRow = new Vector<String>();
            for (String headerName : headerNames) {
               String value = rowData.getString(headerName);
               newRow.add(value);
               ++col;
            }
            sequenceTableModel_.addRow(newRow);
         }
         sequenceLoopCountTextField_.setText(String.valueOf(jsonData.getInt("SequenceRepeats")));
         Roi[] rois = roisFromJSON(jsonData.getJSONArray("Rois"));
         RoiManager roiManager = showRoiManager();
         roiManager.runCommand("reset");
         for (Roi roi:rois) {
            roiManager.addRoi(roi);
         }
      } catch (JSONException ex) {
         ReportingUtils.logError(ex);
      }
   }
   
   private  FileDialogs.FileType fileType = new FileDialogs.FileType("Mosaic", "Mosaic Sequence Description", "MosaicSequence.txt", true, "txt");   
   
   // Save the sequence settings to a file.
   private void save() {
      try {
         File theFile = FileDialogs.save(null, "Please save the Mosaic sequence file.", fileType);
         String content = serializeToJSONString();
         TextUtils.writeTextFile(theFile.getAbsolutePath(), content);
      } catch (IOException ex) {
         ReportingUtils.logError(ex);
      }
   }
   
   // Load the sequence settings from a file.
   private void load() {
      File theFile = FileDialogs.openFile(null, "Please choose a Mosaic sequence file to load.", fileType);
      try {
         deserializeFromJSONString(TextUtils.readTextFile(theFile.getAbsolutePath()));
      } catch (IOException ex) {
         ReportingUtils.showError(ex);
      }
   }
   
   // Starts the sequence when the acquisition engine starts.
   public void attachToAcquisition() {
      gui_.attachRunnable(0, 0, 0, 0, new Runnable() {
         public void run() {
            new Thread() {
               public void run() {
                  runSequence();
               }
            }.start();
         }
      });
   }
   
   // The acquisition engine will no longer run phototargeting after this
   // is called. Called by the "detach" button.
   public void detachFromAcquisition() {
      gui_.clearRunnables();
   }
   
   // Creates a new window, the MosaicSequencingFrame. This frame allows the
   // user to generate sequences of ROIs, and optionally, generate ROIs in
   // a grid pattern.
   public MosaicSequencingFrame(ScriptInterface gui, CMMCore core, ProjectorController projectorController) {
      initComponents();
      gui_ = gui;
      core_ = core;
      projectorController_ = projectorController;
      // Get the first available Mosaic device for now.
      mosaic_ = getMosaicDevices(core_).get(0);
      mosaicWidth_ = (int) core_.getSLMWidth(mosaic_);
      mosaicHeight_ = (int) core.getSLMHeight(mosaic_);
      sequenceTableModel_ = (DefaultTableModel) sequenceTable_.getModel();
      
      GUIUtils.recallPosition(this);
      GUIUtils.enforceIntegerTextField(onDurationTextField_, 0, 200000);
      GUIUtils.enforceIntegerTextField(offDurationTextField_, 0, 200000);
      GUIUtils.enforceIntegerTextField(loopCountTextField_, 0, 65535);
      GUIUtils.enforceIntegerTextField(sequenceLoopCountTextField_, 0, 65535);
      
      GUIUtils.enforceIntegerTextField(numRoisAcrossField_, 1, 30);
      GUIUtils.enforceIntegerTextField(numRoisDownField_, 1, 30);
      GUIUtils.enforceIntegerTextField(this.roiSpacingAcrossField_, 10, 1000);
      GUIUtils.enforceIntegerTextField(this.roiSpacingDownField_, 10, 1000);
      
      GUIUtils.makeIntoMoveRowUpButton(sequenceTable_, upButton);
{'fraction_non_alphanumeric': 0.0998452012383901, 'fraction_numerical': 0.005629045876723895, 'mean_word_length': 2.6237463878973313, 'pattern_counts': {'":': 0, '<': 92, '<?xml version=': 0, '>': 70, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1091324', 'n_tokens_mistral': 12179, 'n_tokens_neox': 11797, 'n_words': 3103}