import de.bezier.guido.*; // try to remove this dependency? /* GUIDO Library classes from https://github.com/fjenett/Guido/blob/master/examples/basics/slider/slider.pde */ //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>// //<>//
import java.util.Iterator;
import java.util.Map;

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.0;
float zoomTranslateXSave = 0.0;
float zoomTranslateYSave = 0.0;

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.0125; // How much obtuse angles are preffered
  double stretchWeight = 2.5; // Spring constant multiplier
  double padWeight = 0.005; // 1 // 20 // How strong the padding is
  double cmWeight = .3; // .1 How attracted to center of mass
  double mass = 25; // Resistance to acceleration
  double velMax = 12; // maximum velocity
  double damping = .5; // .6 // Friction: percent of velocity to be carried over
  double hairpinWeight = 4.5;
  double loopWeight = 1; //.05 
  double branchStraightenWeight = 1.5;
}

float cmx = 0.0;
float cmy = 0.0;

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.0;
float screenResY = 750.0;

void setup()
{
  size(1200, 750); // 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.5 * 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.5 * ds.sliderHeight);
    if ((i % 4) == 1) {
      x += (ds.sliderWidth + ds.sliderLabelWidth + ds.sliderGap) / 2.0;
    } 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.5;
    }

    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.25 * ds.sliderHeight; // Adjust active region of menu
    }
  }
  menuLimitY += 15;
  helpY = menuLimitY + 5;

  checkBoxWidth = (ds.sliderWidth + ds.sliderLabelWidth + ds.sliderGap)*1.5 + 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");
  }
}

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());
  }
}

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

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

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);
}

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;
}

void draw() {
  mydraw();
}    

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(#C9C9C9);
      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.0) - 100 <= mouseX && mouseX <= (width/3.0) + 100) { // Cancel
          aboveCancel = true;
        } else if (((width*2)/3.0) - 100 <= mouseX && mouseX <= ((width*2)/3.0) + 100) {
          aboveOk = true;
        }
      }

      // Ok button
      if (aboveOk) {
        strokeWeight(4);
        fill(#FFF8AD);
      } else {
        strokeWeight(2);
        fill(#FFFFFF);
      }
      stroke(ds.GUIColor);
      rectMode(CENTER);

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

      // Cancel button
      if (aboveCancel) {
        strokeWeight(4);
        fill(#FFF8AD);
      } else {
        strokeWeight(2);
        fill(#FFFFFF);
      }
      rectMode(CENTER);
      rect(width/3.0, height - 100, 200, 50, 10);
      fill(#000000);
      text("CANCEL", width/3.0, 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(#C9C9C9);

      strokeWeight(3);
      stroke(#00A8FF);
      fill(#FFFFFF);
      rectMode(CORNER);
      rect(30, 30, width - 60, height - 60, 10);

      fill(#000000);
      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.0 - 50 <= mouseX && width/2.0 + 50 >= mouseX) {
        aboveOk = true;
      }

      rectMode(CENTER);
      color fc = aboveOk ? #FFFFFF : #000000;
      fill(fc);
      rect(width/2.0, height - 100, 100, 50, 10);
      fc = aboveOk ? #000000 : #FFFFFF;
      fill(fc);
      textAlign(CENTER);
      drawer.TextSize(34);
      text("OK", width/2.0, 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(#FFFFFF); // (19/36.0)*ds.colorMax, abs(sin((float)(frameCount / 500.0)))*ds.colorMax*.9, ds.colorMax);
      fill(#FFFFFF);
      strokeWeight(3);
      stroke(#00A8FF);
      ellipse(width/2.0, height/2.0, 900, 400);

      fill(#000000);
      drawer.TextSize(100);
      textAlign(CENTER);
      text("RiboSketch", width/2.0, (height/2.0) + 20);
      color c;
      rectMode(CENTER);
      if ((width/2.0) - 110 <= mouseX && mouseX <= (width/2.0) + 110 &&
        (height*.68) - 25 <= mouseY && mouseY <= (height*.68) + 25) {
        fill(#FFFFFF);
        c = #000000;
      } else {
        fill(#000000);
        c = #FFFFFF;
      }
      rect(width/2.0, height*.68, 220, 50, 10);
      fill(c);
      drawer.TextSize(32);
      text("Click to Load", width/2.0, height*.68 + 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(#FFFFFF);
    }

    // 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.0; // reset!
        ds.zoomTranslateX = 0.0;
        ds.zoomTranslateY = 0.0;
      }
    }

    /// global coordinate transformation
    translate(ds.zoomTranslateX, ds.zoomTranslateY);
    // apply zoom?
    if (ds.zoomFactor != 1.0 && !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(#000000);
      //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(#FFFFFF, ds.colorMax / 1.5); // 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.2));

        // Print help instructions
        String currentCommand = "";
        for (int i = 0; i < helpLines.length; ++i) {
          fill(#000000);
          drawer.TextSize(ds.helpTextSize);
          textAlign(LEFT);
          currentCommand = helpLines[i];
          text(currentCommand, ds.helpX, helpY + 12 + i * 1.4 * ds.helpTextSize); // 1.4 spaced
          if (helpCommands.containsKey(currentCommand)) {
            textAlign(RIGHT);
            text(helpCommands.get(currentCommand), ds.helpX + 270, helpY + 12 + i * 1.4 * 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*.3);
        }
        text("Click bonding base", width/2.0, 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*.3);
        }
        text("Click a base to add a bond", width/2.0, height - 100);
      } else if (deletingBond) {
        fill(#E82121);
        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*.3);
        }
        text("Click a base to delete bonds", width/2.0, 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.0, (height / 2.0) - 125);
      textAlign(LEFT);
      fill(#000000);
    }

    /*
    // 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) * .5) + 60);
    text(textDisplay, width / 2.0, height/2.0);
    ++textDisplayCount;
    textAlign(LEFT);
    fill(#000000);
  }


  // 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.0);
    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(#000000);
    textSize(20);
    text(msg, 30.0, 38.0);
  }
  // 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 */
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
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;
} 

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());
  }
}

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!");
    }
  }
}

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!");
    }
  }
}


void displayNonCanonicals() {
  for (Map.Entry<String, String> nc : currentState.sim.nonCanonicals.entrySet()) {
    String[] pair = nc.getKey().split(" ");
    Sphere2D s1 = currentState.spheres.get(int(pair[0]));
    Sphere2D s2 = currentState.spheres.get(int(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();

    color ncFill = "c".equals(pairType.substring(0, 1).toLowerCase()) ? ds.bpCol : #FFFFFF; // 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.0;
    float midY = (float)(s1.y + s2.y) / 2.0;
    float symbolSide = d * .16;
    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.0);
      yv.add(0.0);
    }
    float xOffset = (dy0 * (symbolSide/2.0));
    float yOffset = (-1 * dx0 * (symbolSide/2.0));

    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.0) * dx0) + xOffset;
        float p1y = midY - ((symbolSide/2.0) * dy0) + yOffset;

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

        float p3x, p3y;

        if ( "s".equals(edge1) ) {
          p3x = p1x + (cos( atan2((-2*yOffset), (-2*xOffset)) - PI/3.0 ) * symbolSide);
          p3y = p1y + (sin( atan2((-2*yOffset), (-2*xOffset)) - PI/3.0 ) * 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 * .2;

      // Edge 1
      if ( "w".equals(edge1) ) {
        float s1midX = midX - ((symbolSide/2.0 + gap) * dx0);
        float s1midY = midY - ((symbolSide/2.0 + 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.0 ) * symbolSide);
          p3y = p2y - (sin( atan2((-2*yOffset), (-2*xOffset)) - PI/3.0 ) * 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.0 + gap) * dx0);
        float s2midY = midY + ((symbolSide/2.0 + 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.0 ) * symbolSide);
          p3y = p1y + (sin( atan2((-2*yOffset), (-2*xOffset)) - PI/3.0 ) * 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
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
void resetRadial() {
  //println("\nStarting resetRadial...");
  currentState.sim.initTables();
  SphereList spheres = currentState.getSpheres();
  currentState = currentState.sim.createRadialState(spheres);
  updateGUI();
  saveStateForUndo();
  //println("Finished resetRadial!");
}

// Distance
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.
double[] rotatedVector(double vx, double vy, double angle) {
  double len = normFunction(vx, vy, 0.0);
  double[] result = new double[2];
  if (len == 0.0) {
    vx = 1.0;
    vy = 0.0; // 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; //<>//
}

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; //<>// //<>// //<>// //<>//
    } //<>// //<>// //<>// //<>//
  } //<>// //<>// //<>//
} //<>//

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 //<>// //<>//
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;
  } //<>// //<>//
} //<>//

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) int(beforeDec.substring(i-1, i));
    d += num * mul;
    mul *= 10;
  }

  if (splitNum.length > 1) {
    String afterDec = splitNum[1];
    mul = .1;
    for (int i = 0; i < afterDec.length(); ++i) {
      double num = (double) int(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
boolean stringToBoolean(String s) {
  if ("true".equals(s) || "True".equals(s)) {
    return true;
  } else {
    return false;
  }
}

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

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

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
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);
}