var palate = ["white", "cyan", "blue", "#FF00FF", "#00FF00", "red", "orange", "#008000", "#8A2BE2", "black"];
var text_palate = ["black", "black", "white", "white", "black", "white", "black", "white", "white", "white"];
var INT_MAX = 9007199254740992;

var MPG = {
  legend_font: "Helvetica",
  legend_line: 16,
  legend_pad: 5
};

var motif_db_name = [];
var s_motifs = [];
var s_swatches = [];
var s_logos = []
var swap = null;
var dna_alphabet = new Alphabet("ACGT", "A 0.25 C 0.25 G 0.25 T 0.25");
var logo_timer = null; // timer to draw motif when the mouse pauses on a row
var logo_motif = null; // motif to draw when timer expires
//var legend_x = 0;
//var legend_y = 0;
var legend_x = 1000;
var legend_y = 1000;
var legend_width = 10;
var legend_height = 10;

pre_load_setup();


/*
 * name_from_source
 *
 * Makes a file name more human friendly to read.
 */
function name_from_source(source) {
  if (source == "-") {
    return "-"
  }
  //assume source is a file name
  var file = source.replace(/^.*\/([^\/]+)$/,"$1");
  var noext = file.replace(/\.[^\.]+$/, "");
  return noext.replace(/_/g, " ");
}

/*
 * pre_load_setup
 *
 *  Sets up initial variables which may be
 *  required for the HTML document creation.
 */
function pre_load_setup() {
  for (var i = 0; i < palate.length -1; i++) {
    s_motifs[i] = null;
    s_swatches[i] = null;
    s_logos[i] = null;
  }
  var motifs = data["motifs"];
  motifs.sort(sort_evalue);
  var select_num = Math.min(3, motifs.length);
  for (var i = 0; i < select_num; i++) {
    var motif = motifs[i];
    motif["colouri"] = i+1;
    s_motifs[i] = motif;
  }
  var seq_db = data['sequence_db'];
  if (!seq_db['name']) seq_db['name'] = name_from_source(seq_db['source']);
  // get the names of the motif databases
  var dbs = data['motif_dbs'];
  for (var i = 0; i < dbs.length; i++) {
    var db = dbs[i];
    if (!db['name']) db['name'] = name_from_source(db['source']);
  }
}

/*
 * $
 *
 * Shorthand function for getElementById
 */
function $(el) {
  return document.getElementById(el);
}

/*
 * coords
 *
 * Calculates the x and y offset of an element.
 * From http://www.quirksmode.org/js/findpos.html
 */
function coords(elem) {
  var myX = myY = 0;
  if (elem.offsetParent) {
    do {
      myX += elem.offsetLeft;
      myY += elem.offsetTop;
    } while (elem = elem.offsetParent);
  }
  return [myX, myY];
}

/*
 * help_popup
 *
 * Moves around help pop-ups so they appear
 * below an activator.
 */
function help_popup(activator, popup_id) {
  if (help_popup.popup === undefined) {
    help_popup.popup = null;
  }
  if (help_popup.activator === undefined) {
    help_popup.activator = null;
  }

  if (typeof(activator) == 'undefined') { // no activator so hide
    help_popup.popup.style.display = 'none';
    help_popup.popup = null;
    return;
  }
  var pop = $(popup_id);
  if (pop == help_popup.popup) {
    if (activator == help_popup.activator) {
      //hide popup (as we've already shown it for the current help button)
      help_popup.popup.style.display = 'none';
      help_popup.popup = null;
      return; // toggling complete!
    }
  } else if (help_popup.popup != null) {
    //activating different popup so hide current one
    help_popup.popup.style.display = 'none';
  }
  help_popup.popup = pop;
  help_popup.activator = activator;

  //must make the popup visible to measure it or it has zero width
  pop.style.display = 'block';
  var xy = coords(activator);
  var padding = 10;
  var edge_padding = 15;
  var scroll_padding = 15;

  var pop_left = (xy[0] + (activator.offsetWidth / 2)  - (pop.offsetWidth / 2));
  var pop_top = (xy[1] + activator.offsetHeight + padding);

  // ensure the box is not past the top or left of the page
  if (pop_left < 0) pop_left = edge_padding;
  if (pop_top < 0) pop_top = edge_padding;
  // ensure the box does not cause horizontal scroll bars
  var page_width = null;
  if (window.innerWidth) {
    page_width = window.innerWidth;
  } else if (document.body) {
    page_width = document.body.clientWidth;
  }
  if (page_width) {
    if (pop_left + pop.offsetWidth > page_width) {
      pop_left = page_width - pop.offsetWidth - edge_padding - scroll_padding; //account for scrollbars
    }
  }

  pop.style.left = pop_left + "px";
  pop.style.top = pop_top + "px";
}

function num_keys(e) {
  if (!e) var e = window.event;
  var code = (e.keyCode ? e.keyCode : e.which);
  var keychar = String.fromCharCode(code);
  var numre = /\d/;
  // only allow 0-9 and various control characters (Enter, backspace, delete)
  if (code != 8 && code != 13 && code != 46 && !numre.test(keychar)) {
    e.preventDefault();
  }
}

/*
 * toggle_column
 *
 * Adds or removes a class from the table displaying the 
 * centrally enriched motifs. This is primary used to set the visibility
 * of columns by using css rules. If the parameter 'show' is not passed
 * then the existence of the class will be toggled, otherwise it will be
 * included if show is false.
 */
function toggle_column(cls, show) {
  var tbl = $("motifs");
  var classes = tbl.className;
  var list = classes.replace(/^\s+/, '').replace(/\s+$/, '').split(/\s+/);
  var found = false;
  for (var i = 0; i < list.length; i++) {
    if (list[i] == cls) {
      list.splice(i, 1);
      i--;
      found = true;
    }
  }
  if (show === undefined) {
    if (!found) list.push(cls);
  } else {
    if (!show) list.push(cls);
  }
  tbl.className = list.join(" ");
}

/*
 * toggle_filter
 *
 * Called when the user clicks a checkbox
 * to enable/disable a filter option.
 */
function toggle_filter(chkbox, filter_id) {
  var filter = $(filter_id);
  filter.disabled = !(chkbox.checked);
  if (!filter.disabled) {
    filter.focus();
    if (filter.select) filter.select();
  }
}

/*
 * enable_filter
 *
 * Called when the user clicks a filter label.
 * Enables the filter.
 */
function enable_filter(chkbox_id, filter_id) {
  var chkbox = $(chkbox_id);
  if (!chkbox.checked) {
    var filter = $(filter_id);
    $(chkbox_id).checked = true;
    filter.disabled = false;
    filter.focus();
    if (filter.select) filter.select();
  }
}

/*
 * update_filter
 *
 * If the key event is an enter key press then
 * update the filter on the CEM table
 */
function update_filter(e) {
  if (!e) var e = window.event;
  var code = (e.keyCode ? e.keyCode : e.which);
  if (code == 13) {
    e.preventDefault();
    make_CEM_table();
  }
}

/*
 * clear_selection
 *
 * Called when the user clicks the X in the
 * title of the centrally enriched motifs and
 * causes all the motifs to be deselected.
 */
function clear_selection() {
  for (var i = 1; i < palate.length; i++) {
    var motif = s_motifs[i-1];
    var swatch = s_swatches[i-1];
    s_motifs[i-1] = null;
    s_swatches[i-1] = null;
    s_logos[i-1] = null;
    if (motif) delete motif['colouri'];
    if (swatch) swatch.style.backgroundColor = palate[0];
  }
  make_PM_table();
  make_MP_graph();
}

/*
 * toggle_graph_motif
 *
 * Activated when the user clicks on a swatch in the centrally enriched motifs 
 * table. If the motif is already selected it deselects it otherwise it picks 
 * an unused colour and selects it for graphing.
 */
function toggle_graph_motif(e) {
  var swatch;
  if (!e) var e = window.event;
  if (e.target) swatch = e.target;
  else if (e.srcElement) swatch = e.srcElement;
  // in case we land on a text node
  if (swatch.nodeType == 3) swatch = swatch.parentNode;
  // find the containing row
  var row = swatch.parentNode;
  while (!row['motif']) row = row.parentNode;
  // get the attached motif
  var motif = row['motif'];
  if (motif['colouri'] && motif['colouri'] != 0) {
    // deselect the motif
    var i = motif['colouri'];
    s_motifs[i-1] = null;
    s_swatches[i-1] = null;
    s_logos[i-1] = null;
    delete motif['colouri'];
    swatch.style.backgroundColor = palate[0];
  } else {
    // select an unused colour
    var i;
    for (i = 1; i < palate.length; i++) {
      if (!s_motifs[i-1]) break;
    }
    if (i == palate.length) {
      alert("All graph colours used. Please deselect motifs before adding more.");
      return;
    }
    s_motifs[i-1] = motif;
    s_swatches[i-1] = swatch;
    motif['colouri'] = i;
    swatch.style.backgroundColor = palate[i];
  }
  make_PM_table();
  make_MP_graph();
}

/*
 * move_legend
 *
 * Called when the user clicks on the graph
 * to move the legend location.
 */
function move_legend(e) {
  var target;
  if (!e) var e = window.event;
  if (e.target) target = e.target;
  else if (e.srcElement) target = e.srcElement;
  var elemXY = coords(target);
  var posx = 0;
  var posy = 0;
  if (e.pageX || e.pageY)   {
    posx = e.pageX;
    posy = e.pageY;
  }
  else if (e.clientX || e.clientY)   {
    posx = e.clientX + document.body.scrollLeft
      + document.documentElement.scrollLeft;
    posy = e.clientY + document.body.scrollTop
      + document.documentElement.scrollTop;
  }
  var x = posx - elemXY[0];
  var y = posy - elemXY[1];
  legend_x = x - legend_width/2;
  legend_y = y - legend_height/2;
  if (parseInt($("legend").value) != 0) make_MP_graph();
}

/*
 * hover_logo
 *
 * Activated when the user hovers their cursor over a row in the centrally 
 * enriched motifs table. After a fifth of a second delay, displays a box with 
 * the logo and reverse complement logo.
 */
function hover_logo(e) {
  var target;
  if (!e) var e = window.event;
  if (e.target) target = e.target;
  else if (e.srcElement) target = e.srcElement;
  while (!target['motif']) {
    if (target.nodeName == 'BODY') return;
    target = target.parentNode;
  }
  var motif = target['motif'];
  var popup = $("logo_popup");
  popup.style.left = (e.pageX + 20) + "px";
  popup.style.top = (e.pageY + 20) + "px";
  if (popup['motif'] == motif) {
    popup.style.display = "block";
  } else if (logo_motif != motif) {
    if (logo_timer) clearTimeout(logo_timer);
    logo_motif = motif;
    logo_timer = setTimeout(popup_logo, 200);
  }
}

/*
 * dehover_logo
 *
 * Activated when the user moves their cursor off a row in the centrally 
 * enriched motifs table. Hides the logo box (or stops it from being displayed).
 */
function dehover_logo(e) {
  var popup = $("logo_popup");
  popup.style.display = "none";
  if (logo_timer) clearTimeout(logo_timer);
  logo_motif = null;
}

/*
 * popup_logo
 *
 * Activated when the user has had the cursor over a row in the centrally 
 * enriched motifs table for longer than 1/5th of a second. It draws the
 * motif logos in a popup and displays the popup.
 */
function popup_logo() {
  if (logo_motif == null) return;
  var pspm = new Pspm(logo_motif['pwm'], logo_motif['id'], 0, 0, 
      logo_motif['motif_nsites'], logo_motif['motif_evalue']);
  var canvas = $("logo_popup_canvas");
  var logo = logo_1(dna_alphabet, "", pspm);
  draw_logo_on_canvas(logo, canvas, false, 0.5);
  var pspm_rc = pspm.copy().reverse_complement(dna_alphabet);
  var canvas_rc = $("logo_popup_canvas_rc");
  var logo_rc = logo_1(dna_alphabet, "", pspm_rc);
  draw_logo_on_canvas(logo_rc, canvas_rc, false, 0.5);

  var popup = $("logo_popup");
  popup.style.display = "block";
  popup['motif'] = logo_motif;
  logo_timer = null;
  logo_motif = null;
}

/*
 * swap_colour
 *
 * Activated when the user clicks a swatch in the plotted motifs table.
 * If a swatch has already been selected then swap colours with this one,
 * otherwise record which swatch has been selected.
 */
function swap_colour(e) {
  var swatch;
  if (!e) var e = window.event;
  if (e.target) swatch = e.target;
  else if (e.srcElement) swatch = e.srcElement;
  var row = swatch;
  while (!row['motif']) row = row.parentNode;
  var motif = row['motif'];
  if (swap == null) {
    swatch.appendChild(document.createTextNode("\u21c5"));
    swap = motif;
  } else if (swap == motif) {
    while (swatch.firstChild) swatch.removeChild(swatch.firstChild);
    swap = null;
  } else {
    var swapi = swap['colouri'];
    var motifi = motif['colouri'];
    motif['colouri'] = swapi;
    swap['colouri'] = motifi;
    // swap swatches
    var temp = s_swatches[swapi-1];
    s_swatches[swapi-1] = s_swatches[motifi-1];
    s_swatches[motifi-1] = temp;
    // swap motifs
    temp = s_motifs[swapi-1];
    s_motifs[swapi-1] = s_motifs[motifi-1];
    s_motifs[motifi-1] = temp;
    // swap logos
    temp = s_logos[swapi-1];
    s_logos[swapi-1] = s_logos[motifi-1];
    s_logos[motifi-1] = temp;
    // update swatch colours
    s_swatches[swapi-1].style.backgroundColor = palate[swapi];
    s_swatches[motifi-1].style.backgroundColor = palate[motifi];
    swap = null;
    make_PM_table();
    make_MP_graph();
  }
}

/*
 * set_colour
 *
 * Activated when the user clicks a swatch in the unused colours section.
 * If a swatch has already been selected then set its colour to this one,
 * otherwise warn the user that they must select a motif swatch first.
 */
function set_colour(e) {
  var swatch;
  if (!e) var e = window.event;
  if (e.target) swatch = e.target;
  else if (e.srcElement) swatch = e.srcElement;
  var colouri = swatch['colouri'];
  if (swap) {
    var swapi = swap['colouri'];
    swap['colouri'] = colouri;
    s_swatches[colouri-1] = s_swatches[swapi-1];
    s_motifs[colouri-1] = s_motifs[swapi-1];
    s_logos[colouri-1] = s_logos[swapi-1];
    s_swatches[swapi-1] = null;
    s_motifs[swapi-1] = null;
    s_logos[swapi-1] = null;
    s_swatches[colouri-1].style.backgroundColor = palate[colouri];
    swap = null;
    make_PM_table();
    make_MP_graph();
  } else {
    alert("You must select a motif to set to this colour first.");
  }
}

/*
 * page_loaded
 *
 * Called when the page has loaded for the first time.
 */
function page_loaded() {
  post_load_setup();
}

/*
 * page_loaded
 *
 * Called when a cached page is reshown.
 */
function page_shown(e) {
  if (e.persisted) post_load_setup();
}

/*
 * post_load_setup
 *
 * Setup state that is dependant on everything having been loaded already.
 */
function post_load_setup() {
  $("filter_id").disabled = !($("filter_on_id").checked);
  $("filter_alt").disabled = !($("filter_on_alt").checked);
  $("filter_evalue").disabled = !($("filter_on_evalue").checked);
  $("filter_binwidth").disabled = !($("filter_on_binwidth").checked);
  toggle_column("hide_db", $("show_db").checked);
  toggle_column("hide_name", $("show_name").checked);
  toggle_column("hide_pvalue", $("show_pvalue").checked);
  toggle_column("hide_maxprob", $("show_maxprob").checked);
  toggle_column("hide_bsites", $("show_bsites").checked);
  toggle_column("hide_tsites", $("show_tsites").checked);
  make_CEM_table();
  make_PM_table();
  make_MP_graph();
}

/*
 *  sort_id
 *
 *  Takes 2 motif objects and compares them based on id and database.
 */
function sort_id(m1, m2) {
  var diff;
  diff = m1['id'].localeCompare(m2['id']);
  if (diff == 0) {
    diff = m1['db'] - m2['db'];
  }
  return diff;
}

/*
 * sort_alt
 *
 * Takes 2 motif objects and compares them based on alternate id.
 */
function sort_alt(m1, m2) {
  var diff;
  if (m1['alt'] && m2['alt']) {
    diff = m1['alt'].localeCompare(m2['alt']);
    if (diff != 0) return diff;
    return sort_evalue(m1, m2);
  } else {
    if (m1['alt']) {
      return -1;
    } else {
      return 1;
    }
  }
}

/*
 * sort_evalue
 *
 * Takes 2 motif objects and compares them based on the log_pvalue.
 */
function sort_evalue(m1, m2) {
  var diff;
  diff = m1['log_pvalue'] - m2['log_pvalue'];
  if (diff != 0) return diff;
  diff = m1['bin_width'] - m2['bin_width'];
  if (diff != 0) return diff;
  return sort_id(m1, m2);
}

/*
 * sort_binwidth
 *
 * Takes 2 motif objects and compares them based on the bin_width.
 */
function sort_binwidth(m1, m2) {
  var diff;
  diff = m1['bin_width'] - m2['bin_width'];
  if (diff != 0) return diff;
  diff = m1['log_pvalue'] - m2['log_pvalue'];
  if (diff != 0) return diff;
  return sort_id(m1, m2);
}

/*
 * sort_probability
 *
 * Takes 2 motif objects and compares them based on the maximum probability.
 */
function sort_probability(m1, m2) {
  var diff;
  diff =  m2['max_prob'] - m1['max_prob'];
  if (diff != 0) return diff;
  return sort_evalue(m1, m2);
}

/*
 * sort_bin_sites
 *
 * Takes 2 motif objects and compares them based on the bin sites.
 */
function sort_bin_sites(m1, m2) {
  var diff;
  diff = m2['bin_sites'] - m1['bin_sites'];
  if (diff != 0) return diff;
  return sort_evalue(m1, m2);
}

/*
 * sort_total_sites
 *
 * Takes 2 motif objects and compares them based on the total sites.
 */
function sort_total_sites(m1, m2) {
  var diff;
  diff = m2['total_sites'] - m1['total_sites'];
  if (diff != 0) return diff;
  return sort_evalue(m1, m2);
}

/*
 * log2str
 *
 * Converts a log value into scientific notation.
 */
function log2str(log_val, precision) {
  var log10_val = log_val / Math.log(10);
  var e = Math.floor(log10_val);
  var m = Math.pow(10, log10_val - e);
  if (m + (0.5 * Math.pow(10, -precision)) >= 10) {
    m = 1;
    e += 1;
  }
  return "" + m.toFixed(precision) + "e" + e;
}

/*
 * pvstr
 *
 * Gets the p-value of the motif in string form.
 */
function pvstr(motif) {
  return log2str(motif['log_pvalue'], 1);
}

/*
 * evstr
 *
 * Gets the E-value of the motif in string form.
 */
function evstr(motif) {
  return log2str(motif['log_pvalue'] + Math.log(data['tested']), 1);
}

/*
 *  make_link
 *
 *  Creates a text node and if a URL is specified it surrounds it with a link. 
 *  If the URL doesn't begin with "http://" it automatically adds it, as 
 *  relative links don't make much sense in this context.
 */
function make_link(text, url) {
  var textNode = null;
  var link = null;
  if (text) textNode = document.createTextNode(text);
  if (url) {
    if (url.indexOf("//") == -1) {
      url = "http://" + url;
    }
    link = document.createElement('a');
    link.href = url;
    if (textNode) link.appendChild(textNode);
    return link;
  } 
  return textNode;
}

/*
 * make_swatch
 *
 * Make a swatch block.
 */
function make_swatch(colouri) {
  var swatch = document.createElement('div');
  swatch.className = 'swatch';
  swatch.style.backgroundColor = palate[colouri];
  swatch.style.color = text_palate[colouri];
  return swatch;
}

/*
 * add_cell
 *
 * Add a cell to the table row.
 */
function add_cell(row, node, cls) {
  var cell = row.insertCell(row.cells.length);
  if (node) cell.appendChild(node);
  if (cls) cell.className = cls;
}

/*
 * add_text_cell
 *
 * Add a text cell to the table row.
 */
function add_text_cell(row, text, cls) {
  var node = null;
  if (text) node = document.createTextNode(text);
  add_cell(row, node, cls);
}

/*
 * make_CEM_table
 *
 * Generate the table which lists centrally enriched motifs.
 */
function make_CEM_table() {
  var evalue_re = /^([+]?\d+(?:\.\d+)?)(?:[eE]([+-]\d+))?$/;
  //make a list of the filtered data
  // get the db filter
  var filter_on_db = $("filter_on_db").checked;
  var db_num = $("filter_db").value;
  // get the id filter
  var filter_on_id = $("filter_on_id").checked;
  var id_pat = $("filter_id").value;
  var id_re;
  try {
    id_re = new RegExp(id_pat);
    $("filter_id").className = "";
  } catch (err) {
    filter_on_id = false;
    $("filter_id").className = "error";
  }
  // get the name filter
  var filter_on_alt = $("filter_on_alt").checked;
  var alt_pat = $("filter_alt").value;
  var alt_re;
  try {
    alt_re = new RegExp(alt_pat);
    $("filter_alt").className = "";
  } catch (err) {
    filter_on_alt = false;
    $("filter_alt").className = "error";
  }
  // get the evalue filter
  var filter_on_evalue = $("filter_on_evalue").checked;
  var evalue_text = $("filter_evalue").value;
  var log_pvalue_max;
  var parts = evalue_re.exec(evalue_text);
  if (parts && parseFloat(parts[1]) != 0) {
    var m = parseFloat(parts[1]);
    var e = (parts.length == 3 && parts[2] != null ? parseInt(parts[2]) : 0);
    var log_evalue = (((Math.log(m)/Math.log(10)) + e) * Math.log(10));
    log_pvalue_max = log_evalue - Math.log(data['tested']);
    $("filter_evalue").className = "";
  } else {
    filter_on_evalue = false;
    $("filter_evalue").className = "error";
  }
  // get the bin width filter
  var filter_on_binwidth = $("filter_on_binwidth").checked;
  var binwidth_max = parseInt($("filter_binwidth").value);
  if (isNaN(binwidth_max) || binwidth_max < 1) {
    filter_on_binwidth = false;
    $("filter_binwidth").className = "error";
  } else {
    $("filter_binwidth").className = "";
  }
  // iterate over all the motifs and discard those which don't match the filter
  var motifs = data['motifs'];
  var filtered = [];
  for (var i = 0; i < motifs.length; i++) {
    var motif = motifs[i];
    if (filter_on_db && motif['db'] != db_num) continue;
    if (filter_on_id && !id_re.test(motif['id'])) continue;
    if (filter_on_alt && !alt_re.test(motif['alt'])) continue;
    if (filter_on_evalue && motif['log_pvalue'] > log_pvalue_max) continue;
    if (filter_on_binwidth && motif['bin_width'] > binwidth_max) continue;
    filtered.push(motif);
  }
  // sort by evalue (for now)
  var sort_cmp;
  switch(parseInt($('sort').value)) {
    case 0:
      sort_cmp = sort_id;
      break;
    case 1:
      sort_cmp = sort_alt;
      break;
    case 2:
      sort_cmp = sort_evalue;
      break;
    case 3:
      sort_cmp = sort_binwidth;
      break;
    case 4:
      sort_cmp = sort_bin_sites;
      break;
    case 5:
      sort_cmp = sort_total_sites;
      break;
    case 6:
      sort_cmp = sort_probability;
      break;
    default:
      sort_cmp = sort_evalue;
  }
  filtered.sort(sort_cmp);
  // limit the list
  var filter_on_limit = $("filter_on_top").checked;
  var limit = parseInt($("filter_top").value);
  if (isNaN(limit) || limit < 1) {
    filter_on_limit = false;
    $("filter_top").className = "error";
  } else {
    $("filter_top").className = "";
  }
  if (filter_on_limit) {
    if (filtered.length > limit) filtered.length = limit;
  }
  // re-add any omitted s_motifs motifs
  outer_loop:
  for (var i = 0; i < s_motifs.length; i++) {
    if (s_motifs[i] == null) continue;
    for (var j =0; j < filtered.length; j++) {
      if (filtered[j]['id'] === s_motifs[i]['id']) {
        continue outer_loop;
      }
    }
    filtered.push(s_motifs[i]);
  }
  filtered.sort(sort_cmp);

  // clear the table and add the items
  var tbl = $("motifs");
  var tbody = tbl.tBodies[0];
  while (tbody.rows.length > 0) {
    tbody.deleteRow(0);
  }
  var motif_dbs = data['motif_dbs'];
  // add the new rows to the table
  for (var i = 0; i < filtered.length; i++) {
    var motif = filtered[i];
    var colouri = motif['colouri'];
    if (!colouri) colouri = 0;
    var row = tbody.insertRow(tbody.rows.length);
    row.onmouseover = hover_logo;
    row.onmousemove = hover_logo;
    row.onmouseout = dehover_logo;
    row['motif'] = motif;
    var swatch = make_swatch(colouri);
    swatch.onclick = toggle_graph_motif;
    if (colouri != 0) s_swatches[colouri -1] = swatch;
    add_cell(row, swatch);
    add_text_cell(row, motif_dbs[motif['db']]['name'], 'col_db');
    add_cell(row, make_link(motif['id'], motif['url']));
    add_text_cell(row, motif['alt'], "col_name");
    add_text_cell(row, evstr(motif));
    add_text_cell(row, pvstr(motif), "col_pvalue");
    add_text_cell(row, motif['bin_width'], 'col_binwidth');
    add_text_cell(row, Math.round(motif['bin_sites']), "col_bsites");
    add_text_cell(row, motif['total_sites'], "col_tsites");
    add_text_cell(row, motif['max_prob'].toExponential(1), "col_maxprob");
  }
}

/*
 * make_PM_table
 *
 * Generate the table which lists plotted motifs
 */
function make_PM_table() {
  swap = null;
  var graphed = [];
  var unused = [];
  for (var i = 1; i < palate.length; i++) {
    if (s_motifs[i-1]) graphed.push(s_motifs[i-1]);
    else unused.push(i);
  }
  graphed.sort(sort_evalue);

  var tbl = $("graph_list");
  var tbody = tbl.tBodies[0];
  while (tbody.rows.length > 0) {
    tbody.deleteRow(0);
  }
  // add the new rows to the table
  for (var i = 0; i < graphed.length; i++) {
    var motif = graphed[i];
    var colouri = motif['colouri'];
    if (!colouri) colouri = 0;
    var row = tbody.insertRow(tbody.rows.length);
    row['motif'] = motif;
    var swatch = make_swatch(colouri);
    swatch.onmousedown = swap_colour;
    add_cell(row, swatch);
    add_cell(row, make_link(motif['id'], motif['url']));
    if (!s_logos[colouri-1]) {
      s_logos[colouri-1] = document.createElement("canvas");
      var pspm = new Pspm(motif['pwm'], motif['id'], 0, 0, 
          motif['motif_nsites'], motif['motif_evalue']);
      var logo = logo_1(dna_alphabet, "", pspm);
      draw_logo_on_canvas(logo, s_logos[colouri-1], false, 0.3);
    }
    add_cell(row, s_logos[colouri-1]);
  }
  var div = $("unused_colours");
  while (div.firstChild) div.removeChild(div.firstChild);
  $("unused_colours_section").style.display = (unused.length > 0 ? "block" : "none");
  for (var i = 0; i < unused.length; i++) {
    var swatch = make_swatch(unused[i]);
    swatch['colouri'] = unused[i];
    swatch.onclick = set_colour;
    div.appendChild(swatch);
  }
}

/*
 * make_MP_graph
 *
 * Create a motif probability graph on the canvas.
 */
function make_MP_graph() {
  var canvas = $("graph");
  canvas.width = canvas.width;
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');
    make_MP_graph2(ctx, canvas.width, canvas.height);
  }
}

function download_eps() {
  var canvas = $("graph");
  canvas.width = canvas.width;
  if (canvas.getContext) {
    var ctx = canvas.getContext('2d');
    var eps_ctx = new EpsContext(ctx, canvas.width, canvas.height);
    eps_ctx.register_font("14pt Helvetica", "Helvetica", 14 / 3 * 4);
    eps_ctx.register_font("16px Helvetica", "Helvetica", 16);
    eps_ctx.register_font("9px Helvetica", "Helvetica", 9);
    make_MP_graph2(ctx, canvas.width, canvas.height);
    $("eps_content").value = eps_ctx.eps();
  }
}

function make_MP_graph2(ctx, width, height) {
  var motif_list = $("motif_list");
  var windo = parseInt($("windo").value);
  if (isNaN(windo) || windo < 1) {
    windo = 20;
    $("windo").value = 20;
  }
  var type = parseInt($("plot_type").value);
  var legend = parseInt($("legend").value);
  var motifs = [];
  for (var i = 0; i < s_motifs.length; i++) 
    if (s_motifs[i] != null) motifs.push(s_motifs[i]);
  motifs.sort(sort_evalue);
  var graph = calc_graph(type, windo, data['seqlen'], motifs);
  draw_graph(ctx, width, height, graph, legend, legend_x, legend_y);
}

/*
 * calc_graph
 *
 * Calculate the lines that make up the graph
 */
function calc_graph(type, windo, seq_len, motifs) {
  var  min = (motifs.length == 0 ? 0 : motifs[0]['sites'].length);
  for (var i = 1; i < motifs.length; i++) 
    if (motifs[i]['sites'].length < min) min = motifs[i]['sites'].length;
  if (min < windo) {
    return {'lines': [], 'scale': calc_Y_scale(1.0), 'start': -(seq_len / 2), 
      'length': seq_len, 'thin_lines': false};
  }
  var graph = {};
  // find sequence mid point
  var mid = seq_len / 2;
  var lines;
  switch (type) {
    case 0:
      lines = calc_moving_avg(motifs, seq_len, windo);
      break;
    case 1:
      lines = calc_weighted_moving_avg(motifs, seq_len, windo);
      break;
    default:
      throw "Unknown graph type (" + type + ")";
  }
  var max_prob = trim_lines(lines);
  graph['thin_lines'] = windo < 10;
  graph['lines'] = lines;
  graph['scale'] = calc_Y_scale(max_prob);
  graph['start'] = -mid;
  graph['length'] = seq_len;
  return graph;
}

/*
 * calc_weighted_moving_avg
 *
 * Smooth the lines by applying a weighted moving average function
 */
function calc_weighted_moving_avg(motifs, sequence_length, windo) {
  var lines = [];
  // calculate weights
  var half_window = windo / 2;
  var weights = [];
  var total_weight = 0;
  for (var i = 0; i < windo; i++) {
    var pos = i + 0.5;
    var weight = (pos < half_window ? pos / half_window : (windo - pos) / half_window);
    weights.push(weight);
    total_weight += weight;
  }
  // calculate points
  for (var i = 0; i < motifs.length; i++) {
    var motif = motifs[i];
    var sites = motif['sites'];
    var x = [];
    var y = [];
    var xpos = (motif.len / 2) - (sequence_length / 2) + half_window;
    var end = sites.length - windo;
    for (var j = 0; j < end; j++, xpos += 1) {
      var sum = 0;
      for (var k = 0; k < windo; k++) {
        sum += sites[j + k] * weights[k];
      }
      var avg = sum / total_weight;
      var prob = avg / motif['total_sites'];
      y.push(prob);
      x.push(xpos);
    }
    var name = motif['id'];
    if (motif['alt']) name = motif['alt'] + " " + motif['id'];
    lines.push({'x': x, 'y': y, 'id': name, 'pv': 'p=' + pvstr(motif), 'colouri': motif['colouri']});
  }
  return lines;
}

/*
 * calc_moving_avg
 *
 * Takes the probabilities for each of the sites in the
 * sequence and generates smoothed probabilities using
 * a moving average.
 */
function calc_moving_avg(motifs, sequence_length, windo) {
  var lines = [];
  // calculate weights
  var half_window = windo / 2;
  // calculate points
  for (var i = 0; i < motifs.length; i++) {
    var motif = motifs[i];
    var sites = motif['sites'];
    var x = [];
    var y = [];
    var xpos = (motif.len / 2) - (sequence_length / 2) + half_window;
    var end = sites.length - windo;
    for (var j = 0; j < end; j++, xpos += 1) {
      var sum = 0;
      for (var k = 0; k < windo; k++) {
        sum += sites[j + k];
      }
      var avg = sum / windo;
      var prob = avg / motif['total_sites'];
      y.push(prob);
      x.push(xpos);
    }
    var name = motif['id'];
    if (motif['alt']) name = motif['alt'] + " " + motif['id'];
    lines.push({'x': x, 'y': y, 'id': name, 'pv': 'p=' + pvstr(motif), 'colouri': motif['colouri']});
  }
  return lines;
}

/*
 * trim_lines
 *
 * Trims the edges of the lines so they all align.
 */
function trim_lines(lines) {
  var max_prob = -INT_MAX;
  var left = -INT_MAX;
  var right = INT_MAX;
  for (var i = 0; i < lines.length; i++) {
    var line = lines[i];
    var x = line['x'];
    if (x.length > 0) {
      if (x[0] > left) left = x[0];
      if (x[x.length - 1] < right) right = x[x.length - 1];
    }
  }
  if (left == right) {
    lines.splice(0, lines.length);
    return 1.0;
  }
  for (var i = 0; i < lines.length; i++) {
    var line = lines[i];
    var x = line['x'];
    var y = line['y'];
    var left_i = 0;
    while (left_i < x.length && x[left_i] < left) left_i++;
    if (left_i == x.length) { 
      // not enough line to trim, remove line
      lines.splice(i, 1);
      i--;
      continue
    }
    if (left_i == 0) {
      // no trim required
    } else if (x[left_i] == left) {
      // remove some elements
      x.splice(0, left_i);
      y.splice(0, left_i);
    } else {
      // remove some elements and interpolate a new one
      var gradient = (y[left_i] - y[left_i-1]) / (x[left_i] - x[left_i-1]);
      var run = left - x[left_i-1];
      var left_y = y[left_i-1] + gradient * run;
      x.splice(0, left_i, left);
      y.splice(0, left_i, left_y);
    }
    var right_i = x.length - 1;
    while (right_i >= 0 && x[right_i] > right) right_i--;
    if (right_i == 0) {
      // not enough line to trim, remove line
      lines.splice(i, 1);
      i--;
      continue;
    }
    if (right_i == (x.length - 1)) {
      // no trim required
    } else if (x[right_i] == right) {
      // remove some elements
      x.splice(right_i + 1, x.length - right_i - 1);
      y.splice(right_i + 1, y.length - right_i - 1);
    } else {
      // remove some elements and interpolate a new one
      var gradient = (y[right_i+1] - y[right_i]) / (x[right_i+1] - x[right_i]);
      var run = right - x[right_i];
      var right_y = y[right_i] + gradient * run;
      x.splice(right_i + 1, x.length - right_i - 1, right);
      y.splice(right_i + 1, y.length - right_i - 1, right_y);
    }
    for (var j = 0; j < y.length; j++) {
      if (y[j] > max_prob) max_prob = y[j];
    }
  }
  return max_prob;
}

/*
 * calc_Y_scale
 *
 * Calculates the increments on the y axis
 */
function calc_Y_scale(max_prob) {
  if (max_prob == 0) return {'max': 0.01, 'inc': 0.002, 'digits':3};
  // find the minimum number of decimals needed to display the largest
  // digit of the maximum probability
  var decimals = Math.ceil(-(Math.log(max_prob) / Math.log(10)));
  // calculate an increment which is (at minimum) 10 times smaller the the max
  var inc = Math.pow(10, -(decimals+1));
  // round-up the maximum probabilty to its largest digit
  var rounder = Math.pow(10, decimals);
  var max = Math.ceil(max_prob * rounder) / rounder;
  // adjust the increment so it's between 5 and 12 times the maximum
  // probability
  if (inc * 5 < max && inc * 12 > max) {
    inc *= 1;
  } else if (inc * 10 < max && inc * 24 > max) {
    inc *= 2;
  } else if (inc * 25 < max && inc * 60 > max) {
    inc *= 5;
  } else if (inc * 50 < max && inc * 120 > max) {
    inc *= 10;
  }
  while (max - inc >= max_prob) max -= inc;
  return {'max': max, 'inc': inc, 'digits': decimals+1};
  
}

/*
 * draw_graph
 *
 * Draws a motif probability graph
 */
function draw_graph(ctx, w, h, graph, legend, legend_x, legend_y) {
  var gap = 10;
  var l_margin = gap + 30 + 10 * (2 + graph['scale']['digits']);
  var t_margin = 20;
  var b_margin = 60;
  var r_margin = 10;
  legend_metrics = measure_legend(ctx, graph);
  // setting global
  legend_width = legend_metrics['width'];
  legend_height = legend_metrics['height'];
  // constrain legend to within graph area
  legend_x = Math.round(legend_x);
  legend_y = Math.round(legend_y);
  if (legend_x < (l_margin + gap)) {
    legend_x = l_margin + gap;
  } else if ((legend_x + legend_metrics['width']) > (w - r_margin - gap)) {
    legend_x = w - r_margin - legend_metrics['width'] - gap;
  }
  if (legend_y < (t_margin + gap)) {
    legend_y = t_margin + gap;
  } else if ((legend_y + legend_metrics['height']) > (h - b_margin - gap)) {
    legend_y = h - b_margin - legend_metrics['height'] - gap;
  }

  // draw graph
  ctx.save();
  // draw border
  ctx.beginPath();
  ctx.moveTo(l_margin - 0.5, t_margin +0.5);
  ctx.lineTo(l_margin - 0.5, h - (b_margin - 0.5));
  ctx.lineTo(w - r_margin - 0.5, h - (b_margin - 0.5));
  ctx.lineTo(w - r_margin - 0.5, t_margin +0.5);
  ctx.closePath();
  ctx.stroke();
  // draw fineprint
  ctx.save();
  ctx.font = "9px Helvetica";
  ctx.textAlign = "right";
  ctx.textBaseline = "bottom";
  //ctx.fillText("CentriMo " + data["version"], w - r_margin, t_margin - 2);
  ctx.fillText("CentriMo " + data["version"], w - 1, h - 2);
  ctx.restore();
  // draw y axis
  ctx.save();
  ctx.translate(l_margin, t_margin);
  draw_y_axis(ctx, 0, h - (t_margin + b_margin), graph);
  ctx.restore();
  // draw y labels
  ctx.save();
  ctx.translate(gap, t_margin);
  draw_y_axis_label(ctx, l_margin, h - (t_margin + b_margin));
  ctx.restore();
  // draw x axis
  ctx.save();
  ctx.translate(l_margin, h - b_margin);
  draw_x_axis(ctx, w - (l_margin + r_margin), b_margin, graph);
  ctx.restore();
  // draw top axis
  ctx.save();
  ctx.translate(l_margin, t_margin);
  draw_top_axis(ctx, w - (l_margin + r_margin), 0, graph);
  ctx.restore();
  // draw x axis labels
  ctx.save();
  ctx.translate(l_margin, h - b_margin);
  draw_x_axis_label(ctx, w - (l_margin + r_margin), b_margin - gap);
  ctx.restore();
  // draw lines
  ctx.save();
  ctx.translate(l_margin, t_margin);
  draw_lines(ctx, w - (l_margin + r_margin), h - (t_margin + b_margin), graph);
  ctx.restore();
  // draw legend
  if (legend != 0) { 
    ctx.save();
    ctx.translate(legend_x, legend_y);
    draw_legend(ctx, graph, legend_metrics);
    ctx.restore();
  }
  ctx.restore();
}

/*
 * draw_x_axis
 */
function draw_x_axis(ctx, w, h, graph) {
  ctx.save();
  ctx.translate(w / 2, 0);
  var tick_max = graph['length'] / 2;
  var nticks = 5
  var tick_inc = Math.round(tick_max / (10*nticks)) * 10
  var scale_x = w / graph['length'];
  ctx.beginPath();
  ctx.moveTo(0.5, -5);
  ctx.lineTo(0.5, 3);
  ctx.stroke();
  ctx.font = "14pt Helvetica";
  ctx.textBaseline = "top";
  ctx.textAlign = "center";
  ctx.fillText("0", 0, 5);
  //for (var i = 50; i < tick_max; i += 50) {
  for (var i = tick_inc; i < tick_max; i += tick_inc) {
    var x = Math.round(i * scale_x) + 0.5;
    ctx.beginPath();
    ctx.moveTo(x, -5);
    ctx.lineTo(x, 3);
    ctx.stroke();
    ctx.fillText(""+i, x, 5);
    ctx.beginPath();
    ctx.moveTo(-x, -5);
    ctx.lineTo(-x, 3);
    ctx.stroke();
    ctx.fillText(""+(-i), -x, 5);
  }
  ctx.restore();
}

/*
 * draw_top_axis
 */
function draw_top_axis(ctx, w, h, graph) {
  ctx.save();
  ctx.translate(w / 2, 0);
  var tick_max = graph['length'] / 2;
  var scale_x = w / graph['length'];
  ctx.beginPath();
  ctx.moveTo(0.5, 0);
  ctx.lineTo(0.5, 8);
  ctx.stroke();
  for (var i = 50; i < tick_max; i += 50) {
    var x = Math.round(i * scale_x) + 0.5;
    ctx.beginPath();
    ctx.moveTo(x, 0);
    ctx.lineTo(x, 8);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(-x, 0);
    ctx.lineTo(-x, 8);
    ctx.stroke();
  }
  ctx.restore();
}

/*
 * draw_x_axis_label
 */
function draw_x_axis_label(ctx, w, h) {
  ctx.save();
  ctx.font = "14pt Helvetica";
  ctx.textAlign = "center";
  ctx.textBaseline = "bottom";
  ctx.fillText("Position of Best Site in Sequence", w/2, h);
  ctx.restore();
}

/*
 * draw_y_axis
 */
function draw_y_axis(ctx, w, h, graph) {
  var scale = graph['scale'];
  ctx.save();
  ctx.font = "14pt Helvetica";
  ctx.textBaseline = "middle";
  ctx.textAlign = "right";

  var y_scale = h / scale['max'];
  for (var p = 0; p < scale['max']; p += scale['inc']) {
    var y = Math.round(h - p * y_scale) + 0.5;
    draw_y_tic(ctx, scale['digits'], y, p);
  }
  draw_y_tic(ctx, scale['digits'], 0.5, scale['max']);

  ctx.restore();
}

/*
 * draw_y_tic
 */
function draw_y_tic(ctx, digits, y, p) {
  ctx.beginPath();
  ctx.moveTo(5, y);
  ctx.lineTo(-3, y);
  ctx.stroke();
  ctx.fillText(p.toFixed(digits), -5, y);
}

/*
 * draw_y_axis_label
 */
function draw_y_axis_label(ctx, w, h) {
  ctx.save();
  ctx.translate(0, h/2);
  ctx.rotate(-Math.PI / 2);
  ctx.font = "14pt Helvetica";
  ctx.textBaseline = "top";
  ctx.textAlign = "center";
  ctx.fillText("Probability", 0, 0);
  ctx.restore();
}

/*
 * draw_lines
 */
function draw_lines(ctx, w, h, graph, thin_lines) {
  ctx.save();
  ctx.translate(w/2, 0);
  ctx.strokeStyle = '#DDD';
  ctx.beginPath();
  ctx.moveTo(0.5, 8);
  ctx.lineTo(0.5, h-8);
  ctx.stroke();
  ctx.restore();
  if (graph == null) return;
  ctx.save();
  ctx.lineJoin = "bevel";
  ctx.miterLimit = 0;
  ctx.lineWidth = (graph['thin_lines'] ? 1 : 3);
  var lines = graph['lines'];
  var scale_y = h / graph['scale']['max'];
  var scale_x = w / graph['length'];
  for (var i = 0; i < lines.length; i++) {
    var line = lines[i];
    var x_points = line['x'];
    var y_points = line['y'];
    ctx.strokeStyle = palate[line['colouri']];
    ctx.beginPath();
    for (var j = 0; j < x_points.length; j++) {
      if (isNaN(x_points[j])) throw "X NaN!";
      if (isNaN(y_points[j])) throw "Y NaN!";
      var x = (x_points[j] - graph['start']) * scale_x;
      var y = h - y_points[j] * scale_y;
      if (j == 0) ctx.moveTo(x, y);
      else ctx.lineTo(x, y);
    }
    ctx.stroke();
  }
  ctx.restore();
}

/*
 * measure_legend
 */
function measure_legend(ctx, graph) {
  var lw, lh, sq_w, id_w, pv_w, lines;
  // get the lines
  lines = graph['lines'];
  // legend height is a function of the line count
  lh = Math.round(lines.length * MPG.legend_line * 1.2 + 2 * MPG.legend_pad);
  // calculate the column widths
  sq_w = MPG.legend_line - 2;
  id_w = 0;
  pv_w = 0;
  ctx.save();
  ctx.font = "" + MPG.legend_line + "px " + MPG.legend_font; 
  for (var i = 0; i < lines.length; i++) {
    var line, len;
    line = lines[i];
    len = ctx.measureText(line['id']).width;
    if (id_w < len) id_w = len;
    len = ctx.measureText(line['pv']).width;
    if (pv_w < len) pv_w = len;
  }
  ctx.restore();
  // legend width is a function of the column widths
  lw = id_w + pv_w + sq_w + 4 * MPG.legend_pad;

  return {'width': lw, 'height': lh, 'id_width': id_w, 'pv_width': pv_w, 'sq_width': sq_w};
}

/*
 * draw_legend
 */
function draw_legend(ctx, graph, metrics) {
  var ln_h, pad, w, h, id_w, pv_w, sq_w, id_x, pv_x, sq_x;
  var lines = graph['lines'];
  if (lines.length == 0) return;
  ln_h = MPG.legend_line;
  pad = MPG.legend_pad;
  w = metrics['width'];
  h = metrics['height'];
  id_w = metrics['id_width'];
  pv_w = metrics['pv_width'];
  sq_w = metrics['sq_width'];
  id_x = 0;
  pv_x = id_x + id_w + pad;
  sq_x = pv_x + pv_w + pad;

  ctx.save();
  ctx.font = "" + ln_h + "px " + MPG.legend_font; 
  ctx.textAlign = "left";
  ctx.textBaseline = "alphabetic";
  // draw it
  ctx.save();
  ctx.fillStyle = 'white';
  ctx.fillRect(0.5, 0.5, w-1, h-1);
  ctx.strokeStyle = 'black';
  ctx.strokeRect(0.5, 0.5, w-1, h-1);
  ctx.restore();
  ctx.translate(pad, pad);
  ctx.fillStyle = "black";
  for (var i = 0; i < lines.length; i++) {
    var line = lines[i];
    ctx.translate(0, ln_h);
    ctx.save();
    ctx.fillStyle = palate[line['colouri']];
    ctx.fillRect(sq_x, -sq_w + 0.1 * ln_h, sq_w, sq_w);
    ctx.restore();
    ctx.fillText(line['id'], id_x, 0);
    ctx.fillText(line['pv'], pv_x, 0);
    ctx.translate(0, 0.2 * ln_h);
  }
  ctx.restore();
}
