import processing.core.*; 
import processing.data.*; 
import processing.event.*; 
import processing.opengl.*; 

import de.bezier.guido.*; 
import java.util.Iterator; 
import java.util.Map; 

import java.util.HashMap; 
import java.util.ArrayList; 
import java.io.File; 
import java.io.BufferedReader; 
import java.io.PrintWriter; 
import java.io.InputStream; 
import java.io.OutputStream; 
import java.io.IOException; 

public class ribosketch extends PApplet {

 // try to remove this dependency? /* GUIDO Library classes from https://github.com/fjenett/Guido/blob/master/examples/basics/slider/slider.pde */ //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>//



final boolean BROWSER = false; // The one and only thing that must change to shift between browser and desktop versions!

boolean debugPrints = false; // Prints off or on

File userFile;
// String pngOutFileName = null; // if defined, save PNG image to file - once (at end of draw function)
DrawClass drawer = new DrawClass(); // for SVG output
String outputDir = "ribosketch_images";
int pushedMatrices = 0;
float zoomFactorSave = 1.0f;
float zoomTranslateXSave = 0.0f;
float zoomTranslateYSave = 0.0f;

class ForceField {
  double radius; // Mostly used for display and interacting with the user
  final double minRadius = 1;
  final double radiusScale = 50;
  double backboneDist; // Scales much of the forcefield
  final double minBackboneDist = minRadius*2;
  final double backboneDistScale = radiusScale*2;
  double bpDist; // base pair distance
  final double minBpDist = minRadius*3;
  final double bpDistScale = radiusScale*3;

  double vdWeight = 8; // Van der waals repulsion strength
  double eWeight = 5; // Electrostatic repulsion strength
  double eCutoffScale = 12; // factor for which after a certain distance, electrostatics has no influence
  double straightWeight = 0.0125f; // How much obtuse angles are preffered
  double stretchWeight = 2.5f; // Spring constant multiplier
  double padWeight = 0.005f; // 1 // 20 // How strong the padding is
  double cmWeight = .3f; // .1 How attracted to center of mass
  double mass = 25; // Resistance to acceleration
  double velMax = 12; // maximum velocity
  double damping = .5f; // .6 // Friction: percent of velocity to be carried over
  double hairpinWeight = 4.5f;
  double loopWeight = 1; //.05 
  double branchStraightenWeight = 1.5f;
}

float cmx = 0.0f;
float cmy = 0.0f;

ArrayList<String> globalMessages = new ArrayList<String>(); // at each iteration, accumulate messages for user to be printed to screen
ArrayList<SimulationState> undoStates = new ArrayList<SimulationState>();
int undoId = -1;// index of most recent undo state

HashMap<Integer, Integer> attachedIds = new HashMap<Integer, Integer>(); // set of selected residues (Using HashMap as Set because Set is not supported in Processing)

String picInfo = (BROWSER) ? "" : "(saved to folder of input file)";

HashMap<String, String> helpCommands = new HashMap<String, String>();

String[] helpLines = {
  "Area-Select", 
  "Multi-Select", 
  "Select single base", 
  "Select/Deselect All", 
  "", 
  "Move", 
  "",
  "Rotation (Around Cursor):",
  "Clockwise (Large/ Small)", 
  "Counter-Clockwise (Large/ Small)", 
  "", 
  "Undo / Redo", 
  "Add Bond", 
  "Delete Bond", 
  "",
  "Simulation (Toggle)", 
  "Simulate Selected only (Toggle)", 
  "Scroll Screen",
  "Zoom In / Out", 
  "Zoom Reset", 
  "Relax Selected Bonds (Toggle)", 
  "Flip helix strands", 
  "Flip over X-axis / Y-axis", 
  "Bring to Front / Send to Back", 
  "Base Size +/-", 
  "Base Characters (Toggle)", 
  "Base Labels (Toggle)", 
  "Base Information (Toggle)", 
  "Annotations (Toggle)", 
  "Change Color Mode", 
  "PNG Screenshot", 
  picInfo
};

String[] sliderNames = {
  "Base Size", 
  "Bond Length", 
  "Chain Length", 
  "Color Scheme"
  //"Screen Buffer",
  //"Temperature",
};

String[] checkBoxNames = {
  "Save", 
  "Load File", 
  "Load Bonds", 
  "Load Colors", 

  "Radial Layout", 
  "Circle Layout", 
  "Simulation Mode", 
  "Sim. Selected Only", 

  "Rigid Helices", 
  "Rigid Loops", 
  "Rigid Hairpins", 
  "Zoom Reset", 

  "Outlines", 
  "Labels", 
  "PNG Screenshot", 
  "SVG Screenshot", 
};

HashMap<String, CheckBox> checkBoxes = new HashMap<String, CheckBox>(checkBoxNames.length);
Slider[] sliders = new Slider[sliderNames.length];
Listbox fileTypeSelector;
int displaySelectorId;

//class MenuProperties {
//}

int textDisplayCount = 10000; // Start on an arbitrarily high number
String textDisplay = "";

boolean menuVisible = false;
boolean simulationMode = false;
boolean simulateAllMode = true;
boolean simWasOn = false;
//boolean movieMode = false;
//boolean movieWasOn = false;
//int movieInterval = 3; // How many simulation steps between each new folding step in movieMode
//int multiBranchMax = 40; // do not attempt symmetrizing multi-branched loops with more than this many residues
boolean rigidHelices = true;
boolean rigidHairpins = true;
boolean wasRigidHairpins = rigidHairpins;
boolean rigidLoops = true;
boolean wasRigidLoops = rigidLoops;
boolean screenshot = false; // Whether a screenshot should be captured
int screenshotFormat = DrawClass.FORMAT_PNG; // other option: FORMAT_SVG
boolean capturingScreen = false; // Are we recording the screen for the screenshot
boolean viewingBase = false;

boolean checkLengthForRigidLoops = true;

boolean selectingFileType = false;
boolean runningSimulation = false;
String errorMessage = "";
boolean returnToSim = false; // true: after error display, return to the current state (when loading after the program has started)
// false: prompt file selection again. MUST START FALSE
boolean clickCheck = false;
boolean programLaunch = true;

SimulationState currentState;
ForceField ff;
DisplaySettings ds;
float helpY; // Set in setup
float menuLimitY;
float sliderMenuHeight;
float sliderMenuWidth;
float checkBoxWidth;
float screenResX = 1200.0f;
float screenResY = 750.0f;

public void setup()
{
   // sbould be equal to screenResX and screenResY
  if (!BROWSER) { 
    surface.setResizable(true);
  }

  //println("\nStarting setup...");
  ff = new ForceField();
  ds = new DisplaySettings();

  // Create GUI
  Interactive.make( this );

  // Sliders
  float yMax = 0;
  for (int i = 0; i < sliderNames.length; ++i) {
    float x = ds.sliderLabelX;
    float y = (i+1) * ds.sliderHeight;
    if ((i % 2) == 1) { // "odd" cases are sliders in the right column
      x += ds.sliderWidth + ds.sliderLabelWidth + ds.sliderGap;
      y -= ds.sliderHeight;
    }
    sliders[i] = new Slider( x, y, ds.sliderWidth, ds.sliderHeight, sliderNames[i], ds.sliderLabelWidth, i);
    yMax = y + 2.5f * ds.sliderHeight; // Adjust active region of menu
  }

  int sliderLines = floor((sliders.length+1) / 2);
  sliderMenuHeight = (sliderLines * ds.sliderHeight) + ((sliderLines-1) * ds.sliderHeight) + 10;
  drawer.TextSize(ds.menuTextSize);

  // Check Boxes
  for (int i = 0; i < checkBoxNames.length; ++i) {
    float x = ds.sliderLabelX;
    float y = yMax + (((int)(i/4)) * 2.5f * ds.sliderHeight);
    if ((i % 4) == 1) {
      x += (ds.sliderWidth + ds.sliderLabelWidth + ds.sliderGap) / 2.0f;
    } else if ((i % 4) == 2) {
      x += ds.sliderWidth + ds.sliderLabelWidth + ds.sliderGap;
    } else if ((i % 4) == 3) {
      x += (ds.sliderWidth + ds.sliderLabelWidth + ds.sliderGap) * 1.5f;
    }

    if (debugPrints) { 
      println("Creating CheckBox " + checkBoxNames[i] + ",  x: " + x + "  y: " + y);
    }

    if ("Save".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new SaveCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Load File".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new LoadCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Load Bonds".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new BasePairLoadCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Load Colors".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new ColorLoadCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Radial Layout".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new ResetRadialCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Circle Layout".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new ResetCircleCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Simulation Mode".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new MovementCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Rigid Helices".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new RigidHelixCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Labels".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new LabelCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("PNG Screenshot".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new SnapCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("SVG Screenshot".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new SnapSVGCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i]), (int)screenResX, (int)screenResY));
    } else if ("Outlines".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new OutlineCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Rigid Hairpins".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new RigidHairpinsCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Sim. Selected Only".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new SimulateAllCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Zoom Reset".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new ZoomResetBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    } else if ("Rigid Loops".equals(checkBoxNames[i])) {
      checkBoxes.put(checkBoxNames[i], new RigidLoopsCheckBox( checkBoxNames[i], x, y, 15, ds.sliderHeight, textWidth(checkBoxNames[i])));
    }

    if (i == checkBoxNames.length - 1) {
      menuLimitY = y + 1.25f * ds.sliderHeight; // Adjust active region of menu
    }
  }
  menuLimitY += 15;
  helpY = menuLimitY + 5;

  checkBoxWidth = (ds.sliderWidth + ds.sliderLabelWidth + ds.sliderGap)*1.5f + ds.sliderLabelWidth + 15;
  sliderMenuWidth = (ds.sliderLabelWidth + ds.sliderWidth)*2 + ds.sliderGap + 10;

  if (debugPrints) { 
    println("ds.menuLimitY: " + menuLimitY);
  }

  helpCommands.put("Area-Select", "Click and Drag");
  helpCommands.put("Multi-Select", "Hold SHIFT while selecting");
  helpCommands.put("Select single base", "Alt-Click");
  helpCommands.put("Select/Deselect All", "a");
  helpCommands.put("Move", "Select, & Drag or Arrow Keys");
  helpCommands.put("Clockwise (Large/ Small)", "m / M");
  helpCommands.put("Counter-Clockwise (Large/ Small)", "n / N");
  helpCommands.put("Undo / Redo", "z / Z");
  helpCommands.put("Simulation (Toggle)", "s");
  helpCommands.put("Simulate Selected only (Toggle)", "S");
  helpCommands.put("Add Bond", "Hold SPACE & Click 2 Bases");
  helpCommands.put("Delete Bond", "Hold d & Click Base");
  helpCommands.put("Flip helix strands", "f");
  helpCommands.put("Flip over X-axis / Y-axis", "x / y");
  helpCommands.put("Bring to Front / Send to Back", "1 / 2");
  helpCommands.put("Relax Selected Bonds (Toggle)", "r");
  helpCommands.put("Base Size +/-", "b / B");
  helpCommands.put("Base Characters (Toggle)", "i");
  helpCommands.put("Base Labels (Toggle)", "l");
  helpCommands.put("Base Information (Toggle)", "k");
  helpCommands.put("Annotations (Toggle)", "v");
  helpCommands.put("Change Color Mode", "c");
  helpCommands.put("Scroll Screen", "Deselect All, & Arrow Keys");
  helpCommands.put("Zoom In / Out", "+ / -");
  helpCommands.put("Zoom Reset", "0");
  helpCommands.put("PNG Screenshot", "p");

  // Display characteristics
  drawer.TextSize(14);
  colorMode(HSB, ds.colorMax);

  if (!BROWSER) {
    // Call fileSelected
    selectInput("Select a file to load", "fileSelected");
  }
}

public void fileSelected(File selection) {
  if (selection == null) {
    println("Window was closed or the user hit cancel.");
  } else {
    println("User selected file " + selection.getAbsolutePath());
    userFile = selection;
    outputDir = userFile.getParentFile().getAbsolutePath();
    setupState(selection.getAbsolutePath());
  }
}

public void setupState(String fileName) {
  setupState(fileName, null);
}

public void setupState() {
  setupState(null, null);
}

public void setupState(String fileName, String fileType) {
  SimulationSetup sim = new SimulationSetup();
  SimulationState tempState = sim.initFile(fileName, fileType);
  errorMessage = sim.errorMessage;
  cursor(WAIT);
  if (errorMessage.equals("")) {
    currentState = tempState;
    //ds.customColors = new color[currentState.sim.sequences.length + 3];
    if (checkLengthForRigidLoops && currentState.spheres.size() > 1000) {
      rigidLoops = false;
      checkBoxes.get("Rigid Loops").checked = false;
    } else {
      checkLengthForRigidLoops = true;
    }

    undoStates.clear();
    undoId = -1;
    attachedIds.clear();

    updateGUI();
    runningSimulation = true;
    menuVisible = true;
    if ( !("RADIAL LAYOUT".equals(textDisplay) || "SAVE FILE LOADED".equals(textDisplay)) ) {
      textDisplay = "";
    }
    textDisplayCount = 0;
    saveStateForUndo();
  } else {
    errorMessage += "\n" + sim.warningMessage;
    if (BROWSER) {
      errorMessage += "\n***MAKE SURE YOU SELECT THE CORRECT INPUT FILE TYPE BELOW! (\"ct\", \"dbn\", \"bpseq\", \"rs\", \"nts\")***\n";
    } else {
      errorMessage += "\n***MAKE SURE YOUR FILE HAS THE CORRECT EXTENSION (.ct, .dbn, .bpseq, .rs)***\n";
    }

    errorMessage += "\n\n***HOW TO FORMAT A DBN FILE***" +
      "\n  1st line: string of characters representing sequences with either \"&\" or space seperating strands" +
      "\n  2nd line: brackets representing pairs, or dots representing unpaired bases" +
      "\n\n~~EXAMPLE~~" +
      "\n  GGGGUAA&AACCCCUUG" +
      "\n  ((((.[[&..))))]].\n\n" +
      "\n\n***HOW TO FORMAT A CT OR BPSEQ FILE***" +
      "\n  Lines preceded by \"#\" and empty lines are ignored." +
      "\n  If you have multiple strands in a ct file, the first line before the data should be a header line," +
      "\n  or the file should be formatted such that the line for each residue that begins a new strand has 0 as its 3rd column." +
      "\n  (bpseq files with multiple strands must use the header format)" +
      "\n\n~~HEADER FORMAT (A line of space separated numbers before the structure info)~~" +
      "\n  First number: Total number of bases" +
      "\n  Second: Number of strands" +
      "\n  Third: Start index of strand 1 (always 1)" +
      "\n  Next n numbers: Start of strand 2, strand 3..." +
      "\n\n~EXAMPLE HEADER for a structure with 150 bases,  3 strands,  1st strand starts with base #1,  2nd strand starts with base #40,  3rd with base #90~" +
      "\n  150 3 1 40 90";
  }
  cursor(ARROW);
}

public void updateGUI() {
  sliders[0].setValue( (float)((ff.radius - ff.minRadius) / ff.radiusScale) );
  sliders[1].setValue( (float)((ff.bpDist - ff.minBpDist) / ff.bpDistScale) );
  sliders[2].setValue( (float)((ff.backboneDist - ff.minBackboneDist) / ff.backboneDistScale) );
  sliders[3].setValue( ((float)ds.colorMode - 1)/((float)DisplaySettings.COLOR_MODE_MAX - 1) );
  //sliders[4].setValue( (float)(ds.padding / ds.paddingScale) );

  checkBoxes.get("Labels").checked = ds.labelMode;
  checkBoxes.get("Simulation Mode").checked = simulationMode;
  checkBoxes.get("Rigid Helices").checked = rigidHelices;
}

public void draw() {
  mydraw();
}    

public void mydraw()
{
  drawer.clear(); // remove stored graphics data
  pushMatrix(); // for saving untransformed coordinates
  globalMessages.clear(); // reset all messages to the user
  globalMessages.add("Menu");
  // Not running a state
  if (!runningSimulation) {
    menuVisible = false;

    if (selectingFileType) {
      background(0xffC9C9C9);
      drawer.TextSize(16);
      textAlign(LEFT);
      String fileType = fileTypeSelector.lastItemClicked;
      if (fileType != null) {
        text( "File format " + fileType, 30, 35 );
      }

      boolean aboveCancel = false;
      boolean aboveOk = false;
      if (height - 125 <= mouseY && mouseY <= height - 75) {
        if ((width/3.0f) - 100 <= mouseX && mouseX <= (width/3.0f) + 100) { // Cancel
          aboveCancel = true;
        } else if (((width*2)/3.0f) - 100 <= mouseX && mouseX <= ((width*2)/3.0f) + 100) {
          aboveOk = true;
        }
      }

      // Ok button
      if (aboveOk) {
        strokeWeight(4);
        fill(0xffFFF8AD);
      } else {
        strokeWeight(2);
        fill(0xffFFFFFF);
      }
      stroke(ds.GUIColor);
      rectMode(CENTER);

      rect((width*2)/3.0f, height - 100, 200, 50, 10);
      fill(0xff000000);
      textAlign(CENTER);
      drawer.TextSize(35);
      text("OK", (width*2)/3.0f, height - 85);

      // Cancel button
      if (aboveCancel) {
        strokeWeight(4);
        fill(0xffFFF8AD);
      } else {
        strokeWeight(2);
        fill(0xffFFFFFF);
      }
      rectMode(CENTER);
      rect(width/3.0f, height - 100, 200, 50, 10);
      fill(0xff000000);
      text("CANCEL", width/3.0f, height - 85);

      if (mousePressed) {
        clickCheck = true;
      } else {
        if (clickCheck) {
          clickCheck = false;

          if (aboveCancel) { // Cancel
            selectingFileType = false;
            errorMessage = "";
            if (returnToSim) { // Don't prompt again if user was loading a new file after program had been started
              simulationMode = simWasOn;
              //movieMode = movieWasOn;
              runningSimulation = true;
            }
          } else if (aboveOk && fileTypeSelector.lastItemClicked != null) {
            selectingFileType = false;
            errorMessage = "";
            if (!BROWSER) {
              setupState(userFile.getAbsolutePath(), fileTypeSelector.lastItemClicked);
            } else {
              setupState(null, fileTypeSelector.lastItemClicked);
            }
          }
        }
      }
    }

    // Display error message
    else if (!errorMessage.equals("")) {
      background(0xffC9C9C9);

      strokeWeight(3);
      stroke(0xff00A8FF);
      fill(0xffFFFFFF);
      rectMode(CORNER);
      rect(30, 30, width - 60, height - 60, 10);

      fill(0xff000000);
      drawer.TextSize(15);
      textAlign(LEFT);
      text(errorMessage, 50, 50, width - 100, height - 200);

      // OK button
      boolean aboveOk = false;
      if (height - 125 <= mouseY && mouseY <= height - 75 && width/2.0f - 50 <= mouseX && width/2.0f + 50 >= mouseX) {
        aboveOk = true;
      }

      rectMode(CENTER);
      int fc = aboveOk ? 0xffFFFFFF : 0xff000000;
      fill(fc);
      rect(width/2.0f, height - 100, 100, 50, 10);
      fc = aboveOk ? 0xff000000 : 0xffFFFFFF;
      fill(fc);
      textAlign(CENTER);
      drawer.TextSize(34);
      text("OK", width/2.0f, height - 90);

      if (mousePressed) {
        clickCheck = true;
      } else {
        if (clickCheck) {
          clickCheck = false;
          errorMessage = "";
          if (returnToSim) { // Don't prompt again if user was loading a new file after program had been started
            simulationMode = simWasOn;
            //movieMode = movieWasOn;
            runningSimulation = true;
          } else { // File has not been loaded yet, prompt again
            if (!BROWSER) {
              selectInput("Select a file to load:", "fileSelected");
            }
          }
        }
      }
    }

    //else if (colorSelecting) {
    //  palette.render();
    // TODO: colorMode(HSB, ds.colorMax); on exit
    //}

    // Start menu
    else {
      background(0xffFFFFFF); // (19/36.0)*ds.colorMax, abs(sin((float)(frameCount / 500.0)))*ds.colorMax*.9, ds.colorMax);
      fill(0xffFFFFFF);
      strokeWeight(3);
      stroke(0xff00A8FF);
      ellipse(width/2.0f, height/2.0f, 900, 400);

      fill(0xff000000);
      drawer.TextSize(100);
      textAlign(CENTER);
      text("RiboSketch", width/2.0f, (height/2.0f) + 20);
      int c;
      rectMode(CENTER);
      if ((width/2.0f) - 110 <= mouseX && mouseX <= (width/2.0f) + 110 &&
        (height*.68f) - 25 <= mouseY && mouseY <= (height*.68f) + 25) {
        fill(0xffFFFFFF);
        c = 0xff000000;
      } else {
        fill(0xff000000);
        c = 0xffFFFFFF;
      }
      rect(width/2.0f, height*.68f, 220, 50, 10);
      fill(c);
      drawer.TextSize(32);
      text("Click to Load", width/2.0f, height*.68f + 9);

      // ?TODO: Prevent this from happening if file is being processed? (ex: A large 1000+ nucleotide sequence that will display this screen even while it is loading)
      if (mousePressed) { // Prompt file selector again
        clickCheck = true;
      } else {
        if (clickCheck) {
          clickCheck = false;
          if (!BROWSER) {
            selectInput("Select a file to load:", "fileSelected");
          } else if (javascript != null) {
            setupState();
          }
        }
      }
    }
  } else {
    // ***File loaded, running application***

    if (BROWSER || !capturingScreen) { // Transparent background for screenshot
      background(0xffFFFFFF);
    }

    // Check to turn display menu on/off
    if ((mouseY > menuLimitY) && menuVisible) {
      menuVisible = false;
      if (pushedMatrices > 0) {
        popMatrix();
        --pushedMatrices;
        ds.zoomFactor = zoomFactorSave;
        ds.zoomTranslateX = zoomTranslateXSave;
        ds.zoomTranslateY = zoomTranslateYSave;
      }
      for (Slider slider : sliders) {
        slider.isPressed = false;
      }
    } else if (!menuVisible && (mouseY < DisplaySettings.MENU_APPEAR_Y) && (mouseX < DisplaySettings.MENU_APPEAR_X) && !multiSelecting && !dragging ) {
      menuVisible = true;
      if (pushedMatrices == 0) {
        pushMatrix();
        ++pushedMatrices;
        zoomFactorSave = ds.zoomFactor;
        zoomTranslateXSave = ds.zoomTranslateX;
        zoomTranslateYSave = ds.zoomTranslateY;
        ds.zoomFactor = 1.0f; // reset!
        ds.zoomTranslateX = 0.0f;
        ds.zoomTranslateY = 0.0f;
      }
    }

    /// global coordinate transformation
    translate(ds.zoomTranslateX, ds.zoomTranslateY);
    // apply zoom?
    if (ds.zoomFactor != 1.0f && !menuVisible) {
      scale(ds.zoomFactor);
    }

    /*
    // MovieMode: Automatically step forward while there are still folding steps
     if (movieMode && (frameCount % movieInterval == 0)) {
     if (currentState.foldStep >= currentState.sim.foldSteps.size()-1) {
     movieMode = false;
     } else {
     currentState.stepForward();
     }
     }
     */

    // Draw box lines when click and dragging
    if (multiSelecting && !capturingScreen) {
      float x = AbsMouseX(mouseX);
      float y = AbsMouseY(mouseY);
      stroke(0xff000000);
      //strokeWeight(1);
      drawer.StrokeWeight(1 / ds.zoomFactor);
      drawer.Line(mouseXMark, mouseYMark, mouseXMark, y);
      drawer.Line(mouseXMark, y, x, y);
      drawer.Line(x, y, x, mouseYMark);
      drawer.Line(x, mouseYMark, mouseXMark, mouseYMark);
    }

    SphereList spheres = currentState.getSpheres();

    if (simulationMode) { // Movement only when in simulation mode
      PVector centerOfMass = spheres.computeCenterOfMass();
      cmx = centerOfMass.x;
      cmy = centerOfMass.y;

      // Compute forces and update coordinates
      spheres.update();

      if (rigidHelices || rigidHairpins) {
        currentState.spheres.fixHelices(); // Make helices rigid (and/or hairpins)
      }
      if (rigidLoops) {
        currentState.spheres.fixLoops();
      }
    }

    displayNonCanonicals();

    if (addingBond && bondSphereId >= 0 && !capturingScreen) {
      Sphere2D origS = spheres.get(bondSphereId);
      stroke(ds.bpCol);
      strokeWeight(3);
      line((float)origS.x, (float)origS.y, AbsMouseX(mouseX), AbsMouseY(mouseY));
    }

    spheres.display();
    if (ds.labelMode) { 
      spheres.displayLabels();
    }

    if (ds.displayStrandInfo) {
      spheres.displayInfo();
    }

    // Capture screenshot
    if (screenshot) {
      screenshot = false;
      switch (screenshotFormat) {
      case DrawClass.FORMAT_PNG:
        if (!BROWSER) {
          // selectFolder("Select Output Directory", "saveScreen", userFile.getParentFile()); // saveScreen is callback function
          saveScreen(new File(outputDir));
        } else {
          if (javascript != null) {
            boolean wasMenuVisible = menuVisible;
            menuVisible = false;
            javascript.screenshotBrowser();
            menuVisible = wasMenuVisible; // move to javascript.screenshotBrowser()
            capturingScreen = false;
            //textDisplay = "PNG SCREENSHOT CAPTURED";
            //textDisplayCount = 0;
          }
        }
        break;
      case DrawClass.FORMAT_SVG:
        saveScreenSVGFile(new File(outputDir));
        // selectFolder("Select a folder for image creation", "saveScreenSVGFile"); // use callback function saveScreenSVGFile
        capturingScreen = false;
        break;
      default:
        println("Internal error: Unknown image format");
      }
    }
    /*
    if (pngOutFileName != null) {
     if (!BROWSER) {
     if (menuVisible) {
     menuVisible = false; // draw at next call of draw() function, then the menu will n ot be drawn
     } else {
     capturingScreen = true;
     saveFrame(pngOutFileName);
     capturingScreen = false;
     pngOutFileName = null;
     textDisplayCount = 0;
     textDisplay = "PNG SCREENSHOT: " + pngOutFileName;
     }
     }
     }
     */
    if (!capturingScreen) {
      // Draw menu
      if (menuVisible) {
        fill(0xffFFFFFF, ds.colorMax / 1.5f); // Translucent white backgrounds
        noStroke();
        rectMode(CORNER);
        // sliders
        rect( ds.sliderLabelX - ds.sliderLabelWidth - 5, 5, sliderMenuWidth, sliderMenuHeight);
        // checkboxes
        rect( ds.sliderLabelX - 5, sliderMenuHeight + 10, checkBoxWidth, 90);

        // Background to help text
        rect( ds.helpX - 15, helpY, ds.helpX + 275, helpY + (helpLines.length*14.2f));

        // Print help instructions
        String currentCommand = "";
        for (int i = 0; i < helpLines.length; ++i) {
          fill(0xff000000);
          drawer.TextSize(ds.helpTextSize);
          textAlign(LEFT);
          currentCommand = helpLines[i];
          text(currentCommand, ds.helpX, helpY + 12 + i * 1.4f * ds.helpTextSize); // 1.4 spaced
          if (helpCommands.containsKey(currentCommand)) {
            textAlign(RIGHT);
            text(helpCommands.get(currentCommand), ds.helpX + 270, helpY + 12 + i * 1.4f * ds.helpTextSize); // 1.4 spaced
          }
          
        }
      } else {
        // print "Menu" - but only if no zoom or translation is activated (workaround)
        /*  // print "Menu" - now instead using globalMessages
         if ((abs(ds.zoomFactor - 1.0) < 0.01) && (abs(ds.zoomTranslateX) < 0.01) && (abs(ds.zoomTranslateY) < 0.01)) {
         fill(#9B9B9B);
         drawer.TextSize(26);
         textAlign(CENTER);
         text("Menu", width/2.0, 25);
         }
         */
      }


      if (addingBond) {
        fill(ds.GUIColor);
        textAlign(CENTER);

        if (BROWSER) {
          drawer.TextSize(30);
        } else {
          int pulseNum = 40;
          int diff = (pulseNum) - (frameCount % (pulseNum*2));
          if (diff < 0) { 
            diff *= -1;
          }
          drawer.TextSize(20 + diff*.3f);
        }
        text("Click bonding base", width/2.0f, height - 100);
      } else if (keyPressed && " ".equals(str(key))) {
        fill(ds.GUIColor);
        textAlign(CENTER);

        if (BROWSER) {
          drawer.TextSize(30);
        } else {
          int pulseNum = 40;
          int diff = (pulseNum) - (frameCount % (pulseNum*2));
          if (diff < 0) { 
            diff *= -1;
          }
          drawer.TextSize(20 + diff*.3f);
        }
        text("Click a base to add a bond", width/2.0f, height - 100);
      } else if (deletingBond) {
        fill(0xffE82121);
        textAlign(CENTER);

        if (BROWSER) {
          drawer.TextSize(30);
        } else {
          int pulseNum = 40;
          int diff = (pulseNum) - (frameCount % (pulseNum*2));
          if (diff < 0) { 
            diff *= -1;
          }
          drawer.TextSize(20 + diff*.3f);
        }
        text("Click a base to delete bonds", width/2.0f, height - 100);
      } else if (viewingBase) {
        //        drawer.TextSize(ds.messageFontSize);
        //        fill(ds.GUIColor);
        //        textAlign(CENTER);
        double[] closestOutput = spheres.findClosest(AbsMouseX(mouseX), AbsMouseY(mouseY));
        double closestD = closestOutput[1];
        fill(ds.textCol);
        if (closestD <= ds.radiusShowMul * ff.radius) {
          int closestId = (int)closestOutput[0];
          int sid = currentState.sim.seqIds[closestId];
          int relId = closestId - currentState.sim.starts[sid];
          String message = "Residue: " + currentState.sim.seqTot.charAt(closestId) + "  Abs. Index: " + (closestId+1) + "  Strand: " + (sid+1) + "  Strand-Relative Index: " + (relId+1);
          globalMessages.add(message);
          // text(message, ds.messageX, ds.messageY); // width/2.0, height - 100);
        }
      }
    }

    if (programLaunch && BROWSER && textDisplayCount >= 60) {
      int pulseNum = 100;
      int diff = (pulseNum/2) - (frameCount % pulseNum);
      if (diff < 0) { 
        diff *= -1;
      }

      fill(ds.GUIColor);
      textAlign(CENTER);
      drawer.TextSize(50 + diff);
      text("CLICK HERE", width / 2.0f, (height / 2.0f) - 125);
      textAlign(LEFT);
      fill(0xff000000);
    }

    /*
    // Framerate
     if (!capturingScreen) {
     fill(#000000);
     drawer.TextSize(11);
     textAlign(LEFT);
     text((int)frameRate, width - 20, 10);
     }
     */
  }
  popMatrix(); // go back to untransformed coordinates

  /// OUTPUT OF ITEMS IN ABSOLUTE COORDINATES FOR REST OF DRAW METHOD; reset scale and translation

  // Display text from buttons/ commands
  if (textDisplayCount < 60) {
    fill(ds.GUIColor);
    textAlign(CENTER);
    drawer.TextSize(((60 - textDisplayCount) * .5f) + 60);
    text(textDisplay, width / 2.0f, height/2.0f);
    ++textDisplayCount;
    textAlign(LEFT);
    fill(0xff000000);
  }


  // display messages to user in top left corner
  float amx = mouseX;
  float amy = mouseY;
  if ((!menuVisible) && (!capturingScreen) && (globalMessages.size() > 0)
    && (amx > 1) && (amy > 1) && (amx < width) && (amy < height) ) {
    // translate(-ds.zoomTranslateX, -ds.zoomTranslateY);
    // scale(1.0/ds.zoomFactor); // reset transformation
    drawer.TextSize(15.0f);
    textAlign(LEFT);
    String msg = "";
    for (int i = 0; i < globalMessages.size(); ++i) {
      if (i > 0) {
        msg = msg + " | ";
      }
      msg = msg + globalMessages.get(i);
      //      int x = 30;
      //      int y = 30 + i*30;
    }
    fill(0xff000000);
    textSize(20);
    text(msg, 30.0f, 38.0f);
  }
  // DEBUG output of SVG containing HTML file
  //  String html = drawer.toHTML((int)screenResX,(int)screenResY); // TODO for debugging
  //  PrintWriter debugHTML = createWriter("debug_svg_tmp.html");
  //  debugHTML.println(html);
  //  debugHTML.close();
} // end of draw method

/** Screenshot in PNG format */
public void saveScreen(File outputDirFile) {
  if (!BROWSER) {
    if (outputDirFile == null) {
      outputDirFile = new File(".");
    }
    String basename = "ribosketch_out";
    if (userFile != null) {
      String str = userFile.getName();
      if ((str.length() > 3) && str.contains(".")) {
        basename = str.substring(0, str.lastIndexOf('.'));
      }
    }

    //if (outputDir.isEmpty()) {outputDir = folderSelect (get user input)} Make output directory a preference?
    //    String userFileName = userFile.getName();
    String imageFile = outputDirFile.getAbsolutePath() + File.separator + basename + "_" + (frameCount % 10000) + ".png";
    println("Writing to image file " + imageFile);
    boolean wasMenuVisible = menuVisible;
    menuVisible = false;
    //    pngOutFileName = imageFile; // key command !

    float scaleFactor = 4; // make adjustable preference (or automatically adjusts)
    println("Creating PNG graphics device with" + (width * (int)scaleFactor) + " x " + height * (int)scaleFactor);
    PGraphics hiRes = createGraphics(width * (int)scaleFactor, height * (int)scaleFactor);
    if (hiRes == null) {
      println("Warning: cannot create PNG graphics device");
      return;
    }
    beginRecord(hiRes);
    hiRes.scale(scaleFactor);
    mydraw();
    endRecord();

    capturingScreen = false;
    hiRes.save(imageFile);
    menuVisible = false; // wasMenuVisible;
    textDisplayCount = 0;
    textDisplay = "PNG SCREENSHOT: " + imageFile;
  }
}

// output of SVG file
public String saveScreenSVGFile(File folder) {
  if (folder == null) { 
    return null;
  }
  if (!folder.isDirectory()) { 
    return null;
  }
  if (BROWSER) { 
    return null;
  }
  //if (outputDir.isEmpty()) {outputDir = folderSelect (get user input)} Make output directory a preference?
  String userFileName = userFile.getName();
  String fname = userFileName.substring(0, userFileName.length() - 3) + "_" + (frameCount % 10000) + ".svg";
  String imageFile = folder.getAbsolutePath() + File.separator + fname;
  println("Writing to image file " + imageFile);
  String[] svgStrings = drawer.toSVG();    
  saveStrings(imageFile, svgStrings);
  textDisplayCount = 0;
  textDisplay = "SVG SCREENSHOT: " + imageFile;
  return imageFile;
} 

public void saveStateForUndo() {
  if (debugPrints) {
    println("\nSaving state for undo stack! undoId Before: " + undoId);
  }
  int undoSize = undoStates.size()-1;
  for (int i = undoSize; i > undoId; i--) {
    undoStates.remove(i);
  }

  SimulationState undoSaveState = new SimulationState(currentState);
  undoStates.add(undoSaveState);
  undoId = undoStates.size()-1;
  if (debugPrints) {
    println("Id after: " + undoId + ", undoStates size: " + undoStates.size());
  }
}

public void undoState() {
  if (debugPrints) {
    println("Undoing");
  }

  if ((undoStates.size() >= 2) && (undoId >= 1)) {
    undoId--;
    currentState = new SimulationState(undoStates.get(undoId));
    textDisplay = "UNDO";
    textDisplayCount = 0;
  } else {
    if (debugPrints) { 
      println("No undo state available!");
    }
  }
}

public void redoState() {
  if (debugPrints) { 
    println("Redoing");
  }
  if ((undoStates.size() > 0) && ((undoStates.size()-1) > undoId)) {
    ++undoId;
    currentState = new SimulationState(undoStates.get(undoId));
    textDisplay = "REDO";
    textDisplayCount = 0;
  } else {
    if (debugPrints) { 
      println("No redo state available!");
    }
  }
}


public void displayNonCanonicals() {
  for (Map.Entry<String, String> nc : currentState.sim.nonCanonicals.entrySet()) {
    String[] pair = nc.getKey().split(" ");
    Sphere2D s1 = currentState.spheres.get(PApplet.parseInt(pair[0]));
    Sphere2D s2 = currentState.spheres.get(PApplet.parseInt(pair[1]));

    drawer.Stroke(ds.bpCol);
    drawer.StrokeWeight(ds.bpWeight / ds.zoomFactor);
    drawer.Line((float)s1.x, (float)s1.y, (float)s2.x, (float)s2.y);

    String pairType = nc.getValue();

    int ncFill = "c".equals(pairType.substring(0, 1).toLowerCase()) ? ds.bpCol : 0xffFFFFFF; // Shaded if cis, unshaded if trans
    drawer.Fill(ncFill);

    float d = (float) s1.distance(s2);
    float dx = (float)(s2.x - s1.x);
    float dy = (float)(s2.y - s1.y);
    float dx0 = dx/d;
    float dy0 = dy/d;

    float midX = (float)(s1.x + s2.x) / 2.0f;
    float midY = (float)(s1.y + s2.y) / 2.0f;
    float symbolSide = d * .16f;
    if (symbolSide > (ff.radius * 2)) {
      symbolSide = (float) (ff.radius * 2);
    }
    ArrayList<Float> xv = new ArrayList<Float>();
    ArrayList<Float> yv = new ArrayList<Float>();
    for (int i = 0; i < 4; ++i) {
      xv.add(0.0f);
      yv.add(0.0f);
    }
    float xOffset = (dy0 * (symbolSide/2.0f));
    float yOffset = (-1 * dx0 * (symbolSide/2.0f));

    String edge1 = pairType.substring(1, 2).toLowerCase();
    String edge2 = pairType.substring(2, 3).toLowerCase();

    if (edge1.equals(edge2)) {
      if ( "w".equals(edge1) ) {
        drawer.Ellipse(midX, midY, symbolSide, symbolSide);
      } else {
        float p1x = midX - ((symbolSide/2.0f) * dx0) + xOffset;
        float p1y = midY - ((symbolSide/2.0f) * dy0) + yOffset;

        float p2x = midX - ((symbolSide/2.0f) * dx0) - xOffset;
        float p2y = midY - ((symbolSide/2.0f) * dy0) - yOffset;

        float p3x, p3y;

        if ( "s".equals(edge1) ) {
          p3x = p1x + (cos( atan2((-2*yOffset), (-2*xOffset)) - PI/3.0f ) * symbolSide);
          p3y = p1y + (sin( atan2((-2*yOffset), (-2*xOffset)) - PI/3.0f ) * symbolSide);
          drawer.Triangle(p1x, p1y, p2x, p2y, p3x, p3y);
        } else if ( "H".equals(edge1) || "h".equals(edge1) ) {
          p3x = p2x + symbolSide * dx0;
          p3y = p2y + symbolSide * dy0;
          float p4x = p1x + symbolSide * dx0;
          float p4y = p1y + symbolSide * dy0;

          xv.set(0, p1x);
          xv.set(1, p2x);
          xv.set(2, p3x);
          xv.set(3, p4x);
          yv.set(0, p1y);
          yv.set(1, p2y);
          yv.set(2, p3y);
          yv.set(3, p4y);
          drawer.Polygon(xv, yv);
          /*
          beginShape();
           vertex(p1x, p1y);
           vertex(p2x, p2y);
           vertex(p3x, p3y);
           vertex(p4x, p4y);
           endShape(CLOSE); */
        } else {
          continue;
        }
      }
    } else {
      float gap = symbolSide * .2f;

      // Edge 1
      if ( "w".equals(edge1) ) {
        float s1midX = midX - ((symbolSide/2.0f + gap) * dx0);
        float s1midY = midY - ((symbolSide/2.0f + gap) * dy0);
        drawer.Ellipse(s1midX, s1midY, symbolSide, symbolSide);
      } else {
        float midInnerSideX = midX - (gap * dx0);
        float midInnerSideY = midY - (gap * dy0);

        float p1x = midInnerSideX + xOffset;
        float p1y = midInnerSideY + yOffset;

        float p2x = midInnerSideX - xOffset;
        float p2y = midInnerSideY - yOffset;
        float p3x, p3y;

        if ( "s".equals(edge1) ) {
          p3x = p2x - (cos( atan2((-2*yOffset), (-2*xOffset)) - PI/3.0f ) * symbolSide);
          p3y = p2y - (sin( atan2((-2*yOffset), (-2*xOffset)) - PI/3.0f ) * symbolSide);
          drawer.Triangle(p1x, p1y, p2x, p2y, p3x, p3y);
        } else if ( "h".equals(edge1) ) {
          p3x = p2x - symbolSide * dx0;
          p3y = p2y - symbolSide * dy0;
          float p4x = p1x - symbolSide * dx0;
          float p4y = p1y - symbolSide * dy0;

          xv.set(0, p1x);
          xv.set(1, p2x);
          xv.set(2, p3x);
          xv.set(3, p4x);
          yv.set(0, p1y);
          yv.set(1, p2y);
          yv.set(2, p3y);
          yv.set(3, p4y);
          drawer.Polygon(xv, yv);

          /*  beginShape();
           vertex(p1x, p1y);
           vertex(p2x, p2y);
           vertex(p3x, p3y);
           vertex(p4x, p4y);
           endShape(CLOSE); */
        } else {
          continue;
        }
      }

      // Edge 2
      if ( "w".equals(edge2) ) {
        float s2midX = midX + ((symbolSide/2.0f + gap) * dx0);
        float s2midY = midY + ((symbolSide/2.0f + gap) * dy0);
        drawer.Ellipse(s2midX, s2midY, symbolSide, symbolSide);
      } else {
        float midInnerSideX = midX + (gap * dx0);
        float midInnerSideY = midY + (gap * dy0);

        float p1x = midInnerSideX + xOffset;
        float p1y = midInnerSideY + yOffset;
        float p2x = midInnerSideX - xOffset;
        float p2y = midInnerSideY - yOffset;
        float p3x, p3y;

        if ( "s".equals(edge2) ) {
          p3x = p1x + (cos( atan2((-2*yOffset), (-2*xOffset)) - PI/3.0f ) * symbolSide);
          p3y = p1y + (sin( atan2((-2*yOffset), (-2*xOffset)) - PI/3.0f ) * symbolSide);
          drawer.Triangle(p1x, p1y, p2x, p2y, p3x, p3y);
        } else if ( "h".equals(edge2) ) {
          p3x = p2x + symbolSide * dx0;
          p3y = p2y + symbolSide * dy0;
          float p4x = p1x + symbolSide * dx0;
          float p4y = p1y + symbolSide * dy0;

          xv.set(0, p1x);
          xv.set(1, p2x);
          xv.set(2, p3x);
          xv.set(3, p4x);
          yv.set(0, p1y);
          yv.set(1, p2y);
          yv.set(2, p3y);
          yv.set(3, p4y);
          drawer.Polygon(xv, yv);

          /* beginShape();
           vertex(p1x, p1y);
           vertex(p2x, p2y);
           vertex(p3x, p3y);
           vertex(p4x, p4y);
           endShape(CLOSE); */
        } else {
          continue;
        }
      }
    }
  }
}
//<>//
// Reset simulation state
public void resetCircle() {
  //println("\nStarting resetCircle...");
  currentState.sim.initTables();
  SphereList spheres = currentState.getSpheres();
  currentState = currentState.sim.createCircleState(spheres);
  updateGUI();
  saveStateForUndo();
  //println("Finished resetCircle!");
}
//<>//
// Reset simulation state
public void resetRadial() {
  //println("\nStarting resetRadial...");
  currentState.sim.initTables();
  SphereList spheres = currentState.getSpheres();
  currentState = currentState.sim.createRadialState(spheres);
  updateGUI();
  saveStateForUndo();
  //println("Finished resetRadial!");
}

// Distance
public double normFunction(double dx, double dy, double dz) {
  return Math.sqrt((dx*dx) + (dy*dy) + (dz*dz));
}
//<>//
// Returns 2-index array of vector that is perpendicular to input vector. Length is normalized.
public double[] rotatedVector(double vx, double vy, double angle) {
  double len = normFunction(vx, vy, 0.0f);
  double[] result = new double[2];
  if (len == 0.0f) {
    vx = 1.0f;
    vy = 0.0f; // workaround: set to x-axis.
  }
  double ang = Math.atan2(vy, vx);
  ang += angle;
  double x = vx * Math.cos(ang) - vy * Math.sin(ang);
  double y = vx * Math.sin(ang) + vy * Math.cos(ang);
  result[0] = x;
  result[1] = y; //<>// //<>//
  return result; //<>//
}

public void setNewPair(Sphere2D s1, Sphere2D s2, String type, SimulationSetup sim) {
  if (s1.id > s2.id) {
    setNewPair(s2, s1, type, sim); // other way around
    return;
  }
  int id1 = s1.id;
  int id2 = s2.id;
  boolean force = false;
 
  // If neither base is paired, set force bond
  if (sim.pairTable[id1] == -1 && sim.pairTable[id2] == -1) {
    setForcePair(s1, s2, type, sim);
    sim.pairTable[id1] = (short)id2;
    sim.pairTable[id2] = (short)id1;
    force = true;
  }

  if ( !"cWW".equals(type) || (!force && !(sim.pairTable[id1] == id2 && sim.pairTable[id2] == id1)) ) {
    sim.nonCanonicals.put(id1 + " " + id2, type);
   
    if (!force) {
      s1.bonds[id2] = type; //<>//
      s2.bonds[id1] = type; //<>// //<>// //<>// //<>//
    } //<>// //<>// //<>// //<>//
  } //<>// //<>// //<>//
} //<>//

public void setForcePair(Sphere2D s1, Sphere2D s2, String type, SimulationSetup sim) {
  s1.pairedWForce = s2;
  s2.pairedWForce = s1;
  int id1 = s1.id;
  int id2 = s2.id;
  int sq1 = sim.seqIds[id1];
  int sq2 = sim.seqIds[id2];
  if ((s1.fiveP != null) && (s2.threeP != null) && s1.fiveP.pairedWForce == s2.threeP && (sim.seqIds[id1-1] == sq1) && (sim.seqIds[id2+1] == sq2)) {
    s1.hasHelixUp = true;
    s2.hasHelixDown = true;
    s1.fiveP.hasHelixDown = true;
    s2.threeP.hasHelixUp = true;
  } else {
    s1.hasHelixUp = false;
    s2.hasHelixDown = false;
  }
  if ((s1.threeP != null) && (s2.fiveP != null) && s1.threeP.pairedWForce == s2.fiveP && (sim.seqIds[id1+1] == sq1) && (sim.seqIds[id2-1] == sq2)) {
    s1.hasHelixDown = true;
    s2.hasHelixUp = true;
    s1.threeP.hasHelixUp = true;
    s2.fiveP.hasHelixDown = true;
  } else { //<>// //<>//
    s1.hasHelixDown = false; //<>// //<>// //<>// //<>//
    s2.hasHelixUp = false; //<>// //<>// //<>// //<>//
  } //<>//

  // If the residues in this pair are also adjacent on the backbone, s1 does not have helix down and s2 does not have helix up.
  if (s1.threeP != null && (s1.threeP.id == s2.id && s2.strandId == s1.strandId)) { //<>// //<>//
    s1.hasHelixDown = false; //<>//
    s2.hasHelixUp = false;
  }
  s1.bonds[id2] = type;
  s2.bonds[id1] = type;
}
//<>//
// Unpair residues //<>// //<>//
public void unsetForcePair(Sphere2D s1, Sphere2D s2) { //<>//
  if (s1 != null) {
    if (s1.hasHelixUp) {
      s1.fiveP.hasHelixDown = false;
    }
    if (s1.hasHelixDown) {
      s1.threeP.hasHelixUp = false;
    }

    s1.pairedWForce = null;
    s1.hasHelixUp = false;
    s1.hasHelixDown = false;
  } //<>// //<>//
  //<>//
  if (s2 != null) {
    if (s2.hasHelixDown) {
      s2.threeP.hasHelixUp = false;
    } //<>// //<>//
    if (s2.hasHelixUp) { //<>//
      s2.fiveP.hasHelixDown = false;
    }

    s2.pairedWForce = null;
    s2.hasHelixUp = false;
    s2.hasHelixDown = false;
  } //<>// //<>//
} //<>//

public double stringToDouble(String s) {
  if (!s.matches("[-+]?\\d*\\.?\\d*")) {
    if (debugPrints) { 
      println("NaN given to stringToDouble! Returning dummy value of 0!");
    }
    return 0;
  }
  double d = 0;

  String[] splitNum = split(s, ".");

  String beforeDec = splitNum[0];
  double mul = 1;
  for (int i = beforeDec.length(); i > 0; i--) {
    double num = (double) PApplet.parseInt(beforeDec.substring(i-1, i));
    d += num * mul;
    mul *= 10;
  }

  if (splitNum.length > 1) {
    String afterDec = splitNum[1];
    mul = .1f;
    for (int i = 0; i < afterDec.length(); ++i) {
      double num = (double) PApplet.parseInt(afterDec.substring(i, i+1));
      d += num * mul;
      mul /= 10;
    }
  }

  if ( beforeDec.length() > 0 && "-".equals(beforeDec.substring(0, 1)) ) {
    d *= -1;
  }

  return d;
}

// Necessary for Processing.js
public boolean stringToBoolean(String s) {
  if ("true".equals(s) || "True".equals(s)) {
    return true;
  } else {
    return false;
  }
}

// Necessary for Processing.js
public String[] stringArrayClone(String[] orig) {
  String[] result = new String[orig.length];
  for (int i = 0; i < orig.length; ++i) {
    result[i] = orig[i];
  }
  return result;
}

public int[] intArrayClone(int[] orig) {
  int[] result = new int[orig.length];
  for (int i = 0; i < orig.length; ++i) {
    result[i] = orig[i];
  }
  return result;
}

public Short[] shortArrayClone(Short[] orig) {
  Short[] result = new Short[orig.length];
  for (int i = 0; i < orig.length; ++i) {
    result[i] = orig[i];
  }
  return result;
}


// Necessary for Processing.js
public float angleBetweenFunct(PVector v1, PVector v2) {
  // We get NaN if we pass in a zero vector which can cause problems
  // Zero seems like a reasonable angle between a (0,0,0) vector and something else
  if (v1.x == 0 && v1.y == 0 && v1.z == 0 ) return 0.0f;
  if (v2.x == 0 && v2.y == 0 && v2.z == 0 ) return 0.0f;

  double dot = v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
  double v1mag = Math.sqrt(v1.x * v1.x + v1.y * v1.y + v1.z * v1.z);
  double v2mag = Math.sqrt(v2.x * v2.x + v2.y * v2.y + v2.z * v2.z);
  // This should be a number between -1 and 1, since it's "normalized"
  double amt = dot / (v1mag * v2mag);
  // But if it's not due to rounding error, then we need to fix it
  // http://code.google.com/p/processing/issues/detail?id=340
  // Otherwise if outside the range, acos() will return NaN
  // http://www.cppreference.com/wiki/c/math/acos
  if (amt <= -1) {
    return PConstants.PI;
  } else if (amt >= 1) {
    // http://code.google.com/p/processing/issues/detail?id=435
    return 0;
  }
  return (float) Math.acos(amt);
}

class DrawClass {

  public static final int FORMAT_NONE = 0;
  public static final int FORMAT_SVG = 1;
  public static final int FORMAT_PNG = 2;

  ArrayList<String> sb = new ArrayList<String>();
  ArrayList<String> sb_last = new ArrayList<String>();
  String fontFamily = "Ariel, sans-serif";
  int format = FORMAT_SVG;
  int fillCol = 0xff000000;
  float fontSize = 14;
  boolean noStrokeMode = false;
  int strokeCol = 0xff000000;
  float strokeWidth = 1.0f;
  String textAnchor = "start"; // "middle";
  float viewBoxXMin = 0.0f;
  float viewBoxXMax = width;
  float viewBoxYMin = 0.0f;
  float viewBoxYMax = height;
  float viewBoxPad = 50.0f;

  public void Ellipse(float cx, float cy, float rx, float ry) {
   if ((cx -rx) < viewBoxXMin) {
     viewBoxXMin = cx-rx - viewBoxPad;
   } else if ((cx+rx) > viewBoxXMax) {
     viewBoxXMax = cx + rx + viewBoxPad;
   }
   if ((cy -ry) < viewBoxYMin) {
     viewBoxYMin = cy-ry - viewBoxPad;
   } else if ((cy+ry) > viewBoxYMax) {
     viewBoxYMax = cy + ry + viewBoxPad;
   }

   switch(format) {
    case FORMAT_SVG:
     if (!noStrokeMode) {
      sb.add("<ellipse cx=\"" + cx + "\" cy=\"" + cy + "\" rx=\"" + rx/2 + "\" ry=\"" + ry/2 + "\" stroke=\"" + Hex(strokeCol) + "\" fill=\"" + Hex(fillCol) + "\" stroke-width=\"" + strokeWidth + "\"/>\n");
     } else { // simulate noStroke mode
      sb.add("<ellipse cx=\"" + cx + "\" cy=\"" + cy + "\" rx=\"" + rx/2 + "\" ry=\"" + ry/2 + "\" fill=\"" + Hex(fillCol) + "\"/>\n");
     }
     break;
   }
   ellipse(cx,cy,rx,ry);
  }

  public void Polygon(ArrayList<Float> xv, ArrayList<Float> yv) {
   int n = xv.size();
   if (yv.size() < n) {
     n = yv.size(); // strange, should have same length - issue warning? TODO
   }
   String s = "";
   switch(format) {
    case FORMAT_SVG:
     s = "<polygon points=\"";
     for (int i = 0; i < n; ++i) {
       float x = xv.get(i);
       float y = yv.get(i);
       s = s + x + "," + y + " ";
     }
     s = s + "\"";
     if (!noStrokeMode) {
       s = s + " stroke=\"" + Hex(strokeCol) + "\" stroke-width=\"" + strokeWidth + "\"";
     }
     s = s + " fill=\"" + Hex(fillCol) + "\"/>\n";
     sb.add(s); // + x1 + "," + y1 + " " + x2 + "," + y2 + " " + x3 + "," + y3 + "\" stroke=\"" + Hex(strokeCol) + "\" fill=\"" + Hex(fillCol) + "\" />\n";
     break;
   }
   beginShape();
   for (int i = 0; i < n; ++i) {
     vertex(xv.get(i), yv.get(i));
   }
   if (n > 1) {
    vertex(xv.get(0), yv.get(0));
   }
   endShape();
  }

  public void Triangle(float x1, float y1, float x2, float y2, float x3, float y3) {
   switch(format) {
    case FORMAT_SVG:
     if (!noStrokeMode) {
      sb.add("<polygon points=\"" + x1 + "," + y1 + " " + x2 + "," + y2 + " " + x3 + "," + y3 + "\" stroke=\"" + Hex(strokeCol) + "\" fill=\"" + Hex(fillCol) + "\" stroke-width=\"" + strokeWidth + "\"/>\n");
     } else { // simulate noStroke mode
      sb.add("<polygon points=\"" + x1 + "," + y1 + " " + x2 + "," + y2 + " " + x3 + "," + y3 + "\" stroke=\"" + Hex(strokeCol) + "\" fill=\"" + Hex(fillCol) + "\"/>\n");
     }
     break;
   }
   triangle(x1,y1,x2,y2,x3,y3);
  }

  /** Copy fill color. "color" is actually a synonym for the primitive type int, so this is a "deep" copy */
  public void Fill(int col) {
      fillCol = col;
      fill(col);
  }

  public String Hex(int c) {
    String s = hex(c);
    s = "#" + s.substring(2); // skip first two characters that are opacity
    return s;
  }
  
  public void Line(float x1, float y1, float x2, float y2) {
   if (format == FORMAT_SVG) {
    sb.add("<line x1=\"" + x1 + "\" y1=\""+ y1 + "\" x2=\"" + x2 + "\" y2=\"" + y2 + "\" stroke=\"black\" stroke-width=\"" + strokeWidth + "\" />\n");
   }
   line(x1,y1,x2,y2);
  }

  public void NoStroke() { 
    noStrokeMode = true;
    noStroke();
  }

  public void setFormat(int formatId) {
    format = formatId;   
  }

  public void Stroke(int c) {
    noStrokeMode = false;
    strokeCol = c;
    stroke(c);
  }

  public void StrokeWeight(float x) {
      strokeWidth = x;
      strokeWeight(x);
  }

  public void Text(String txt, float x, float y) {
   if (format == FORMAT_SVG) {
    sb.add("<text x=\"" + x + "\" y=\"" + y + "\" text-anchor=\"" + textAnchor + "\" fill=\"" + Hex(fillCol) + "\" font-size=\"" + fontSize + "px\" font-family=\"" + fontFamily + "\" >" + txt + "</text>\n");
   }
   text(txt, x, y);
  }

  public void TextSize(float size) {
    fontSize = size;
    textSize(size);
  }

  public void clear() {
     sb_last = new ArrayList<String>(sb); // shallow copy
     sb.clear();
     viewBoxXMin = 0;
     viewBoxXMax = width;
     viewBoxYMin = 0;
     viewBoxYMax = height;
  }

  public String toString() { return sb.toString(); }  

  /** Output as SVG formatted Strings. Uses Processing variables width and height that correspond to screen dimensions */
  public synchronized String[] toSVG() { // int width, int height) { 
    ArrayList<String> sbc = new ArrayList<String>(); // sb.size()); // (sb.clone());
    for (int i = 0; i < sb_last.size(); ++i) {
      if (i < sb_last.size()) {
       sbc.add(sb_last.get(i));
      }
    }
    int n = sbc.size(); 
    String[] result = new String[n +2];
   // viewBox="0 0 " + screenResX + " " + screenResY + "\" xmlns=\"http://www.w3.org/2000/svg\"
    String s = "<svg width=\"" + width + "\" height=\"" + height + "\" viewBox=\"" + viewBoxXMin + " " + viewBoxYMin + " " + viewBoxXMax + " " + viewBoxYMax + "\" xmlns=\"http://www.w3.org/2000/svg\">\n";
    result[0] = s;
//    println("DEBUG SVG Sizes: " + n + " " + sbc.size() + " " + result.length);
    for (int i = 0; i < n; ++i) {
//      println("working on svg line" + i);
      if (i >= sbc.size()) {
       println("Warning!!! DrawClass.pde L168");
      }
      if ((i+1) >= result.length) {
       println("Warning!!! DrawClass.pde L171");
      }
      result[i+1] = sbc.get(i);
    }
//    println("setting last line:" + (result.length-1));
    result[result.length-1] = "</svg>";
    return result;
  }  

/*  String toHTML(int width, int height) {
    String s = "<html>\n"
              +"<head>\n"
	      +"</head>\n"
	      +"<body>\n"
	      + toSVG(width, height)
	      +"</body\n"
	      +"</html>"; 
    return s;	      
  }
*/

}
class Base {
	private int mate;
	private double x, y;
	private boolean extracted;
	private Region region = new Region();

	public int getMate() {
		return mate;
	}

	public void setMate(int mate) {
		this.mate = mate;
	}

	public double getX() {
		return x;
	}

	public void setX(double x) {
		//System.out.println("  Base.setX()");
		this.x = x;
	}

	public double getY() {
		return y;
	}

	public void setY(double y) {
		this.y = y;
	}

	public boolean isExtracted() {
		return extracted;
	}

	public void setExtracted(boolean extracted) {
		this.extracted = extracted;
	}

	public Region getRegion() {
		return region;
	}

	public void setRegion(Region region) {
		this.region = region;
	}
}
public class CheckBox
{
  boolean checked;
  float x, y, boxWidth, boxHeight;
  String label;
  float padx = 7;
  boolean visible = true;
  float labelWidth;
  
  CheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    label = l;
    labelWidth = labelSize;
    x = xx; y = yy; boxWidth = ww; boxHeight = hh;
    Interactive.add( this );
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    checked = !checked;
  }
   
  public void draw ()
  {
    if (!menuVisible) {
      return;
    }
    noStroke();
    if (x <= mouseX && mouseX <= x+boxWidth &&
      y <= mouseY && mouseY <= y+boxHeight) {
      fill(0xff4B9EA5);
    } else {
      fill(0xff000000);
    }
    rectMode(CORNER);
    rect( x, y, boxWidth, boxHeight, 4 );
    if ( checked ) {
      fill(ds.GUIColor);
      rect( x+2, y+2, boxWidth-4, boxHeight-4, 4 );
    }
    fill(0xff000000);
    textAlign( LEFT );
    textSize(ds.menuTextSize);
    text( label, x+boxWidth+padx, y+boxHeight );
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside ( float mx, float my)
  {
    if (!menuVisible) {
      return false;
    }
    return Interactive.insideRect( x, y, boxWidth+labelWidth, boxHeight, mx, my );
    
  }

  public void setVisible(boolean flag)
  {
    visible = flag;
  }

}

/*
public class MovieCheckBox extends CheckBox
{
  MovieCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize); 
  }
  
  void mouseReleased ( float mx, float my )
  {
     if (!menuVisible) {
       return;
     }  
     saveStateForUndo();
     checked = !checked;
     movieMode = !movieMode;
     attachedIds.clear();
  }

  void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}
*/

public class LabelCheckBox extends CheckBox
{
  LabelCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize); 
    checked = ds.labelMode;
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    checked = !checked;
    ds.labelMode = !ds.labelMode;
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}


public class ResetCircleCheckBox extends CheckBox
{  
  ResetCircleCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize); 
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    saveStateForUndo();
    resetCircle();
    textDisplay = "CIRCLE LAYOUT";
    textDisplayCount = 0;
  }
  
  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}


public class ResetRadialCheckBox extends CheckBox
{  
  ResetRadialCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize); 
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    saveStateForUndo();
    resetRadial();
    textDisplay = "RADIAL LAYOUT";
    textDisplayCount = 0;
  }
  
  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}


public class SnapCheckBox extends CheckBox
{
  // boolean checked;
  // float x, y, width, height;
  // String label;
  // float padx = 7;
  // boolean visible = true;

  SnapCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    screenshot = true;
    capturingScreen = true;
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}

public class SnapFormatCheckBox extends CheckBox
{
  // boolean checked;
  // float x, y, width, height;
  // String label;
  // float padx = 7;
  // boolean visible = true;

  SnapFormatCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    checked = !checked;
    if (checked) {
     screenshotFormat = DrawClass.FORMAT_SVG;
    } else {
     screenshotFormat = DrawClass.FORMAT_PNG;
    }
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}

public class SnapSVGCheckBox extends CheckBox
{
  // boolean checked;
  // float x, y, width, height;
  // String label;
  // float padx = 7;
  // boolean visible = true;
  int resx;
  int resy;

  SnapSVGCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize, int _resx, int _resy ) {
    super(l, xx, yy, ww, hh, labelSize);
    resx = _resx;
    resy = _resy;
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    saveStateForUndo();
    if (!BROWSER) {
  //    simWasOn = simulationMode;
  //    simulationMode = false;
      // let user select folder after which function saveScreenSVGFile is envoked as callback
      selectFolder("Select a folder to save to", "saveScreenSVGFile", userFile.getParentFile());
  //    String imageFile = saveScreenSVGFile(new File(outputDir)); // userFile.getParentFile());
  //    if (imageFile != null) {
  //     textDisplay = "SVG file saved to: " + imageFile;
  //     textDisplayCount = 0;
  //    } else {
  //     textDisplay = "Warning: SVG file not saved!";
  //     textDisplayCount = 0;
  //    }
    }
    else {
      if (javascript != null) {
        saveSVGBrowser(drawer.toSVG()); // hook to javascript
      }
    }

  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}

public class OutlineCheckBox extends CheckBox
{
  // boolean checked;
  // float x, y, width, height;
  // String label;
  // float padx = 7;
  // boolean visible = true;

  OutlineCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
    checked = ds.outlineMode;
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    checked = !checked;
    ds.outlineMode = checked;
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}


public class SimulateAllCheckBox extends CheckBox
{
  // boolean checked;
  // float x, y, width, height;
  // String label;
  // float padx = 7;
  // boolean visible = true;

  SimulateAllCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
    checked = !simulateAllMode;
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    checked = !checked;
    simulateAllMode = !checked;
    
    if (!simulateAllMode) {
      textDisplay = "SIMULATE SELECTED ONLY";
    } else {
      textDisplay = "SIMULATE ALL";
    }
    textDisplayCount = 0;
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}


public class RigidHairpinsCheckBox extends CheckBox
{
  // boolean checked;
  // float x, y, width, height;
  // String label;
  // float padx = 7;
  // boolean visible = true;

  RigidHairpinsCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
    checked = rigidHairpins;
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    checked = !checked;
    rigidHairpins = checked;
    wasRigidHairpins = rigidHairpins;
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}

public class RigidLoopsCheckBox extends CheckBox
{
  // boolean checked;
  // float x, y, width, height;
  // String label;
  // float padx = 7;
  // boolean visible = true;

  RigidLoopsCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
    checked = rigidLoops;
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    checked = !checked;
    rigidLoops = checked;
    wasRigidLoops = rigidLoops;
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}


public class MovementCheckBox extends CheckBox
{
  MovementCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize); 
    checked = simulationMode;
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    saveStateForUndo();
    checked = !checked;
    simulationMode = !simulationMode;
    if (!simulationMode) {
      textDisplay = "FREEZE";
    } else {
      textDisplay = "UNFREEZE";
    }
    textDisplayCount = 0;
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}


public class RigidHelixCheckBox extends CheckBox
{
  // boolean checked;
  // float x, y, width, height;
  // String label;
  // float padx = 7;
  // boolean visible = true;

  RigidHelixCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize); 
    checked = rigidHelices;
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    saveStateForUndo();
    checked = !checked;
    rigidHelices = !rigidHelices;
    if (rigidHelices) {
      rigidHairpins = wasRigidHairpins;
      rigidLoops = wasRigidLoops;
      checkBoxes.get("Rigid Loops").checked = rigidLoops;
      checkBoxes.get("Rigid Hairpins").checked = rigidHairpins;
    } else {
      wasRigidLoops = rigidLoops;
      wasRigidHairpins = rigidHairpins;
      
      rigidLoops = false;
      checkBoxes.get("Rigid Loops").checked = false;
      
      rigidHairpins = false;
      checkBoxes.get("Rigid Hairpins").checked = false;
    }
    
    if (rigidHelices) {
      textDisplay = "HELICES FIXED";
    } else {
      textDisplay = "HELICES UNFIXED";
    }
    textDisplayCount = 0;
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}

/*
public class ColorCheckBox extends CheckBox
{
  ColorCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize); 
  }
  
  void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    textDisplay = "FEATURE COMING SOON";
    textDisplayCount = 0;
    
    saveStateForUndo();
    colorSelecting = true;
    runningSimulation = false;
    menuVisible = false;
    colorMode(RGB, 255);
    int paletteW = (int)((width-40) / 2);
    int paletteH = (int)((height-40) / 2);
    println(paletteW + " " + paletteH);
    palette = new ColorPicker(20, 20, paletteW, paletteH, 255);
    
  }

  void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}
*/

public class ColorLoadCheckBox extends CheckBox
{
  ColorLoadCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    
    if (!BROWSER) {
      selectInput("Select a color file to load", "loadColorFile");
    }
    else {
      loadColorBrowserCall();
    }
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}


public class BasePairLoadCheckBox extends CheckBox
{
  BasePairLoadCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    
    if (!BROWSER) {
      selectInput("Select a basepair file to load", "loadBondFile");
    }
    else {
      loadBondsBrowserCall();
    }
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}


public class SaveCheckBox extends CheckBox
{
  SaveCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
    return;
    }
    saveStateForUndo();
    if (!BROWSER) {
      simWasOn = simulationMode;
      simulationMode = false;
      //movieMode = false;
      
      selectFolder("Select a folder to save to", "saveFolderSelected", userFile.getParentFile());
    }
    else {
      if (javascript != null) {
        saveStateBrowser();
      }
    }
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}


public class LoadCheckBox extends CheckBox
{
  LoadCheckBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
    
    loadCalled();
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}

public class ZoomResetBox extends CheckBox
{
  // boolean checked;
  // float x, y, width, height;
  // String label;
  // float padx = 7;
  // boolean visible = true;

  ZoomResetBox ( String l, float xx, float yy, float ww, float hh, float labelSize )
  {
    super(l, xx, yy, ww, hh, labelSize);
  }
  
  public void mouseReleased ( float mx, float my )
  {
    if (!menuVisible) {
      return;
    }
//    print("Resetting zoom factor!");
    ds.resetZoom(); // reset zoom factor
    zoomFactorSave = 1.0f; // workaround: these stored values are used to recreate the zoom state after the menue is deactivated
    zoomTranslateXSave = 0.0f; // in this special case we want to reset these saved values
    zoomTranslateYSave = 0.0f; 
  }

  public void draw ()
  {
    super.draw();
  }
  
  // this is a special inside test that includes the label text
  public boolean isInside( float mx, float my )
  {
    return super.isInside(mx,my);
  }

  public void setVisible(boolean flag) {
    super.setVisible(flag);
  }
}

public void loadCalled() {
  if (!BROWSER) {
    if (runningSimulation) {
      saveStateForUndo();
      simWasOn = simulationMode;
      simulationMode = false;
      //movieWasOn = movieMode;
      //movieMode = false;
      selectInput("Select a .ct, .bpseq, or .rs file to load", "loadFile");
    }
  }
  else {
    //println("In browser loadCalled");
    if (javascript != null && !selectingFileType) {
      if (errorMessage.equals("")) {
        if (runningSimulation) {
          saveStateForUndo();
          multiSelecting = false;
          dragging = false;
          simWasOn = simulationMode;
          simulationMode = false;
          //movieWasOn = movieMode;
          //movieMode = false;
          returnToSim = true;
          runningSimulation = false;
          
        }
        setupState();
      } else {
        clickCheck = true;
      }
    }
  }
}

public void loadFile(File selection) {
  if (selection == null) {
    println("Window was closed or the user hit cancel.");
    simulationMode = simWasOn;
    //movieMode = movieWasOn;
  } else {
    multiSelecting = false;
    dragging = false;
    runningSimulation = false;
    userFile = selection;
    returnToSim = true;
    setupState(selection.getAbsolutePath());
  }
}

public void loadColorFile(File selection) {
  if (selection == null) {
    println("Window was closed or the user hit cancel.");
  } else {
    loadColor(loadStrings(selection));
  }
}

public void loadColorBrowserCall() {
  if (javascript != null) {
    String rawInput = javascript.getTextInput("inputcolor");
    String[] rawInputLines = rawInput.trim().split("\\r\\n|\\n|\\r");
    loadColor(rawInputLines);
  } else {
    println("javascript object is null in loadColorBrowserCall()");
  }
}

public void loadColor(String[] rawLines) {
  ArrayList<Double> colorData = new ArrayList<Double>();
  
  // Seperate based on whitespace
  boolean flag = false;
  String splitter = BROWSER ? " " : "\\s++";
  for(String line : rawLines) {
    if (flag) { break; }
    String[] line2 = line.trim().split(splitter);
    if ("".equals(line2[0]) || line2[0].trim().substring(0, 1).equals("#")) { continue; } // Ignore comments and empty lines
    
    for (String number : line2) {
      number = number.trim();
      if (number.length() > 0 && number.substring(0, 1).equals("#")) { break; } // ignore comments
      if (number.matches("-?\\d+(\\.\\d+)?")) { // Check if it is numeric
        colorData.add(stringToDouble(number));
      }
      else {
        flag = true;
        break;
      }
    }
  }
  
  if (colorData.size() == 0) {
    textDisplay = BROWSER ? "INPUT COLORING BELOW" : "COLOR FILE IS EMPTY";
    textDisplayCount = 0;
    return;
  }
  
  if (currentState.spheres == null || colorData.size() > currentState.spheres.size()) {
    if (debugPrints) {
      println("Warning: invalid color input. Should be a space or line seperated list of numbers, equal in length to the number of residues.");
      println("Spheres size: " + currentState.spheres.size() + ", color size: " + colorData.size());
    }
    textDisplay = "TOO MANY NUMBERS";
    textDisplayCount = 0;
    return;
  }
  
  double minColor = colorData.get(0);
  double maxColor = minColor;
  for (Double number : colorData) {
    if (number < minColor) {
      minColor = number;
    }
    
    if (number > maxColor) {
      maxColor = number;
    }
  }
  
  double dist0 = 0 - minColor;
  for (int i = 0, l = colorData.size(); i < l; ++i) {
    double denominator = (maxColor + dist0);
    float colorNum;
    if (denominator <= 0) {
      colorNum = 0;
    } else {
      colorNum = (float)((colorData.get(i) + dist0) / denominator);
    }
    
    if (colorNum < .001f) {
      colorNum = 0;
    }
    
    currentState.spheres.get(i).baseColor = lerpColor(ds.startGradient, ds.endGradient, colorNum);
    currentState.spheres.get(i).colorNum = colorNum;
  }
  
  for (int i = colorData.size(); i < currentState.spheres.size(); ++i) {
    currentState.spheres.get(i).baseColor = lerpColor(ds.startGradient, ds.endGradient, 0);
    currentState.spheres.get(i).colorNum = 0;
  }
  
  ds.colorMode = DisplaySettings.COLOR_MODE_CUSTOM;
  sliders[3].setValue( ((float)ds.colorMode - 1)/((float)DisplaySettings.COLOR_MODE_MAX - 1) );
  displayColorMode();
}

public void loadBondFile(File selection) {
  if (selection == null) {
    println("Window was closed or the user hit cancel.");
  } else {
    loadBonds(loadStrings(selection));
  }
}

public void loadBondsBrowserCall() {
  if (javascript != null) {
    String rawInput = javascript.getTextInput("inputbonds");
    String[] rawInputLines = rawInput.trim().split("\\r\\n|\\n|\\r");
    loadBonds(rawInputLines);
  } else {
    println("javascript object is null in loadBondsBrowserCall()");
  }
}

public void loadBonds(String[] rawLines) {
  // Turn off movie mode
  HashMap<ArrayList<Integer>, String> inputPairs = new HashMap<ArrayList<Integer>, String>();
  boolean flag = false;
  textDisplay = "ERROR: ";
  SimulationSetup s = currentState.sim;
  
  String splitter = BROWSER ? " " : "\\s++";
  outer: for(String line : rawLines) {
    String[] pairs = line.trim().split(",");
    if ("".equals(pairs[0]) || pairs[0].trim().substring(0, 1).equals("#")) { continue; } // Ignore comments and empty lines
    
    for (String pair : pairs) {
      String[] entries = pair.trim().split(splitter);
      if (entries.length < 2 || entries.length > 3 || !entries[0].matches("^\\d+$") || !entries[1].matches("^\\d+$")) {
        flag = true;
        break outer;
      }
      
      int id1 = PApplet.parseInt(entries[0]) - 1;
      int id2 = PApplet.parseInt(entries[1]) - 1;
      
      if (id1 < 0 || id1 >= s.lenTot || id2 < 0 || id2 >= s.lenTot) {
        flag = true;
        textDisplay += "ID OUT OF BOUNDS!";
        break outer;
      }
      if (id1 == id2) {
        flag = true;
        textDisplay += "A BASE CANNOT PAIR WITH ITSELF!";
        break outer;
      }
      
      if (id1 > id2) {
        int tempId;
        tempId = id1;
        id1 = id2;
        id2 = tempId;
      }
      
      String type = "cWW";
      if (entries.length == 3 && !"".equals(entries[2])) {
        type = entries[2];
      }
      
      ArrayList<Integer> inputKey = new ArrayList<Integer>(2);
      inputKey.add(id1);
      inputKey.add(id2);
      inputPairs.put(inputKey, type);
    }
  }
  
  if (debugPrints) {
    println("\nNew non-canonicals:");
    for (Map.Entry<ArrayList<Integer>, String> nc : inputPairs.entrySet()) {
      ArrayList<Integer> bases = nc.getKey();
      println(bases.get(0) + " " + bases.get(1) + " " + nc.getValue());
    }
  }
  
  if (!flag) {
    if (inputPairs.size() == 0) {
      textDisplay = BROWSER ? "INPUT BONDS BELOW" : "BOND FILE EMPTY";
    } else {
      for (Map.Entry<ArrayList<Integer>, String> nc : inputPairs.entrySet()) {
        ArrayList<Integer> bases = nc.getKey();
        setNewPair(currentState.spheres.get(bases.get(0)), currentState.spheres.get(bases.get(1)), nc.getValue(), s);
      }
      textDisplay = "ADDED BONDS";
    }
  }
  
  textDisplayCount = 0;
}

public void saveStateBrowser() {
  String[] saveLines = currentState.writeSaveFile();
  javascript.saveFileBrowser(saveLines);
  textDisplay = "SAVED STATE TO BOX BELOW";
  textDisplayCount = 0;
}

public void saveSVGBrowser(String[] saveLines) {
//  String[] saveLines = drawer.toSVG(width, height);
  javascript.saveSVGBrowser(saveLines);
  textDisplay = "SAVED SVG IMAGE";
  textDisplayCount = 0;
}

public void saveFolderSelected(File selection) {
  if (selection == null) { return; }
  if (!selection.isDirectory()) { return; }
  try {
    // Determine save file name
    String inputFileName = userFile.getName();
    String fileBase = inputFileName; // default if this checker fails
    for (int i = inputFileName.length(); i > 0; i--) {
      if ( ".".equals(inputFileName.substring(i-1, i)) ) {
        fileBase = inputFileName.substring(0, i-1);
    
        if (inputFileName.substring(i).equals("rs")) {
          for (int j = fileBase.length() - 5; j > 0; j--) {
            if (inputFileName.substring(j, j+5).equals("_save")) {
              fileBase = inputFileName.substring(0, j);
              break;
            }
          }
        }
        break;
      }
    }
    File saveFile;
    int saveNum = 1;
    while (true) {
      String saveFileName = fileBase + "_save" + saveNum + ".rs";
      saveFile = new File(selection.getAbsolutePath(), saveFileName);
      if (!saveFile.exists()) { break; }
      ++saveNum;
    }
  
    simulationMode = simWasOn;
  
    String[] saveLines = currentState.writeSaveFile();
    PrintWriter output = createWriter(saveFile.getAbsolutePath());
    for (String s : saveLines) {
      output.println(s);
    }
    output.flush();
    output.close();
  
    textDisplay = "STATE SAVED AS " + saveFile.getName();
    textDisplayCount = 0;
  
    surface.setTitle(saveFile.getName());
  
  } catch (Exception e) {
    e.printStackTrace();
    simulationMode = simWasOn;
    textDisplay = "COULD NOT SAVE";
    textDisplayCount = 0;
  }
}
/*
public class ColorPicker 
{
  int x, y, w, h, c;
  int pickX = 0, pickY = 0;
  PImage cpImage;

  public ColorPicker ( int x, int y, int w, int h, int c )
  {
    this.x = x;
    this.y = y;
    this.w = w;
    this.h = h;
    this.c = c;

    cpImage = new PImage( w, h );

    init();
  }

  private void init ()
  {
    // draw color.
    int cw = w - 35;
    for ( int i=0; i<cw; ++i ) 
    {
      float nColorPercent = i / (float)cw;
      float rad = (-360 * nColorPercent) * (PI / 180);
      int nR = (int)(cos(rad) * 127 + 128) << 16;
      int nG = (int)(cos(rad + 2 * PI / 3) * 127 + 128) << 8;
      int nB = (int)(Math.cos(rad + 4 * PI / 3) * 127 + 128);
      int nColor = nR | nG | nB;

      setGradient( i, 0, 1, h/2, 0xFFFFFF, nColor );
      setGradient( i, (h/2), 1, h/2, nColor, 0x000000 );
    }

    // draw black/white.
    drawRect( cw, 0, 30, h/2, 0xFFFFFF );
    drawRect( cw, h/2, 30, h/2, 0 );

    // draw grey scale.
    for ( int j=0; j<h; ++j )
    {
      int g = 255 - (int)(j/(float)(h-1) * 255 );
      drawRect( w-20, j, 20, 1, color( g, g, g ) );
    }
  }

  private void setGradient(int x, int y, float w, float h, int c1, int c2 )
  {
    float deltaR = red(c2) - red(c1);
    float deltaG = green(c2) - green(c1);
    float deltaB = blue(c2) - blue(c1);

    for (int j=y; j<(y+h); ++j)
    {
      int c = color( red(c1)+(j-y)*(deltaR/h), green(c1)+(j-y)*(deltaG/h), blue(c1)+(j-y)*(deltaB/h) );
      cpImage.set( x, j, c );
    }
  }

  private void drawRect( int rx, int ry, int rw, int rh, int rc )
  {
    for (int i=rx; i<rx+rw; ++i) 
    {
      for (int j=ry; j<ry+rh; ++j) 
      {
        cpImage.set( i, j, rc );
      }
    }
  }

  public void render ()
  {
    image( cpImage, x, y );
    if ( mousePressed && mouseX >= x && mouseX < x + w && !(mouseX > w+x-35 && mouseX < w+x-20) && mouseY >= y && mouseY < y + h )
    {
      c = get( mouseX, mouseY );
      pickX = mouseX;
      pickY = mouseY;
    }
    fill( c );
    strokeWeight(1);
    rect( x, y+h+10, 20, 20 );

    // Draw current color selected circle
    if (pickX != 0 && pickY != 0) {
      noFill();
      stroke(#000000);
      strokeWeight(1);
      ellipse(pickX, pickY, 10, 10);
    }

    // Draw borders
    noFill();
    stroke(#000000);
    strokeWeight(1.5);
    rect(x, y, w-35, h);
    rect(x+w-20, y, 20, h);
  }
}
*/
public class Connection {
	private Loop loop = new Loop();
	private Region region = new Region();
	// Start and end form the 1st base pair of the region.
	private int start, end;
	private double xrad, yrad, angle;
	// True if segment between this connection and the
	// next must be extruded out of the circle
	private boolean extruded;
	// True if the extruded segment must be drawn long.
	private boolean broken;

	private boolean _isNull=false;

	public boolean isNull() {
		return _isNull;
	}

	public void setNull(boolean isNull) {
		_isNull = isNull;
	}


	public Loop getLoop() {
		return loop;
	}

	public void setLoop(Loop loop) {
		this.loop = loop;
	}

	public Region getRegion() {
		return region;
	}

	public void setRegion(Region region) {
		this.region = region;
	}

	public int getStart() {
		return start;
	}

	public void setStart(int start) {
		this.start = start;
	}

	public int getEnd() {
		return end;
	}

	public void setEnd(int end) {
		this.end = end;
	}

	public double getXrad() {
		return xrad;
	}

	public void setXrad(double xrad) {
		this.xrad = xrad;
	}

	public double getYrad() {
		return yrad;
	}

	public void setYrad(double yrad) {
		this.yrad = yrad;
	}

	public double getAngle() {
		return angle;
	}

	public void setAngle(double angle) {
		this.angle = angle;
	}

	public boolean isExtruded() {
		return extruded;
	}

	public void setExtruded(boolean extruded) {
		this.extruded = extruded;
	}

	public boolean isBroken() {
		return broken;
	}

	public void setBroken(boolean broken) {
		this.broken = broken;
	}
}
class DisplaySettings {
  static final int COLOR_MODE_FEW = 1; // from Stephen Few's book "Show Me The Numbers"
  static final int COLOR_MODE_WHITE = 2;
  static final int COLOR_MODE_RESIDUE = 3;
  static final int COLOR_MODE_STRUCTURE = 4;
  static final int COLOR_MODE_RAINBOW = 7;
  static final int COLOR_MODE_LIGHT = 5;
  static final int COLOR_MODE_BYG = 6;
  static final int COLOR_MODE_GREY = 8;
  static final int COLOR_MODE_RNADNA = 9; // distinguish RNA and DNA
  static final int COLOR_MODE_CUSTOM = 10;
  static final int COLOR_MODE_MAX = 11; // +1
  int colorMode = 1;
  // RGB values from Show Me the Numbers from Stephen Few:
  // 4D4D4D (gray)
  // 5DA5DA (blue)
  // FAA43A (orange)
  // 60BD68 (green)
  // F17CB0 (pink)
  // B2912F (brown)
  // B276B2 (purple)
  // DECF3F (yellow)
  // F15854 (red)
  int GUIColor = 0xff38B2E8; //#38B2E8 //#42C6D3
  int colorMax = 360; // For color mode

  int bpCol = 0xff1502CE; // Color of pair line
  float bpWeight = 1.5f; // Weight of pair line
  int bbCol = 0xff000000; // Backbone color
  float bbWeight = 2.25f; // Backbone weight

  double radiusShowMul = 2.25f;

  //color[] customColors;
  int startGradient = 0xffFFFFFF; //#00FFFF; // Teal
  int endGradient = 0xffFF0000; // Red

  boolean labelMode = true;
  boolean textMode = true;
  boolean outlineMode = true;
  boolean displayStrandInfo = false;
  float textReduction = 0.8f;
  float textOffsX = -0.3f;
  float textOffsY = 0.3f;

  static final int MENU_APPEAR_X = 100;
  static final int MENU_APPEAR_Y = 60;
  float helpX = 20.0f;
  int textCol = 0xff000000;
  float helpTextSize = 13.2f;

  float messageX = screenResX/3.0f;
  float messageY = 50;
  float messageFontSize = 20.0f;

  float labelWeight = 5; // Width of label line
  int highlightCol = 0xffFFFF00; // Hightlight color
  double highlightMul = 1.5f; // Size increase of selected residues

  float menuTextSize = 13.7f;
  float sliderLabelWidth = 100;
  float sliderWidth = 300;
  float sliderHeight = 10;
  float sliderGap = 50;
  float sliderLabelX = (width / 2.0f) - (sliderLabelWidth + sliderWidth + sliderGap/2.0f); // layout of slider table

  float zoomFactor = 1.0f; // for global zoom function
  float zoomMul = 1.1f; // factor for zooming and and out
  float zoomTranslateX = 0.0f; // translate view before applying scale
  float zoomTranslateY = 0.0f; //

  double startPadding = 20;
  double padding = startPadding;
  double paddingScale = 250;

  /** Apply zoom factor "mul". Typical values: zoomMul (1.5) or 1/zoomMul
   */
   // DERIVATION:
   // we want that AbsMouseX(screenResX/2) remains unchanged!
   //    (mx - ds.zoomTranslateXorig)/ds.zoomFactorOrig == (mx - ds.zoomTranslateXorigNew)/ds.zoomFactorNew;
   // ds.zoomFactorNew = ds.zoomFactor*1.5
   //    (mx - ds.zoomTranslateXorig)/ds.zoomFactorOrig == (mx - ds.zoomTranslateXorigNew)/(1.5*ds.zoomFactorOrig);
   //    mx - ds.zoomTranslateXorig == (mx - ds.zoomTranslateXorigNew)/1.5;
   //    1.5*(mx - ds.zoomTranslateXorig) == mx - ds.zoomTranslateXorigNew;
   // ds.zoomTranslateXNew = mx - 1.5 * (mx - ds.zoomTranslateXorig)
   // ds.zoomTranslateXNew = (1-1.5)*mx + 1.5 * ds.zoomTranslateXorig
   // mx = screenResX/2
   // ds.zoomTranslateXNew = (1-1.5)*(screenResX/2) + 1.5 * ds.zoomTranslateXorig
   // ds.zoomFactor *= ds.zoomMul;
   // ds.zoomTranslateX = (1-ds.zoomMul)*(screenResX/2) + ds.zoomMul * ds.zoomTranslateX;
   // ds.zoomTranslateY = (1-ds.zoomMul)*(screenResY/2) + ds.zoomMul * ds.zoomTranslateY;
   
  public void applyZoom(float mul) {
   zoomFactor *= mul;
   zoomTranslateX = (1-mul)*(screenResX/2) + mul * ds.zoomTranslateX;
   zoomTranslateY = (1-mul)*(screenResY/2) + mul * ds.zoomTranslateY;
  }

  public void resetZoom() {
    zoomFactor = 1.0f;
    zoomTranslateX = 0.0f;
    zoomTranslateY = 0.0f;
    // println("Zoom values after reset: " + zoomFactor + " " + zoomTranslateX + " " + zoomTranslateY);
  }
}
/* Used to detect pseudoknots for radial placement algorithm. */
class Helix {
  int id;
  int[] ends = new int[2];
  int size = 0; // number of pairs
  int numCrossed = 0; // Number of external helices' basepair ends that this helix encapsulates.
  HashMap<Integer, Helix> crossedW = new HashMap<Integer, Helix>();
  
  Helix(int end1, int end2, int id) {
    ends[0] = end1;
    ends[1] = end2;
    this.id = id;
  }
  
  public void setSize(int size) {
    this.size = size;
  }
}


/* Used for strand order function. */
class HelixEnd {
  int strandId;
  int relId; // Index relative to strand start
  int origId; // Index in ct file
  
  public void setStrandId(int strandId) {
    this.strandId = strandId;
  }
  
  public void setRelId(int relId) {
    this.relId = relId;
  }
  
  public void setOrigId(int origId) {
    this.origId = origId;
  }
}

class Strand {
  int id;
  ArrayList<Integer> strandIds = new ArrayList<Integer>();
  int length;
  ArrayList<HelixEnd> helixEnds = new ArrayList<HelixEnd>();
  
  Strand(int id, int length) {
    this.id = id;
    strandIds.add(id);
    this.length = length;
  }
  
  public void addEnd(HelixEnd _end) {
    helixEnds.add(_end);
  }
  
  public void addId(int strandId) {
    strandIds.add(strandId);
  }
}

class InterHelix {
  HelixEnd end1 = new HelixEnd();
  HelixEnd end2 = new HelixEnd();
  
  InterHelix(int strand1, int relId1, int strand2, int relId2) {
    end1.setStrandId(strand1);
    end1.setRelId(relId1);
    end2.setStrandId(strand2);
    end2.setRelId(relId2);
  }
  
  InterHelix(int strand1, int relId1, int origId1, int strand2, int relId2, int origId2) {
    this(strand1, relId1, strand2, relId2);
    end1.setOrigId(origId1);
    end2.setOrigId(origId2);
  }
}
JavaScript javascript; // global instance

interface JavaScript {
  public String getTextInput(String inputId);
  
  public String getFileTypeBrowser();
  
  public void screenshotBrowser();
  
  public void saveFileBrowser(String[] saveFileText);

  public void saveSVGBrowser(String[] svgText);
}

public void bindJavaScript(JavaScript js) {
  javascript = js;
}

public void startBrowser() {
  if (javascript != null) {
    setupState();
  }
}
public void keyPressed(KeyEvent e) {
  /*
  String keyS = str(key);
  if ("5".equals(keyS)) {
    println("\n\nrunningSimulation = " + runningSimulation);

    println("simulationMode = " + simulationMode);
    println("rigidHelices = " + rigidHelices);
    println("screenshot = " + screenshot);
    println("capturingScreen = " + capturingScreen);
    println("menuVisible = " + menuVisible);
    println("");
    for (Slider slid : sliders) {
      print(slid.label + ": isPressed=" + slid.isPressed + ", updating=" + slid.updating);
      println("");
    }
    println("");
    for (CheckBox cb : checkBoxes.values()) {
      print(cb.label + ": checked=" + cb.checked);
      println("");
    }
  }
  */

  if (!runningSimulation) { return; }

  double angleUnit = PI / 360.0f;
  double angleShift = 25;

  String keyS = str(key); // Using String instead of just checking against char because Processing.js does not support the char type
  SphereList spheres = currentState.getSpheres();

  if (keyS.equals("a")) { // toggle select-all
    //if (!movieMode) {
      if (attachedIds.size() == currentState.sim.lenTot) { // deselect all
        attachedIds.clear();

      } else { // select all
        for (int i = 0; i < spheres.size(); ++i) {
          attachedIds.put(i, 1);
        }
      }
    //}

  } else if (keyS.equals("b")) { // make sphere radii bigger
    if (!sliders[0].isPressed) {
      ff.radius *= 1.1f;
      if (ff.radius > ff.minRadius + ff.radiusScale) { ff.radius = ff.minRadius + ff.radiusScale; }
      sliders[0].setValue( (float)((ff.radius - ff.minRadius) / ff.radiusScale) );
    }

  } else if (keyS.equals("B")) { // make sphere radius smaller
    if (!sliders[0].isPressed) {
      ff.radius /= 1.1f;
      if (ff.radius < ff.minRadius) { ff.radius = ff.minRadius; }
      sliders[0].setValue( (float)((ff.radius - ff.minRadius) / ff.radiusScale) );
    }


  } else if (keyS.equals("c")) { // change color mode
    if (!sliders[3].isPressed) {
      ++ds.colorMode;
      if (ds.colorMode >= DisplaySettings.COLOR_MODE_MAX) {
        ds.colorMode = 1;
      }
      displayColorMode();
      sliders[3].setValue( ((float)ds.colorMode - 1)/((float)DisplaySettings.COLOR_MODE_MAX - 1) );
    }


  } else if (keyS.equals("f")) { // flip helix
    if (attachedIds.size() > 0) {
      saveStateForUndo();
      for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
        int id = it.next();
        Sphere2D s1 = spheres.get(id);
        if (s1.pairedWForce != null) {
          Sphere2D s2 = s1.pairedWForce;

          // Do not flip twice if both sides of the helix are selected
          if (attachedIds.containsKey(s2.id)) {
            if (s1.id > s2.id) {
              continue;
            }
          }

          // flip coordinates
          double hx = s1.x; // Temporary helper vairables
          double hy = s1.y;
          s1.x = s2.x;
          s1.y = s2.y;
          s2.x = hx;
          s2.y = hy;

          // Set velocities to 0
          s1.vx = 0.0f;
          s1.vy = 0.0f;
          s2.vx = 0.0f;
          s2.vy = 0.0f;
        }
      }
    }

  } else if (keyS.equals("l")) { // toggle labels
    ds.labelMode = ! ds.labelMode;
    checkBoxes.get("Labels").checked = !checkBoxes.get("Labels").checked;

  } else if (keyS.equals("v")) {
    ds.displayStrandInfo = !ds.displayStrandInfo;

  } else if (keyS.equals("s")) { // toggle simulation
    saveStateForUndo();
    simulationMode = !simulationMode;
    checkBoxes.get("Simulation Mode").checked = !checkBoxes.get("Simulation Mode").checked;
    if (!simulationMode) {
      textDisplay = "FREEZE";
      spheres.stopVelocities();
    } else {
      textDisplay = "UNFREEZE";
    }
    textDisplayCount = 0;

  } else if (keyS.equals("S")) { // toggle simulation type
    simulateAllMode = !simulateAllMode;
    checkBoxes.get("Sim. Selected Only").checked = !checkBoxes.get("Sim. Selected Only").checked;
    if (!simulateAllMode) {
      textDisplay = "SIMULATE SELECTED ONLY";
    } else {
      textDisplay = "SIMULATE ALL";
    }
    textDisplayCount = 0;
    
  } else if (keyS.equals("i")) { // toggle display of residue characters
    ds.textMode = !ds.textMode;

  } else if (keyS.equals("o")) { // toggle outline of residues
    ds.outlineMode = !ds.outlineMode;
    checkBoxes.get("Outlines").checked = !checkBoxes.get("Outlines").checked;
    

  } else if (keyS.equals("p")) { // screenshot
    screenshot = true;
    capturingScreen = true;

  } else if (keyS.equals("z") || keyS.equals("Z")) {
    simulationMode = false;
    checkBoxes.get("Simulation Mode").checked = false;
    //movieMode = false;
    if (keyS.equals("z")) {
      undoState();
    } else {
      redoState();
    }

  } else if (keyS.equals("y")) { // reflect over y-axis
    if (attachedIds.size() > 0) {
      saveStateForUndo();
      for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
        Sphere2D sphere = spheres.get(it.next());
        sphere.x = width - sphere.x;
        sphere.vx = 0;
        sphere.vy = 0;
      }
    }

  } else if (keyS.equals("x")) { // reflect over x-axis
    if (attachedIds.size() > 0) {
      saveStateForUndo();
      for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
        Sphere2D sphere = spheres.get(it.next());
        sphere.y = height - sphere.y;
        sphere.vx = 0;
        sphere.vy = 0;
      }
    }

  } else if (key == CODED) { // arrow keys
    double keyShift = 18;
    double keyShiftZoom = 80;
    /* Does not work in processing.js
    if (e.isShiftDown()) {
      keyShift = 2;
    } else {
      keyShift = 20;
    }
    */

    if (!menuVisible && attachedIds.size() == 0) { // nothing selected; use arrow keys to transform coordinate system"
     if (keyCode == LEFT) {
      ds.zoomTranslateX += keyShiftZoom;
     } else if (keyCode == RIGHT) {
      ds.zoomTranslateX -= keyShiftZoom;
     } else if (keyCode == UP) {
      ds.zoomTranslateY += keyShiftZoom;
     } else if (keyCode == DOWN) {
      ds.zoomTranslateY -= keyShiftZoom;
     }

    } else {

    if (keyCode == LEFT) {
      for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
        int id = it.next();
        spheres.get(id).x = spheres.get(id).x - keyShift;
      }
    } else if (keyCode == RIGHT) {
      for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
        int id = it.next();
        spheres.get(id).x = spheres.get(id).x + keyShift;
      }
    } else if (keyCode == UP) {
      for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
        int id = it.next();
        spheres.get(id).y = spheres.get(id).y - keyShift;
      }
    } else if (keyCode == DOWN) {
      for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
        int id = it.next();
        spheres.get(id).y = spheres.get(id).y + keyShift;
      }
    }
  } // else if ... in case some residues have been selected
  /*
  } else if (keyS.equals("j")) { // left
    for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
       int id = it.next();
       spheres.get(id).x = spheres.get(id).x - smallShift;
    }
  } else if (keyS.equals("l")) { // right
    for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
       int id = it.next();
       spheres.get(id).x = spheres.get(id).x + smallShift;
    }
  } else if (keyS.equals("i")) { // up
    for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
       int id = it.next();
       spheres.get(id).y = spheres.get(id).y - smallShift;
    }
  } else if (keyS.equals("k")) { // down
    for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
       int id = it.next();
       spheres.get(id).y = spheres.get(id).y + smallShift;
    }
  */

  } else if (keyS.equals("n")) { // rotate CC
    for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
      int id = it.next();
      spheres.get(id).rotateSphere(-angleUnit * angleShift, AbsMouseX(mouseX), AbsMouseY(mouseY));
    }
  } else if (keyS.equals("m")) { // rotate clockwise
    for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
      int id = it.next();
      spheres.get(id).rotateSphere(angleUnit * angleShift, AbsMouseX(mouseX), AbsMouseY(mouseY));
    }
  } else if (keyS.equals("N")) { // rotate CC small
    for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
      int id = it.next();
      spheres.get(id).rotateSphere(-angleUnit, AbsMouseX(mouseX), AbsMouseY(mouseY));
    }
  } else if (keyS.equals("M")) { // rotate clockwise small
    for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
      int id = it.next();
      spheres.get(id).rotateSphere(angleUnit, AbsMouseX(mouseX), AbsMouseY(mouseY));
  }

  } else if (keyS.equals("r")) { // toggle relaxed state, meaning certain forces are not computed on it
    saveStateForUndo();
    if (attachedIds.size() == 0) { // clear relaxed
      spheres.relaxedIds.clear();
    }
    else {
      for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
        int id = it.next();
        if (!spheres.relaxedIds.containsKey(id)) {
          spheres.relaxedIds.put(id, 1);
          spheres.get(id).vx = 0;
          spheres.get(id).vy = 0;
        } else {
          spheres.relaxedIds.remove(id);
        }
      }
    }

  } else if (keyS.equals("1")) { // bring to front
    if (attachedIds.size() > 0) {
      saveStateForUndo();
      for (int index = 0, i = 0; i < spheres.displayOrder.size(); ++i) {
        if ( attachedIds.containsKey(spheres.displayOrder.get(index)) ) {
          int id = spheres.displayOrder.remove(index);
          spheres.displayOrder.add(id);
        } else {
          ++index;
        }
      }
    }

  } else if (keyS.equals("2")) { // sent to back
    if (attachedIds.size() > 0) {
      saveStateForUndo();
      for (int index = spheres.displayOrder.size()-1, i = 0; i < spheres.displayOrder.size(); ++i) {
        if ( attachedIds.containsKey(spheres.displayOrder.get(index)) ) {
          int id = spheres.displayOrder.remove(index);
          spheres.displayOrder.add(0, id);
        } else {
          index--;
        }
      }
    }
  } else if (keyS.equals("d")) {
    deletingBond = true;
    addingBond = false;
    bondSphereId = -1;

  } else if (keyS.equals(" ")) {
    deletingBond = false;

  } else if (keyS.equals("k")) {
    viewingBase = !viewingBase;


  //} else if (keyS.equals("q")) { // movieMode
    /* TODO
    saveStateForUndo();
    if (!movieMode) {
      currentState.sim.initFoldSteps();
      // deleteAllForceBonds();

      attachedIds.clear();
      simulationMode = true;
      checkBoxes.get("Simulation Mode").checked = true;
      textDisplay = "MOVIE MODE";
      textDisplayCount = 0;
    } else {
      // finish re-bonding all force pairs
    }

    movieMode = !movieMode;
    */

  /*
  } else if (keyS.equals("e")) { // Interpolate positions of consecutive selected residues
    int startId = -1;
    int endId = -1;

    for (int i = 0, l = spheres.size(); i < l; ++i) {
      if (attachedIds.containsKey(i)) {
        endId = i;
        break;
      }
    }
    for (int i = lenTot -1; i >= 0; --i) {
      if (attachedIds.containsKey(i)) {
        startId = i;
        break;
      }
    }
    println("Interpolating between residues " + str(startId+1) + " and " + str(endId+1));
    if ((endId - startId) < 2) {
      println("There must be at least one intermediate residue for position equalization!");
      return;
    }

    for (int i = startId; i <= endId; ++i) {
      if (!attachedIds.containsKey(i)) {
        println("Cannot equalize positions of selected residues, because not all intermediate residues were selected.");
        return;
      }
    }
    double xMin = spheres.get(startId).x;
    double yMin = spheres.get(startId).y;
    double zMin = spheres.get(startId).z;
    double xMax = spheres.get(endId).x;
    double yMax = spheres.get(endId).y;
    double zMax = spheres.get(endId).z;
    double dx = (xMax - xMin)/(endId-startId);
    double dy = (yMax - yMin)/(endId-startId);
    double dz = (zMax - zMin)/(endId-startId);
    for (int i = startId + 1; i < endId; ++i) {
      double x = xMin + (i - startId) * dx;
      double y = yMin + (i - startId) * dy;
      double z = zMin + (i - startId) * dz;
      spheres.get(i).x = x;
      spheres.get(i).y = y;
      spheres.get(i).z = z;
      // println("Setting interpolated position to " + str(x) + " " + str(y) + " " + str(z));
    }
    */
  }  else if (!menuVisible && (keyS.equals("+") || keyS.equals("="))) { // zoom in
   if (ds.zoomFactor < 16) {
    // we want that AbsMouseX(screenResX/2) remains unchanged!
    //    (mx - ds.zoomTranslateXorig)/ds.zoomFactorOrig == (mx - ds.zoomTranslateXorigNew)/ds.zoomFactorNew;
    // ds.zoomFactorNew = ds.zoomFactor*1.5
    //    (mx - ds.zoomTranslateXorig)/ds.zoomFactorOrig == (mx - ds.zoomTranslateXorigNew)/(1.5*ds.zoomFactorOrig);
    //    mx - ds.zoomTranslateXorig == (mx - ds.zoomTranslateXorigNew)/1.5;
    //    1.5*(mx - ds.zoomTranslateXorig) == mx - ds.zoomTranslateXorigNew;
    // ds.zoomTranslateXNew = mx - 1.5 * (mx - ds.zoomTranslateXorig)
    // ds.zoomTranslateXNew = (1-1.5)*mx + 1.5 * ds.zoomTranslateXorig
    // mx = screenResX/2
    // ds.zoomTranslateXNew = (1-1.5)*(screenResX/2) + 1.5 * ds.zoomTranslateXorig
    // ds.zoomFactor *= ds.zoomMul;
    // ds.zoomTranslateX = (1-ds.zoomMul)*(screenResX/2) + ds.zoomMul * ds.zoomTranslateX;
    // ds.zoomTranslateY = (1-ds.zoomMul)*(screenResY/2) + ds.zoomMul * ds.zoomTranslateY;
    ds.applyZoom(ds.zoomMul);
   }
   // TODO set center to mouse coordinates
  } else if (!menuVisible && keyS.equals("-")) { // zoom out
   if (1/ds.zoomFactor < 16) {
     ds.applyZoom(1.0f/ds.zoomMul);
   }
  } else if (keyS.equals("0")) { // reset zoom
   ds.resetZoom();
  }
}

public void keyReleased() {
  if (addingBond) {
    addingBond = false;
    bondSphereId = -1;
  }
  if (deletingBond) { deletingBond = false; }
  //  if (viewingBase) { viewingBase = false; }
}

public void displayColorMode() {
  if (ds.colorMode == DisplaySettings.COLOR_MODE_FEW) {
    textDisplay = "PASTEL";
  } else if (ds.colorMode == DisplaySettings.COLOR_MODE_WHITE) {
    textDisplay = "WHITE";
  } else if (ds.colorMode == DisplaySettings.COLOR_MODE_BYG) {
    textDisplay = "BRIGHT";
  } else if (ds.colorMode == DisplaySettings.COLOR_MODE_GREY) {
    textDisplay = "GREY";
  } else if (ds.colorMode == DisplaySettings.COLOR_MODE_RAINBOW) {
    textDisplay = "RAINBOW";
  } else if (ds.colorMode == DisplaySettings.COLOR_MODE_RESIDUE) {
    textDisplay = "BASE TYPE";
  } else if (ds.colorMode == DisplaySettings.COLOR_MODE_CUSTOM) {
    textDisplay = "CUSTOM";
  } else if (ds.colorMode == DisplaySettings.COLOR_MODE_STRUCTURE) {
    textDisplay = "STRUCTURE";
  } else if (ds.colorMode == DisplaySettings.COLOR_MODE_LIGHT) {
    textDisplay = "LIGHT";
  } else if (ds.colorMode == DisplaySettings.COLOR_MODE_RNADNA) {
    textDisplay = "RNADNA";
  }
  textDisplayCount = 0;
}
public class Listbox // REMOVE GUIDO DEPENDENCY HERE?
{
    float x, y, width, height;
    
    ArrayList<String> items;
    int itemHeight;
    int listStartAt;
    int hoverItem;
    int selectorId;
    
    float valueY;
    boolean hasSlider;
    
    String lastItemClicked;
    
    Listbox ( float xx, float yy, float ww, float hh, int itemHeight, String[] fileTypes, int selectorId ) 
    {
        x = xx; y = yy;
        valueY = y;
        
        this.width = ww; this.height = hh;
        this.selectorId = selectorId;
        this.itemHeight = itemHeight;
        this.listStartAt = 0;
        this.hoverItem = -1;
        this.hasSlider = false;
        this.items = new ArrayList<String>();
        
        for (String type : fileTypes) {
          this.addItem(type);
        }
        
        // register it
        Interactive.add( this );
    }
    
    public void addItem ( String item )
    {
        items.add( item );
        hasSlider = items.size() * itemHeight > this.height;
    }
    
    public void mouseMoved ( float mx, float my )
    {
        if (!selectingFileType) { return; }
        if ( hasSlider && mx > this.width-20 ) return;
        
        hoverItem = listStartAt + PApplet.parseInt((my-y) / itemHeight);
    }
    
    public void mouseExited ( float mx, float my )
    {
        hoverItem = -1;
    }
    
    // For slider
    public void mouseDragged ( float mx, float my, float dx, float dy )
    {
        if (!selectingFileType) { return; }
        if (!hasSlider) return;
        if ( mx < x+this.width-20 ) return;
        
        valueY = my-10;
        valueY = constrain( valueY, y, y+this.height-20 );
        
        update();
    }
    
    // Called from manager
    public void mouseScrolled ( float step )
    {
        if (!selectingFileType) { return; }
        valueY += step;
        valueY = constrain( valueY, y, y+this.height-20 );
        
        update();
    }
    
    public void update ()
    {
        if (!selectingFileType) { return; }
        float totalHeight = items.size() * itemHeight;
        float itemsInView = this.height / itemHeight;
        float listOffset = map( valueY, y, y+this.height-20, 0, totalHeight-this.height );
        
        listStartAt = PApplet.parseInt( listOffset / itemHeight );
    }
    
    public void mousePressed ( float mx, float my )
    {
        if (!selectingFileType) { return; }
        if ( hasSlider && mx > this.width-20 ) return;
        
        int item = listStartAt + PApplet.parseInt( (my-y) / itemHeight );
        lastItemClicked = items.get(item);
        if (debugPrints) { println(lastItemClicked); }
    }

    public void draw ()
    {
        if (!selectingFileType || selectorId != displaySelectorId) { return; }
        noStroke();
        fill(0xffFFFFFF);
        rectMode(CORNER);
        rect( x, y, this.width, this.height );
        strokeWeight(2);
        if ( items != null )
        {
          for ( int i = 0; i < PApplet.parseInt(this.height/itemHeight) && i < items.size(); ++i )
          {
            String currentItem = items.get(i+listStartAt);
            stroke( ds.GUIColor );
            if (currentItem.equals(lastItemClicked)) {
              fill(0xff000000);
            } else {
              fill(0xffFFFFFF);
            }
            
            if ((i+listStartAt) == hoverItem) { fill(0xffFFF8AD); }
            rect( x, y + (i*itemHeight), this.width, itemHeight );
            
            noStroke();
            if (currentItem.equals(lastItemClicked)) {
              if ((i+listStartAt) == hoverItem) {
                fill(0xff0052FF);
              } else {
                fill(0xffFFFFFF);
              }
            } else {
              fill(0xff000000);
            }
            textSize(itemHeight * .75f);
            textAlign(LEFT);
            text( currentItem, x+5, y+(i+1)*itemHeight-5 );
          }
        }
        
        if ( hasSlider )
        {
            rectMode(CORNER);
            stroke( 80 ); // TODO: change color
            fill( 100 );
            rect( x+this.width-20, y, 20, this.height );
            fill( 120 );
            rect( x+this.width-20, valueY, 20, 20 );
        }
    }
}
public class Loop {
	private int nconnection;
	private ArrayList<Connection> connections = new ArrayList<Connection>();
	private HashMap<Integer,Connection> _connections = new HashMap<Integer,Connection>();
	private int number;
	private int depth;
	private boolean mark;
	private double x, y, radius;

	public int getNconnection() {
		return nconnection;
	}

	public void setNconnection(int nconnection) {
		this.nconnection = nconnection;
	}

	public void setConnection(int i, Connection c)
	{
		Integer n = i;
		if (c != null)
    		_connections.put(n, c);
		else
		{
			if (!_connections.containsKey(n))
			{
				_connections.put(n, new Connection());
			}
			_connections.get(i).setNull(true);
		}
	}

	public Connection getConnection(int i)
	{
		Integer n = i;
		if (!_connections.containsKey(n))
		{ _connections.put(n, new Connection()); }
		Connection c = _connections.get(n);
		if (c.isNull())
			return null;
		else
			return c;
	}

	public void addConnection(int i, Connection c)
	{
		_connections.put(_connections.size(),c);
	}


	public int getNumber() {
		return number;
	}

	public void setNumber(int number) {
		this.number = number;
	}

	public int getDepth() {
		return depth;
	}

	public void setDepth(int depth) {
		this.depth = depth;
	}

	public boolean isMark() {
		return mark;
	}

	public void setMark(boolean mark) {
		this.mark = mark;
	}

	public double getX() {
		return x;
	}

	public void setX(double x) {
		this.x = x;
	}

	public double getY() {
		return y;
	}

	public void setY(double y) {
		this.y = y;
	}

	public double getRadius() {
		return radius;
	}

	public void setRadius(double radius) {
		this.radius = radius;
	}

	public String toString()
	{
		String result = "Loop:";
		result += " nconnection "+nconnection;
		result += " depth "+depth;
		return result;
	}
}
// convert screen coordinates of mouse into universe coordinates (may be different due to mouse factor)
public float AbsMouseX(float mx) {
      return (mx - ds.zoomTranslateX)/ds.zoomFactor;
}

// convert screen coordinates of mouse into universe coordinates (may be different due to mouse factor)
public float AbsMouseY(float my) {
      return (my - ds.zoomTranslateY)/ds.zoomFactor;
}

float mouseXMark = -1;
float mouseYMark = -1;
boolean multiSelecting = false;
boolean dragging = false;
int dragId = -1;
int pressTime;
int bondSphereId = -1;
boolean addingBond = false;
boolean deletingBond = false;

public void mousePressed() {
  pressTime = millis();
  if (!runningSimulation) { return; }
  if (mouseButton == LEFT) { // && !movieMode
    if (keyPressed && " ".equals(str(key)) && !deletingBond) {
      SphereList spheres = currentState.getSpheres();
      double[] closestOutput = spheres.findClosest(AbsMouseX(mouseX), AbsMouseY(mouseY));
      double closestD = closestOutput[1];
      if (closestD <= ds.radiusShowMul * ff.radius) {
        int closestId = (int) closestOutput[0];
        if (!addingBond) {
          bondSphereId = closestId;
          addingBond = true;
          
        } else { // Bond spheres
          Sphere2D s1 = spheres.get(bondSphereId);
          addingBond = false;
          bondSphereId = -1;
          saveStateForUndo();
          setNewPair(s1, spheres.get(closestId), "cWW", currentState.sim);
        }
      } else {
        addingBond = false;
        bondSphereId = -1;
      }
    
    } else if (!menuVisible) {
      SphereList spheres = currentState.getSpheres();
      double[] closestOutput = spheres.findClosest(AbsMouseX(mouseX), AbsMouseY(mouseY));
      int closest = (int) closestOutput[0];
      double closestD = closestOutput[1];
      if (closestD <= ff.radius * ds.radiusShowMul) {
        if (attachedIds.containsKey(closest)) {
          dragging = true;
          dragId = closest;
        }
      }
      if (!dragging) {
        multiSelecting = true;
        mouseXMark = AbsMouseX(mouseX);
        mouseYMark = AbsMouseY(mouseY);
      }
    }
  }
  
  if (programLaunch) {
    programLaunch = false;
  }
}

public void mouseDragged() {
  if (!runningSimulation) { return; }
  if (dragging) {
    SphereList spheres = currentState.getSpheres();
    double dx = AbsMouseX(mouseX) - spheres.get(dragId).x;
    double dy = AbsMouseY(mouseY) - spheres.get(dragId).y;
    for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
      Sphere2D currentSphere = spheres.get(it.next());
      currentSphere.x += dx;
      currentSphere.y += dy;
    }
  }
  
  /*
  // Guido replacement code
  if (menuVisible) {
    for (Slider slider : sliders) {
      if (slider.x <= AbsMouseX(mouseX) && AbsMouseX(mouseX) <= slider.x+slider.width &&
          slider.y <= AbsMouseY(mouseY) && AbsMouseY(mouseY) <= slider.y+slider.height) {
        slider.dragged();
      }
    }
  }
  */
}

public void mouseReleased() {
  int elapsedTime = millis() - pressTime;
  if (runningSimulation) {
    if (dragging) {
      dragging = false;
      saveStateForUndo();
    
    } else if (multiSelecting) {
      multiSelecting = false;
      //if (!movieMode) {
        if ( !(keyPressed && key == CODED && (keyCode == SHIFT || keyCode == ALT)) ) {
          attachedIds.clear();
        }
        
        SphereList spheres = currentState.getSpheres();
        float mouseXMark2 = AbsMouseX(mouseX);
        float mouseYMark2 = AbsMouseY(mouseY);
        
        // Make sure first mark is less than the second for the selection check:
        if (mouseXMark2 < mouseXMark) {
          float help = mouseXMark;
          mouseXMark = mouseXMark2;
          mouseXMark2 = help;
        }
        if (mouseYMark2 < mouseYMark) {
          float help = mouseYMark;
          mouseYMark = mouseYMark2;
          mouseYMark2 = help;
        }
    
        for (int i = 0; i < spheres.size(); ++i) {
          double x = spheres.get(i).x;
          double y = spheres.get(i).y;
          if ((x >= mouseXMark) && (x <= mouseXMark2) && (y >= mouseYMark) && (y <= mouseYMark2) && !attachedIds.containsKey(i)) {
            attachedIds.put(i, 1); // The mapped value "1" is a dummy placeholder; workaround to use HashMap instead of Set
            
            if (!(keyPressed && key == CODED && keyCode == ALT)) { // alt clicking target selects
              Sphere2D sphere = spheres.get(i);
              if (sphere.pairedWForce != null) {
                Sphere2D s2 = sphere;
                while (s2.hasHelixDown) {
                  s2 = s2.threeP;
                  if (!attachedIds.containsKey(s2.id)) {
                    attachedIds.put(s2.id, 1);
                  }
                }
                s2 = sphere;
                while (s2.hasHelixUp) {
                  s2 = s2.fiveP;
                  if (!attachedIds.containsKey(s2.id)) {
                    attachedIds.put(s2.id, 1);
                  }
                }
              }
            }
          }
        }
      //}
    }
    
    for (Slider slider : sliders) {
      slider.isPressed = false;
    }
  }
  if (elapsedTime < 170) { // make an adjustable preference?
    mouseClickedFunction();
  }
}

public void mouseClickedFunction() {
  if (runningSimulation && mouseButton == LEFT && !menuVisible && !addingBond) { // && !movieMode
    
    SphereList spheres = currentState.getSpheres();
    double[] closestOutput = spheres.findClosest(AbsMouseX(mouseX), AbsMouseY(mouseY));
    int closest = (int) closestOutput[0];
    double closestD = closestOutput[1];
    
    if (deletingBond) {
      if (closestD <= ds.radiusShowMul * ff.radius) { // Deletebond
        saveStateForUndo();
        Sphere2D sphereD = spheres.get(closest);
        
        int pairId = currentState.sim.pairTable[sphereD.id];
        if (pairId >= 0) {
          currentState.sim.pairTable[sphereD.id] = -1;
          currentState.sim.pairTable[pairId] = -1;
        }
        
        unsetForcePair(sphereD, sphereD.pairedWForce);
        
        for (int i = 0; i < sphereD.bonds.length; ++i) {
          String bond = sphereD.bonds[i];
          if (bond != null && !"".equals(bond)) {
            sphereD.bonds[i] = null;
            spheres.get(i).bonds[closest] = null;
            
            String ncKey = closest < i ? closest + " " + i : i + " " + closest;
            currentState.sim.nonCanonicals.remove(ncKey);
          }
        }
      }
    } else if ( (attachedIds.size() == 0 && closestD <= 15) || (closestD <= ds.radiusShowMul * ff.radius) ) { // add residue
      Sphere2D sphere = spheres.get(closest);
      
      if (attachedIds.containsKey(closest) &&
          (keyPressed && key == CODED && (keyCode == SHIFT || keyCode == ALT))) { // deselect if shift/alt-clicking an already selected residue
        attachedIds.remove(closest);
        
        if (!(keyPressed && key == CODED && keyCode == ALT)) { // alt-clicking deselects only clicked on residue
          if (sphere.pairedWForce != null) { // Watson-Crick paired
            Sphere2D s2 = sphere;
            while (s2.hasHelixDown) {
              s2 = s2.threeP;
              if (attachedIds.containsKey(s2.id)) {
                attachedIds.remove(s2.id);
              }
            }
            
            // Check here to deselect dangly ends as well
            
            s2 = sphere;
            while (s2.hasHelixUp) {
              s2 = s2.fiveP;
              if (attachedIds.containsKey(s2.id)) {
                attachedIds.remove(s2.id);
              }
            }
          }
        }
      } else { // Select
        
        if ( !(keyPressed && key == CODED && (keyCode == SHIFT || keyCode == ALT)) ) {
          attachedIds.clear();
        }
        
        attachedIds.put(closest, 1); // 1 is a dummy value because we are using HashMap as a Set due to Processing's lack of support for Set
        
        if (!(keyPressed && key == CODED && keyCode == ALT)) { // alt-clicking selects only clicked on residue
          if (sphere.pairedWForce != null) { // Watson-Crick paired
            Sphere2D s2 = sphere;
            while (s2.hasHelixDown) {
              s2 = s2.threeP;
              if (!attachedIds.containsKey(s2.id)) {
                attachedIds.put(s2.id, 1);
              }
              if (!attachedIds.containsKey(s2.pairedWForce.id)) {
                attachedIds.put(s2.pairedWForce.id, 1);
              }
            }
            
            s2 = sphere;
            while (s2.hasHelixUp) {
              s2 = s2.fiveP;
              if (!attachedIds.containsKey(s2.id)) {
                attachedIds.put(s2.id, 1);
              }
              if (!attachedIds.containsKey(s2.pairedWForce.id)) {
                attachedIds.put(s2.pairedWForce.id, 1);
              }
            }
            
            if (!attachedIds.containsKey(sphere.pairedWForce.id)) {
              attachedIds.put(sphere.pairedWForce.id, 1);
            }
          }
        }
      }
    } else if (!(keyPressed && key == CODED && keyCode == SHIFT)) {
      attachedIds.clear();
    }
  }
}
String radialAlgorithmException = "";

public class Radial {
	private final double ANUM = 9999.0f;
	private final int MAXITER = 500;

	private ArrayList<Base> bases;
	private int nbase, nregion, loop_count;

	private Loop root = new Loop();
	private ArrayList<Loop> loops;

	private ArrayList<Region> regions;

	private Radloop rlphead = new Radloop();

	private double lencut=0.8f;
	private final double RADIUS_REDUCTION_FACTOR = 1.4f;

	// show algorithm step by step
	private boolean debug = false;

	private double angleinc;

	private double _h;


	private boolean noIterationFailureYet = true;

	double HELIX_FACTOR = 0.6f;
	double BACKBONE_DISTANCE = 27;

  public Radial() {
    radialAlgorithmException = "";
  }

	public int xyCoordinates(ArrayList<Short> pair_table2,
			ArrayList<Double> x, ArrayList<Double> y) {
		if (debug) {
			println("xy_coordinates");
    }
		if (pair_table2.size() == 0)
			return 0;
		int i;
		ArrayList<Integer> pair_table = new ArrayList<Integer>(pair_table2.size() + 1); // pair_table: first index: total # of bases. The id (1 based) of the base the base at id index (1 based) is paired with.
		pair_table.add(pair_table2.size());

		for (int j = 0; j < pair_table2.size(); ++j) {
			pair_table.add(pair_table2.get(j) + 1);
		}

		if (debug) {
			infoStructure(pair_table);
		}
		// length
		nbase = pair_table.get(0);
		bases = new ArrayList<Base>(nbase + 1);

		for (int index = 0; index < bases.size(); ++index) {
			bases.add(new Base());
		}

		regions = new ArrayList<Region>();
		for (int index = 0; index < nbase + 1; ++index) {
			regions.add(new Region());
		}

		read_in_bases(pair_table);

		if (debug)
			infoBasesMate();

		rlphead = null;

		find_regions();

		//if (debug)
		//	infoRegions();

		loop_count = 0;
		loops = new ArrayList<Loop>(nbase + 1);
		for (int index = 0; index < nbase + 1; ++index) {
			loops.add(new Loop());
		}

		construct_loop(0);
    if (!"".equals(radialAlgorithmException)) { return -1; } // workaround exception for javascript

		if (debug)
			infoBasesExtracted();

		find_central_loop();

		if (debug)
			infoRoot();

		if (debug)
			dump_loops();

		traverse_loop(root, null);
    if (!"".equals(radialAlgorithmException)) { return -1; } // workaround exception for javascript

		for (i = 0; i < nbase; ++i) {
			x.add(100 + BACKBONE_DISTANCE * bases.get(i + 1).getX());
			y.add(100 + BACKBONE_DISTANCE * bases.get(i + 1).getY());
		}

		return nbase;
	}

	private void infoStructure(ArrayList<Integer> pair_table) {
		println("structure:");
		for (int j = 0; j < pair_table.size(); ++j) {
			print("#" + j + ":" + pair_table.get(j) + "\t");
			if (j % 10 == 0)
				println("");
		}
		println("");
	}

	private void infoBasesMate() {
		println("Bases mate:");
		for (int index = 0; index < bases.size(); ++index) {
			print("#" + index + ":" + bases.get(index).getMate()
					+ "\t");
			if (index % 10 == 0)
				println("");
		}
		println("");
	}

	private void infoRegions() {
		println("regions:");
		for (int index = 0; index < regions.size(); ++index) {
			print("(" + regions.get(index).getStart1() + ","
					+ regions.get(index).getStart2() + ";"
					+ regions.get(index).getEnd1() + ","
					+ regions.get(index).getEnd2() + ")\t\t");
			if (index + 1 % 5 == 0)
				println("");
		}
		println("");
	}

	private void infoBasesExtracted() {
		println("Bases extracted:");
		for (int index = 0; index < bases.size(); ++index) {
			print("i=" + index + ":"
					+ bases.get(index).isExtracted() + "\t");
			if (index % 5 == 0)
				println("");
		}
		println("");
	}

	private void infoRoot() {
		println("root" + root.getNconnection() + ";"
				+ root.getNumber());
		println("\troot : ");
		println("\tdepth=" + root.getDepth());
		println("\tmark=" + root.isMark());
		println("\tnumber=" + root.getNumber());
		println("\tradius=" + root.getRadius());
		println("\tx=" + root.getX());
		println("\ty=" + root.getY());
		println("\tnconnection=" + root.getNconnection());
	}

	private void read_in_bases(ArrayList<Integer> pair_table) {
		if (debug)
			println("read_in_bases");

		int i, npairs;

		// Set up an origin.
		bases.add(new Base());
		bases.get(0).setMate(0);
		bases.get(0).setExtracted(false);
		bases.get(0).setX(ANUM);
		bases.get(0).setY(ANUM);

		for (npairs = 0, i = 1; i <= nbase; ++i) {
			bases.add(new Base());
			bases.get(i).setExtracted(false);
			bases.get(i).setX(ANUM);
			bases.get(i).setY(ANUM);
			bases.get(i).setMate(pair_table.get(i));
			if ((int) pair_table.get(i) > i)
				++npairs;
		}
		// must have at least 1 pair to avoid segfault
		if (npairs == 0) {
			bases.get(1).setMate(nbase);
			bases.get(nbase).setMate(1);
		}
	}

	/**
	 * Identifies the regions in the structure.
	 */
	private void find_regions()
	{
		if (debug) {
			println("find_regions");
    }
		int i, mate, nb1;
		nb1 = nbase + 1;
		ArrayList<Boolean> mark = new ArrayList<Boolean>(nb1);
		for (i = 0; i < nb1; ++i) {
			mark.add(false);
    }
		nregion = 0;
		for (i = 0; i <= nbase; ++i) {
			if ((mate = bases.get(i).getMate()) != 0 && !mark.get(i)) {
				regions.get(nregion).setStart1(i);
				regions.get(nregion).setEnd2(mate);
				mark.set(i, true);
				mark.set(mate, true);
				bases.get(i).setRegion(regions.get(nregion));
				bases.get(mate).setRegion(regions.get(nregion));
				for (i++, mate--; i < mate && bases.get(i).getMate() == mate; i++, mate--) {
					mark.set(mate, true);
					mark.set(i, true);
					bases.get(i).setRegion(regions.get(nregion));
					bases.get(mate).setRegion(regions.get(nregion));
				}
				regions.get(nregion).setEnd1(--i);
				regions.get(nregion).setStart2(mate + 1);
				if (debug) {
					if (nregion == 0)
						println("\nRegions are:\n");
					  println(
							"Region %d is %d-%d and %d-%d with gap of %d.\n",
							nregion + 1, regions.get(nregion).getStart1(),
							regions.get(nregion).getEnd1(), regions
									.get(nregion).getStart2(), regions.get(
									nregion).getEnd2(), regions.get(nregion)
									.getStart2()
									- regions.get(nregion).getEnd1() + 1);
				}
				nregion++;
			}
		}
	}

	/**
	 * Starting at residue ibase, recursively constructs the loop containing
	 * said base and all deeper bases.
	 */
	private Loop construct_loop(int ibase) {
		if (debug) {
			println("construct_loop");
    }
		int i, mate;
		Loop retloop = new Loop(), lp = new Loop();
		Connection cp = new Connection();
		Region rp = new Region();
		Radloop rlp = new Radloop();
		retloop = loops.get(loop_count++);
		retloop.setNconnection(0);
		retloop.setDepth(0);
		retloop.setNumber(loop_count);
		retloop.setRadius(0.0f);
		for (rlp = rlphead; rlp != null; rlp = rlp.getNext())
			if (rlp.getLoopnumber() == loop_count)
				retloop.setRadius(rlp.getRadius());
		i = ibase;
		do {
			if ((mate = bases.get(i).getMate()) != 0) {
				rp = bases.get(i).getRegion();
				if (!bases.get(rp.getStart1()).isExtracted()) {
					if (i == rp.getStart1()) {
						bases.get(rp.getStart1()).setExtracted(true);
						bases.get(rp.getEnd1()).setExtracted(true);
						bases.get(rp.getStart2()).setExtracted(true);
						bases.get(rp.getEnd2()).setExtracted(true);
						lp = construct_loop(rp.getEnd1() < nbase ? rp.getEnd1() + 1
								: 0);
            if (!"".equals(radialAlgorithmException)) { return new Loop(); } // workaround exception for javascript
					} else if (i == rp.getStart2()) {
						bases.get(rp.getStart2()).setExtracted(true);
						bases.get(rp.getEnd2()).setExtracted(true);
						bases.get(rp.getStart1()).setExtracted(true);
						bases.get(rp.getEnd1()).setExtracted(true);
						lp = construct_loop(rp.getEnd2() < nbase ? rp.getEnd2() + 1
								: 0);
            if (!"".equals(radialAlgorithmException)) { return new Loop(); } // workaround exception for javascript
					} else {
						println("Error detected in construct_loop. i = " + i + " not found in region table.\n");
            radialAlgorithmException = "Error detected in construct_loop. i = " + i + " not found in region table.\n";
            return new Loop();
					}
					retloop.setNconnection(retloop.getNconnection() + 1);
					cp = new Connection();
					retloop.setConnection(retloop.getNconnection() - 1,	cp);
					retloop.setConnection(retloop.getNconnection(), null);
					cp.setLoop(lp);
					cp.setRegion(rp);
					if (i == rp.getStart1()) {
						cp.setStart(rp.getStart1());
						cp.setEnd(rp.getEnd2());
					} else {
						cp.setStart(rp.getStart2());
						cp.setEnd(rp.getEnd1());
					}
					cp.setExtruded(false);
					cp.setBroken(false);
					lp.setNconnection(lp.getNconnection() + 1);
					cp = new Connection();
					lp.setConnection(lp.getNconnection() - 1, cp);
					lp.setConnection(lp.getNconnection(), null);
					cp.setLoop(retloop);
					cp.setRegion(rp);
					if (i == rp.getStart1()) {
						cp.setStart(rp.getStart2());
						cp.setEnd(rp.getEnd1());
					} else {
						cp.setStart(rp.getStart1());
						cp.setEnd(rp.getEnd2());
					}
					cp.setExtruded(false);
					cp.setBroken(false);
				}
				i = mate;
			}
			if (++i > nbase)
				i = 0;
		} while (i != ibase);
		return retloop;
	}

	/**
	 * Displays all the loops.
	 */
	private void dump_loops() {
		println("dump_loops");
		int il, ilp, irp;
		Loop lp;
		Connection cp;

		for (il = 0; il < loop_count; il++) {
			lp = loops.get(il);
			for (int i = 0; (cp = lp.getConnection(i)) != null; i++) {
				ilp = (loops.indexOf(cp.getLoop())) + 1;
				irp = (regions.indexOf(cp.getRegion())) + 1;
			}
		}
	}

	/**
	 * Find node of greatest branching that is deepest.
	 */
	private void find_central_loop() {
		if (debug)
			println("find_central_loop");
		Loop lp = new Loop();
		int maxconn, maxdepth, i;

		determine_depths();
		maxconn = 0;
		maxdepth = -1;
		for (i = 0; i < loop_count; i++) {
			lp = loops.get(i);
			if (lp.getNconnection() > maxconn) {
				maxdepth = lp.getDepth();
				maxconn = lp.getNconnection();
				root = lp;
			} else if (lp.getDepth() > maxdepth
					&& lp.getNconnection() == maxconn) {
				maxdepth = lp.getDepth();
				root = lp;
			}
		}
	}

	/**
	 * Determine the depth of all loops.
	 */
	private void determine_depths() {
		if (debug) {
			println("determine_depths");
    }
		Loop lp = new Loop();
		int i, j;

		for (i = 0; i < loop_count; i++) {
			lp = loops.get(i);
			for (j = 0; j < loop_count; j++)
				loops.get(j).setMark(false);
			lp.setDepth(depth(lp));
		}
	}

	/**
	 * Determines the depth of loop, lp. Depth is defined as the minimum
	 * distance to a leaf loop where a leaf loop is one that has only one or no
	 * connections.
	 */
	private int depth(Loop lp) {
		//if (debug) {
			//println("depth");
    //}
		int count, ret, d;

		if (lp.getNconnection() <= 1)
			return 0;
		if (lp.isMark())
			return -1;
		lp.setMark(true);
		count = 0;
		ret = 0;
		for (int i = 0; lp.getConnection(i) != null; i++) {
			d = depth(lp.getConnection(i).getLoop());
			if (d >= 0) {
				if (++count == 1)
					ret = d;
				else if (ret > d)
					ret = d;
			}
		}
		lp.setMark(false);
		return ret + 1;
	}

	/**
	 * This is the workhorse of the display program. The algorithm is recursive
	 * based on processing individual loops. Each base pairing region is
	 * displayed using the direction given by the circle diagram, and the
	 * connections between the regions are drawn by equally spaced points. The
	 * radius of the loop is set to minimize the square error for lengths
	 * between sequential bases in the loops. The "correct" length for base
	 * links is 1. If the least squares fitting of the radius results in loops
	 * being less than 1/2 unit apart, then that segment is extruded.
	 *
	 * The variable, anchor_connection, gives the connection to the loop
	 * processed in an previous level of recursion.
	 */
	private void traverse_loop(Loop lp, Connection anchor_connection){
		if (debug)
			println("  traverse_loop");
		double xs, ys, xe, ye, xn, yn, angleinc, r;
		double radius, xc, yc, xo, yo, astart, aend, a;
		Connection cp, cpnext, acp, cpprev;
		int i, j, n, ic;
		double da, maxang;
		int count, icstart, icend, icmiddle, icroot;
		boolean done, done_all_connections, rooted;
		int sign;
		double midx, midy, nrx, nry, mx, my, vx, vy, dotmv, nmidx, nmidy;
		int icstart1, icup, icdown, icnext, direction;
		double dan, dx, dy, rr;
		double cpx, cpy, cpnextx, cpnexty, cnx, cny, rcn, rc, lnx, lny, rl, ac, acn, sx, sy, dcp;
		int imaxloop = 0;

		angleinc = 2 * PI / (nbase + 1);
		acp = null;
		icroot = -1;
		int indice = 0;

		for (ic = 0; (cp = lp.getConnection(indice)) != null; indice++, ic++) {
			// xs = cos(angleinc*cp.setStart(); ys = sin(angleinc*cp.setStart();
			// xe =
			// cos(angleinc*cp.setEnd()); ye = sin(angleinc*cp.setEnd());
			xs = -Math.sin(angleinc * cp.getStart());
			ys = Math.cos(angleinc * cp.getStart());
			xe = -Math.sin(angleinc * cp.getEnd());
			ye = Math.cos(angleinc * cp.getEnd());
			xn = ye - ys;
			yn = xs - xe;
			r = Math.sqrt(xn * xn + yn * yn);
			cp.setXrad(xn / r);
			cp.setYrad(yn / r);
			cp.setAngle(Math.atan2(yn, xn));
			if (cp.getAngle() < 0.0f)
				cp.setAngle(cp.getAngle() + 2 * Math.PI);
			if (anchor_connection != null
					&& anchor_connection.getRegion() == cp.getRegion()) {
				acp = cp;
				icroot = ic;
			}
		}
		// remplacement d'une etiquette de goto
		set_radius: while (true) {
			determine_radius(lp, lencut);
			radius = lp.getRadius()/RADIUS_REDUCTION_FACTOR;
			if (anchor_connection == null)
				xc = yc = 0.0f;
			else {
				xo = (bases.get(acp.getStart()).getX() + bases
						.get(acp.getEnd()).getX()) / 2.0f;
				yo = (bases.get(acp.getStart()).getY() + bases
						.get(acp.getEnd()).getY()) / 2.0f;
				xc = xo - radius * acp.getXrad();
				yc = yo - radius * acp.getYrad();
			}

			// The construction of the connectors will proceed in blocks of
			// connected connectors, where a connected connector pairs means two
			// connectors that are forced out of the drawn circle because they
			// are too close together in angle.

			// First, find the start of a block of connected connectors

			if (icroot == -1)
				icstart = 0;
			else
				icstart = icroot;
			cp = lp.getConnection(icstart);
			count = 0;
			if (debug)
			{
				print("Now processing loop " + lp.getNumber());
				println("  " + lp);
			}
			done = false;
			do {
				j = icstart - 1;
				if (j < 0)
					j = lp.getNconnection() - 1;
				cpprev = lp.getConnection(j);
				if (!connected_connection(cpprev, cp)) {
					done = true;
				} else {
					icstart = j;
					cp = cpprev;
				}
				if (++count > lp.getNconnection()) {
					// Here everything is connected. Break on maximum angular
					// separation between connections.

					maxang = -1.0f;
					for (ic = 0; ic < lp.getNconnection(); ic++) {
						j = ic + 1;
						if (j >= lp.getNconnection())
							j = 0;
						cp = lp.getConnection(ic);
						cpnext = lp.getConnection(j);
						ac = cpnext.getAngle() - cp.getAngle();
						if (ac < 0.0f)
							ac += 2 * Math.PI;
						if (ac > maxang) {
							maxang = ac;
							imaxloop = ic;
						}
					}
					icend = imaxloop;
					icstart = imaxloop + 1;
					if (icstart >= lp.getNconnection())
						icstart = 0;
					cp = lp.getConnection(icend);
					cp.setBroken(true);
					done = true;
				}
			} while (!done);
			done_all_connections = false;
			icstart1 = icstart;
			if (debug)
				println("  Icstart1 = " + icstart1);
			while (!done_all_connections) {
				count = 0;
				done = false;
				icend = icstart;
				rooted = false;
				while (!done) {
					cp = lp.getConnection(icend);
					if (icend == icroot)
						rooted = true;
					j = icend + 1;
					if (j >= lp.getNconnection()) {
						j = 0;
					}
					cpnext = lp.getConnection(j);
					if (connected_connection(cp, cpnext)) {
						if (++count >= lp.getNconnection())
							break;
						icend = j;
					} else {
						done = true;
					}
				}
				icmiddle = find_ic_middle(icstart, icend, anchor_connection,
						acp, lp);
        if (!"".equals(radialAlgorithmException)) { return; }
				ic = icup = icdown = icmiddle;
				done = false;
				direction = 0;
				while (!done) {
					if (direction < 0) {
						ic = icup;
					} else if (direction == 0) {
						ic = icmiddle;
					} else {
						ic = icdown;
					}
					if (ic >= 0) {
						cp = lp.getConnection(ic);
						if (anchor_connection == null || acp != cp) {
							if (direction == 0) {
								astart = cp.getAngle()
										- Math.asin(1.0f / 2.0f / radius);
								aend = cp.getAngle()
										+ Math.asin(1.0f / 2.0f / radius);
								bases.get(cp.getStart()).setX(
										xc + radius * Math.cos(astart));
								bases.get(cp.getStart()).setY(
										yc + radius * Math.sin(astart));
								bases.get(cp.getEnd()).setX(
										xc + radius * Math.cos(aend));
								bases.get(cp.getEnd()).setY(
										yc + radius * Math.sin(aend));
							} else if (direction < 0) {
								j = ic + 1;
								if (j >= lp.getNconnection())
									j = 0;
								cp = lp.getConnection(ic);
								cpnext = lp.getConnection(j);
								cpx = cp.getXrad();
								cpy = cp.getYrad();
								ac = (cp.getAngle() + cpnext.getAngle()) / 2.0f;
								if (cp.getAngle() > cpnext.getAngle())
									ac -= Math.PI;
								cnx = Math.cos(ac);
								cny = Math.sin(ac);
								lnx = cny;
								lny = -cnx;
								da = cpnext.getAngle() - cp.getAngle();
								if (da < 0.0f)
									da += 2 * Math.PI;
								if (cp.isExtruded()) {
									if (da <= Math.PI / 2)
										rl = 2.0f;
									else
										rl = 1.5f;
								} else {
									rl = 1.0f;
								}
								bases.get(cp.getEnd()).setX(
										bases.get(cpnext.getStart()).getX()
												+ rl * lnx);
								bases.get(cp.getEnd()).setY(
										bases.get(cpnext.getStart()).getY()
												+ rl * lny);
								bases.get(cp.getStart()).setX(
										bases.get(cp.getEnd()).getX() + cpy);
								bases.get(cp.getStart()).setY(
										bases.get(cp.getEnd()).getY() - cpx);
							} else {
								j = ic - 1;
								if (j < 0)
									j = lp.getNconnection() - 1;
								cp = lp.getConnection(j);
								cpnext = lp.getConnection(ic);
								cpnextx = cpnext.getXrad();
								cpnexty = cpnext.getYrad();
								ac = (cp.getAngle() + cpnext.getAngle()) / 2.0f;
								if (cp.getAngle() > cpnext.getAngle())
									ac -= Math.PI;
								cnx = Math.cos(ac);
								cny = Math.sin(ac);
								lnx = -cny;
								lny = cnx;
								da = cpnext.getAngle() - cp.getAngle();
								if (da < 0.0f)
									da += 2 * Math.PI;
								if (cp.isExtruded()) {
									if (da <= Math.PI / 2)
										rl = 2.0f;
									else
										rl = 1.5f;
								} else {
									rl = 1.0f;
								}
								bases.get(cpnext.getStart()).setX(
										bases.get(cp.getEnd()).getX() + rl
												* lnx);
								bases.get(cpnext.getStart()).setY(
										bases.get(cp.getEnd()).getY() + rl
												* lny);
								bases.get(cpnext.getEnd()).setX(
										bases.get(cpnext.getStart()).getX()
												- cpnexty);
								bases.get(cpnext.getEnd()).setY(
										bases.get(cpnext.getStart()).getY()
												+ cpnextx);
							}
						}
					}
					if (direction < 0) {
						if (icdown == icend) {
							icdown = -1;
						} else if (icdown >= 0) {
							if (++icdown >= lp.getNconnection()) {
								icdown = 0;
							}
						}
						direction = 1;
					} else {
						if (icup == icstart)
							icup = -1;
						else if (icup >= 0) {
							if (--icup < 0) {
								icup = lp.getNconnection() - 1;
							}
						}
						direction = -1;
					}
					done = icup == -1 && icdown == -1;
				}
				icnext = icend + 1;
				if (icnext >= lp.getNconnection())
					icnext = 0;
				if (icend != icstart
						&& (!(icstart == icstart1 && icnext == icstart1))) {

					// Move the bases just constructed (or the radius) so that
					// the bisector of the end points is radius distance away
					// from the loop center.

					cp = lp.getConnection(icstart);
					cpnext = lp.getConnection(icend);
					dx = bases.get(cpnext.getEnd()).getX()
							- bases.get(cp.getStart()).getX();
					dy = bases.get(cpnext.getEnd()).getY()
							- bases.get(cp.getStart()).getY();
					midx = bases.get(cp.getStart()).getX() + dx / 2.0f;
					midy = bases.get(cp.getStart()).getY() + dy / 2.0f;
					rr = Math.sqrt(dx * dx + dy * dy);
					mx = dx / rr;
					my = dy / rr;
					vx = xc - midx;
					vy = yc - midy;
					rr = Math.sqrt(dx * dx + dy * dy);
					vx /= rr;
					vy /= rr;
					dotmv = vx * mx + vy * my;
					nrx = dotmv * mx - vx;
					nry = dotmv * my - vy;
					rr = Math.sqrt(nrx * nrx + nry * nry);
					nrx /= rr;
					nry /= rr;

					// Determine which side of the bisector the center should
					// be.

					dx = bases.get(cp.getStart()).getX() - xc;
					dy = bases.get(cp.getStart()).getY() - yc;
					ac = Math.atan2(dy, dx);
					if (ac < 0.0f)
						ac += 2 * Math.PI;
					dx = bases.get(cpnext.getEnd()).getX() - xc;
					dy = bases.get(cpnext.getEnd()).getY() - yc;
					acn = Math.atan2(dy, dx);
					if (acn < 0.0f)
						acn += 2 * Math.PI;
					if (acn < ac)
						acn += 2 * Math.PI;
					if (acn - ac > Math.PI)
						sign = -1;
					else
						sign = 1;
					nmidx = xc + sign * radius * nrx;
					nmidy = yc + sign * radius * nry;
					if (rooted) {
						xc -= nmidx - midx;
						yc -= nmidy - midy;
					} else {
						for (ic = icstart;;) {
							cp = lp.getConnection(ic);
							i = cp.getStart();
							bases.get(i).setX(
									bases.get(i).getX() + nmidx - midx);
							bases.get(i).setY(
									bases.get(i).getY() + nmidy - midy);
							i = cp.getEnd();
							bases.get(i).setX(
									bases.get(i).getX() + nmidx - midx);
							bases.get(i).setY(
									bases.get(i).getY() + nmidy - midy);
							if (ic == icend)
								break;
							if (++ic >= lp.getNconnection())
								ic = 0;
						}
					}
				}
				icstart = icnext;
				done_all_connections = icstart == icstart1;
			}
			for (ic = 0; ic < lp.getNconnection(); ic++) {
				cp = lp.getConnection(ic);
				j = ic + 1;
				if (j >= lp.getNconnection())
					j = 0;
				cpnext = lp.getConnection(j);
				dx = bases.get(cp.getEnd()).getX() - xc;
				dy = bases.get(cp.getEnd()).getY() - yc;
				rc = Math.sqrt(dx * dx + dy * dy);
				ac = Math.atan2(dy, dx);
				if (ac < 0.0f)
					ac += 2 * Math.PI;
				dx = bases.get(cpnext.getStart()).getX() - xc;
				dy = bases.get(cpnext.getStart()).getY() - yc;
				rcn = Math.sqrt(dx * dx + dy * dy);
				acn = Math.atan2(dy, dx);
				if (acn < 0.0f)
					acn += 2 * Math.PI;
				if (acn < ac)
					acn += 2 * Math.PI;
				dan = acn - ac;
				dcp = cpnext.getAngle() - cp.getAngle();
				if (dcp <= 0.0f)
					dcp += 2 * Math.PI;
				if (Math.abs(dan - dcp) > Math.PI) {
					if (cp.isExtruded()) {
						warningEmition("Warning from traverse_loop. Loop "
								+ lp.getNumber() + " has crossed regions\n");
            return;
					} else if ((cpnext.getStart() - cp.getEnd()) != 1) {
						cp.setExtruded(true);
						continue set_radius; // remplacement du goto
					}
				}
				if (cp.isExtruded()) {
					construct_extruded_segment(cp, cpnext);
          if (!"".equals(radialAlgorithmException)) { return; } // workaround exception for javascript
				} else {
					n = cpnext.getStart() - cp.getEnd();
					if (n < 0)
						n += nbase + 1;
					angleinc = dan / n;
					for (j = 1; j < n; j++) {
						i = cp.getEnd() + j;
						if (i > nbase)
							i -= nbase + 1;
						a = ac + j * angleinc;
						rr = rc + (rcn - rc) * (a - ac) / dan;
						bases.get(i).setX(xc + rr * Math.cos(a));
						bases.get(i).setY(yc + rr * Math.sin(a));
					}
				}
			}
			break;
		}
		for (ic = 0; ic < lp.getNconnection(); ic++) {
			if (icroot != ic) {
				cp = lp.getConnection(ic);
				generate_region(cp);
        if (!"".equals(radialAlgorithmException)) { return; } // workaround exception for javascript
				traverse_loop(cp.getLoop(), cp);
        if (!"".equals(radialAlgorithmException)) { return; } // workaround exception for javascript
			}
		}
		n = 0;
		sx = 0.0f;
		sy = 0.0f;
		for (ic = 0; ic < lp.getNconnection(); ic++) {
			j = ic + 1;
			if (j >= lp.getNconnection())
				j = 0;
			cp = lp.getConnection(ic);
			cpnext = lp.getConnection(j);
			n += 2;
			sx += bases.get(cp.getStart()).getX()
					+ bases.get(cp.getEnd()).getX();
			sy += bases.get(cp.getStart()).getY()
					+ bases.get(cp.getEnd()).getY();
			if (!cp.isExtruded()) {
				for (j = cp.getEnd() + 1; j != cpnext.getStart(); j++) {
					if (j > nbase)
						j -= nbase + 1;
					n++;
					sx += bases.get(j).getX();
					sy += bases.get(j).getY();
				}
			}
		}
		lp.setX(sx / n);
		lp.setY(sy / n);
	}

	/**
	 * For the loop pointed to by lp, determine the radius of the loop that will
	 * ensure that each base around the loop will have a separation of at least
	 * lencut around the circle. If a segment joining two connectors will not
	 * support this separation, then the flag, extruded, will be set in the
	 * first of these two indicators. The radius is set in lp.
	 *
	 * The radius is selected by a least squares procedure where the sum of the
	 * squares of the deviations of length from the ideal value of 1 is used as
	 * the error function.
	 */
	private void determine_radius(Loop lp, double lencut) {
		if (debug)
			println("  Determine_radius");
		double mindit, ci, dt, sumn, sumd, radius, dit;
		int i, j, end, start, imindit = 0;
		Connection cp = new Connection(), cpnext = new Connection();
		double rt2_2 = 0.7071068f;

		do {
			mindit = 1.0e10f;
			for (sumd = 0.0f, sumn = 0.0f, i = 0; i < lp.getNconnection(); i++) {
				cp = lp.getConnection(i);
				j = i + 1;
				if (j >= lp.getNconnection())
					j = 0;
				cpnext = lp.getConnection(j);
				end = cp.getEnd();
				start = cpnext.getStart();
				if (start < end)
					start += nbase + 1;
				dt = cpnext.getAngle() - cp.getAngle();
				if (dt <= 0.0f)
					dt += 2 * Math.PI;
				if (!cp.isExtruded())
					ci = start - end;
				else {
					if (dt <= Math.PI / 2)
						ci = 2.0f;
					else
						ci = 1.5f;
				}
				sumn += dt * (1.0f / ci + 1.0f);
				sumd += dt * dt / ci;
				dit = dt / ci;
				if (dit < mindit && !cp.isExtruded() && ci > 1.0f) {
					mindit = dit;
					imindit = i;
				}
			}
			radius = sumn / sumd;
			if (radius < rt2_2)
				radius = rt2_2;
			if (mindit * radius < lencut) {
				lp.getConnection(imindit).setExtruded(true);
			}
		} while (mindit * radius < lencut);
		if (lp.getRadius() > 0.0f)
			radius = lp.getRadius();
		else
			lp.setRadius(radius);
	}

	/**
	 * Determines if the connections cp and cpnext are connected
	 */
	private boolean connected_connection(Connection cp, Connection cpnext) {
		if (debug)
			println("  Connected_connection");
		if (cp.isExtruded()) {
			return true;
		} else if (cp.getEnd() + 1 == cpnext.getStart()) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Finds the middle of a set of connected connectors. This is normally the
	 * middle connection in the sequence except if one of the connections is the
	 * anchor, in which case that connection will be used.
	 */
	private int find_ic_middle(int icstart, int icend,
			Connection anchor_connection, Connection acp, Loop lp){
		if (debug)
			println("  Find_ic_middle");
		int count, ret, ic, i;
		boolean done;

		count = 0;
		ret = -1;
		ic = icstart;
		done = false;
		while (!done) {
			if (count++ > lp.getNconnection() * 2) {
				println("Infinite loop detected in find_ic_middle");
        radialAlgorithmException = "Infinite loop detected in find_ic_middle";
        return -1;
			}
			if (anchor_connection != null && lp.getConnection(ic) == acp) {
				ret = ic;
			}
			done = ic == icend;
			if (++ic >= lp.getNconnection()) {
				ic = 0;
			}
		}
		if (ret == -1) {
			for (i = 1, ic = icstart; i < (count + 1) / 2; i++) {
				if (++ic >= lp.getNconnection())
					ic = 0;
			}
			ret = ic;
		}
		return ret;
	}

	/**
	 * Generates the coordinates for the base pairing region of a connection
	 * given the position of the starting base pair.
	 */
	private void generate_region(Connection cp) {
		if (debug) {
			println("  Generate_region");
    }
		int l, start, end, i, mate;
		Region rp;

		rp = cp.getRegion();
		l = 0;
		if (cp.getStart() == rp.getStart1()) {
			start = rp.getStart1();
			end = rp.getEnd1();
		} else {
			start = rp.getStart2();
			end = rp.getEnd2();
		}
		if (bases.get(cp.getStart()).getX() > ANUM - 100.0f
				|| bases.get(cp.getEnd()).getX() > ANUM - 100.0f) {
			println("Bad region passed to generate_region. Coordinates not defined.");
      radialAlgorithmException = "Bad region passed to generate_region. Coordinates not defined.";
      return;
		}
		for (i = start + 1; i <= end; i++) {
			l++;
			bases.get(i).setX(
					bases.get(cp.getStart()).getX() + HELIX_FACTOR * l
							* cp.getXrad());
			bases.get(i).setY(
					bases.get(cp.getStart()).getY() + HELIX_FACTOR * l
							* cp.getYrad());
			mate = bases.get(i).getMate();
			bases.get(mate).setX(
					bases.get(cp.getEnd()).getX() + HELIX_FACTOR * l
							* cp.getXrad());
			bases.get(mate).setY(
					bases.get(cp.getEnd()).getY() + HELIX_FACTOR * l
							* cp.getYrad());

		}
	}

	/**
	 * Draws the segment of residue between the bases numbered start through
	 * end, where start and end are presumed to be part of a base pairing
	 * region. They are drawn as a circle which has a chord given by the ends of
	 * two base pairing regions defined by the connections.
	 */
	private void construct_circle_segment(int start, int end){
		if (debug)
			println("  Construct_circle_segment");
		double dx, dy, rr, midx, midy, xn, yn, nrx, nry, mx, my, a;
		int l, j, i;

		dx = bases.get(end).getX() - bases.get(start).getX();
		dy = bases.get(end).getY() - bases.get(start).getY();
		rr = Math.sqrt(dx * dx + dy * dy);
		l = end - start;
		if (l < 0)
			l += nbase + 1;
		if (rr >= l) {
			dx /= rr;
			dy /= rr;
			for (j = 1; j < l; j++) {
				i = start + j;
				if (i > nbase)
					i -= nbase + 1;
				bases.get(i).setX(
						bases.get(start).getX() + dx * (double) j / (double) l);
				bases.get(i).setY(
						bases.get(start).getY() + dy * (double) j / (double) l);
			}
		} else {
			find_center_for_arc((l - 1), rr);
      if (!"".equals(radialAlgorithmException)) { return; } // workaround exception for javascript
			dx /= rr;
			dy /= rr;
			midx = bases.get(start).getX() + dx * rr / 2.0f;
			midy = bases.get(start).getY() + dy * rr / 2.0f;
			xn = dy;
			yn = -dx;
			nrx = midx + _h * xn;
			nry = midy + _h * yn;
			mx = bases.get(start).getX() - nrx;
			my = bases.get(start).getY() - nry;
			rr = Math.sqrt(mx * mx + my * my);
			a = Math.atan2(my, mx);
			for (j = 1; j < l; j++) {
				i = start + j;
				if (i > nbase)
					i -= nbase + 1;
				bases.get(i).setX(nrx + rr * Math.cos(a + j * angleinc));
				bases.get(i).setY(nry + rr * Math.sin(a + j * angleinc));
			}
		}
	}

	/**
	 * Constructs the segment between cp and cpnext as a circle if possible.
	 * However, if the segment is too large, the lines are drawn between the two
	 * connecting regions, and bases are placed there until the connecting
	 * circle will fit.
	 */
	private void construct_extruded_segment(Connection cp, Connection cpnext){
		if (debug) {
			println("  Construct_extruded_segment");
    }
		double astart, aend1, aend2, aave, dx, dy, a1, a2, ac, rr, da, dac;
		int start, end, n, nstart, nend;
		boolean collision;

		astart = cp.getAngle();
		aend2 = aend1 = cpnext.getAngle();
		if (aend2 < astart)
			aend2 += 2 * PI;
		aave = (astart + aend2) / 2.0f;
		start = cp.getEnd();
		end = cpnext.getStart();
		n = end - start;
		if (n < 0)
			n += nbase + 1;
		da = cpnext.getAngle() - cp.getAngle();
		if (da < 0.0f) {
			da += 2 * PI;
		}
		if (n == 2)
			construct_circle_segment(start, end);
      if (!"".equals(radialAlgorithmException)) { return; } // workaround exception for javascript
		else {
			dx = bases.get(end).getX() - bases.get(start).getX();
			dy = bases.get(end).getY() - bases.get(start).getY();
			rr = Math.sqrt(dx * dx + dy * dy);
			dx /= rr;
			dy /= rr;
			if (rr >= 1.5f && da <= Math.PI / 2) {
				nstart = start + 1;
				if (nstart > nbase)
					nstart -= nbase + 1;
				nend = end - 1;
				if (nend < 0)
					nend += nbase + 1;
				bases.get(nstart).setX(bases.get(start).getX() + 0.5f * dx);
				bases.get(nstart).setY(bases.get(start).getY() + 0.5f * dy);
				bases.get(nend).setX(bases.get(end).getX() - 0.5f * dx);
				bases.get(nend).setY(bases.get(end).getY() - 0.5f * dy);
				start = nstart;
				end = nend;
			}
			do {
				collision = false;
				construct_circle_segment(start, end);
        if (!"".equals(radialAlgorithmException)) { return; } // workaround exception for javascript
				nstart = start + 1;
				if (nstart > nbase)
					nstart -= nbase + 1;
				dx = bases.get(nstart).getX() - bases.get(start).getX();
				dy = bases.get(nstart).getY() - bases.get(start).getY();
				a1 = Math.atan2(dy, dx);
				if (a1 < 0.0f)
					a1 += 2 * Math.PI;
				dac = a1 - astart;
				if (dac < 0.0f)
					dac += 2 * Math.PI;
				if (dac > Math.PI)
					collision = true;
				nend = end - 1;
				if (nend < 0)
					nend += nbase + 1;
				dx = bases.get(nend).getX() - bases.get(end).getX();
				dy = bases.get(nend).getY() - bases.get(end).getY();
				a2 = Math.atan2(dy, dx);
				if (a2 < 0.0f)
					a2 += 2 * Math.PI;
				dac = aend1 - a2;
				if (dac < 0.0f)
					dac += 2 * Math.PI;
				if (dac > Math.PI)
					collision = true;
				if (collision) {
					ac = minf2(aave, astart + 0.5f);
					bases.get(nstart).setX(
							bases.get(start).getX() + Math.cos(ac));
					bases.get(nstart).setY(
							bases.get(start).getY() + Math.sin(ac));
					start = nstart;
					ac = maxf2(aave, aend2 - 0.5f);
					bases.get(nend).setX(bases.get(end).getX() + Math.cos(ac));
					bases.get(nend).setY(bases.get(end).getY() + Math.sin(ac));
					end = nend;
					n -= 2;
				}
			} while (collision && n > 1);
		}
	}

	/**
	 * Given n points to be placed equidistantly and equiangularly on a polygon
	 * which has a chord of length, b, find the distance, h, from the midpoint
	 * of the chord for the center of polygon. Positive values mean the center
	 * is within the polygon and the chord, whereas negative values mean the
	 * center is outside the chord. Also, the radial angle for each polygon side
	 * is returned in theta.
	 *
	 * The procedure uses a bisection algorithm to find the correct value for
	 * the center. Two equations are solved, the angles around the center must
	 * add to 2*Math.PI, and the sides of the polygon excluding the chord must
	 * have a length of 1.
	 */
	private void find_center_for_arc(double n, double b){
		if (debug) {
			println("  Find_center_for_arc");
    }
		double h, hhi, hlow, r, disc, theta, e, phi;
		int iter;

		hhi = (n + 1.0f) / Math.PI;
		// changed to prevent div by zero if (ih)
		hlow = -hhi - b / (n + 1.000001f - b);
		if (b < 1)
			// otherwise we might fail below (ih)
			hlow = 0;
		iter = 0;
		do {
			h = (hhi + hlow) / 2.0f;
			r = sqrt((float)((h * h) + ((b * b) / 4.0f)));
			// if (r<0.5) {r = 0.5; h = 0.5*Math.sqrt(1-b*b);}
			disc = 1.0f - 0.5f / ((h * h) + ((b * b) / 4.0f));
			if (abs((float) disc) > 1.0f) {
				println("WARNING! Unexpectedly large magnitude discriminant = " + disc + " r: " + r);

        if (disc < -1) {
          disc = -1;
          println("WARNING! forcing disc to be -1 from less than -1");
        }
        //radialAlgorithmException = "Unexpected large magnitude discriminant = " + disc + " " + r;
        //return;
			}
			theta = Math.acos(disc);
			// theta = 2*Math.acos(Math.sqrt(1-1/(4*r*r)));
			phi = Math.acos(h / r);
			e = theta * (n + 1) + 2 * phi - 2 * Math.PI;
			if (e > 0.0f) {
				hlow = h;
			} else {
				hhi = h;
			}
		} while (Math.abs(e) > 0.0001f && ++iter < MAXITER);
		if (iter >= MAXITER) {
			if (noIterationFailureYet) {
        noIterationFailureYet = false;
				warningEmition("Iteration failed in find_center_for_arc");
				return;
			}
			h = 0.0f;
			theta = 0.0f;
		}
		_h = h;
		angleinc = theta;
	}

	private double minf2(double x1, double x2) {
		return ((x1) < (x2)) ? (x1) : (x2);
	}

	private double maxf2(double x1, double x2) {
		return ((x1) > (x2)) ? (x1) : (x2);
	}

	public void warningEmition(String warningMessage) {
		radialAlgorithmException = warningMessage;
    println(warningMessage);
	}
}
public class Radloop {
	private double radius;
	private int loopnumber;
	private Radloop next, prev;

	public double getRadius() {
		return radius;
	}

	public void setRadius(double radius) {
		this.radius = radius;
	}

	public int getLoopnumber() {
		return loopnumber;
	}

	public void setLoopnumber(int loopnumber) {
		this.loopnumber = loopnumber;
	}

	public Radloop getNext() {
		return next;
	}

	public void setNext(Radloop next) {
		this.next = next;
	}

	public Radloop getPrev() {
		return prev;
	}

	public void setPrev(Radloop prev) {
		this.prev = prev;
	}
}
public class Region {
	private int _start1, _end1, _start2, _end2;

	public int getStart1() {
		return _start1;
	}

	public void setStart1(int start1) {
		this._start1 = start1;
	}

	public int getEnd1() {
		return _end1;
	}

	public void setEnd1(int end1) {
		this._end1 = end1;
	}

	public int getStart2() {
		return _start2;
	}

	public void setStart2(int start2) {
		this._start2 = start2;
	}

	public int getEnd2() {
		return _end2;
	}

	public void setEnd2(int end2) {
		this._end2 = end2;
	}
}
class SimulationSetup {
  String seqTot; // String of all bases
  int lenTot; // Total number of bases
  int[] starts; // Start index of each strand (starting on 0)
  String[] sequences; // String of each strand's bases
  int[] seqIds;  // Strand ID of each base
  ArrayList<int[]> foldSteps = new ArrayList<int[]>();
  int[] strandOrder; // Optimal order of strands to minimize knotting
  int[] residueOrder; // New residue order based off of strandOrder
  Short[] pairTable; // Holds index of base paired with the baseId at each index (-1 if unpaired)
  ArrayList<Short> pairTableOptimal = new ArrayList<Short>(); // Stores the position in the new order that each newly ordered residue is paired with
  //ArrayList<String> nonCanonicals = new ArrayList<String>(); // example: "id1 id2 cHS"
  HashMap<String, String> nonCanonicals = new HashMap<String, String>(); // "id1 id2", "cww" ID1 ALWAYS LESS THAN ID2!
  String errorMessage = "";
  String warningMessage = "";
  
  public SimulationSetup cloneFunction() {
    SimulationSetup result = new SimulationSetup();
    result.nonCanonicals.putAll(this.nonCanonicals);
    result.pairTableOptimal.addAll(this.pairTableOptimal);
    result.seqTot = this.seqTot;
    result.lenTot = this.lenTot;
    result.starts = intArrayClone(this.starts);
    result.sequences = stringArrayClone(this.sequences);
    result.seqIds = intArrayClone(this.seqIds);
    result.foldSteps.addAll(this.foldSteps);
    result.strandOrder = intArrayClone(this.strandOrder);
    result.residueOrder = intArrayClone(this.residueOrder);
    result.pairTable = shortArrayClone(this.pairTable);
    result.errorMessage = this.errorMessage;
    result.warningMessage = this.warningMessage;
    
    return result;
  }
  
  /** Determines file type, calls initFileType */
  public SimulationState initFile(String fileName, String fileType) {
    if (!BROWSER) {
      if (debugPrints) { println("\nStarting SimulationSetup.initFile for input file " + fileName); }
      
      File loadFile = new File(fileName);
      if (!loadFile.isFile()) {
        println("Could not find file with name " + fileName); 
        errorMessage = "File  " + fileName + "  could not be loaded";
        return new SimulationState();
      }
      
      String userFileName = userFile.getName();
      surface.setTitle(userFileName); // Set the title of the window
      
      if (fileType == null) { // default
        fileType = "";
        for (int i = userFileName.length(); i > 0; --i) {
          if ( ".".equals(userFileName.substring(i-1, i)) ) {
            if (i < userFileName.length()) {
              fileType = userFileName.substring(i);
            }
            break;
          }
        }
      }
      if (debugPrints) { println("File type is " + fileType); }
      return initFileType(fileName, fileType);
    }
    else {
      //println("\nStarting SimulationSetup.initFile in browser!");
      if (javascript != null) {
        if (fileType == null) { // default
          fileType = javascript.getFileTypeBrowser();
        }
        if (debugPrints) { println("File type is " + fileType); }
        return initFileType(null, fileType);
      }
      else {
        if (debugPrints) { println("JAVASCRIPT NOT BOUND!"); }
        errorMessage = "Error in communication with web browser.";
        return new SimulationState();
      }
    }
  }
  
  
  /** Determines which file parser to call, and which setup method */
  public SimulationState initFileType(String fileName, String fileType) {
    if ("ct".equals(fileType) || "bpseq".equals(fileType)) {
      initColumnFile(fileName, fileType);
    }
    else if ("dbn".equals(fileType)) {
      initBracketFile(fileName);
    }
    else if ("rs".equals(fileType)) {
      SimulationState simState = initSaveFile(fileName);
      if ("".equals(errorMessage)) {
        checkLengthForRigidLoops = false;
        return simState;
      }
      else {
        if (debugPrints) {
          println("InitFileException caught from rs file type in initFile:");
          println(errorMessage);
        }
        return new SimulationState();
      }
    }
    else if ("nts".equals(fileType)) {
      initNtsPairs(fileName);
    }
    else if ("seq".equals(fileType)) {
      initSequenceFile(fileName);
    }
    else {
      // Prompt user to select file type
      String[] fileTypes = {"ct", "bpseq", "dbn", "rs", "nts", "seq"};
      int itemHeight = 40;
      float listHeight = fileTypes.length * itemHeight;
      float yStart = 100;
      if (listHeight + yStart > (height - 135)) {
        listHeight = PApplet.parseInt((height - 135 - yStart)/itemHeight) * itemHeight;
        if (listHeight < itemHeight) {
          listHeight = (float)itemHeight;
        }
      }
      fileTypeSelector = new Listbox(30, yStart, 150, listHeight, itemHeight, fileTypes, ++displaySelectorId);
      selectingFileType = true;
      if (debugPrints) { println("Please input file type"); }
      errorMessage += "Could not identify file type automatically";
      return new SimulationState();
    }
    
    if ("".equals(errorMessage)) {
      initTables();
      
      if (debugPrints) { setupDebug(); }
      ds = new DisplaySettings();
      if (lenTot > 1300) {
        ds.labelMode = false;
        if (lenTot > 2500) {
          ds.outlineMode = false;
        }
      }
      
      SphereList spheres = this.initDefSphereList();
      return createRadialState(spheres);
    }
    else {
      if (debugPrints) {
        println("InitFileException caught in initFile:");
        println(errorMessage);
      }
      return new SimulationState();
    }
  }
  
  public void initFoldSteps() {
    ArrayList<int[]> pairs = new ArrayList<int[]>();
    for (int i = 0; i < pairTable.length; ++i) {
      int pairId = pairTable[i];
      if (pairId > i) {
        int[] pair = new int[2];
        pair[0] = i;
        pair[1] = pairId;
        pairs.add(pair);
      }
    }
    foldSteps = pairs;
  }
  
  public void initTables() {
    initFoldSteps();
    strandOrder = orderStrands();
    residueOrder = initOrder(this);
    generatePairTableOptimal();
  }
  
  public SphereList initDefSphereList() { // Outputs the base spherelist without coordinates
    SphereList sl = new SphereList();
    for (int i = 0; i < lenTot; ++i) {
      sl.add(new Sphere2D(i, seqIds[i], seqTot.substring(i, i+1), new String[lenTot]));
      
      if ((i > 0) && (seqIds[i-1] == seqIds[i])) {  // connect them if they are in the same strand
        sl.get(i-1).threeP = sl.get(i);
        sl.get(i).fiveP = sl.get(i-1);
      }
    }
    
    // Initialize relative IDs
    int currStrandId = sl.get(0).strandId;
    int idOffset = 0;
    for (int i = 0; i < lenTot; ++i) {
      int strandId = sl.get(i).strandId;
      if (strandId != currStrandId) {
        idOffset = i;
        currStrandId = strandId;
      }
      int relId = i - idOffset;
      sl.get(i).relId = relId;
    }
    
    // Initialize bond arrays based on pair table and nonCanonicals
    for (int i = 0; i < pairTable.length; ++i) {
      int pairId = pairTable[i];
      if (pairId > i) {
        String type = "cWW";
        if (nonCanonicals.containsKey(i + " " + pairId)) {
          type = nonCanonicals.get(i + " " + pairId);
        }
        setForcePair(sl.get(i), sl.get(pairId), type, this);
      }
    }
    for (Map.Entry<String, String> nc : nonCanonicals.entrySet()) {
      String[] pair = nc.getKey().split(" ");
      int id1 = PApplet.parseInt(pair[0]);
      int id2 = PApplet.parseInt(pair[1]);
      String bondType = nc.getValue();
      sl.get(id1).bonds[id2] = bondType;
      sl.get(id2).bonds[id1] = bondType;
    }

    return sl;
  }
  
  
  /** Parses ct or bpseq file. */
  public void initColumnFile(String fileName, String fileType) {
    int numColumns = 6;
    int pairedCol = 4;
    if ("bpseq".equals(fileType)) {
      numColumns = 3;
      pairedCol = 2;
    }
    String[] lines;
    
    if (!BROWSER) {
      lines = loadStrings(fileName);
    }
    else {
      if (javascript != null) {
        String inputText = javascript.getTextInput("inputtext");
        lines = inputText.split("\\r\\n|\\n|\\r");
      }
      else {
        if (debugPrints) { println("ERROR: JAVASCRIPT NOT INITIALIZED!"); }
        errorMessage = "ERROR: JAVASCRIPT NOT INITIALIZED!";
        return;
      }
    }
    
    // Take out extra whitespace
    for (int i = 0; i < lines.length; ++i) {
      lines[i] = lines[i].trim().replaceAll("\\s+", " ");
    }
    
    // First line not preceded by # is header
    String[] header = null;
    int headerIndex = -1;
    for (int i = 0; i < lines.length; ++i) {
      if (!lines[i].equals("") && !lines[i].substring(0, 1).equals("#")) {
        header = split(lines[i], " ");
        headerIndex = i;
        break;
      }
    }
    if (header == null) {
      if (debugPrints) { println("No valid lines"); }
      errorMessage = "No valid lines!";
      return;
    }
    // If the first line is data
    if (header.length == numColumns && header[0].matches("^\\d+$") && PApplet.parseInt(header[0]) == 1 && header[1].matches("[a-zA-Z]") && header[pairedCol].matches("^\\d+$")) {
      if ( "bpseq".equals(fileType) || ("ct".equals(fileType) && header[2].matches("^\\d+$") && header[3].matches("^\\d+$") && header[5].matches("^\\d+$")) ) {
        header = null;
        headerIndex--;
      }
    }
    if (debugPrints) { println("headerIndex: " + headerIndex); }
    
    int numSeqs = 1; // Number of strands
    starts = new int[1]; // Array of start index of each strand (starting on 0)
    starts[0] = 0;
    sequences = new String[1];
    int dataLength = lines.length - (headerIndex + 1);
    if (debugPrints) { println("dataLength: " + dataLength); }
    boolean multipleStrands = false;
    
    // Check to see if it is in Shapiro Lab header format
    if (header != null && header.length > 3) {
      // Check if all arguments are numbers
      boolean flag = false;
      for (String argument : header) {
        if (!(argument.matches("^\\d+$"))) {
          flag = true;
          break;
        }
      }
      // Check if second argument equals number of strand start arguments, and first strand starts on 1
      if ((!flag) && PApplet.parseInt(header[1]) == (header.length - 2) && PApplet.parseInt(header[2]) == 1) {
        numSeqs = PApplet.parseInt(header[1]);
        starts = new int[numSeqs];
        starts[0] = 0;
        
        // Check if strand start arguments are in ascending order
        flag = false;
        for (int i = 1; i < numSeqs; ++i) { // for each strand
          if (PApplet.parseInt(header[i+2]) > PApplet.parseInt(header[i+1])) {
            starts[i] = PApplet.parseInt(header[i+2]) - 1; // set start
          } else {
            flag = true;
            if (debugPrints) { println("The strand start indexes (arguments 3 and up in header) must be in ascending order!"); }
            break;
          }
        }
        
        // Check if first and last arguments are in-bounds and compatible with the other arguments
        if (!flag) {
          int arg1 = PApplet.parseInt(header[0]);
          if (arg1 >= 2 && (arg1 <= dataLength) && starts[starts.length-1] < arg1 && arg1 >= numSeqs) {
            multipleStrands = true;
            sequences = new String[numSeqs];
          }
        }
      }
    }
    if (debugPrints && multipleStrands) { println("multipleStrands = true"); }
    
    if (!multipleStrands) {
      numSeqs = 1;
      starts = new int[1];
      starts[0] = 0;
      sequences = new String[1];
    }
    
    /**
    // Sets lenInHeader to header value if it is valid. If not, sets to default if not multiple strands, or throws error if there are multiple strands.
    int dataLength;
    if (header != null && header[0].matches("^\\d+$")) {
      dataLength = int(header[0]);
      if (dataLength < 1 || dataLength >= foldSteps.length - headerIndex) {
        if (debugPrints) { println("Header out of bounds"); }
        if (multipleStrands) {
          errorMessage = "\"Total length\" argument (first # in the header) is out of bounds!";
          return;
        } else {
          dataLength = foldSteps.length - (headerIndex + 1);
        }
      }
    } else {
      dataLength = foldSteps.length - (headerIndex + 1);
    }
    
    if (multipleStrands && starts[starts.length-1] >= dataLength) {
      if (debugPrints) {
        println("Last strand start (last argument in header) is greater than the length of the sequence (first argument), which is impossible!");
      }
      multipleStrands = false;
    }

    if (multipleStrands && header != null) {
      numSeqs = int(header[1]);
      
      // Check to see if numSeqs matches second argument
      int validNums = 2;
      for (int i = 4; i < header.length; i++) {
        if (!header[i].matches("^\\d+$")) { break; }
        validNums++;
      }
      if (numSeqs != validNums) {
        if (debugPrints) {
          println("\"Number of strands\" argument (2nd number in the file header) is not equal to the amount of strand starts provided!");
        }
        errorMessage = "\"Number of strands\" (2nd argument in the file header) is not equal to the amount of strand start indexes provided!\n\n";
        return;
      }
      if (int(header[2]) != 1) {
        if (debugPrints) { println("The first strand start index (3rd argument in the file header) must be \"1\" by nature!"); }
        errorMessage = "The first strand start index (3rd argument in the file header) must be \"1\" by nature!";
        return;
      }
    } 
    
    if (multipleStrands) {
      if (debugPrints) { println("The number of strands is " + str(numSeqs)); }
      
      // Check to see if last start is in bounds
      if (starts[starts.length-1] >= dataLength) {
        if (debugPrints) {
          println("Last strand start (last argument in header) is greater than the length of the sequence (first argument), which is impossible!");
        }
        errorMessage = "Last strand start (last argument in header) is greater than the length of the sequence (first argument), which is impossible!";
        return;
      }
    }
    */
    
    String[] columns;
    seqTot = ""; // String of all bases
    ArrayList<Short> pairedIdCol = new ArrayList<Short>();
    
    // Native header format
    if ("bpseq".equals(fileType) || multipleStrands) {
      int index = headerIndex + 1; // index in actual file
      boolean endOfData = false;
      for (int i = 0; i < numSeqs; ++i) { // for each strand
        if (endOfData) { break; }
        int endIndex; // end id for strand in the strucutre 
        if (i+1 < starts.length) {
          endIndex = starts[i+1];
        } else { // Last strand
          endIndex = dataLength;
        }
        sequences[i] = "";
        
        for (int j = starts[i]; j < endIndex; index++) { // index is actual index, j is the # of found valid lines
          if (index >= lines.length) { // out of bounds, so break
            if (debugPrints) { println("End of file"); }
            break;
          }
          
          if (lines[index].equals("") || lines[index].substring(0, 1).equals("#")) { // Ignore empty lines and comments
            continue;
          }
          
          columns = split(lines[index], " ");
          if (columns.length != numColumns || !columns[0].matches("^\\d+$") || (PApplet.parseInt(columns[0]) != j+1) || !columns[1].matches("[a-zA-Z]") || columns[1].length() != 1 || !columns[pairedCol].matches("^\\d+$") || PApplet.parseInt(columns[pairedCol]) > 32765) {
            if (j < dataLength) {
              if (debugPrints) { println("CAUTION, INVALID LINE: " + str(index+1)); }
              warningMessage += "Caution: File line " + str(index + 1) + " (info for base " + str(j + 1) + ") is invalid.\n";
            }
            endOfData = true;
            break;
          }
          
          j++; // found valid line
          seqTot += columns[1];
          sequences[i] += columns[1];
          int pairedId = PApplet.parseInt(columns[pairedCol]);
          
          pairedIdCol.add( (short) (pairedId - 1) );
        }
      }
    }
    
    // Looking for 0s in the 3rd column, which some labs use to mark a new strand start.
    else {
      if (debugPrints) { println("Looking for strand starts as 0s in the 3rd column of ct"); }
      ArrayList<String> sequencesList = new ArrayList<String>();
      ArrayList<Integer> startsList = new ArrayList<Integer>();
      int baseCount = 0;
      String currentSeq = "";
      
      for (int i = headerIndex+1; i < lines.length && baseCount < dataLength; i++) {
        if (lines[i].equals("") || lines[i].substring(0, 1).equals("#")) { // Ignore empty lines and comments
          continue;
        }
        
        columns = split(lines[i], " ");
        if (columns.length != numColumns || !columns[0].matches("^\\d+$") || (PApplet.parseInt(columns[0]) != baseCount+1) || !columns[1].matches("[a-zA-Z]") || columns[1].length() != 1 || !columns[pairedCol].matches("^\\d+$") || PApplet.parseInt(columns[pairedCol]) > 32765 || !columns[2].matches("^\\d+$") || !columns[3].matches("^\\d+$")) {
          if (baseCount < dataLength) {
            if (debugPrints) { println("CAUTION: INVALID LINE: " + str(i+1)); }
            warningMessage += "Caution: File line " + str(i+1) + " (info for base " + str(seqTot.length() + 1) + ") is invalid.\n";
          }
          break;
        }
        
        pairedIdCol.add( (short) (PApplet.parseInt(columns[pairedCol]) - 1) );
        
        if ("0".equals(columns[2])) {
          startsList.add(baseCount);
          if (baseCount != 0) {
            sequencesList.add(currentSeq);
            currentSeq = "";
          }
        }
        
        currentSeq += columns[1];
        seqTot += columns[1];
        
        baseCount++;
      }
      sequencesList.add(currentSeq);
      
      // copy to starts
      starts = new int[startsList.size()];
      Iterator<Integer> iterator = startsList.iterator();
      for (int i = 0; i < starts.length; i++) {
        starts[i] = iterator.next(); // Removed .intValue()
      }
      
      sequences = sequencesList.toArray(new String[sequencesList.size()]); // copy sequences
      
      numSeqs = starts.length;
    }
    
    pairTable = pairedIdCol.toArray(new Short[pairedIdCol.size()]); // copy
    
    if (debugPrints) { println("Number of pairs: " + lines.length); }
    
    lenTot = seqTot.length();
    
    if (lenTot <= 0 || starts.length == 0) {
      errorMessage = "No valid lines found!";
      return;
    }
    
    if ( starts[starts.length-1] >= lenTot || (multipleStrands && header != null && lenTot != PApplet.parseInt(header[0])) ) {
      if (debugPrints) { println("First arg incorrect"); }
      warningMessage += "Caution: The first argument in the file header (total sequence length) does not equal the # of valid lines.\n\n";
      numSeqs = 1;
      starts = new int[1];
      starts[0] = 0;
      sequences = new String[1];
      sequences[0] = seqTot;
      multipleStrands = false;
    }
    
    checkPairTable(pairTable, lenTot);
    if (!"".equals(errorMessage)) { return; }
    
    // Creates int array seqIds that stores the strand number (starting at 0) for each residue starting at index 0
    seqIds = new int[lenTot];
    int sId = 0; // Strand ID
    int pc = 0; // residue index
    for (int i = 0; i < numSeqs; ++i) { // each strand
      for (int j = 0; j < sequences[i].length(); ++j) { // for the length of each strand
        seqIds[pc] = sId;
        pc++;
      }
      sId++;
    }
  }
  
  
  /** Parses dot bracket notation file */
  public void initBracketFile(String fileName) {
    String[] lines;
  
    if (!BROWSER) {
      lines = loadStrings(fileName);
    }
    else {
      if (javascript != null) {
        String inputText = javascript.getTextInput("inputtext");
        lines = inputText.split("\\r\\n|\\n|\\r");
      }
      else {
        if (debugPrints) { println("ERROR: JAVASCRIPT NOT INITIALIZED!"); }
        errorMessage = "ERROR: JAVASCRIPT NOT INITIALIZED!";
        return;
      }
    }
    
    if (debugPrints) {
      println("Raw input:");
      for (String s : lines) {
        println(s);
      }
    }
    
    // Take out extra whitespace
    for (int i = 0; i < lines.length; ++i) {
      lines[i] = lines[i].trim().replaceAll("\\s+", " ");
    }
    
    int sequenceIndex = -1;
    int structureIndex = -1;
    
    for (int i = 0; i < lines.length; ++i) {
      String line = lines[i];
      if (line.length() > 0) {
        String seqChar = line.substring(0, 1);
        if ( (!BROWSER && line.matches("[a-zA-Z& ]+")) || (BROWSER && !(seqChar.equals("#") || seqChar.equals(" ") || seqChar.equals(">"))) ) {
          sequenceIndex = i;
          for (int j = i+1; j < lines.length; ++j) {
            if (lines[j].length() < 1) { continue; }
            String startChar = lines[j].substring(0,1);
            if ( !(startChar.equals("#") || startChar.equals(" ")) ) {
              structureIndex = j;
            }
          }
          break;
        }
      }
    }
    
    if (debugPrints) {
      println("sequenceIndex: " + sequenceIndex);
      println("structureIndex: " + structureIndex);
    }
  
    // Assert both indices in bounds
    if (sequenceIndex < 0 || sequenceIndex >= lines.length-1 || structureIndex < 1 || structureIndex >= lines.length) {
      errorMessage = "Could not read input \"dot bracket\" file";
      return;
    }
    
    String sequenceInput = lines[sequenceIndex];
    String structureInput = lines[structureIndex];
    
    ArrayList<Integer> startsList = new ArrayList<Integer>();
    startsList.add(0);
  
    // Build strings without spaces or ampersands
    seqTot = "";
    for (int i = 0, l = 0; i < sequenceInput.length(); ++i) {
      String currentChar = sequenceInput.substring(i, i+1);
      if ( !(" ".equals(currentChar) || "&".equals(currentChar)) ) {
        seqTot += currentChar;
        ++l;
        if (l > 1) {
          String prevChar = sequenceInput.substring(i-1, i);
          if (" ".equals(prevChar) || "&".equals(prevChar)) {
            startsList.add(l-1);
          }
        }
      }
    }
    
    // copy
    starts = new int[startsList.size()];
    Iterator<Integer> iterator = startsList.iterator();
    for (int i = 0; i < starts.length; i++) {
      starts[i] = iterator.next(); // Removed .intValue()
    }
    
    // Build structure string without spaces or ampersands
    String catStruct = "";
    for (int i = 0; i < structureInput.length(); ++i) {
      String currentChar = structureInput.substring(i, i+1);
      if ( !(" ".equals(currentChar) || "&".equals(currentChar)) ) {
        catStruct += currentChar;
      }
    }
  
    // Assert seqTot and catStruct are same length
    if (seqTot.length() != catStruct.length()) {
      if (debugPrints) { println("seqTot and catStruct not same length"); }
      errorMessage = "Sequence and structure input lines are not the same length";
      return;
    }
    
    lenTot = seqTot.length();
    
    if (lenTot > 32765) {
      if (debugPrints) { println("Input structure (" + lenTot + " nucleotides) is too large."); }
      errorMessage = "Input structure (" + lenTot + " nucleotides) is too large.";
      return;
    }
    
    pairTable = new Short[lenTot];
    
    String[] pairChars = {"()", "[]", "{}", "<>"};
    ArrayList<OpenPair> pairs = new ArrayList<OpenPair>();
  
    if (debugPrints) { println("\n***Starting structure building loop***"); }
  
    for (int i = 0; i < lenTot; ++i) { // For each open side of a helix
      String currentChar = catStruct.substring(i, i+1);
      boolean match = false;
      for (int j = 0; j < pairChars.length; ++j) {
        if (pairChars[j].substring(0, 1).equals(currentChar)) {
          match = true;
          break;
        }
      }
      if (match) { // Found 5' open end of a pair
        pairs.add(new OpenPair(i, currentChar));
        if (debugPrints) { println("Added new open pair " + i + ", " + currentChar); }
      }
      else {
        pairTable[i] = -1;
        String pairChar = "";
        for (int j = 0; j < pairChars.length; ++j) {
          if (pairChars[j].substring(1, 2).equals(currentChar)) {
            match = true;
            pairChar = pairChars[j].substring(0, 1);
            break;
          }
        }
        if (match) { // Found 3' closing end of a pair
          if (debugPrints) { println("Found closing " + i + ", " + currentChar); }
          for (int j = pairs.size()-1; j >= 0; --j) {
            OpenPair op = pairs.get(j);
            if ( pairChar.equals(op.openChar) ) {
              // Add bond
              pairTable[op.index] = (short) i;
              pairTable[i] = (short) op.index;
              if (debugPrints) { println("Adding bond " + i + ", " + op.index); }
              pairs.remove(j);
              break;
            }
          }
          if (pairTable[i] == -1) {
            if (debugPrints) { println("Unpaired " + currentChar + " at position " + str(i+1)); }
            errorMessage = "Unpaired \"" + currentChar + "\" at position " + str(i+1) + " in structure file";
            return;
          }
        }
      }
    }
    
    checkPairTable(pairTable, lenTot);
    if (!"".equals(errorMessage)) { return; }
    
    // Initialize sequences
    int numSeqs = starts.length;
    sequences = new String[numSeqs];
    for (int i = 0, l = numSeqs-1; i < l; ++i) {
      sequences[i] = seqTot.substring(starts[i], starts[i+1]);
    }
    sequences[numSeqs-1] = seqTot.substring(starts[numSeqs-1], lenTot);
    
    // Creates int array seqIds that stores the strand number (starting at 0) for each residue starting at index 0
    seqIds = new int[lenTot];
    int sId = 0; // Strand ID
    int pc = 0; // residue index
    for (int i = 0; i < numSeqs; ++i) { // each strand
      for (int j = 0; j < sequences[i].length(); ++j) { // for the length of each strand
        seqIds[pc] = sId;
        pc++;
      }
      sId++;
    }
  }

  /** Parses sequence file (code is derived from parser of dot bracket notation file without the bracket part */
  public void initSequenceFile(String fileName) {
    String[] lines;
  
    if (!BROWSER) {
      lines = loadStrings(fileName);
    }
    else {
      if (javascript != null) {
        String inputText = javascript.getTextInput("inputtext");
        lines = inputText.split("\\r\\n|\\n|\\r");
      }
      else {
        if (debugPrints) { println("ERROR: JAVASCRIPT NOT INITIALIZED!"); }
        errorMessage = "ERROR: JAVASCRIPT NOT INITIALIZED!";
        return;
      }
    }
    
    if (debugPrints) {
      println("Raw input:");
      for (String s : lines) {
        println(s);
      }
    }
    
    // Take out extra whitespace
    for (int i = 0; i < lines.length; ++i) {
      lines[i] = lines[i].trim().replaceAll("\\s+", " ");
    }
    
    int sequenceIndex = -1;
    int structureIndex = -1;
    
    for (int i = 0; i < lines.length; ++i) {
      String line = lines[i];
      if (line.length() > 0) {
        String seqChar = line.substring(0, 1);
        if ( (!BROWSER && line.matches("[a-zA-Z& ]+")) || (BROWSER && !(seqChar.equals("#") || seqChar.equals(" ") || seqChar.equals(">"))) ) {
          sequenceIndex = i;
          for (int j = i+1; j < lines.length; ++j) {
            if (lines[j].length() < 1) { continue; }
            String startChar = lines[j].substring(0,1);
            if ( !(startChar.equals("#") || startChar.equals(" ")) ) {
              structureIndex = j;
            }
          }
          break;
        }
      }
    }
    
    if (debugPrints) {
      println("sequenceIndex: " + sequenceIndex);
      println("structureIndex: " + structureIndex);
    }
  
    // Assert both indices in bounds
    if (sequenceIndex < 0 || sequenceIndex >= lines.length-1) {
      errorMessage = "Could not read input \"dot bracket\" file";
      return;
    }
    
    String sequenceInput = lines[sequenceIndex];
    
    ArrayList<Integer> startsList = new ArrayList<Integer>();
    startsList.add(0);
  
    // Build strings without spaces or ampersands
    seqTot = "";
    for (int i = 0, l = 0; i < sequenceInput.length(); ++i) {
      String currentChar = sequenceInput.substring(i, i+1);
      if ( !(" ".equals(currentChar) || "&".equals(currentChar)) ) {
        seqTot += currentChar;
        ++l;
        if (l > 1) {
          String prevChar = sequenceInput.substring(i-1, i);
          if (" ".equals(prevChar) || "&".equals(prevChar)) {
            startsList.add(l-1);
          }
        }
      }
    }
    
    // copy
    starts = new int[startsList.size()];
    Iterator<Integer> iterator = startsList.iterator();
    for (int i = 0; i < starts.length; i++) {
      starts[i] = iterator.next(); // Removed .intValue()
    }
        
    lenTot = seqTot.length();
    
    if (lenTot > 32765) {
      if (debugPrints) { println("Input structure (" + lenTot + " nucleotides) is too large."); }
      errorMessage = "Input structure (" + lenTot + " nucleotides) is too large.";
      return;
    }
    
    if (!"".equals(errorMessage)) { return; }
    
    // Initialize sequences
    int numSeqs = starts.length;
    sequences = new String[numSeqs];
    for (int i = 0, l = numSeqs-1; i < l; ++i) {
      sequences[i] = seqTot.substring(starts[i], starts[i+1]);
    }
    sequences[numSeqs-1] = seqTot.substring(starts[numSeqs-1], lenTot);
    
    // Creates int array seqIds that stores the strand number (starting at 0) for each residue starting at index 0
    seqIds = new int[lenTot];
    int sId = 0; // Strand ID
    int pc = 0; // residue index
    for (int i = 0; i < numSeqs; ++i) { // each strand
      for (int j = 0; j < sequences[i].length(); ++j) { // for the length of each strand
        seqIds[pc] = sId;
        pc++;
      }
      sId++;
    }
  }
  
  /** Helper data structure for bracket parser */
  class OpenPair {
    int index = -1;
    String openChar = "";
  
    OpenPair(int id, String oc) {
      this.index = id;
      this.openChar = oc;
    }
  }
  
  
  /** Parse native nucleotide pairs format file */
  public void initNtsPairs(String fileName) {
    String[] lines;
  
    if (!BROWSER) {
      lines = loadStrings(fileName);
    } else {
      if (javascript != null) {
        String inputText = javascript.getTextInput("inputtext");
        lines = inputText.split("\\r\\n|\\n|\\r");
      } else {
        if (debugPrints) { println("ERROR: JAVASCRIPT NOT INITIALIZED!"); }
        errorMessage = "ERROR: JAVASCRIPT NOT INITIALIZED!";
        return;
      }
    }
    
    // Take out extra whitespace
    for (int i = 0; i < lines.length; ++i) {
      lines[i] = lines[i].trim().replaceAll("\\s+", " ");
    }
    
    int ntsIndex = -1;
    int pairsIndex = -1;
    
    for (int i = 0; i < lines.length; ++i) {
      String line = lines[i];
      if ("NTS:".equals(line) || "nts:".equals(line) || "NUCLEOTIDES:".equals(line) || "nucleotides:".equals(line) || "Nucleotides:".equals(line)) {
        ntsIndex = i;
      }
      else if ("PAIRS:".equals(line) || "pairs:".equals(line) || "Pairs:".equals(line)) {
        pairsIndex = i;
      }
      
      if (ntsIndex != -1 && pairsIndex != -1) {
        break;
      }
    }
    
    if (ntsIndex < 0 || ntsIndex >= lines.length || pairsIndex < 0 || pairsIndex >= lines.length || ntsIndex == pairsIndex) {
      if (debugPrints) { println("Could not read Nucleotides / Pairs file!"); }
      errorMessage = "Could not read Nucleotides / Pairs file!";
      return;
    }
    
    int ntsEndIndex, pairsEndIndex;
    
    if (ntsIndex < pairsIndex) {
      ntsEndIndex = pairsIndex -1;
      pairsEndIndex = lines.length;
    }
    else {
      pairsEndIndex = ntsIndex -1;
      ntsEndIndex = lines.length;
    }
    
    if (debugPrints) {
      println("Starts: Nts=" + ntsIndex + ", pairs=" + pairsIndex);
      println("Ends: Nts=" + ntsEndIndex + ", pairs=" + pairsEndIndex);
    }
    
    HashMap<String, Short> ntCodes = new HashMap<String, Short>(); // Key: string code. Value: index of residue
    ArrayList<Integer> startsList = new ArrayList<Integer>();
    
    seqTot = "";
    
    for (int i = ntsIndex+2, count = 0; i < ntsEndIndex; ++i) {
      String[] line = lines[i].split(" ");
      if ( line.length == 0 || line[0].trim().length() == 0 || "#".equals(line[0].trim().substring(0, 1)) ) {
        continue;
      }
      
      // Check if it is data. else, break
      if ( line.length != 8 || !line[2].matches("^\\d+$") || line[6].length() != 3 ) {
        break;
      }
      
      if ("1".equals(line[2])) {
        startsList.add(count);
      }
      seqTot += line[6].substring(1, 2);
      ntCodes.put( line[7].substring(1, line[7].length()), (short) count );
      
      ++count;
    }
    
    // copy to starts
    starts = new int[startsList.size()];
    Iterator<Integer> iterator = startsList.iterator();
    for (int i = 0; i < starts.length; i++) {
      starts[i] = iterator.next(); // Removed .intValue()
    }
    
    if (starts.length == 0 || starts[0] != 0) {
      if (debugPrints) { println("Strand indecies in Nucleotides / Pairs file not formatted correctly!"); }
      errorMessage = "Strand indecies in Nucleotides / Pairs file not formatted correctly!";
      return;
    }
    
    lenTot = seqTot.length();
    
    if (lenTot > 32765) {
      if (debugPrints) { println("Input structure (" + lenTot + " nucleotides) is too large."); }
      errorMessage = "Input structure (" + lenTot + " nucleotides) is too large.";
      return;
    }
    
    
    int numSeqs = starts.length;
    sequences = new String[numSeqs];
    for (int i = 0, l = numSeqs-1; i < l; ++i) {
      sequences[i] = seqTot.substring(starts[i], starts[i+1]);
    }
    sequences[numSeqs-1] = seqTot.substring(starts[numSeqs-1], lenTot);
    
    pairTable = new Short[lenTot];
    for (int i = 0, l = pairTable.length; i < l; ++i) {
      pairTable[i] = -1;
    }
    
    for (int i = pairsIndex+2; i < pairsEndIndex; ++i) {
      String[] line = lines[i].split(" ");
      if ( line.length == 0 || "#".equals(lines[i].trim().substring(0, 1)) ) {
        continue;
      }
      
      if ( line.length != 9 || line[7].length() != 5 ) {
        break;
      }
      
      String ntCode1 = line[2].substring( 1, line[2].length() );
      String ntCode2 = line[3].substring( 1, line[3].length() );
      
      int id1 = ntCodes.get(ntCode1);
      int id2 = ntCodes.get(ntCode2);
      
      if (id1 > id2) {
        int tempId;
        tempId = id1;
        id1 = id2;
        id2 = tempId;
      }
      
      boolean firstBond = false;
      if (pairTable[id1] == -1 && pairTable[id2] == -1) {
        pairTable[id1] = (short)id2;
        pairTable[id2] = (short)id1;
        firstBond = true;
      }
      if (!"\"cWW\"".equals(line[7]) || !firstBond) {
        nonCanonicals.put(id1 + " " + id2, line[7].substring(1, 4));
      }
    }
    
    checkPairTable(pairTable, lenTot);
    if (!"".equals(errorMessage)) { return; }
    
    // Creates int array seqIds that stores the strand number (starting at 0) for each residue starting at index 0
    seqIds = new int[lenTot];
    int sId = 0; // Strand ID
    int pc = 0; // residue index
    for (int i = 0; i < numSeqs; ++i) { // each strand
      for (int j = 0; j < sequences[i].length(); ++j) { // for the length of each strand
        seqIds[pc] = sId;
        pc++;
      }
      sId++;
    }
    
    // Initialize foldSteps
    /*
    ArrayList<String> foldStepsList = new ArrayList<String>();
    for (int i = 0; i < pairTable.length; ++i) {
      if (i < pairTable[i]) {
        foldStepsList.add("F " + str(i+1) + " " + str(pairTable[i]+1));
      }
    }
    foldSteps = foldStepsList.toArray(new String[foldStepsList.size()]); // copy
    */
  }
  
  
  /** Parses native save file format, and sets up state. */
  public SimulationState initSaveFile(String loadFileName) {
    SimulationState simState = new SimulationState();
    SphereList spheres = new SphereList();
    simState.sim = new SimulationSetup();
    SimulationSetup s = simState.sim;
    ForceField tempff = new ForceField();
    DisplaySettings tempds = new DisplaySettings();
    
    //boolean tempRigidHelices;
    //boolean tempRigidHairpins;
    
    boolean noRigidLoopsEntry = true;
    
    String[] coordinates = null;
    
    ArrayList<Double> colorData = new ArrayList<Double>();
    
    String[] input;
    if (!BROWSER) {
      input = loadStrings(loadFileName);
    }
    else {
      if (javascript != null) {
        String inputText = javascript.getTextInput("inputtext");
        input = inputText.split("\\r\\n|\\n|\\r");
      }
      else {
        if (debugPrints) { println("ERROR: JAVASCRIPT NOT INITIALIZED!"); }
        errorMessage = "ERROR: JAVASCRIPT NOT INITIALIZED!";
        return new SimulationState();
      }
    }
    
    for (int i = 0; i < input.length; ++i) {
      if (input[i].equals("") || input[i].substring(0, 1).equals("#")) { // Ignore empty lines and comments
        continue;
      }
      String[] words = input[i].split("=");
      if (words.length >= 2) {
        if (words[0].equals("sequences")) {
          s.sequences = words[1].split("; ");
        }
        else if (words[0].equals("starts")) {
          String[] words2 = words[1].split(",");
          s.starts = new int[words2.length];
          for (int j = 0; j < words2.length; j++) {
            if (words2[j].matches("^\\d+$")) {
              s.starts[j] = PApplet.parseInt(words2[j]);
            }
            else {
              errorMessage = "Start " + (j+1) + " is not a valid number!";
              return new SimulationState();
            }
          }
        }
        else if (words[0].equals("positions")) {
          coordinates = words[1].split("; ");
        }
        else if (words[0].equals("radius")) {
          if (words[1].matches("[-+]?\\d*\\.?\\d*")) {
            tempff.radius = stringToDouble(words[1]);
            
            if (tempff.radius < tempff.minRadius) {
              tempff.radius = tempff.minRadius;
            } else if (tempff.radius > tempff.minRadius + tempff.radiusScale) {
              tempff.radius = tempff.minRadius + tempff.radiusScale;
            }
          }
          else {
            errorMessage = "Radius is not a valid number!";
            return new SimulationState();
          }
        }
        else if (words[0].equals("backbone_distance")) {
          if (words[1].matches("[-+]?\\d*\\.?\\d*")) {
            tempff.backboneDist = stringToDouble(words[1]);
            
            if (tempff.backboneDist < tempff.minBackboneDist) {
              tempff.backboneDist = tempff.minBackboneDist;
            } else if (tempff.backboneDist > (tempff.minBackboneDist + tempff.backboneDistScale)) {
              tempff.backboneDist = tempff.minBackboneDist + tempff.backboneDistScale;
            }
          }
          else {
            errorMessage = "Backbone distance is not a valid number!";
            return new SimulationState();
          }
        }
        else if (words[0].equals("basepair_distance")) {
          if (words[1].matches("[-+]?\\d*\\.?\\d*")) {
            tempff.bpDist = stringToDouble(words[1]);
            
            if (tempff.bpDist < tempff.minBpDist) {
              tempff.bpDist = tempff.minBpDist;
            } else if (tempff.bpDist > (tempff.minBpDist + tempff.bpDistScale)) {
              tempff.bpDist = tempff.minBpDist + tempff.bpDistScale;
            }
          }
          else {
            errorMessage = "Basepair distance is not a valid number!";
            return new SimulationState();
          }
        }
        /*
        else if (words[0].equals("padding")) {
          if (words[1].matches("[-+]?\\d*\\.?\\d*")) {
            tempds.padding = stringToDouble(words[1]);
            
            if (tempds.padding < 0) {
              tempds.padding = 0;
            } else if (tempds.padding > tempds.paddingScale) {
              tempds.padding = tempds.paddingScale;
            }
          }
          else {
            errorMessage = "Padding is not a valid number!";
            return new SimulationState();
          }
        }
        */
        else if (words[0].equals("color_scheme")) {
          if ( words[1].matches("^\\d+$") && PApplet.parseInt(words[1]) < DisplaySettings.COLOR_MODE_MAX ) {
            tempds.colorMode = PApplet.parseInt(words[1]);
          }
        }
        else if (words[0].equals("outline")) {
          tempds.outlineMode = stringToBoolean(words[1]);
        }
        else if (words[0].equals("movement")) {
          simulationMode = stringToBoolean(words[1]);
        }
        else if (words[0].equals("simulation_type")) {
          simulateAllMode = stringToBoolean(words[1]);
        }
        else if (words[0].equals("labels")) {
          tempds.labelMode = stringToBoolean(words[1]);
        }
        else if (words[0].equals("rigid_helices")) {
          rigidHelices = stringToBoolean(words[1]);
        }
        else if (words[0].equals("rigid_hairpins")) {
          rigidHairpins = stringToBoolean(words[1]);
          wasRigidHairpins = rigidHairpins;
        }
        else if (words[0].equals("rigid_loops")) {
          noRigidLoopsEntry = false;
          rigidLoops = stringToBoolean(words[1]);
          wasRigidLoops = rigidLoops;
        }
        else if (words[0].equals("relaxed_ids")) {
          String[] ids = words[1].split(" ");
          for (String id : ids) {
            if (id.matches("^\\d+$")) {
              spheres.relaxedIds.put(PApplet.parseInt(id), 1);
            }
          }
        }
        else if (words[0].equals("display_order")) {
          String[] ids = words[1].split(",");
          for (String id : ids) {
            if (id.matches("^\\d+$")) {
              spheres.displayOrder.add(PApplet.parseInt(id));
            }
          }
        }
        else if (words[0].equals("pair_table")) {
          String[] words2 = words[1].split(",");
          s.pairTable = new Short[words2.length];
          for (int j = 0; j < words2.length; j++) {
            String pairedW = words2[j];
            if (pairedW.matches("^-?\\d+$")) {
              if (PApplet.parseInt(pairedW) < 32765) {
                s.pairTable[j] = (short) PApplet.parseInt(pairedW);
              }
              else {
                errorMessage = "pair_table value " + (j+1) + " is greater than the max value (32764)!";
                return new SimulationState();
              }
            }
            else {
              errorMessage = "pair_table value " + (j+1) + " is not a valid number";
              return new SimulationState();
            }
          }
        }
        else if (words[0].equals("non_canonicals")) {
          String[] words2 = words[1].split(", ");
          s.nonCanonicals = new HashMap<String, String>(words2.length);
          for (int j = 0; j < words2.length; ++j) {
            String word = words2[j];
            String[] entries = word.split(" ");
            if (entries.length == 3 && entries[0].matches("^\\d+$") && entries[1].matches("^\\d+$") && entries[2].length() == 3) {
              s.nonCanonicals.put(entries[0] + " " + entries[1], entries[2]);
            } else if (debugPrints) {
              println("Non Canonical entry number " + (j+1) + " in save file incorrectly formatted");
            }
          }
        }
        else if (words[0].equals("base_colors")) {
          String[] nums = words[1].split(",");
          for (String num : nums) {
            if (num.matches("-?\\d+(\\.\\d+)?")) {
              colorData.add(stringToDouble(num));
            }
            else {
              break;
            }
          }
        }
      }
    }
    
    // File corruption checks
    if (s.starts == null) {
      if (debugPrints) { println("Starts is null!"); }
      errorMessage = "Missing starts argument in load file";
      return new SimulationState();
    }
    else if (s.starts[0] != 0) {
      if (debugPrints) { println("First start is not 0!"); }
      errorMessage = "First sequence start is not 0 (cooresponding to first residue)!";
      return new SimulationState();
    }
    for (int i = 1; i < s.starts.length; i++) {
      if (s.starts[i] <= s.starts[i-1]) {
        if (debugPrints) { println("Strand starts not in ascending order!"); }
        errorMessage = "Strand starts not in ascending order!";
        return new SimulationState();
      }
    }
    
    if (s.sequences == null) {
      if (debugPrints) { println("Sequences is null!"); }
      errorMessage = "Missing sequences argument in load file";
      return new SimulationState();
    }
    else if (s.starts.length != s.sequences.length) {
      if (debugPrints) { println("Sequences and starts arrays not of same length!"); }
      errorMessage = "Sequences and starts arrays not of same length!";
      return new SimulationState();
    }
    for (int i = 1; i < s.starts.length; i++) {
      int size = s.starts[i] - s.starts[i-1];
      if (size != s.sequences[i-1].length()) {
        if (debugPrints) { println("Start " + (i-1) + " to " + i + " does not match sequence " + i + " length!"); }
        errorMessage = "Start " + (i-1) + " to " + i + " does not match sequence " + i + " length!";
        return new SimulationState();
      }
    }
    
    // Create seqTot, lenTot, and seqIds
    s.seqTot = "";
    for (String seq : s.sequences) {
      s.seqTot += seq;
    }
    s.lenTot = s.seqTot.length();
    
    s.seqIds = new int[s.lenTot];
    int sId = 0; // Strand ID
    int pc = 0; // residue index
    for (int i = 0; i < s.sequences.length; ++i) { // each strand
      for (int j = 0; j < s.sequences[i].length(); ++j) { // for the length of each strand
        s.seqIds[pc] = sId;
        pc++;
      }
      sId++;
    }
    
    if (s.pairTable == null) {
      if (debugPrints) { println("pairTable array is null!"); }
      errorMessage = "pair_table argument is missing from the load file!";
      return new SimulationState();
    }
    else if (s.pairTable.length != s.lenTot) {
      if (debugPrints) { println("pairTable length does not equal lenTot!"); }
      errorMessage = "Length of pair_table does not match the number of residues!";
      return new SimulationState();
    }
    checkPairTable(s.pairTable, s.lenTot);
    if (!"".equals(errorMessage)) { return new SimulationState(); }
    
    s.initTables();
    
    for (Map.Entry<String, String> nc : s.nonCanonicals.entrySet()) {
      String[] saveKey = nc.getKey().split(" ");
      int id1 = PApplet.parseInt(saveKey[0]);
      int id2 = PApplet.parseInt(saveKey[1]);
      if (id1 == id2) {
        if (debugPrints) {
          println("In save file, non canonical pair \"" + nc + "\" describes a base paired with itself, which is impossible");
          warningMessage += "In save file, non canonical pair \"" + nc + "\" describes a base paired with itself, which is impossible";
        }
      } else if (id1 < 0 || id1 >= s.lenTot || id2 < 0 || id2 >= s.lenTot) {
        println("In save file, non canonical pair \"" + nc + "\" is out of bounds!");
        warningMessage += "In save file, non canonical pair \"" + nc + "\" is out of bounds!";
      }
    }
    
    if (coordinates == null) {
      if (debugPrints) { println("coordinates array is null!"); }
      errorMessage = "coordinates of bases are missing from the load file!";
      return new SimulationState();
    }
    else if (coordinates.length != s.lenTot) {
      if (debugPrints) { println("Number of coordinate arguments does not match the number of residues!"); }
      errorMessage = "Number of coordinate arguments does not match the number of residues!";
      return new SimulationState();
    }
    
    if (spheres.displayOrder.size() > s.lenTot) {
      if (debugPrints) { println("displayOrder greater than lenTot!"); }
      errorMessage = "display_order is longer than the number of residues!";
      return new SimulationState();
    }
    else if (spheres.displayOrder.size() < s.lenTot) {
      if (debugPrints) { println("displayOrder less than lenTot!"); }
      errorMessage = "display_order is shorter than the number of residues!";
      return new SimulationState();
    }
    boolean[] numChecks = new boolean[s.lenTot];
    for (int i : spheres.displayOrder) {
      if (i < 0 || i >= s.lenTot) {
        if (debugPrints) { println("Value " + i + " in displayOrder is out of bounds!"); }
        errorMessage = "Value " + i + " in display_order is out of bounds!";
        return new SimulationState();
      }
      else if (numChecks[i]) {
        if (debugPrints) { println("Repeated value in displayOrder. A residue cannot have two positions in the order!"); }
        errorMessage = "Repeated value in display_order. A residue cannot have two positions in the order!";
        return new SimulationState();
      }
      
      numChecks[i] = true;
    }
    
    numChecks = new boolean[s.lenTot];
    for (Iterator<Integer> it = spheres.relaxedIds.keySet().iterator(); it.hasNext(); ) {
      int relaxedId = it.next();
      if (relaxedId < 0 || relaxedId >= s.lenTot) {
        if (debugPrints) { println("Relaxed ids are out of bounds!"); }
        errorMessage = "Relaxed ids are out of bounds!";
        return new SimulationState();
      }
      else if (numChecks[relaxedId]) {
        if (debugPrints) { println("Relaxed id " + relaxedId + " is repeated!"); }
        errorMessage = "Relaxed id " + relaxedId + " is repeated!";
        return new SimulationState();
      }
      
      numChecks[relaxedId] = true;
    }

    //movieMode = false;
    
    
    simState.spheres = s.initDefSphereList();
    SphereList newSpheres = simState.getSpheres();
    newSpheres.relaxedIds = spheres.relaxedIds;
    newSpheres.displayOrder = spheres.displayOrder;
    
    for (int i = 0; i < s.lenTot; ++i) {
      double x, y, z;
      String[] words = coordinates[i].split(",");
      if (words.length < 2) {
        if (debugPrints) { println("Not enough values for coordinate " + i + "!!!"); }
        // WARN MESSAGE?
        x = 0;
        y = 0;
        z = 0;
        
      } else {
        if ( words[0].matches("[-+]?\\d*\\.?\\d*") && words[1].matches("[-+]?\\d*\\.?\\d*") && (words.length == 2 || words[2].matches("[-+]?\\d*\\.?\\d*")) ) {
          x = stringToDouble(words[0]);
          y = stringToDouble(words[1]);
          
          if (words.length >= 3) {
            z = stringToDouble(words[2]);
          } else {
            z = 0;
          }
        }
        else {
          if (debugPrints) { println("Coordinate " + i + " has non-number values!"); }
          errorMessage = "Coordinate " + i + " has non-number values!";
          return new SimulationState();
        }
      }
      newSpheres.get(i).x = x;
      newSpheres.get(i).y = y;
      newSpheres.get(i).z = z;
    }
    
    if (colorData.size() == s.lenTot) {
      if (debugPrints) { println("Loading color data"); }
      for (int i = 0; i < s.lenTot; i++) {
        double colorNumber = colorData.get(i);
        newSpheres.get(i).colorNum = (float) colorNumber;
        newSpheres.get(i).baseColor = lerpColor(tempds.startGradient, tempds.endGradient, (float)colorNumber);
      }
    }
    
    ff = tempff;
    ds = tempds;
    
    if (noRigidLoopsEntry) {
      rigidLoops = false;
    }
    
    checkBoxes.get("Outlines").checked = ds.outlineMode;
    checkBoxes.get("Labels").checked = ds.labelMode;
    checkBoxes.get("Rigid Loops").checked = rigidLoops;
    checkBoxes.get("Rigid Hairpins").checked = rigidHairpins;
    checkBoxes.get("Sim. Selected Only").checked = !simulateAllMode;
    
    textDisplay = "SAVE FILE LOADED";
    
    return simState;
  }
  
  
  public void checkPairTable(Short[] pairTable, int lenTot) {
    boolean[] pairCheck = new boolean[lenTot];
    for (short i = 0; i < pairTable.length; i++) {
      short pairId = pairTable[i];
      if (pairId < -1 || pairId >= lenTot) {
        if (debugPrints) { println("Residue " + (i+1) + " paired with an out-of-bounds number"); }
        errorMessage = "Residue " + (i+1) + " paired with an out of bounds number!";
        return;
      }
      
      if (pairId != -1) {
        if (pairId == i) {
          if (debugPrints) { println("Residue " + i + " paired with itself!"); }
          errorMessage = "Residue " + (i+1) + " cannot be paired with itself!";
          return;
        }
        else if (pairCheck[pairId]) {
          if (debugPrints) { println("Residue " + (pairId+1) + " is paired with multiple others!"); }
          errorMessage = "Residue " + (pairId+1) + " is paired with multiple other residues!";
          return;
        }
        else if (pairTable[pairId] != i) {
          if (debugPrints) {
            println("Residue " + (i+1) + " is paired with residue " + (pairId+1) + ", but residue " + (pairId+1) + " is paired with " + (pairTable[pairId]+1));
          }
          errorMessage = "Residue " + (i+1) + " is paired with residue " + (pairId+1) + ", but residue " + (pairId+1) + " is paired with " + (pairTable[pairId]+1);
          return;
        }
        
        pairCheck[pairId] = true;
      }
    }
  }
  
  /** Determine optimal order to place strands. */
  public int[] orderStrands() {
    ArrayList<InterHelix> helices = new ArrayList<InterHelix>(); // stores interStrand helices
    int startStrandId = -1, endStrandId = -1;
    boolean foundHelix = false;
    int curHelSize = 0; // Number of pairs in last helix (default 1)
    int curStartStrand = -1, curEndStrand = -1;

    for (int i = 0, l = foldSteps.size(); i < l; ++i) {
      int start = foldSteps.get(i)[0];
      for (int j = 0; j < starts.length; j++) {
        if (start >= starts[j] && (j >= starts.length-1 || start < starts[j+1])) {
          startStrandId = j;
          break;
        }
      }
      int end = foldSteps.get(i)[1];
      for (int j = 0; j < starts.length; j++) {
        if (end >= starts[j] && (j >= starts.length-1 || end < starts[j+1])) {
          endStrandId = j;
          break;
        }
      }

      if (foundHelix) {
        InterHelix lastHelix = helices.get(helices.size() - 1);
        boolean sameHelix = (lastHelix.end1.origId + curHelSize == start && lastHelix.end2.origId - 1 == end);
        boolean connectedStrands = (curStartStrand == startStrandId || curEndStrand == endStrandId);
        if (sameHelix && connectedStrands) {
          curHelSize++;
          curStartStrand = startStrandId;
          curEndStrand = endStrandId;
          lastHelix.end2.origId = end;
          lastHelix.end2.strandId = endStrandId;
          lastHelix.end2.relId = end - starts[endStrandId];
        } else {
          foundHelix = false;
        }
      }
      if (!foundHelix) {
        if (startStrandId >= 0 && endStrandId >= 1 && startStrandId < endStrandId) { // Found an inter-strand helix
          helices.add( new InterHelix(startStrandId, start - starts[startStrandId], start, endStrandId, end - starts[endStrandId], end) );
          foundHelix = true;
          curHelSize = 1;
          curStartStrand = startStrandId;
          curEndStrand = endStrandId;
        }
      }
    }

    ArrayList<Strand> strands = new ArrayList<Strand>();
    for (int i = 0; i < sequences.length; i++) {
      int strandLength = 0;
      int strandStart = starts[i];
      if (i < sequences.length-1) {
        strandLength = starts[i+1] - strandStart;
      } else {
        strandLength = lenTot - strandStart;
      }
      strands.add(new Strand(i, strandLength));
    }

    // Add helix ends to respective strands
    for (InterHelix h : helices) {
      strands.get(h.end1.strandId).addEnd(h.end1);
      strands.get(h.end2.strandId).addEnd(h.end2);
    }

    for (int j = 0; j < sequences.length - 1; j++) {
      // Find next helix to place by finding the smallest possible distance to place.
      int smallestDistance = 32765;
      int bestHelixId = -1;
      boolean forward = true;
      for (int i = 0; i < helices.size(); i++) {
        InterHelix h = helices.get(i);
        if (h.end1.strandId == h.end2.strandId) {
          continue;
        }
        int strandLength1 = strands.get(h.end1.strandId).length;
        int strandLength2 = strands.get(h.end2.strandId).length;
        //assert(strandLength1 > h.end1.relId);
        //assert(strandLength2 > h.end2.relId);
        int distanceSum;
        distanceSum = (strandLength1 - h.end1.relId) + h.end2.relId + 1;
        if (distanceSum < smallestDistance) {
          smallestDistance = distanceSum;
          bestHelixId = i;
          forward = true;
        }
        distanceSum = (strandLength2 - h.end2.relId) + h.end1.relId + 1;
        if (distanceSum < smallestDistance) {
          smallestDistance = distanceSum;
          bestHelixId = i;
          forward = false;
        }
      }
      if (bestHelixId == -1) { // no more interstrand helices
        break;
      }  
      InterHelix bestHelix = helices.get(bestHelixId);
      Strand strand1 = strands.get(bestHelix.end1.strandId);
      Strand strand2 = strands.get(bestHelix.end2.strandId);
      if (!forward) {
        Strand temp = strand1;
        strand1 = strand2;
        strand2 = temp;
      }
      
      // Concatenate strands (Shift strand2's helices' relative ids and place them into strand1)
      for (HelixEnd end : strand2.helixEnds) {
        end.relId += strand1.length;
        end.strandId = strand1.id;
        strand1.helixEnds.add(end);
      }
      strand1.length += strand2.length;
      for (int id : strand2.strandIds) {
        strand1.strandIds.add(id);
      }
      
      // Clear strand 2
      strand2.length = 0;
      strand2.helixEnds.clear();
      strand2.strandIds.clear();
    }
    
    ArrayList<Integer> strandOrder = new ArrayList<Integer>();
    for (Strand s : strands) {
      strandOrder.addAll(s.strandIds);
    }
    int[] strandOrderOutput = new int[strandOrder.size()];
    if (debugPrints) { print("\nOrder:"); }
    for (int i = 0; i < strandOrder.size(); i++) {
      strandOrderOutput[i] = strandOrder.get(i);
      if (debugPrints) { print(" " + strandOrderOutput[i]); }
    }
    if (debugPrints) { println(""); }
    
    return strandOrderOutput;
  }
  
  
  /** Outputs an array storing the place in the new order (based on the strand order calculated from orderStrands) at each index cooresponding to the original id of each residue. */
  public int[] initOrder(SimulationSetup s) {
    int[] residueOrderIds = new int[s.lenTot]; // Each index is the original id, stored value is order in index
    int orderId = 0;
    for (int i : s.strandOrder) {
      int strandLength = 0;
      int strandStart = s.starts[i];
      if (i < s.sequences.length-1) {
        strandLength = s.starts[i+1] - strandStart;
      } else {
        strandLength = s.lenTot - strandStart;
      }
      for (int j = strandStart; j < strandStart + strandLength; j++) {
        residueOrderIds[j] = orderId;
        orderId++;
      }
    }
    
    return residueOrderIds;
  }
  
  
  public void generatePairTableOptimal() {
    int[] residueOrderSequence = new int[lenTot]; // Reverse of residueOrderIds: each index is the place in the new order, each stored value is the original id
    for (int i = 0; i < lenTot; i++) {
      residueOrderSequence[ residueOrder[i] ] = i;
    }
    
    pairTableOptimal = new ArrayList<Short>(pairTable.length); // Stores the position in the new order that each newly ordered residue is paired with
    
    for (int i = 0; i < pairTable.length; i++) { // Runs through the new order, building the new pairTableOptimal
      int pairedId = pairTable[residueOrderSequence[i]]; // The original ID of the residue paired with the residue in the new order at position i
      
      short pairedOrderedId; // The position in the new order of the residue paired with the residue in the new order at position i
      
      if (pairedId != -1) {
        pairedOrderedId = (short)residueOrder[ pairedId ];
      } else {
        pairedOrderedId = -1;
      }
      
      pairTableOptimal.add( pairedOrderedId );
    }
    
    ArrayList<Helix> helices = new ArrayList<Helix>();
    int end2 = -1;
    int size = 0;
    boolean inHelix = false;
    short pairedW;
    
    // Form helices
    for (int i = 0; i < pairTableOptimal.size(); ) {
      pairedW = pairTableOptimal.get(i);
      if (!inHelix) {
        if (i < pairedW) { // "i < pairedW" ensures each bond is only counted once and that pairedW is not -1 (meaning unpaired). WILL THIS BREAK IF A RESIDUE IS BONDED TO ITSELF?
          inHelix = true;
          size = 1;
          end2 = pairedW;
          helices.add(new Helix(i, end2, i));
        }
        i++;
      } else {
        end2--;
        if (end2 == pairedW) {
          i++;
          size++;
        } else { // end of helix
          helices.get(helices.size() - 1).setSize(size);
          inHelix = false;
        }
      }
    }
    
    for (int i = 0, l = helices.size(); i < l; i++) {
      Helix h1 = helices.get(i);
      for (int j = i+1; j < l; j++) {
        Helix h2 = helices.get(j);
        if ( (h1.ends[0] < h2.ends[0] && h2.ends[0] < h1.ends[1]) ^
             (h1.ends[0] < h2.ends[1] && h2.ends[1] < h1.ends[1]) ) {
          h1.crossedW.put(h2.id, h2);
          h2.crossedW.put(h1.id, h1);
          h1.numCrossed += h2.size;
          h2.numCrossed += h1.size;
        }
      }
    }
    
    ArrayList<Helix> ignoredHelices = new ArrayList<Helix>(); // The helices whose basepairs will be ignored in the radial placement algorithm, so the input is pseudoknot free.
    
    Helix mostCrossed;
    boolean finished = false;
    for (int i = 0; i < helices.size()-1; i++) {
      if (finished) { break; }
      mostCrossed = helices.get(0); // arbitrary start
      for (int j = 1; j < helices.size(); j++) {
        Helix nextH = helices.get(j);
        if (nextH.numCrossed > mostCrossed.numCrossed) {
          mostCrossed = nextH;
        }
      }
      
      if (mostCrossed.numCrossed < 0) {
        if (debugPrints) { println("NUM CROSSED OUT OF BOUNDS IN generatePairTableOptimal!"); }
      }
      
      if (mostCrossed.numCrossed == 0) { // No more pseudoknots!
        finished = true;
        break;
      }
      
      ignoredHelices.add(mostCrossed);
      
      for (int id : mostCrossed.crossedW.keySet()) {
        Helix h = mostCrossed.crossedW.get(id);
        h.crossedW.remove(mostCrossed.id);
        h.numCrossed -= mostCrossed.size;
      }
      
      mostCrossed.numCrossed = 0;
      mostCrossed.crossedW.clear();
    }
    
    
    // Modify pairTableOptimal to take out the basepairs in the ignored helices
    for (Helix h : ignoredHelices) {
      for (int i = 0; i < h.size; i++) {
        pairTableOptimal.set(h.ends[0]+i, (short) (-1));
        pairTableOptimal.set(h.ends[1]-i, (short) (-1));
      }
    }
  }
  
  public void setupDebug() {
    println("\n*** SETUP ***");
    println("lenTot: " + lenTot);
    println("seqTot: " + seqTot);
    print("Starts:");
    for (int s : starts) {
      print(" " + s);
    }
    println("");
    
    print("Pair Table:");
    for (int i = 0; i < pairTable.length; ++i) {
      print(" " + pairTable[i]);
    }
    println("\n");
    
    println("foldSteps:");
    for (int[] pair : foldSteps) {
      println(pair[0] + " " + pair[1]);
    }
  }
  
  
  /** Sets up initial state as a circular diagram. */
  public SimulationState createCircleState(SphereList spheres) {
    if (debugPrints) { println("\nStarting SimulationSetup.createCircleState..."); }
    
    SimulationState simState = new SimulationState();
    simState.sim = this; // sets the SimulationSetup object held in the SimulationState to be this SimulationSetup
    
    // Sets the spheres in a circle
    double circleReduction = 10.0f;
    double startCircleRadius = (height / 2.0f) - (ds.startPadding + (height / circleReduction));
    if (startCircleRadius < height / 8.0f) {
      startCircleRadius = height / 8.0f;
    }
    ff.radius = (Math.PI * startCircleRadius) / lenTot;
    if (ff.radius < ff.minRadius) { ff.radius = ff.minRadius; }
    if (ff.radius > ff.minRadius + ff.radiusScale) { ff.radius = ff.minRadius + ff.radiusScale; }
    
    ff.backboneDist = ff.radius * 2.5f;
    if (ff.backboneDist < ff.minBackboneDist) { ff.backboneDist = ff.minBackboneDist; }
    if (ff.backboneDist > (ff.minBackboneDist + ff.backboneDistScale)) { ff.backboneDist = ff.minBackboneDist + ff.backboneDistScale; }
    
    ff.bpDist = ff.radius * 3.75f;
    if (ff.bpDist < ff.minBpDist) { ff.bpDist = ff.minBpDist; }
    if (ff.bpDist > (ff.minBpDist + ff.bpDistScale)) { ff.bpDist = ff.minBpDist + ff.bpDistScale; }
    
    
    ds.padding = ds.startPadding;
    startCircleRadius += height / circleReduction;
    spheres.displayOrder.clear();
    spheres.relaxedIds.clear();
    for (int i = 0; i < lenTot; ++i) {
      double ang = residueOrder[i] * 2.0f * Math.PI / (double)(lenTot);
      spheres.get(i).x = (width/2) + Math.cos(ang) * startCircleRadius;
      spheres.get(i).y = (height/2) + Math.sin(ang) * startCircleRadius;
      spheres.displayOrder.add(i);
    }
    
    simulationMode = false;
    //movieMode = false;
    rigidHelices = true;
    
    simState.spheres = spheres;
    
    if (debugPrints) { println("Finished SimulationSetup.createCircleState!"); }
    return simState;
  }
  
  
  /** Sets up the state based on a radial algorithm. */
  public SimulationState createRadialState(SphereList spheres) {
    if (debugPrints) { println("\nStarting SimulationSetup.createRadialState..."); }
    
    SimulationState simState = new SimulationState();
    simState.sim = this; // sets the SimulationSetup object held in the SimulationState to be this SimulationSetup
    
    ArrayList<Double> X = new ArrayList<Double>();
    ArrayList<Double> Y = new ArrayList<Double>();
    
    Radial radial = new Radial();
    radial.xyCoordinates(pairTableOptimal, X, Y); // Takes in a pseudoknot free pairTable of shorts.
    if (!"".equals(radialAlgorithmException)) {
      if (debugPrints) {
        println("Caught RadialAlgorithmException in createRadialState!");
        println(radialAlgorithmException);
      }
      errorMessage = radialAlgorithmException;
      return new SimulationState();
    }
    
    if (X.size() == Y.size()) {
      //println("Both coordinate arrays sized at " + X.size());
    } else {
      if (debugPrints) { println("Coordinate arrays of unequal size!"); }
      errorMessage = "Coordinate arrays of unequal size";
      return new SimulationState();
    }
    
    
    // Determine initial layout based on screen size and radial structure size
    double minX = X.get(0), maxX = X.get(0);
    double minY = Y.get(0), maxY = Y.get(0);
    
    for (int i = 0; i < X.size(); i++) {
      double x = X.get(i);
      double y = Y.get(i);
      
      if (x < minX) {
        minX = x;
      } else if (x > maxX) {
        maxX = x;
      }
      
      if (y < minY) {
        minY = y;
      } else if (y > maxY) {
        maxY = y;
      }
    }
    
    double xDif = maxX - minX;
    double yDif = maxY - minY;
    
    double xRatio = xDif / width;
    double yRatio = yDif / height;
    double scaleFactor;
    
    if (xRatio > yRatio) {
      scaleFactor = .85f / xRatio; // Change .9 to reflect start buffer?
    } else {
      scaleFactor = .85f / yRatio;
    }
    
    double xShift = (width / 2.0f) - (scaleFactor * ((xDif/2.0f) + minX));
    double yShift = (height / 2.0f) - (scaleFactor * ((yDif/2.0f) + minY));
    
    for (int i = 0; i < X.size(); i++) {
      X.set(i, (X.get(i) * scaleFactor) + xShift);
      Y.set(i, (Y.get(i) * scaleFactor) + yShift);
    }
    
    
    // Calculate starting backbone length
    double avgDist = 0;
    int numDists = 0;
    
    int numCounted = PApplet.parseInt(X.size() / 4);
    if (numCounted < 50) { numCounted = 50; }
    if (numCounted > X.size()) { numCounted = X.size(); }
    
    for (int i = 1; i < numCounted; i++) {
      if (i >= X.size()) { break; }
      numDists++;
      avgDist += normFunction(X.get(i) - X.get(i-1), Y.get(i) - Y.get(i-1), 0);
    }
    
    if (numDists != 0) {
      avgDist /= numDists;
    } else {
      avgDist = ff.minBackboneDist;
    }
    
    if (avgDist < 8) {
      ff.backboneDist = avgDist * .35f;
    } else {
      ff.backboneDist = avgDist * .95f;
    }
    
    if (ff.backboneDist < ff.minBackboneDist) { ff.backboneDist = ff.minBackboneDist; }
    if (ff.backboneDist > (ff.minBackboneDist + ff.backboneDistScale)) { ff.backboneDist = ff.minBackboneDist + ff.backboneDistScale; }
    
    ff.radius = avgDist / 3;
    if (ff.radius < ff.minRadius) { ff.radius = ff.minRadius; }
    if (ff.radius > ff.minRadius + ff.radiusScale) { ff.radius = ff.minRadius + ff.radiusScale; }
    
    
    // Calculate starting bond length
    avgDist = 0;
    numDists = 0;
    for (int i = 0; i < pairTableOptimal.size(); i++) {
      int pairedWId = pairTableOptimal.get(i);
      if (pairedWId != -1 && i < pairedWId) {
        numDists++;
        avgDist += normFunction( X.get(i) - X.get(pairedWId), Y.get(i) - Y.get(pairedWId), 0 );
      }
    }
    
    if (numDists != 0) {
      avgDist /= numDists;
    } else {
      avgDist = ff.minBpDist;
    }
    
    ff.bpDist = avgDist;
    if (ff.bpDist < ff.minBpDist) { ff.bpDist = ff.minBpDist; }
    if (ff.bpDist > (ff.minBpDist + ff.bpDistScale)) { ff.bpDist = ff.minBpDist + ff.bpDistScale; }
    
    ds.padding = ds.startPadding;
    spheres.displayOrder.clear();
    spheres.relaxedIds.clear();
    for (int i = 0; i < lenTot; ++i) {
      int pairTableId = residueOrder[i];
      spheres.get(i).x = X.get(pairTableId);
      spheres.get(i).y = Y.get(pairTableId);
      spheres.displayOrder.add(i);
    }
    
    simulationMode = false;
    //movieMode = false;
    rigidHelices = true;
    
    textDisplay = "RADIAL LAYOUT";
    
    simState.spheres = spheres;
    
    if (debugPrints) { println("Finished SimulationSetup.createRadialState!"); }
    return simState;
  }
}
class SimulationState { // Holds state of program
  SimulationSetup sim; // Holds file information
  SphereList spheres;
  int foldStep = -1; // The current bond

  SimulationState() {
    this.spheres = new SphereList();
    this.sim = new SimulationSetup();
  }
  
  SimulationState(SimulationState other) {
    this.sim = other.sim.cloneFunction();
    this.spheres = other.getSpheres().cloneFunction();
    this.foldStep = other.foldStep;
  }

  SimulationState(SimulationSetup _sim, SphereList _spheres, int _foldStep) {
    this.sim = _sim.cloneFunction();
    this.spheres = _spheres.cloneFunction();
    this.foldStep = _foldStep;
  }

  public SimulationState cloneFunction() {
    SimulationState result = new SimulationState(sim, spheres, foldStep);
    return result;
  }

  public SphereList getSpheres() { 
    return spheres;
  }

  public String[] writeSaveFile() {
    if (debugPrints) { println("Starting writeSaveFile..."); }
    SimulationSetup s = sim;
    
    ArrayList<String> saveLines = new ArrayList<String>();
    
    String line = "sequences=";
    for (int i = 0; i < s.sequences.length; ++i) {
      line += s.sequences[i];
      if ((i+1) < s.sequences.length) {
        line += "; ";
      }
    }
    saveLines.add(line);
    
    line = "starts=";
    for (int i = 0; i < s.starts.length; ++i) {
      line += s.starts[i];
      if ((i+1) < s.starts.length) {
        line += ",";
      }
    }
    saveLines.add(line);
    
    line = "positions="; // Writes a semicolon separated string of coordinates
    for (int i = 0; i < spheres.size(); ++i) {
      line += (int)(spheres.get(i).x) + "," + (int)(spheres.get(i).y) + "," + (int)(spheres.get(i).z);
      if ((i+1) < spheres.size()) {
        line += "; ";
      }
    }
    saveLines.add(line);
    
    saveLines.add("radius=" + ff.radius);
    saveLines.add("backbone_distance=" + ff.backboneDist);
    saveLines.add("basepair_distance=" + ff.bpDist);
    //saveLines.add("padding=" + ds.padding);
    saveLines.add("color_scheme=" + ds.colorMode);
    
    saveLines.add("outline=" + ds.outlineMode);
    saveLines.add("movement=" + simulationMode);
    saveLines.add("simulation_type=" + simulateAllMode);
    saveLines.add("labels=" + ds.labelMode);
    saveLines.add("rigid_helices=" + rigidHelices);
    saveLines.add("rigid_hairpins=" + rigidHairpins);
    saveLines.add("rigid_loops=" + rigidLoops);
    
    line = "relaxed_ids=";
    for (Iterator<Integer> it = spheres.relaxedIds.keySet().iterator(); it.hasNext(); ) {
      line += it.next() + " ";
    }
    saveLines.add(line.trim());

    line = "display_order=";
    for (int i = 0; i < spheres.displayOrder.size(); ++i) {
      line += spheres.displayOrder.get(i);
      if ( i+1 < spheres.displayOrder.size() ) {
        line += ",";
      }
    }
    saveLines.add(line);
    
    line = "pair_table=";
    for (short i = 0; i < s.pairTable.length; ++i) {
      line += s.pairTable[i];
      if ( i+1 < s.pairTable.length ) {
        line += ",";
      }
    }
    saveLines.add(line);
    
    line = "non_canonicals=";
    for (Map.Entry<String, String> nc : s.nonCanonicals.entrySet()) {
      line += nc.getKey() + " " + nc.getValue() + ", ";
    }
    line = line.substring(0, line.length() - 2);
    saveLines.add(line);
    
    line = "base_colors=";
    for (int i = 0, l = spheres.size(); i < l; i++) {
      line += spheres.get(i).colorNum;
      if ( i+1 < l ) {
        line += ",";
      }
    }
    saveLines.add(line);
    
    if (debugPrints) { println("Finished writeSaveFile!"); }
    return saveLines.toArray(new String[saveLines.size()]);
  }

 /*
 Base number: index n
 Base (A, C, G, T, U, X)
 Index n-1
 Index n+1
 Number of the base to which n is paired. No pairing is indicated by 0 (zero).
 Natural numbering. RNAstructure ignores the actual value given in natural numbering, so it is easiest to repeat n here.

*HOW TO FORMAT A CT OR BPSEQ FILE*
 Lines preceded by \"#\" and empty lines are ignored.
 If you have multiple strands in a ct file, the first line before the data should be a header line,
 or the file should be formatted such that the line for each residue that begins a new strand has 0 as its 3rd column.
*/

  /** Output of secondary structure in mfold "CT" (i.e. connectivity) file format */
  public String[] writeCtFile() {
    if (debugPrints) { println("Starting writeCtFile..."); }
    SimulationSetup s = sim;    
    ArrayList<String> saveLines = new ArrayList<String>();    
    String concatSeq = "";
    for (int i = 0; i < s.sequences.length; ++i) {
      concatSeq += s.sequences[i];
    }
 //   saveLines.add(line);
    String DELI = " ";
    String headerLine = "" + concatSeq.length() + "  " + s.starts.length;
    for (int i = 0; i < s.starts.length; ++i) {
      headerLine += (s.starts[i] + 1);
      if ((i+1) < s.starts.length) {
        headerLine += " ";
      }
    }
    saveLines.add(headerLine);

    for (short i = 0; i < s.pairTable.length; ++i) {
      int firstId = i;
      if ((i > 0) && (s.seqIds[i] != s.seqIds[i-1])) {
       firstId = 0; // must be beggining of new strand
      }
      int bp = s.pairTable[i] + 1; // pairTable[i] is -1 if unpaired, so bp == 0 if unpaired
      String line = "" + (i+1) + DELI + concatSeq.charAt(i) + DELI + firstId + DELI + (i+2) + DELI + bp + DELI + (i+1);
      saveLines.add(line);
    }
    if (debugPrints) { println("Finished writeCtFile!"); }
    return saveLines.toArray(new String[saveLines.size()]);
  }
}
public class Slider
{
  float x, y, width, height, labelWidth;
  float valueX = 0;
  float value;
  String label;
  boolean visible = true;
  boolean isPressed = false;
  int sliderId;
  boolean updating = false;
  
  Slider ( float xx, float yy, float ww, float hh, String _label, float _labelWidth, int id) 
  {
    x = xx; 
    y = yy; 
    this.width = ww; 
    this.height = hh;
    
    valueX = x;
    label = _label;
    labelWidth = _labelWidth;
    sliderId = id;
  
    // register it with Guido
    Interactive.add( this );
  }
  
  public void mousePressed() {
    isPressed = true;
  }
  
  public void dragged()
  {
    if (updating) { return; }
    
    valueX = mouseX - this.height/2.0f; // height is also the width of the slider bubble
    if ( valueX < x ) valueX = x;
    if ( valueX > x+this.width-this.height ) valueX = x+this.width-this.height;
    
    value = map( valueX, x, x+this.width-this.height, 0, 1 );
    updateFromSlider();
  }
  
  public void draw () 
  {
    if (!menuVisible) {
      return;
    }
    
    if (x <= mouseX && mouseX <= x+this.width &&
        y <= mouseY && mouseY <= y+this.height) {
      strokeWeight(1.5f);
      stroke(ds.GUIColor);
    } else {
      noStroke();
    }
    rectMode(CORNER);        
    fill(0,0,0);
    rect(x, y, this.width, this.height, 4);
    fill(ds.GUIColor);
    rect(valueX, y, this.height, this.height, 3);
    fill(0,0,0); // return to black
    
    if (valueX > x+(this.height/2.0f)) { // "thermostat" line
      stroke(ds.GUIColor);
      strokeWeight(2.5f);
      line( x + this.height/2.0f, y + this.height/2.0f, valueX, y + this.height/2.0f );
      stroke(0xff000000);
    }
    
    if (label != null) {
      textSize(ds.menuTextSize);
      textAlign(LEFT);
      text(label, x-labelWidth, y+this.height);
    }
    
    if (isPressed) { dragged(); }
  }
  
  public void setValue(float _value) {
    value = _value;
    valueX = map( value, 0, 1, x, x+this.width-this.height );
  }
  
  public void updateFromSlider() {
    // Todo: add Id, only update the specific slider being dragged
    
    if (sliderId == 0) { // radius
      ff.radius = ff.minRadius + sliders[sliderId].value * ff.radiusScale;
    }
    
    else if (sliderId == 1) { // Bond distance
      double newBpDist = ff.minBpDist + (sliders[sliderId].value * ff.bpDistScale);
      
      // Directly move residues when not in simulation mode
      if (!simulationMode) {
        double distChange = (newBpDist - ff.bpDist);
        if (!updating && (distChange >= .5f || distChange <= -.5f)) {
          updating = true;
          SphereList spheres = currentState.getSpheres();
        
          for (Sphere2D s : spheres) {
            if (s.pairedWForce != null && s.id < s.pairedWForce.id) {
              Sphere2D pairedBase = s.pairedWForce;
              double pairDist = s.distance(pairedBase);
              double moveDist = distChange / 2.0f;
              
              if (distChange < 0) {
                if (pairDist <= ff.minBpDist) { // If bond distance is below the minimum
                  continue;
                }
                else if ((pairDist + distChange) < ff.minBpDist) { // If change would take it past the minimum
                  moveDist = (ff.minBpDist - pairDist) / 2.0f;
                }
              }
              
              double moveX = (moveDist * (s.x - pairedBase.x)) / pairDist;
              s.x += moveX;
              pairedBase.x -= moveX;
              
              double moveY = (moveDist * (s.y - pairedBase.y)) / pairDist;
              s.y += moveY;
              pairedBase.y -= moveY;
            }
          }
          ff.bpDist = newBpDist;
          updating = false;
        }
      }
      else {
        ff.bpDist = newBpDist;
      }
    }
    
    else if (sliderId == 2) { // Chain length
      ff.backboneDist = (ff.minBackboneDist) + sliders[sliderId].value * ff.backboneDistScale;
    }
    
    else if (sliderId == 3) { // Color mode
      int prevColor = ds.colorMode;
      ds.colorMode = PApplet.parseInt(sliders[sliderId].value * (DisplaySettings.COLOR_MODE_MAX - 1)) + 1;
      if (ds.colorMode >= DisplaySettings.COLOR_MODE_MAX) ds.colorMode = DisplaySettings.COLOR_MODE_MAX - 1;
      if (prevColor != ds.colorMode) {
        displayColorMode();
      }
    }
    
    else {
      println("Warning: invalid sliderId: " + sliderId);
    }
    
    // ds.padding = sliders[4].value * ds.paddingScale; // Screen boundary size
    // ff.jitterWeight = sliders[5].value * 5000; // temperature
  }
}
class Sphere2D { // Each residue
  int id; // position in the sphereList (based on the original file initialization)
  int relId; // id relative to strand start
  int strandId = 0; // sequence it belongs to (starting on 0)
  String residue = ""; // Residue letter
  Sphere2D fiveP; // five prime backbone adjacent residue
  Sphere2D threeP; // three prime adjacent residue
  Sphere2D pairedWForce; // pair that applies force
  Sphere2D pairedH; // paired Hoogsteen residue DEPRACATE
  Sphere2D pairedS; // paired Sugar residue DEPRACATE
  boolean hasHelixUp = false; // true if this residue is paired and the adjacent previous residue on the same strand (1 lower strandId) is paired to the adjacent next residue (1 higher strandId) of the residue this is paired with.
  boolean hasHelixDown = false;
  // boolean inHairpin = false; // For hairpin optimization.
  double vx=0, vy=0, vz=0; // velocities
  double x, y, z=0; // coordinates
  double forceX=0, forceY=0, forceZ=0;
  float colorNum = 0;
  int baseColor = 0xffFFFFFF;
  String[] bonds; // stores bond type at that location

  Sphere2D(int _id, int _strandId, String _residue, String[] _bonds) {
    this.id = _id;
    this.strandId = _strandId;
    this.residue = _residue;
    this.bonds = _bonds;
  }

  Sphere2D(int _id, int _strandId, String _residue, double xpos, double ypos, double zpos) {
    this.id = _id;
    this.strandId = _strandId;
    this.residue = _residue;
    this.x = xpos;
    this.y = ypos;
    this.z = zpos;
  }

  public Sphere2D cloneFunction() {
    Sphere2D result = new Sphere2D(id, strandId, residue, x, y, z);
    result.fiveP = this.fiveP;
    result.threeP = this.threeP;
    result.hasHelixUp = this.hasHelixUp;
    result.hasHelixDown = this.hasHelixDown;
    result.relId = this.relId;
    result.pairedWForce = this.pairedWForce;
    result.pairedH = this.pairedH;
    result.pairedS = this.pairedS;
    result.vx = this.vx;
    result.vy = this.vy;
    result.colorNum = this.colorNum;
    result.baseColor = this.baseColor;
    result.forceX = this.forceX;
    result.forceY = this.forceY;
    result.bonds = stringArrayClone(this.bonds);
    return result;
  }

  public boolean isPaired(Sphere2D other) {
    if (other == null) {
      return false;
    }
    return (pairedWForce == other) || (pairedH == other) || (pairedS == other);
  }

  public boolean isPaired() {
    return (pairedWForce != null) || (pairedH != null) || (pairedS != null);
  }

  public boolean isAdjacent(Sphere2D other) {
    if (other == null) {
      return false;
    }
    return (fiveP == other) || (threeP == other);
  }

  // Distance to another sphere
  public double distance(Sphere2D other) {
    double dx = other.x - x;
    double dy = other.y - y;
    //double dz = other.z - z;
    return normFunction(dx, dy, 0);
  }

  // Distance to a point
  public double distance(double otherX, double otherY) {
    double dx = otherX - x;
    double dy = otherY - y;
    //double dz = otherZ - z;
    return normFunction(dx, dy, 0);
  }

  // Rotate by angle (in radians) about center of circle rx, ry
  public void rotateSphere(double angle, double rx, double ry) {
    double x2 = x - rx;
    double y2 = y - ry;
    double ca = Math.cos(angle);
    double sa = Math.sin(angle);
    double x2b = x2 * ca - y2 * sa;
    double y2b = x2 * sa + y2 * ca; 
    x = x2b + rx;
    y = y2b + ry;
  }

  
  // Compute force on this sphere by every other outside force and every other sphere, update velocity and position accordingly.
  public void update() {
    // Stay within boundaries:
    if (ds.padding > 0) { // slider at 0 turns off all padding
      if (x < ds.padding) {
        forceX += ff.padWeight * ( ds.padding - x );
      } else if (x > (width - ds.padding)) {
        forceX -= ff.padWeight * (x - (width - ds.padding));
      }
      if (y < ds.padding) {
        forceY += ff.padWeight * (ds.padding - y);
      } else if (y > (height - ds.padding)) {
        forceY -= ff.padWeight * (y - (height - ds.padding));
      }
    }
    
    // Attract to center of mass
    forceX -= ff.cmWeight * (cmx - (width/2.0f));
    forceY -= ff.cmWeight * (cmy - (height/2.0f));
    
    
    // Straighten
    if (fiveP != null && fiveP.fiveP != null) {
      double dx = fiveP.x - fiveP.fiveP.x; // Vector from the base 2 positions upstream to base upstream
      double dy = fiveP.y - fiveP.fiveP.y;
      //double dz = fiveP.z - fiveP.fiveP.z;
      double len = normFunction(dx, dy, 0);
      dx /= len; // normalize
      dy /= len;
      //dz /= len;
      
      double dx2 = x - fiveP.x; // Vector from upstream base to this base
      double dy2 = y - fiveP.y;
      //double dz2 = z - fiveP.z;
      double len2 = normFunction(dx2, dy2, 0);
      dx2 /= len2;
      dy2 /= len2;
      //dz2 /= len2;
      
      PVector p55 = new PVector((float)dx, (float)dy, 0);
      PVector p5 = new PVector((float)dx2, (float)dy2, 0);
      double angTerm = angleBetweenFunct(p5, p55); // bending angle: ideally 0

      double px = fiveP.x + len2 * dx; // Continuation of line made by 5'5' to 5': Ideal point of this base
      double py = fiveP.y + len2 * dy;
      //double pz = fiveP.z + len2 * dz;
      double ddx = px - x; // Vector from this base's position to ideal position. Pull is proportional to deviation from ideal
      double ddy = py - y;
      //double ddz = pz - z;
      
      double straightFactor = ff.straightWeight * angTerm * angTerm; // Proportional to square of angle
      
      forceX += ddx * straightFactor;
      forceY += ddy * straightFactor;
      //forceZ += ddz * straightFactor;
      
      /**
      // debugging
      double sForce = Math.sqrt(Math.pow(ddx * straightFactor, 2) + Math.pow(ddy * straightFactor, 2));
      sForceTot += sForce;
      if (sForce > sMax) { sMax = sForce; }
      ++sCount;
      */
    }
    
    // Straighten
    if (threeP != null && threeP.threeP != null) {
      double dx = threeP.x - threeP.threeP.x; // Vector from the base 2 positions upstream to base upstream
      double dy = threeP.y - threeP.threeP.y;
      //double dz = threeP.z - threeP.threeP.z;
      double len = normFunction(dx, dy, 0);
      dx /= len; // normalize
      dy /= len;
      //dz /= len;
      
      double dx2 = x - threeP.x; // Vector from upstream base to this base
      double dy2 = y - threeP.y;
      //double dz2 = z - fiveP.z;
      double len2 = normFunction(dx2, dy2, 0);
      dx2 /= len2;
      dy2 /= len2;
      //dz2 /= len2;
      
      PVector p55 = new PVector((float)dx, (float)dy, 0);
      PVector p5 = new PVector((float)dx2, (float)dy2, 0);
      double angTerm = angleBetweenFunct(p5, p55); // bending angle: ideally 0

      double px = threeP.x + len2 * dx; // Continuation of line made by 5'5' to 5': Ideal point of this base
      double py = threeP.y + len2 * dy;
      //double pz = threeP.z + len2 * dz;
      double ddx = px - x; // Vector from this base's position to ideal position. Pull is proportional to deviation from ideal
      double ddy = py - y;
      //double ddz = pz - z;
      
      double straightFactor = ff.straightWeight * angTerm * angTerm; // Proportional to square of angle
      
      forceX += ddx * straightFactor;
      forceY += ddy * straightFactor;
      //forceZ += ddz * straightFactor;
      
      /**
      // debugging
      double sForce = Math.sqrt(Math.pow(ddx * straightFactor, 2) + Math.pow(ddy * straightFactor, 2));
      sForceTot += sForce;
      if (sForce > sMax) { sMax = sForce; }
      ++sCount;
      */
    }
    
    /*
    // Temperature slider adds random shake
    if (ff.jitterWeight > 0) {
      PVector randVec = PVector.random3D(); // random direction
      PVector randVec2 = PVector.random2D(); // random magnitude
      double jitter = ff.jitterWeight * randVec2.x * randVec2.y;
      forceX += jitter * randVec.x;
      forceY += jitter * randVec.y;
      forceZ += jitter * randVec.z;
    }
    */
    
    double ax = forceX / ff.mass;
    vx = (ff.damping * vx) + ax; // Old: vx = ff.damping * (vx + ax);
    
    double ay = forceY / ff.mass;
    vy = (ff.damping * vy) + ay;

    double vel = normFunction(vx, vy, 0);
    
    // debugging
    //double vtemp = Math.sqrt(Math.pow(vx, 2) + Math.pow(vy, 2));
    //if (vtemp > vMax) { vMax = vtemp; }
    
    // Limit velocities
    if (vel > ff.velMax) {
      double velReduce = ff.velMax / vel;
      vx *= velReduce;
      vy *= velReduce;
    }
    
    //vTot += Math.sqrt(Math.pow(vx, 2) + Math.pow(vy, 2)); // debugging

    // Apply velocity to residue
    x += vx;
    y += vy;
    //z += vz;
    
    forceX = 0;
    forceY = 0;
    //forceZ = 0;
  }

  /* UNUSED
  double[] directionTo(Spring2D other) {
    double[] result = new double[3];
    result[0] = other.x - x;
    result[1] = other.y - y;
    result[2] = other.z - z;
    double d = distance(other);
    if (d > 0.0) {
      result[0] /= d;
      result[1] /= d;
      result[2] /= d;
    }
    return result;
  }
  */

  // Draw bond and backbone lines
  public void displayLines(boolean[] drawn) { // TODO: MOVE somewhere else?
    // Backbone
    drawer.Stroke(ds.bbCol);
    drawer.StrokeWeight(ds.bbWeight / ds.zoomFactor);
    if (threeP != null && !drawn[threeP.id]) { // If threeP has not been drawn yet.
      //       line((float)x, (float)y, (float)threeP.x, (float)threeP.y);
      drawer.Line((float)x, (float)y, (float)threeP.x, (float)threeP.y);
    }
    if (fiveP != null && !drawn[fiveP.id]) { // If threeP has not been drawn yet.
      //       line((float)x, (float)y, (float)fiveP.x, (float)fiveP.y);
      drawer.Line((float)x, (float)y, (float)fiveP.x, (float)fiveP.y);
    }
    
    // Base pairing
    if (pairedWForce != null && "cWW".equals(bonds[pairedWForce.id]) && !drawn[pairedWForce.id]) { // If pairedW has not been drawn yet. // ADDing: Check if its cWW!
      drawer.Stroke(ds.bpCol);
      drawer.StrokeWeight(ds.bpWeight / ds.zoomFactor);
      //       line((float)x, (float)y, (float)pairedWForce.x, (float)pairedWForce.y);
      drawer.Line((float)x, (float)y, (float)pairedWForce.x, (float)pairedWForce.y);
    }
  }

  // Draw base
  // TODO: contains code that should be part of DisplaySettings such as color palettes etc
  public void displayBody(float displayDiam, float offsetX, float offsetY) {
    drawer.Fill(0xffAAAAAA); // must be called before NoStroke
    if (ds.outlineMode) {
      drawer.Stroke(0xff000000);
      drawer.StrokeWeight( (sqrt((float)(ff.radius * ds.radiusShowMul)) / 3.0f) / ds.zoomFactor );
    } else {
      drawer.NoStroke();
    }
    
    // Light colors
    if (ds.colorMode == DisplaySettings.COLOR_MODE_LIGHT) {
      int c = color(0xffAAAAAA); // grey
      int id = (strandId) % 9;
      switch (id) {
      case 0: 
        c = color(0xffBFF9FF); // light blue 
        break;
      case 1:
        c = color(0xffFFE5B2);  // light orange 
        break;
      case 2: 
        c = color(0xffB2FFB9); // green 
        break;
      case 3: 
        c = color(0xffFFCBF6); // pink 
        break;
      case 4: 
        c = color(0xffFFFF98); // gold 
        break;
      case 5: 
        c = color(0xffFF9A98); // light red 
        break;
      case 6: 
        c = color(0xffDA8BFF);  // purple
        break;
      case 7: 
        c = color(0xffFFFFFF); // white
        break;
      case 8: 
        c = color(0xff7EB9FF); // Blue
        break;
      default: {
        if (debugPrints) { println("Internal error in color pallete"); }
        c = color(0xffFFFFFF);
        break;
        }
      }
      //       fill(c);
      drawer.Fill(c);

    // Dark pastels
    } else if (ds.colorMode == DisplaySettings.COLOR_MODE_FEW) {
      // RGB values from Show Me the Numbers from Stephen Few:
      int c = color(0xffAAAAAA); // grey
      int id = (strandId) % 8;
      switch (id) {
      case 0: 
        c = color(0xff65B2E8); // light blue #65B2E8
        break;
      case 1:
        c = color(0xffFAA43A);  // light orange #FAA43A
        break;
      case 2: 
        c = color(0xff60BD68); // green #60BD68
        break;
      case 3: 
        c = color(0xffF17CB0); // salmon #F17CB0
        break;
      case 4: 
        c = color(0xffF2DE44); // gold #F2DE44
        break;
      case 5: 
        c = color(0xffF15854); // light red #F15854
        break;
      case 6: 
        c = color(0xffB276B2);  // purple #B276B2
        break;
      case 7: 
        c = color(0xffC0C0C0); // light grey #C0C0C0
        break;
      default: {
        if (debugPrints) { println("Internal error in color pallete"); }
        c = color(0xffFFFFFF);
        break;
        }
      }
      //       fill(c);
      drawer.Fill(c);

    // Color by nucleotide type
    } else if (ds.colorMode == DisplaySettings.COLOR_MODE_RESIDUE) {
      String temp = residue.toUpperCase();
      if ("G".equals(temp)) {
        // fill(#64E4FF); //  #5DA5DA
        drawer.Fill(0xff64E4FF); //  #5DA5DA
      } else if ("A".equals(temp)) {
        // fill(#FF7471); // #60BD68
        drawer.Fill(0xffFF7471); // #60BD68
      } else if ("U".equals(temp)) {
        // fill(#64FF72); //  #98FFAA
        drawer.Fill(0xff64FF72); //  #98FFAA
      } else if ("T".equals(temp)) {
        // fill(#17FF8D);
        drawer.Fill(0xff17FF8D);
      } else if ("C".equals(temp)) {
        //  fill(#FFF68B); // #F17CB0
        drawer.Fill(0xffFFF68B); // #F17CB0
      } else {
        // fill(#FFFFFF);
        drawer.Fill(0xffFFFFFF);
      }
    
    // Rainbow, gap at dark purple
    } else if (ds.colorMode == DisplaySettings.COLOR_MODE_RAINBOW) {
      double frac = ( ds.colorMax * (double)(id) / ((double)currentState.sim.lenTot + (ds.colorMax/8.0f)));
      if (frac > ((ds.colorMax*5.0f) / 8.0f)) {
        frac += ds.colorMax / 8.0f;
      }
      // fill(color((int)frac, ds.colorMax, ds.colorMax));
      drawer.Fill(color((int)frac, ds.colorMax, ds.colorMax));
        
    // Bright neons
    } else if (ds.colorMode == DisplaySettings.COLOR_MODE_BYG) {
      int c = color(0xffAAAAAA); // grey
      int id = (strandId) % 9;
      switch (id) {
      case 0: 
        c = color(0xff00FFFF); // cyan
        break;
      case 1:
        c = color(0xffFF8400); // orange
        break;
      case 2:
        c = color(0xff00FF00); // green
        break;
      case 3:
        c = color(0xffFE00FF); // magenta
        break;
      case 4:
        c = color(0xffFFF63E); // yellow
        break;
      case 5: 
        c = color(0xffFF0000); // red
        break;
      case 6:
        c = color(0xff0092FF); // blue
        break;
      case 7:
        c = color(0xffD400FF);  // purple
        break;
      case 8:
        c = color(0xffFFFFFF); // white
        break;
      default: 
      { 
        if (debugPrints) { println("Internal error in color pallete"); }
        c = color(0xffFFFFFF);
        break;
      }
    }
    // fill(c);
    drawer.Fill(c);
  
    } else if (ds.colorMode == DisplaySettings.COLOR_MODE_GREY) {
      // colorMode(RGB);
      int c = color(0xffDBDBDB);
      // fill(c);
      drawer.Fill(c);
      
    } else if (ds.colorMode == DisplaySettings.COLOR_MODE_WHITE) {
      drawer.Fill(0xffFFFFFF);
      if (ds.outlineMode) {
        drawer.Stroke(0xff000000);
      } else {
        drawer.NoStroke();
      }	
      float saveWeight = drawer.strokeWidth;
      // drawer.StrokeWeight( (sqrt((float)(ff.radius * ds.radiusShowMul)) / 3.0) / ds.zoomFactor );
      // fill(#FFFFFF);
      drawer.StrokeWeight(saveWeight / ds.zoomFactor);
    // TODO: add: hairpin loop, internal loop, bulge, psuedoknot
    } else if (ds.colorMode == DisplaySettings.COLOR_MODE_STRUCTURE) {
      int c = 0xffBFF9FF; // Default light blue
      // Helix
      if (pairedWForce != null) {
        c = 0xffFFF63E; // Yellow
      }
      // fill(c);
      drawer.Fill(c);
    
    // User input
    } else if (ds.colorMode == DisplaySettings.COLOR_MODE_CUSTOM) {
      // fill(baseColor);
      drawer.Fill(baseColor);
    } else if (ds.colorMode == DisplaySettings.COLOR_MODE_RNADNA) {
      int c = color(0xffAAAAAA); // grey
      if (residue.length() > 0){
       if (residue.toUpperCase().equals(residue)) { // upper case: RNA
         c = color(0xffFF8400); // orange
       } else { // lower case: DNA
         c = color(0xff00FFFF); // cyan
       }
      }
      drawer.Fill(c);
    }
    
    // If it is selected, highlight and enlarge
    if (attachedIds.containsKey(id)) {
      displayDiam *= ds.highlightMul;
      drawer.Stroke(ds.highlightCol);
    }
    
    // Draw residue
    drawer.Ellipse((float)x, (float)y, displayDiam, displayDiam);
       //  ellipse((float)x, (float)y, displayDiam, displayDiam);

    // Draw letter
    if (ds.textMode) {
//             fill(ds.textCol);
      drawer.Fill(ds.textCol);
//             text(residue, (float)x + offsetX, (float)y + offsetY);
      drawer.Text(residue, (float)x + offsetX, (float)y + offsetY);
    }
  }
}
//debugging
/**
 double vTot;
 double sForceTot;
 double sCount;
 double sMax;
 double vMax;
 */

class SphereList extends ArrayList<Sphere2D> {
  ArrayList<Integer> displayOrder = new ArrayList<Integer>(); // Order that bases are drawn

  HashMap<Integer, Integer> relaxedIds = new HashMap<Integer, Integer>(); // Set of bases to ignore tension forces on (HashMap used because Set is not supported in Processing)

  public SphereList() {
    super();
    relaxedIds = new HashMap<Integer, Integer>();
    displayOrder = new ArrayList<Integer>();
  }

  public SphereList cloneFunction() {
    SphereList spheres = new SphereList();
    for (int i = 0; i < size(); ++i) {
      spheres.add(get(i).cloneFunction());
    }
    for (int i = 0; i < spheres.size(); ++i) {
      if (spheres.get(i).fiveP != null) { // still points to original array
        spheres.get(i).fiveP = spheres.get(spheres.get(i).fiveP.id);
      }
      if (spheres.get(i).threeP != null) { // still points to original array
        spheres.get(i).threeP = spheres.get(spheres.get(i).threeP.id);
      }
      if (spheres.get(i).pairedWForce != null) { // still points to original array
        spheres.get(i).pairedWForce = spheres.get(spheres.get(i).pairedWForce.id);
      }
      if (get(i).pairedH != null) { // still points to original array
        get(i).pairedH = get(get(i).pairedH.id);
      }
      if (get(i).pairedS != null) { // still points to original array
        get(i).pairedS = get(get(i).pairedS.id);
      }
    }
    spheres.relaxedIds = (HashMap<Integer, Integer>)relaxedIds.clone();
    spheres.displayOrder = (ArrayList<Integer>)displayOrder.clone();
    return spheres;
  }

  /** Returns sphere with this id. */
  public Sphere2D get(int id) {
    return (Sphere2D)(super.get(id));
  }

  /** Returns number of spheres. */
  public int size() {
    return super.size();
  }

  // Compute forces, update velocities and positions
  public void update() {
    /**
     // debugging
     double bbDiffTot = 0;
     int bbCount = 0;
     double bpDiffTot = 0;
     int bpCount = 0;
     double minPosViol = 100;
     double tensionTot = 0;
     int TCount = 0;
     double tMax = 0;
     double eForceTot = 0;
     int eCount = 0;
     double eMax = 0;
     double vdTot = 0;
     int vdCount = 0;
     double vdMax = 0;
     sForceTot = 0;
     sMax = 0;
     sCount = 0;
     vTot = 0;
     vMax = 0;
     */

    double eCutoff = Math.sqrt(ff.backboneDist) * ff.eCutoffScale;
    if (size() > 1800) {
      eCutoff /= 2;
    }

    // Interactions between residues
    for (int i = 0, l = size(); i < l; ++i) {
      Sphere2D s1 = get(i);

      for (int j = i+1; j < l; ++j) {
        Sphere2D s2 = get(j);
        double d = s1.distance(s2);
        if (d < 1) {
          d = 1; // avoid singularity
        }

        double d2 = d * d;
        double dx = s1.x - s2.x;
        double dy = s1.y - s2.y;

        double dx0 = dx/d;
        double dy0 = dy/d;

        double pushX = (ff.backboneDist * dx0) / d2;
        double pushY = (ff.backboneDist * dy0) / d2;


        // Electrostatic repulsion
        if ((d < eCutoff) && (s1.pairedWForce != s2) && (!s1.isAdjacent(s2))) {
          s1.forceX += ff.eWeight * pushX;
          s1.forceY += ff.eWeight * pushY;

          s2.forceX -= ff.eWeight * pushX;
          s2.forceY -= ff.eWeight * pushY;

          // debugging
          /**
           double eForce = Math.sqrt(Math.pow(ff.eWeight * pushX, 2) + Math.pow(ff.eWeight * pushY, 2));
           eForceTot += eForce;
           if (eForce > eMax) { eMax = eForce; }
           ++eCount;
           */
        }

        // Van Der Waals repulsion
        s1.forceX += ff.vdWeight * pushX;
        s1.forceY += ff.vdWeight * pushY;

        s2.forceX -= ff.vdWeight * pushX;
        s2.forceY -= ff.vdWeight * pushY;

        // debugging
        /**
         double vdForce = Math.sqrt(Math.pow(ff.vdWeight * pushX, 2) + Math.pow(ff.vdWeight * pushY, 2));
         vdTot += vdForce;
         if (vdForce > vdMax) { vdMax = vdForce; }
         ++vdCount;
         */

        // Tension force
        double viol = -1;
        if ( s1.pairedWForce == s2 && !(relaxedIds.containsKey(s1.id) || relaxedIds.containsKey(s2.id))) { //  && !(s1.inHairpin && s2.inHairpin)
          viol = d - ff.bpDist;
          //bpDiffTot += viol; // debuging
          //++bpCount; // debuging
        } else if (s1.isAdjacent(s2)) {
          viol = d - ff.backboneDist;
          //bbDiffTot += viol; // debuging
          //++bbCount; // debuging
        }

        //if (viol > 0 && viol < minPosViol) { minPosViol = viol; } // debugging

        // ?TODO: Add a soft (perhaps scaled somehow) outwards restoring force for when violation is negative?
        if (viol > 0.1f) {
          s1.forceX -= ff.stretchWeight * viol * dx0;
          s1.forceY -= ff.stretchWeight * viol * dy0;

          s2.forceX += ff.stretchWeight * viol * dx0;
          s2.forceY += ff.stretchWeight * viol * dy0;

          // debugging
          /**
           double TForce = Math.sqrt(Math.pow(ff.stretchWeight * viol * dx0, 2) + Math.pow(ff.stretchWeight * viol * dy0, 2));
           tensionTot += TForce;
           if (TForce > tMax) { tMax = TForce; }
           ++TCount;
           */
        }
      }
    }

    if (attachedIds.size() == 0 || simulateAllMode) { // simulate all residues
      for (int i = 0; i < size(); ++i) {
        get(i).update();
      }
    } else { // only simulate selected residues
      for (Iterator<Integer> it = attachedIds.keySet().iterator(); it.hasNext(); ) {
        get(it.next()).update();
      }
    }

    // Force debugging
    /**
     if (debugPrints) {
     // Check for 0s in denominators
     double bbAvg = bbCount > 0 ? bbDiffTot / bbCount : 0;
     double bpAvg = bpCount > 0 ? bpDiffTot / bpCount : 0;
     double tAvg = TCount > 0 ? tensionTot / TCount : 0;
     double vAvg = size() > 0 ? vTot / size() : 0;
     double eAvg = eCount > 0 ? eForceTot / eCount : 0;
     double sAvg = sCount > 0 ? sForceTot / sCount : 0;
     double vdAvg = vdCount > 0 ? vdTot / vdCount : 0;
     println("\n\nBackbone: " + ff.backboneDist);
     println("Avg backbone viol: " + bbAvg);
     println("Basepair: " + ff.bpDist);
     println("Avg basepair viol: " + bpAvg);
     println("minPosViol: " + minPosViol);
     println("TensionAvg: " + tAvg + ",  tMax: " + tMax);
     
     println("\nE-Avg: " + eAvg + ",  eMax: " + eMax + ",  eCount: " + eCount + ",  eCutoff: " + eCutoff);
     
     println("\nvd-Avg: " + vdAvg + ",  vdMax: " + vdMax);
     
     println("\nStraight Avg: " + sAvg + ",  sMax: " + sMax);
     
     println("\nV-Avg: " + vAvg + ",  vMax: " + vMax);
     }
     */
  }

  // Make helices rigid by fixing residue coordinates
  public void fixHelices() {
    SphereList spheres = this;
    for (int i = 0; i < spheres.size(); ++i) {
      Sphere2D res = spheres.get(i);
      if (res.pairedWForce != null && (!res.hasHelixUp) && res.hasHelixDown && res.id < res.pairedWForce.id) { // found beginning of helix strand. 
        Sphere2D resEnd = res;
        int j;
        for (j = i+1; j < spheres.size(); ++j) { // from here till the end of the helix
          resEnd = spheres.get(j);
          if (!resEnd.hasHelixDown) { // If this is the end of the helix
            break;
          }
        }
        
        //if (rigidHelices) ...

        int len = j - i + 1; // length of helix
        double stacks = len - 1; // how many bonds between residues of the same backbone in one side of the helix
        double dx = resEnd.x - res.x; // Distance between the first and last residues in the helix (vector from start to end)
        double dy = resEnd.y - res.y;
        double dx2 = resEnd.pairedWForce.x - res.pairedWForce.x; // vector running along the other half of the helix (same direction as lower half)
        double dy2 = resEnd.pairedWForce.y - res.pairedWForce.y;
        double dxInterval = dx / stacks; // Step intervals between the first and last residues in the helix
        double dyInterval = dy / stacks;
        double dx2Interval = dx2 / stacks;
        double dy2Interval = dy2 / stacks;
        double distanceSum = 0;
        int count = 0;
        for (int k = 0; k < len; ++k) {
          // SET NEW COORDINATES FOR EACH SPHERE IN BOTH SIDES OF HELIX
          Sphere2D resNew = spheres.get(i + k);
          if ( !relaxedIds.containsKey(resNew.id) && !relaxedIds.containsKey(resNew.pairedWForce.id) ) {
            resNew.x = res.x + (k * dxInterval);
            resNew.y = res.y + (k * dyInterval);

            resNew.pairedWForce.x = res.pairedWForce.x + (k * dx2Interval);
            resNew.pairedWForce.y = res.pairedWForce.y + (k * dy2Interval);
            ++count;
            distanceSum += resNew.pairedWForce.distance(resNew);
          }
        }

        double distanceAvg = 0;
        if (count > 0) {
          distanceAvg = distanceSum / count;
        }

        if (distanceAvg < ff.bpDist) { 
          distanceAvg = ff.bpDist;
        }
        double dxMid = (dx+dx2) / 2.0f; // Distance between midpoints of starts and ends of helices
        double dyMid = (dy+dy2) / 2.0f;
        PVector midLine = new PVector((float)dxMid, (float)dyMid);
        float angle = midLine.heading(); // Gives angle from 0 to PI running clockwise starting on positive x-axis to negative x-axis,
        // and 0 to -PI running counter clockwise

        double xDifference = (distanceAvg * sin(angle)) / 2.0f; // Right angle correction
        double yDifference = (-distanceAvg * cos(angle)) / 2.0f;

        double xStartMidpoint = (res.x + res.pairedWForce.x) / 2.0f; // Midpoint between the starts of the helix
        double yStartMidpoint = (res.y + res.pairedWForce.y) / 2.0f;

        double xMidInterval = dxMid / stacks; // Step interval along midline
        double yMidInterval = dyMid / stacks;
        double xNew, yNew;

        double mul = 0;
        for (int k = 0; k < len; ++k) { // Determine which side of the midline the strand is on
          Sphere2D resNew = spheres.get(i + k);
          double sign = (midLine.x) * (resNew.y - yStartMidpoint) - (midLine.y) * (resNew.x - xStartMidpoint);
          if (sign > 0) {
            mul -= 1;
          } else if (sign < 0) {
            mul += 1;
          }
        }
        if (mul >= 0) {
          mul = 1;
        } else {
          mul = -1;
        }

        // Pull residues to midline
        for (int k = 0; k < len; ++k) {
          Sphere2D resNew = spheres.get(i + k);

          if ( !relaxedIds.containsKey(resNew.id) && !relaxedIds.containsKey(resNew.pairedWForce.id) ) {
            xNew = xStartMidpoint + (k * xMidInterval);
            yNew = yStartMidpoint + (k * yMidInterval);

            if (simulateAllMode || (attachedIds.size() == 0 || attachedIds.containsKey(resNew.id))) {
              resNew.x = xNew + (xDifference * mul);
              resNew.y = yNew + (yDifference * mul);
            }

            if (simulateAllMode || (attachedIds.size() == 0 || attachedIds.containsKey(resNew.pairedWForce.id))) {
              resNew.pairedWForce.x = xNew - (xDifference * mul);
              resNew.pairedWForce.y = yNew - (yDifference * mul);
            }
          }
        }
        
        if (rigidHairpins && !rigidLoops) {
          // Circularize hairpins
          res = resEnd;
          while (res.threeP != null) {
            res = res.threeP;
            if (res.isPaired()) {
              break;
            }
          }

          if (resEnd.pairedWForce.id == res.id && resEnd.id != res.id) {
            //println("\nFound hairpin from residue " + resEnd.id + " - " + res.id);
            //Ensure last bond of the helix is within a range of the basepair distance
            //double dist = res.distance(resEnd);
            double sideL =  res.distance(resEnd); // ff.backboneDist
            //double distDiff = dist - ff.bpDist;
            //distDiff = distDiff < 0 ? distDiff*-1 : distDiff;
            //println("distDiff: " + distDiff);

            //if (distDiff < 10) {

            // ***Circularize Hairpin***
            // println("Circularizing!");

            Sphere2D resStart = resEnd;
            resEnd = res;

            //resStart.inHairpin = true;
            //resEnd.inHairpin = true;

            // Treat it like a regular polygon of n-sides
            int n = (resEnd.id - resStart.id) + 1;
            
            // Vector from end to start
            PVector v1 = new PVector((float)(resStart.x - resEnd.x), (float)(resStart.y - resEnd.y));
            // Vector from fiveP to start
            PVector v2 = new PVector((float)(resStart.x - resStart.fiveP.x), (float)(resStart.y - resStart.fiveP.y));

            double angChange = (2 * PI) / (double) n; // Exterior angle
            //            println("angChange: " + angChange);
            if (v1.y * v2.x > 0) {
              angChange *= -1;
            }

            float ang = v1.heading();
            res = resStart;
            double targetX = res.x;
            double targetY = res.y;

            for (int k = 0; k < n - 2; ++k) {
              ang += angChange;

              double xDiff = sideL * cos(ang);
              double yDiff = sideL * sin(ang);

              targetX += xDiff;
              targetY += yDiff;

              res = res.threeP;

              res.forceX += ff.hairpinWeight * (targetX - res.x);
              res.forceY += ff.hairpinWeight * (targetY - res.y);

              // res.threeP.forceX +=  ff.hairpinWeight * ((res.x + xDiff) - res.threeP.x);  Force correction
              //res.threeP.forceY +=  ff.hairpinWeight * ((res.y + yDiff) - res.threeP.y);

              //res.threeP.x = res.x + xDiff;  Perfect correction
              //res.threeP.y = res.y + yDiff;

              //res = res.threeP;  Place for the other
              //res.inHairpin = true;  Unimplemented idea
            }
          }
        }
      } // if res.pairedForce ...
    } // for int i = 0; i < spheres.size ...
  } // fixHelices
  
  // Recursive algorithm: find helix, search border for next hairpin, if found call again, if not call circularize. Treat previous pairs as residues on the circumference

  //ArrayList<Integer> searchEnds = new ArrayList<Integer>();
  
  public void fixLoops() {
    //println("\n\n\n*****STARTING FIX LOOPS*****");
    SphereList spheres = this;
    //searchEnds = new ArrayList<Integer>();
    for (int i = 0; i < spheres.size(); ++i) {
      Sphere2D res = spheres.get(i);
      if (res.pairedWForce != null && res.pairedWForce.strandId == res.strandId && (!res.hasHelixUp) && res.hasHelixDown && res.id < res.pairedWForce.id) { // found beginning of an intra-strand helix
        //println("\n\nCalling findLoop: " + res.id + " , from fixLoops");
        findLoop(res);
        i = res.pairedWForce.id;
      }
    }
  }
  
  public void findLoop(Sphere2D helixStart) {
    SphereList spheres = this;
    Sphere2D loopStart = helixStart; // loopStart: residue on the end of the 5' to 3' helix strand
    for (int i = helixStart.id+1; i < spheres.size(); ++i) { // from here till the end of the helix
      loopStart = spheres.get(i);
      if (!loopStart.hasHelixDown) {
        break;
      }
    }
    Sphere2D loopEnd = loopStart.pairedWForce;
    //println("loopStart: " + loopStart.id);
    
    ArrayList<Sphere2D> loopResidues = new ArrayList<Sphere2D>();
    int endSearch = loopEnd.id;
    //searchEnds.add(endSearch); // For ensuring we aren't messing with pseudoknotted helixes
    Sphere2D searchRes = loopStart;
    while (searchRes.id < (endSearch-1)) {
      searchRes = searchRes.threeP;
      loopResidues.add(searchRes);
      if (searchRes.pairedWForce != null && searchRes.id < searchRes.pairedWForce.id) {
        // Interstrand helix or pseudoknot, abort circularization of this loop
        if (searchRes.pairedWForce.strandId != searchRes.strandId || searchRes.pairedWForce.id > endSearch) {
          return;
        }
        /*
        if (searchRes.pairedWForce.strandId != searchRes.strandId || searchRes.pairedWForce.id > searchEnds.get(searchEnds.size() - 1)) {
          searchEnds.remove(searchEnds.size() - 1);
          return;
        }
        */
        
        /*
        for (int i = 0, int l = searchEnds.size(); i < l; ++i) {
          if (searchRes.pairedWForce.id > searchEnds.get(i))
        }
        */
        
        if ((!searchRes.hasHelixUp) && searchRes.hasHelixDown) {
          //println("found nested helix inside start: " + loopStart.id + "  at searchRes " + searchRes.id);
          findLoop(searchRes);
        }
        //println("loop at searchRes: " + searchRes.id);
        searchRes = searchRes.pairedWForce;
        //println("Also adding the inside of helix: " + searchRes.id);
        loopResidues.add(searchRes);
      }
    }
    
    // ***Circularize*** //
    //println("\ncircularizing loop from loopStart " + loopStart.id + " to " + loopEnd.id);
    double dist = loopStart.distance(loopEnd);
    
    // Treat it like a regular polygon of n-sides
    int n = loopResidues.size() + 2;
    double sideL =  ff.backboneDist; //  ff.bpDist;
    
    // Vector from end to start
    PVector v1 = new PVector((float)(loopStart.x - loopEnd.x), (float)(loopStart.y - loopEnd.y));
    // Vector from fiveP to start
    PVector v2 = new PVector((float)(loopStart.x - loopStart.fiveP.x), (float)(loopStart.y - loopStart.fiveP.y));
    
    double angChange = (2 * PI) / (double) n; // Exterior angle
    // println("angChange: " + angChange);
    if (v1.y * v2.x > 0) {
      angChange *= -1;
    }
    
    float ang = v1.heading();
    Sphere2D res = loopStart;
    double targetX = res.x;
    double targetY = res.y;
  
    for (int k = 0; k < n - 2; ++k) {
      ang += angChange;
  
      double xDiff = sideL * cos(ang);
      double yDiff = sideL * sin(ang);
  
      targetX += xDiff;
      targetY += yDiff;
      
      if (loopResidues.get(k).id < res.id) {
        println("\n****ERROR!!!!****\n");
      }
      res = loopResidues.get(k);
      
      //fill(#FF0D0D);
      //ellipse((float)targetX, (float)targetY, 15, 15);
      
      res.forceX += ff.loopWeight * (targetX - res.x);
      res.forceY += ff.loopWeight * (targetY - res.y);
  
      // res.threeP.forceX +=  ff.hairpinWeight * ((res.x + xDiff) - res.threeP.x);  Force correction
      //res.threeP.forceY +=  ff.hairpinWeight * ((res.y + yDiff) - res.threeP.y);
  
      //res.threeP.x = res.x + xDiff;  Perfect correction
      //res.threeP.y = res.y + yDiff;
      
      // Push away and straighten nested stems
      if (res.hasHelixDown) {
        
        float newAng = ang + (float)angChange;
        if (angChange < 0) {
          newAng += PI / 2.0f;
        } else {
          newAng -= PI / 2.0f;
        }
        
        Sphere2D outerHelixEnd = res.threeP;
        while (outerHelixEnd.hasHelixDown) {
          outerHelixEnd = outerHelixEnd.threeP;
        }
        //println("\n outerHelixEnd: " + outerHelixEnd.id);
        
        int outerHelixLength = outerHelixEnd.id - res.id;
        double helixDiffX = outerHelixLength * sideL * cos(newAng);
        double helixDiffY = outerHelixLength * sideL * sin(newAng);
        
        double outer1TargetX = targetX + helixDiffX;
        double outer1TargetY = targetY + helixDiffY;
        
        Sphere2D outerHelixEnd2 = outerHelixEnd.pairedWForce;
        
        if (angChange < 0) {
          newAng -= PI / 2.0f;
        } else {
          newAng += PI / 2.0f;
        }
        
        double outer2TargetX = outer1TargetX + (dist * cos(newAng));
        double outer2TargetY = outer1TargetY + (dist * sin(newAng));
        
        /*
        fill(#0DF9FF);
        ellipse((float)outer1TargetX, (float)outer1TargetY, 15, 15);
        ellipse((float)outer2TargetX, (float)outer2TargetY, 15, 15);
        */
        
        outerHelixEnd.forceX += ff.branchStraightenWeight * (outer1TargetX - outerHelixEnd.x);
        outerHelixEnd.forceY += ff.branchStraightenWeight * (outer1TargetY - outerHelixEnd.y);
        
        outerHelixEnd2.forceX += ff.branchStraightenWeight * (outer2TargetX - outerHelixEnd2.x);
        outerHelixEnd2.forceY += ff.branchStraightenWeight * (outer2TargetY - outerHelixEnd2.y);
      }
    }
  }


  // Set all velocities to zero
  public void stopVelocities() {
    for (int i = 0; i < size(); ++i) {
      Sphere2D sphere = get(i);
      sphere.vx = 0.0f;
      sphere.vy = 0.0f;
    }
  }


  /** Returns coordinates of sphere that is closest to given coordinates */
  public double[] findClosest(double x, double y) {
    int closestId = 0;
    double dBest = get(0).distance(x, y);
    for (int i = 1; i < size(); ++i) {
      double d = get(i).distance(x, y);
      if (d < dBest) {
        dBest = d;
        closestId = i;
      }
    }
    double[] output = {closestId, dBest};
    return output;
  }

  public PVector computeCenterOfMass() {
    cmx = 0.0f;
    cmy = 0.0f;
    for (int i = 0; i < size(); ++i) {
      cmx += get(i).x;
      cmy += get(i).y;
    }
    cmx /= size(); // center of mass
    cmy /= size();
    return new PVector(cmx, cmy);
  }

  public void display() {
    boolean[] drawn = new boolean[displayOrder.size()];
    float displayDiameter = (float) (ff.radius * ds.radiusShowMul);
    float offsetX = ds.textOffsX * displayDiameter;
    float offsetY = ds.textOffsY * displayDiameter;
    drawer.TextSize(ds.textReduction * displayDiameter);
    textAlign(LEFT);
    for (int id : displayOrder) {
      get(id).displayLines(drawn);
      get(id).displayBody(displayDiameter, offsetX, offsetY);
      drawn[id] = true;
    }
  }

  public void displayLabels() {
    float radius2 = (float) (ff.radius * ds.radiusShowMul);
    drawer.TextSize(constrain(ds.textReduction * radius2, 6, 65));
    textAlign(CENTER, CENTER);
    drawer.Stroke(0xff9B9B9B);
    drawer.StrokeWeight(constrain(sliders[0].value * ds.labelWeight, .7f, ds.labelWeight));
    drawer.Fill(ds.textCol);

    if (!capturingScreen) {
      for (Iterator<Integer> it = relaxedIds.keySet().iterator(); it.hasNext(); ) {
        PVector pv = findPositionForPrint( it.next(), radius2, radius2 );
        if (pv != null) {
          drawer.Text("R", pv.x, pv.y);
        }
      }
    }

    drawer.Fill(ds.textCol);
    int[] strandStarts = currentState.sim.starts;
    for (int i = 0; i < strandStarts.length; ++i) {
      int start = strandStarts[i];
      int end;
      if (i < strandStarts.length-1) {
        end = strandStarts[i+1];
      } else {
        end = size();
      }
      PVector pv = findPositionForPrint(start, (radius2 + 10), radius2 );
      if (pv != null) {
        String textString = str(1);
        float ntX = (float)get(start).x;
        float ntY = (float)get(start).y;
        float dx = pv.x - ntX;
        float dy = pv.y - ntY;
        float magn = sqrt( dx*dx + dy*dy );
        float normX = dx/magn;
        float normY = dy/magn;
        drawer.Fill(ds.textCol);
        drawer.Line( pv.x-(normX*8), pv.y-(normY*8), ntX + (normX * radius2/2.0f), ntY + (normY * radius2/2.0f));
        drawer.Text(textString, pv.x, pv.y);
      }
      for (int j = start+9; j < end; j += 10) {
        pv = findPositionForPrint(j, (radius2 + 10), radius2 );
        if (pv != null) {
          String textString = str((j-start)+1);
          float ntX = (float)get(j).x;
          float ntY = (float)get(j).y;
          float dx = pv.x - ntX;
          float dy = pv.y - ntY;
          float magn = sqrt( dx*dx + dy*dy );
          float normX = dx/magn;
          float normY = dy/magn;
          drawer.Fill(ds.textCol);
          drawer.Line( pv.x-(normX*8), pv.y-(normY*8), ntX + (normX * radius2/2.0f), ntY + (normY * radius2/2.0f));
          drawer.Text(textString, pv.x, pv.y);
        }
      }
    }
  }

  public void displayInfo() {
    float radius2 = (float)(ff.radius * ds.radiusShowMul);
    drawer.TextSize(constrain(ds.textReduction * radius2, 6, 65));
    textAlign(CENTER, CENTER);
    drawer.Fill(ds.textCol);

    int[] strandStarts = currentState.sim.starts;
    for (int i = 0; i < strandStarts.length; ++i) {
      int start = strandStarts[i];
      int end;
      if (i < strandStarts.length-1) {
        end = strandStarts[i+1];
      } else {
        end = size();
      }
      int id = PApplet.parseInt((start + end) / 2);
      PVector pv = findPositionForPrint( id, radius2 * 1.5f, radius2 );
      if (pv != null) {
        String textString = "Strand " + (get(id).strandId + 1);
        drawer.Text(textString, pv.x, pv.y);
      }

      // 5' 3' labels
      if (end - start >= 4) {
        pv = findPositionForPrint( start+1, radius2 * 1.5f, radius2 );
        if (pv != null) {
          drawer.Text("5'", pv.x, pv.y);
        }
        if ((end - start) % 10 == 0) {
          --end;
        }
        pv = findPositionForPrint( end-1, radius2 * 1.5f, radius2 );
        if (pv != null) {
          drawer.Text("3'", pv.x, pv.y);
        }
      }
    }
  }

  public PVector findPositionForPrint(int i0, double delta, double targetBuffer) {
    double x = get(i0).x;
    double y = get(i0).y;
    for (int ix = -1; ix <= 1; ++ix) {
      for (int iy = 1; iy >= -1; --iy) {
        if (ix == 0 && iy == 0) { 
          continue;
        }
        double dist = delta;
        if (ix != 0 && iy != 0) { 
          dist *= .707f;
        }

        double x2 = x + (ix * dist);
        double y2 = y + (iy * dist);
        boolean violation = false;
        for (int ii = 0; ii < size(); ++ii) {
          if (ii == i0) { 
            continue;
          }
          Sphere2D sphere = get(ii);
          double d = normFunction(x2-sphere.x, y2-sphere.y, 0);
          if (d < targetBuffer) {
            violation = true;
            break;
          }
        }
        if (!violation) {
          return new PVector((float)x2, (float)y2);
        }
      }
    }
    return null;
  }
}
  public void settings() {  size(1200, 750); }
  static public void main(String[] passedArgs) {
    String[] appletArgs = new String[] { "ribosketch" };
    if (passedArgs != null) {
      PApplet.main(concat(appletArgs, passedArgs));
    } else {
      PApplet.main(appletArgs);
    }
  }
}
