changeset 6:740c9df2ffe8 draft

Should auto-open a layout.
author adam-novak
date Fri, 18 Oct 2013 18:40:35 -0400
parents a9213a30d0f9
children a9bf691d4713
files hexagram/hexagram.css~ hexagram/hexagram.html~ hexagram/hexagram.js hexagram/hexagram.js~ hexagram/hexagram.py~ hexagram/hexagram.xml~ hexagram/layer_5.tab~ hexagram/layers.tab~ hexagram/matrices.tab~ hexagram/matrix_0.tab~ hexagram/statistics.js~ hexagram/tool_dependencies.xml~ hexagram/tools.js~
diffstat 12 files changed, 6721 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.css~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,441 @@
+/* Global font stuff */
+body {
+    font-family: sans-serif;
+}
+
+/*
+The visualization element needs to take up all available space.
+*/
+#visualization {
+    position: absolute;
+    left: 0;
+    right: 0;
+    top: 0;
+    bottom: 0;
+    overflow: hidden;
+}
+
+/* 
+Row and column layout boilerplate
+From http://blog.stevensanderson.com/2011/10/05/full-height-app-layouts-a-css-trick-to-make-it-easier/
+*/
+
+body {
+    margin: 0;
+}
+
+.row, .col {
+    overflow: hidden;
+    position: absolute;
+}
+
+.row { 
+    left: 0;
+    right: 0; 
+}
+
+.col { 
+    top: 0; 
+    bottom: 0;
+}
+
+.scroll-x {
+    overflow-x: auto;
+}
+
+.scroll-y {
+    overflow-y: auto;
+}
+
+/*
+Read as "The content row", distinct from whatever else that element might be.
+*/
+.content.row {
+    top: 6em;
+    bottom: 0;
+    z-index: 1;
+}
+
+.header.row {
+    height: 4em;
+    top: 2em;
+    border-bottom: 1px solid black;
+    overflow: visible;
+}
+
+.browse.col {
+    left: 0;
+    width: 22em;
+    /* Not sure what's up with the lack of dynamic height here, but... */
+    height: 4em;
+}
+
+.shortlist.col {
+    right: 0;
+    width: 21em;
+    overflow: visible;
+}
+
+.error.row{
+    height: 2em;
+    top: 0;
+    border-bottom: 1px solid black;
+    background: #F7EFAD;
+    z-index: 101;
+    display: none;
+}
+
+.tools.row {
+    height: 2em;
+    top: 0em;
+    border-bottom: 1px solid black;
+    background: #e0e0e0;
+}
+
+/*
+These are all supposed to stack against the left end in a toolbar thingy.
+*/
+.stacker {
+    float: left;
+    line-height: 2em;
+    margin-left: 0.5em;
+    height: 2em;
+}
+
+#error-notification {
+    color: red;
+    font-weight: bold;
+    text-align: center;
+    line-height: 2em;
+    margin-left: 0.5em;
+    height: 2em;
+}
+
+/* Except these which stack on the right */
+.stacker.right {
+    float: right;
+    margin-left: 0;
+    margin-right: 0.5em;
+}
+
+/* Code for fancy expandy side pannels */
+
+.panel-holder {
+    overflow: hidden;
+    height: 3.9em;
+    max-height: 4em;
+    top: 2.1em;
+    width: 20em;
+    position: fixed;
+    z-index: 100;
+    transition: max-height 100ms;
+}
+
+/* When a holder gets moused over, open it up to 100% window height */
+.panel-holder:hover {
+    height: auto;
+    max-height: 100%;
+    bottom: 0;
+}
+
+.panel {
+    position: relative;
+    top: 0;
+    left: 0;
+    height: 100%;
+    border-radius: 10px;
+    border: 1px solid gray;
+    background: white;
+    z-index: 100;
+}
+
+.panel-contents {
+    overflow-y: auto;
+    overflow-x: hidden;
+    position: absolute;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    top: 1em;
+}
+
+.panel-title {
+    text-align: center;
+    font-weight: bold;
+    font-family: sans-serif;
+    height: 1em;
+}
+
+/* Browsing stuff */
+
+#browse-holder {
+    margin-left: 0.5em;
+}
+
+#search {
+    width: 20em;
+}
+
+#recalculate-statistics {
+    padding: 0.2em;
+}
+
+.recalculate-throbber {
+    display: none;
+}
+
+/* Do some custom styling of browse results */
+.layer-entry {
+    padding-right: 20px;
+}
+.select2-results .select2-highlighted {
+    background-image: url("right.svg");
+    background-repeat: no-repeat;
+    background-position: right center;
+}
+
+.layer-name {
+    /* Force silly underscore names into shape */
+    word-wrap: break-word;
+}
+
+/* Make the browse dropdown tall */
+.results-dropdown.select2-container .select2-results {
+    max-height: 40em;
+}
+
+.results-dropdown .select2-results {
+    max-height: 40em;
+}
+
+.layer-metadata {
+    font-size: 70%;
+    /* Serif is more visible at small sizes */
+    font-family: serif;
+}
+
+/* Vertical alignment tables */
+.vertical-table {
+    display: table;
+}
+
+.vertical-cell {
+    display: table-cell;
+    vertical-align: middle;
+}
+
+/* 
+    Not much room for the results for this column. Just get them out of the way.
+*/
+#browse-results.panel-contents {
+    top: 4em;
+}
+
+/* Shortlist UI stuff */
+
+.shortlist-controls {
+    position: absolute;
+    left: 0;
+    top: 0;
+    bottom: 0;
+    width: 2em;
+    background-image: url("drag.svg");
+    background-repeat: no-repeat;
+    background-position: 50% 70%; 
+    border-radius: 5px;
+    padding: 5px;
+}
+
+.shortlist-entry {
+    position: relative;
+    height: auto;
+    width: auto;
+    background: #E0E0FF;
+    border-radius: 5px;
+    padding: 5px;
+    padding-left: 3em;
+    margin: 0.5em;
+    word-wrap: break-word;
+}
+
+.shortlist-entry.selection {
+    background: #FFE0FF;
+}
+
+.shortlist-controls {
+    cursor: grab;
+    cursor: -moz-grab;
+    cursor: -webkit-grab;
+}
+
+.shortlist-controls:activate {
+    cursor: grabbing;
+    cursor: -moz-grabbing;
+    cursor: -webkit-grabbing;
+}
+
+.shortlist-entry:after {
+    content: "";
+    display: block;
+    clear: both;
+}
+
+#shortlist-holder {
+    right: 0;
+}
+
+.layer-on {
+
+}
+
+.radio-label, .radio-clear {
+    margin-right: 0.1em;
+    margin-left: 0.1em;
+}
+
+/* Hide the radio button clearing links unless the radio button is selected. */
+input[type="radio"] + .radio-clear {
+    display: none;
+}
+
+input[type="radio"]:checked + .radio-clear {
+    display: inline;
+}
+
+
+/* These are the layer scaling sliders */
+.range-slider {
+    width: 10em;
+    margin-left: 0.3em;
+    margin-right: 0.3em;
+    margin-top: 0.5em;
+}
+
+/* Filtering stuff */
+.filter {
+    line-height: 1em;
+    vertical-align: center;
+}
+
+.filter-threshold, .filter-value {
+    display: none;
+    width: 5em;
+}
+
+/* This is the color key */
+.key {
+    overflow: visible;
+    position: absolute;
+    z-index: 2;
+    top: 2em;
+    right: 2em;
+    height: 150px;
+    width: 150px;
+    display: none;
+    pointer-events: none;
+    font-family: serif;
+}
+
+#color-key {
+    width: 100px;
+    height: 100px;
+    position: absolute;
+    display: block;
+    left:25px;
+    top: 25px;
+    border: 1px solid white;
+}
+
+.label {
+    color: white;
+}
+
+.axis {
+    word-wrap: break-word;
+}
+
+#low-both {
+    position: absolute;
+    left: 0;
+    bottom: 0;
+}
+
+#high-x {
+    position: absolute;
+    right: 0;
+    bottom: 0;
+}
+
+#high-y {
+    position: absolute;
+    left: 0;
+    top: 0;
+}
+
+#x-axis {
+    position: absolute;
+    top: 155px;
+    width: 150px;
+    margin: auto;
+    text-align: center;
+}
+
+/* 
+    Complicated table centering thing from 
+    http://blog.themeforest.net/tutorials/vertical-centering-with-css/
+*/
+
+#y-axis-holder {
+    display: table;
+    right: 155px;
+    width: 150px;
+    top: 0;
+    height: 100%;
+    position: absolute;
+}
+
+#y-axis-cell {
+    display: table-cell;
+    vertical-align: middle;
+}
+
+#y-axis {
+    text-align: right;
+}
+
+/* This is the info window/infocard styling */
+.infocard {
+    word-break: break-all;
+    font-family: sans-serif;
+}
+
+.info-row {
+    margin-bottom: 0.1em;
+}
+
+.info-key {
+    background: green;
+    color: white;
+    text-align: center;
+    font-weight: bold;
+}
+
+.info-value {
+    background: #F0F0F0;
+}
+
+/* Tool stuff */
+textarea.import {
+    height: 10em;
+    width: 100%;
+    font-size: 10pt;
+}
+
+input.import {
+    width: 100%;
+    /* 
+        Apparently Firefox ignores your width and makes it some random size 
+        depending on font size. Make that small enough. 
+    */
+    font-size: 0.5em;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.html~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="UTF-8" />
+        <link rel="stylesheet" type="text/css" href="http://code.jquery.com/ui/1.10.2/themes/smoothness/jquery-ui.css" />
+        <link rel="stylesheet" type="text/css" href="select2.css" />
+        <link rel="stylesheet" type="text/css" href="hexagram.css" />
+        <script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>
+        <script type="text/javascript" src="http://code.jquery.com/jquery-1.9.1.min.js"></script>
+        <script type="text/javascript" src="http://code.jquery.com/ui/1.10.2/jquery-ui.js"></script>
+        <script type="text/javascript" src="jquery.tsv.js"></script>
+        <script type="text/javascript" src="color-0.4.1.js"></script>
+        <script type="text/javascript" src="maplabel-compiled.js"></script>
+        <script type="text/javascript" src="jstat-1.0.0.js"></script>
+        <script type="text/javascript" src="select2.js"></script>
+        <script type="text/javascript" src="hexagram.js"></script>
+        <script type="text/javascript" src="tools.js"></script>
+        <title>Hexagram Visualization</title>
+    </head>
+    <body>
+        <div class="tools row" id="toolbar">
+            <div class="stacker">
+                Tools:
+            </div>
+            <div class="stacker right">
+                <img src="throbber.svg" class="recalculate-throbber" title="Recalculating..." />
+                <span id="jobs-running">0</span>/<span id="jobs-ever">0</span> jobs running.
+            </div>
+        </div>
+        <div class="header row" id="header">
+            <div class="browse col vertical-table">
+                <div class="vertical-cell">
+                    <div id="browse-holder">
+                        <input id="search" type="hidden"/>
+                        <img src="statistics.svg" id="recalculate-statistics" title="Enrich Attributes"/>
+						<!--<img src="set.svg" id="calculate-set-operation" title="Calculate Set Operation"/>-->
+						<img src="set.svg" id="set-operation" title="Calculate Set Operation"/>
+						<img src="inflate.svg" id="inflate" title="Inflate Map"/>
+                        <img src="throbber.svg" class="recalculate-throbber" title="Recalculating..." />
+                    </div>
+					<div id="layout-holder">
+						<input id="layout-search" type="hidden"/>
+					</div>
+				</div>
+				<div id="current-layout">Current Layout:</div> 	
+            </div>
+			<div class="set-operation-col">
+				<div class="set-operation-panel-holder">
+					<div class="set-operation-panel">
+						<div class="set-operation-panel-title">Set Operation Paramaters:</div>
+						<select id="set-operations-list" onchange="update_set_operation_drop_down()">
+  								<option value = "0" selected = 'selected'>Select Set Operation:</option>
+  								<option value = "1">∩ - Intersection</option>
+  								<option value = "2">U - Union</option>  
+ 								<option value = "3">\ - Set Difference</option>
+  								<option value = "4">∆ - Symmetric Difference</option>
+  								<option value = "5">Not:</option>
+							</select>
+						<div id="set-operations" class="set-operation-panel-contents">
+						</div>
+					</div>
+				</div>
+			</div>
+            <div class="shortlist col">
+                <div id="shortlist-holder" class="panel-holder">
+                    <div id="shortlist-panel" class="panel">
+                        <div class="panel-title">Shortlist</div>
+                        <div id="shortlist" class="panel-contents">
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="error row" id="error">
+            <div id="error-notification">
+            </div>
+        </div>
+        <div class="content row" id="content">
+            <div class="key">
+                <canvas id="color-key" width="100" height="100"></canvas>
+                <div class="label" id="low-both">Low</div>
+                <div class="label" id="high-x">High</div>
+                <div class="label y" id="high-y">High</div>
+                <div class="label axis" id="x-axis"></div>
+                <div id="y-axis-holder">
+                    <div id="y-axis-cell">
+                        <div class="label axis" id="y-axis"></div>
+                    </div>
+                </div>
+            </div>
+            <div id="visualization">
+            </div>
+        </div>
+    </body>
+</html>
--- a/hexagram/hexagram.js	Wed Oct 16 18:48:28 2013 -0400
+++ b/hexagram/hexagram.js	Fri Oct 18 18:40:35 2013 -0400
@@ -3743,6 +3743,11 @@
             }
             // Add layout names to global array of names
             layout_names.push(label);
+            
+            if(layout_names.length == 1) {
+                // This is the very first layout. Pull it up.
+                $("#layout-search").select2("val", layout_names[0]);
+            }
         }     
     }, "text");
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.js~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,3832 @@
+// hexagram.js
+// Run the hexagram visualizer client.
+
+// Globals
+// This is a mapping from coordinates [x][y] in the global hex grid to signature
+// name
+var signature_grid = [];
+
+// This holds a global list of layer pickers in layer order. It is also the
+// authority on what layers are currently selected.
+var layer_pickers = [];
+
+// This holds a list of layer objects by name.
+// Layer objects have:
+// A downloading function "downloader"
+// A data object (from hex name to float) "data"
+// A magnitude "magnitude"
+// A boolean "selection" that specifies whether this is a user selection or not.
+// (This may be absent, which is the same as false.)
+// Various optional metadata fields
+var layers = {};
+
+// This is a list of layer names maintained in sorted order.
+var layer_names_sorted = [];
+
+// This is a list of the map-layour names mantained in order of entry
+var layout_names = [];
+
+// This holds an array of layer names that the user has added to the "shortlist"
+// They can be quickly selected for display.
+var shortlist = [];
+
+// This holds an object form shortlisted layer names to jQuery shortlist UI
+// elements, so we can efficiently tell if e.g. one is selected.
+var shortlist_ui = {};
+
+// This is a list of layer names whose intersection checkbox has been selected.
+var shortlist_intersection = [];
+
+//This is the number of intersection checkboxes that have been selected.
+var shortlist_intersection_num = 0;
+
+// This is a list of layer names whose union checkbox has been selected.
+var shortlist_union = [];
+
+//This is the number of union checkboxes that have been selected.
+var shortlist_union_num = 0;
+
+//This is a list of layer names whose set difference checkbox has been selected.
+var shortlist_set_difference = [];
+
+// This is the number of set difference checkboxes that have been selected.
+var shortlist_set_difference_num = 0;
+
+// This is a list of the layer names whose symmetric difference checkbox
+// has been selected.
+var shortlist_symmetric_difference = [];
+
+// This is the number of symmetric difference checkboxes that have been
+// selected.
+var shortlist_symmetric_difference_num = 0;
+
+// This is an array containing the layer whose absolute complement checkbox
+// has been selected.
+var shortlist_absolute_complement = [];
+
+// This is the number of absolute complement checkboxes that have been selected.
+var shortlist_absolute_complement_num = 0;
+
+// Records number of set-operation clicks
+var set_operation_clicks = 0;
+
+// Boolean stating whether this is the first time the set operation popup
+// has been created so that "Select Layer" Default is added only once
+var first_opening = true;
+
+// Boolean for Creating Layer from Filter
+var created = false;
+
+// Stores the Name of Current Layer Displayed
+var current_layout_name;
+
+// This holds colormaps (objects from layer values to category objects with a 
+// name and color). They are stored under the name of the layer they apply to.
+var colormaps = {}
+
+// This holds an array of the available score matrix filenames
+var available_matrices = [];
+
+// This holds the Google Map that we use for visualization
+var googlemap = null;
+
+// This is the global Google Maps info window. We only want one hex to have its
+// info open at a time.
+var info_window = null;
+
+// This holds the signature name of the hex that the info window is currently
+// about.
+var selected_signature = undefined;
+
+// Which tool is the user currently using (string name or undefined for no tool)
+// TODO: This is a horrible hack, replace it with a unified tool system at once.
+var selected_tool = undefined;
+
+// This holds the grid of hexagon polygons on that Google Map.
+var polygon_grid = [];
+
+// This holds an object of polygons by signature name
+var polygons = {};
+
+// How big is a hexagon in google maps units? This gets filled in once we have 
+// the hex assignment data. (This is really the side length.)
+var hex_size;
+
+// This holds a handle for the currently enqueued view redrawing timeout.
+var redraw_handle;
+
+// This holds all the currently active tool event listeners.
+// They are indexed by handle, and are objects with a "handler" and an "event".
+var tool_listeners = {};
+
+// This holds the next tool listener handle to give out
+var tool_listener_next_id = 0;
+
+// This holds the next selection number to use. Start at 1 since the user sees 
+// these.
+var selection_next_id = 1;
+
+// This is a pool of statistics Web Workers.
+var rpc_workers = [];
+
+// This holds which RPC worker we ought to give work to next.
+// TODO: Better scheduling, and wrap all this into an RPC object.
+var next_free_worker = 0;
+
+// This holds how namy RPC jobs are currently running
+var jobs_running = 0;
+
+// This is the object of pending callbacks by RPC id
+var rpc_callbacks = {};
+
+// This is the next unallocated RPC id
+var rpc_next_id = 0;
+
+// How many statistics Web Workers should we start?
+var NUM_RPC_WORKERS = 10;
+
+// What's the minimum number of pixels that hex_size must represent at the 
+// current zoom level before we start drawing hex borders?
+var MIN_BORDER_SIZE = 10;
+
+// And how thick should the border be when drawn?
+var HEX_STROKE_WEIGHT = 2;
+
+// How many layers do we know how to draw at once?
+var MAX_DISPLAYED_LAYERS = 2;
+
+// How many layer search results should we display at once?
+var SEARCH_PAGE_SIZE = 10;
+
+// How big is our color key in pixels?
+var KEY_SIZE = 100;
+
+// This is an array of all Google Maps events that tools can use.
+var TOOL_EVENTS = [
+    "click",
+    "mousemove"
+];
+
+// This is a global variable that keeps track of the current Goolge Map zoom
+// This is needed to keep viewing consistent across layouts
+var global_zoom = 0;
+
+function print(text) {
+    // Print some logging text to the browser console
+        
+    if(console && console.log) {
+        // We know the console exists, and we can log to it.
+        console.log(text);
+    }
+}
+
+function complain(text) {
+    // Display a temporary error message to the user.
+    $("#error-notification").text(text);
+    $(".error").show().delay(1250).fadeOut(1000);
+    
+    if(console && console.error) {
+        // Inform the browser console of this problem.as
+        console.error(text);
+    }
+}
+
+function make_hexagon(row, column, hex_side_length, grid_offset) {
+    // Make a new hexagon representing the hexagon at the given grid coordinates.
+    // hex_side_length is the side length of hexagons in Google Maps world 
+    // coordinate units. grid_offset specifies a distance to shift the whole 
+    // grid down and right from the top left corner of the map. This lets us 
+    // keep the whole thing away from the edges of the "earth", where Google 
+    // Maps likes to wrap.
+    // Returns the Google Maps polygon.
+    
+    // How much horizontal space is needed per hex on average, stacked the 
+    // way we stack them (wiggly)?
+    var hex_column_width = 3.0/2.0 * hex_side_length;
+    
+    // How tall is a hexagon?
+    var hex_height = Math.sqrt(3) * hex_side_length;
+    
+    // How far apart are hexagons on our grid, horizontally (world coordinate units)?
+    var hex_padding_horizontal = 0;
+    
+    // And vertically (world coordinate units)?
+    var hex_padding_veritcal = 0;
+    
+    // First, what are x and y in 0-256 world coordinates fo this grid position?
+    var x = column * (hex_column_width + hex_padding_horizontal);
+    var y = row * (hex_height + hex_padding_veritcal);
+    if(column % 2 == 1) {
+        // Odd columns go up
+        y -= hex_height / 2; 
+    }
+    
+    // Apply the grid offset to this hex
+    x += grid_offset;
+    y += grid_offset;
+    
+    // That got X and Y for the top left corner of the bounding box. Shift to 
+    // the center.
+    x += hex_side_length;
+    y += hex_height / 2;
+    
+    // Offset the whole thing so no hexes end up off the map when they wiggle up
+    y += hex_height / 2;
+    
+    // This holds an array of all the hexagon corners
+    var coords = [
+        get_LatLng(x - hex_side_length, y),
+        get_LatLng(x - hex_side_length / 2, y - hex_height / 2),
+        get_LatLng(x + hex_side_length / 2, y - hex_height / 2),
+        get_LatLng(x + hex_side_length, y),
+        get_LatLng(x + hex_side_length / 2, y + hex_height / 2),
+        get_LatLng(x - hex_side_length / 2, y + hex_height / 2),
+    ];
+    
+    // We don't know whether the hex should start with a stroke or not without 
+    // looking at the current zoom level.
+    // Get the current zoom level (low is out)
+    var zoom = googlemap.getZoom();
+        
+    // API docs say: pixelCoordinate = worldCoordinate * 2 ^ zoomLevel
+    // So this holds the number of pixels that the global length hex_size 
+    // corresponds to at this zoom level.
+    var hex_size_pixels = hex_size * Math.pow(2, zoom);
+    
+    // Construct the Polygon
+    var hexagon = new google.maps.Polygon({
+        paths: coords,
+        strokeColor: "#000000",
+        strokeOpacity: 1.0,
+        // Only turn on the border if we're big enough
+        strokeWeight: hex_size_pixels < MIN_BORDER_SIZE ? 0 : HEX_STROKE_WEIGHT, 
+        fillColor: "#FF0000",
+        fillOpacity: 1.0
+    });
+    
+    // Attach the hexagon to the global map
+    hexagon.setMap(googlemap);
+    
+    // Set up the click listener to move the global info window to this hexagon
+    // and display the hexagon's information
+    google.maps.event.addListener(hexagon, "click", function(event) {
+        if(selected_tool == undefined) {
+            // The user isn't trying to use a tool currently, so we can use 
+            // their clicks for the infowindow.
+            
+            // Remove the window from where it currently is
+            info_window.close();
+
+            // Place the window in the center of this hexagon.
+            info_window.setPosition(get_LatLng(x, y));
+            
+            // Record that this signature is selected now
+            selected_signature = hexagon.signature;
+            
+            // Calculate the window's contents and make it display them.
+            redraw_info_window();
+        }
+    });
+    
+    // Subscribe the tool listeners to events on this hexagon
+    subscribe_tool_listeners(hexagon);
+    
+    return hexagon;
+} 
+
+function set_hexagon_signature(hexagon, text) {
+    // Given a polygon representing a hexagon, set the signature that the
+    // hexagon represents.
+    hexagon.signature = text;
+}
+
+function set_hexagon_color(hexagon, color) {
+    // Given a polygon, set the hexagon's current background 
+    // color.
+    
+    hexagon.setOptions({
+        fillColor: color
+    });
+}
+
+function set_hexagon_stroke_weight(hexagon, weight) {
+    // Given a polygon, set the weight of hexagon's border stroke, in number of
+    // screen pixels.
+    
+    hexagon.setOptions({
+        strokeWeight: weight
+    });
+}
+
+function redraw_info_window() {
+    // Set the contents of the global info window to reflect the currently 
+    // visible information about the global selected signature. 
+    
+    if(selected_signature == undefined) {
+        // No need to update anything
+        return;
+    }
+
+    // Go get the infocard that goes in the info_window and, when it's 
+    // prepared, display it.
+    with_infocard(selected_signature, function(infocard) {
+        // The [0] is supposed to get the DOM element from the jQuery 
+        // element.
+        info_window.setContent(infocard[0]);
+        
+        // Open the window. It may already be open, or it may be closed but 
+        // properly positioned and waiting for its initial contents before 
+        // opening.
+        info_window.open(googlemap);
+    });
+}
+
+function with_infocard(signature, callback) {
+    // Given a signature, call the callback with a jQuery element representing 
+    // an "info card" about that signature. It's the contents of the infowindow 
+    // that we want to appear when the user clicks on the hex representing this 
+    // signature, and it includes things like the signature name and its values
+    // under any displayed layers (with category names if applicable).
+    // We return by callback because preparing the infocard requires reading 
+    // from the layers, which are retrieved by callback.
+    // TODO: Can we say that we will never have to download a layer here and 
+    // just directly access them? Is that neater or less neat?
+    
+    // Using jQuery to build this saves us from HTML injection by making jQuery
+    // do all the escaping work (we only ever set text).
+    
+    function row(key, value) {
+        // Small helper function that returns a jQuery element that displays the
+        // given key being the given value.
+        
+        // This holds the root element of the row
+        var root = $("<div/>").addClass("info-row");
+        
+        // Add the key and value elements
+        root.append($("<div/>").addClass("info-key").text(key));
+        root.append($("<div/>").addClass("info-value").text(value));
+        
+        return root;
+    }
+    
+    // This holds a list of the string names of the currently selected layers,
+    // in order.
+    // Just use everything on the shortlist.
+    var current_layers = shortlist;
+    
+    // Obtain the layer objects (mapping from signatures/hex labels to colors)
+    with_layers(current_layers, function(retrieved_layers) { 
+        
+        // This holds the root element of the card.
+        var infocard = $("<div/>").addClass("infocard");
+        
+        infocard.append(row("Name", signature).addClass("info-name"));
+    
+        for(var i = 0; i < current_layers.length; i++) {
+            // This holds the layer's value for this signature
+            var layer_value = retrieved_layers[i].data[signature];
+            
+            if(have_colormap(current_layers[i])) {
+                // This is a color map
+                
+                // This holds the category object for this category number, or
+                // undefined if there isn't one.
+                var category = colormaps[current_layers[i]][layer_value];
+                
+                if(category != undefined) {
+                    // There's a specific entry for this category, with a 
+                    // human-specified name and color.
+                    // Use the name as the layer value
+                    layer_value = category.name;
+                }
+            }
+            
+            if(layer_value == undefined) {
+                // Let the user know that there's nothing there in this layer.
+                layer_value = "<undefined>";
+            }
+            
+            // Make a listing for this layer's value
+            infocard.append(row(current_layers[i], layer_value));
+        }
+        
+        // Return the infocard by callback
+        callback(infocard);
+    }); 
+    
+}
+
+function add_layer_url(layer_name, layer_url, attributes) {
+    // Add a layer with the given name, to be downloaded from the given URL, to
+    // the list of available layers.
+    // Attributes is an object of attributes to copy into the layer.
+    
+    // Store the layer. Just keep the URL, since with_layer knows what to do
+    // with it.
+    layers[layer_name] = {
+        url: layer_url,
+        data: undefined,
+        magnitude: undefined
+    };
+    
+    for(var name in attributes) {
+        // Copy over each specified attribute
+        layers[layer_name][name] = attributes[name];
+    }
+    
+    // Add it to the sorted layer list.
+    layer_names_sorted.push(layer_name);
+    
+    // Don't sort because our caller does that when they're done adding layers.
+
+}
+
+function add_layer_data(layer_name, data, attributes) {
+    // Add a layer with the given name, with the given data to the list of 
+    // available layers.
+    // Attributes is an object of attributes to copy into the layer.
+    
+    // Store the layer. Just put in the data. with_layer knows what to do if the
+    // magnitude isn't filled in.
+    layers[layer_name] = {
+        url: undefined,
+        data: data,
+        magnitude: undefined
+    };
+	
+	var check_layer_exists = layers[layer_name];    
+
+    for(var name in attributes) {
+        // Copy over each specified attribute
+        layers[layer_name][name] = attributes[name];
+    }
+    
+    // Add it to the sorted layer list and sort
+    layer_names_sorted.push(layer_name);
+
+    // Don't sort because our caller does that when they're done adding layers.
+}
+
+function with_layer(layer_name, callback) {
+    // Run the callback, passing it the layer (object from hex label/signature
+    // to float) with the given name.
+    // This is how you get layers, and allows for layers to be downloaded 
+    // dynamically. 
+    // have_layer must return true for the given name.
+    
+    // First get what we have stored for the layer
+    var layer = layers[layer_name];
+    
+	var data_val = layer.data;
+    if(layer.data == undefined) {
+        // We need to download the layer.
+        print("Downloading \"" + layer.url + "\"");
+        
+        // Go get it (as text!)
+        $.get(layer.url, function(layer_tsv_data) {
+
+            // This is the TSV as parsed by our TSV-parsing plugin
+            var layer_parsed = $.tsv.parseRows(layer_tsv_data);
+
+            // This is the layer we'll be passing out. Maps from 
+            // signatures to floats on -1 to 1.
+            var layer_data = {};
+
+            for(var j = 0; j < layer_parsed.length; j++) {
+                // This is the label of the hex
+                var label = layer_parsed[j][0];
+                
+                if(label == "") {
+                    // Skip blank lines
+                    continue;
+                }
+                
+                // This is the heat level (-1 to 1)
+                var heat = parseFloat(layer_parsed[j][1]);
+                
+                // Store in the layer
+                layer_data[label] = heat;
+            }
+    
+            // Save the layer data locally
+            layers[layer_name].data = layer_data;
+ 
+            // Now the layer has been properly downloaded, but it may not have
+            // metadata. Recurse with the same callback to get metadata.
+            with_layer(layer_name, callback);
+        }, "text");
+    } else if(layer.magnitude == undefined) {
+        // We've downloaded it already, or generated it locally, but we don't
+        // know the magnitude. Compute that and check if it's a colormap.
+       
+        // Grab the data, which we know is defined.
+        var layer_data = layers[layer_name].data;
+       
+        // Store the maximum magnitude in the layer
+        // -1 is a good starting value since this always comes out positive
+        var magnitude = -1;
+        
+        // We also want to know if all layer entries are non-negative 
+        // integers (and it is thus valid as a colormap).
+        // If so, we want to display it as a colormap, so we will add an 
+        // empty entry to the colormaps object (meaning we should 
+        // auto-generate the colors on demand).
+        // This stores whether the layer is all integrs
+        all_nonnegative_integers = true;
+        
+        for(var signature_name in layer_data) {
+            // Take the new max if it's bigger (and thus not something silly
+            // like NaN).
+            // This holds the potential new max magnitude.
+            var new_magnitude = Math.abs(layer_data[signature_name]);
+            if(new_magnitude > magnitude) {
+                magnitude = new_magnitude;
+            }
+            
+            if(layer_data[signature_name] % 1 !== 0 || 
+                layer_data[signature_name] < 0 ) {
+                
+                // If we have an illegal value for a colormap, record that
+                // fact
+                // See http://stackoverflow.com/a/3886106
+                
+                all_nonnegative_integers = false;
+            }
+        }
+        
+        // Save the layer magnitude for later.
+        layer.magnitude = magnitude;
+        
+        if(!have_colormap(layer_name) && all_nonnegative_integers) {
+            // Add an empty colormap for this layer, so that 
+            // auto-generated discrete colors will be used.
+            // TODO: Provide some way to override this if you really do want
+            // to see integers as a heatmap?
+            // The only overlap with the -1 to 1 restricted actual layers
+            // is if you have a data set with only 0s and 1s. Is it a
+            // heatmap layer or a colormap layer?
+            colormaps[layer_name] = {};
+            print("Inferring that " + layer_name + 
+                " is really a colormap");
+        }
+        
+        // Now layer metadata has been filled in. Call the callback.
+        callback(layer);
+    } else {
+        // It's already downloaded, and already has metadata.
+        // Pass it to our callback
+        callback(layer);
+    }
+}
+
+function with_layers(layer_list, callback) {
+    // Given an array of layer names, call the callback with an array of the 
+    // corresponding layer objects (objects from signatures to floats).
+    // Conceptually it's like calling with_layer several times in a loop, only
+    // because the whole thing is continuation-based we have to phrase it in
+    // terms of recursion.
+    
+    // See http://marijnhaverbeke.nl/cps/
+    // "So, we've created code that does exactly the same as the earlier 
+    // version, but is twice as confusing."
+    
+    if(layer_list.length == 0) {
+        // Base case: run the callback with an empty list
+        callback([]);
+    } else {
+        // Recursive case: handle the last thing in the list
+        with_layers(layer_list.slice(0, layer_list.length - 1), 
+            function(rest) {
+            
+            // We've recursively gotten all but the last layer
+            // Go get the last one, and pass the complete array to our callback.
+            
+            with_layer(layer_list[layer_list.length - 1], 
+                function(last) {
+            
+                // Mutate the array. Shouldn't matter because it won't matter 
+                // for us if callback does it.
+                rest.push(last);
+                
+                // Send the complete array to the callback.
+                callback(rest);
+            
+            });
+            
+        });
+       
+    }
+}
+
+function have_layer(layer_name) {
+    // Returns true if a layer exists with the given name, false otherwise.
+    return layers.hasOwnProperty(layer_name);
+}
+
+function make_shortlist_ui(layer_name) {
+    // Return a jQuery element representing the layer with the given name in the
+    // shortlist UI.
+    
+
+    // This holds the root element for this shortlist UI entry
+    var root = $("<div/>").addClass("shortlist-entry");
+    root.data("layer", layer_name);
+    
+    // If this is a selection, give the layer a special class
+    // TODO: Justify not having to use with_layer because this is always known 
+    // client-side
+    if(layers[layer_name].selection) {
+        root.addClass("selection");
+    }
+    
+    // We have some configuration stuff and then the div from the dropdown
+    // This holds all the config stuff
+    var controls = $("<div/>").addClass("shortlist-controls");
+    
+    // Add a remove link
+    var remove_link = $("<a/>").addClass("remove").attr("href", "#").text("X");
+    
+    controls.append(remove_link);
+    
+    // Add a checkbox for whether this is enabled or not
+    var checkbox = $("<input/>").attr("type", "checkbox").addClass("layer-on");
+    
+    controls.append(checkbox);
+    
+    root.append(controls);
+    
+    var contents = $("<div/>").addClass("shortlist-contents");
+    
+    // Add the layer name
+    contents.append($("<span/>").text(layer_name));
+    
+    // Add all of the metadata. This is a div to hold it
+    var metadata_holder = $("<div/>").addClass("metadata-holder");
+    
+    // Fill it in
+    fill_layer_metadata(metadata_holder, layer_name);
+    
+    contents.append(metadata_holder);
+
+	// Add a div to hold the filtering stuff so it wraps together.
+    var filter_holder = $("<div/>").addClass("filter-holder");
+    
+    // Add an image label for the filter control.
+    // TODO: put this in a label    
+	var filter_image = $("<img/>").attr("src", "filter.svg");
+    filter_image.addClass("control-icon");
+	filter_image.addClass("filter-image");
+    filter_image.attr("title", "Filter on Layer");
+    filter_image.addClass("filter");
+    
+    // Add a control for filtering
+    var filter_control = $("<input/>").attr("type", "checkbox");
+    filter_control.addClass("filter-on");
+    
+    filter_holder.append(filter_image);
+    filter_holder.append(filter_control);
+    
+    // Add a text input to specify a filtering threshold for continuous layers
+    var filter_threshold = $("<input/>").addClass("filter-threshold");
+    // Initialize to a reasonable value.
+    filter_threshold.val(0);
+    filter_holder.append(filter_threshold);
+    
+    // Add a select input to pick from a discrete list of values to filter on
+    var filter_value = $("<select/>").addClass("filter-value");
+	filter_holder.append(filter_value);
+
+	// Add a image for the save function
+	var save_filter = $("<img/>").attr("src", "save.svg");
+	save_filter.addClass("save-filter");
+	save_filter.attr("title", "Save Filter as Layer");
+
+	contents.append(filter_holder);
+	contents.append(save_filter);
+
+    if(layers[layer_name].selection) {
+        // We can do statistics on this layer.
+        
+        // Add a div to hold the statistics stuff so it wraps together.
+        var statistics_holder = $("<div/>").addClass("statistics-holder");
+        
+        // Add an icon
+        var statistics_image = $("<img/>").attr("src", "statistics.svg");
+        statistics_image.addClass("control-icon");
+        statistics_image.attr("title", "Statistics Group");
+        statistics_holder.append(statistics_image);
+        
+        // Label the "A" radio button.
+        var a_label = $("<span/>").addClass("radio-label").text("A");
+        statistics_holder.append(a_label);
+        
+        // Add a radio button for being the "A" group
+        var statistics_a_control = $("<input/>").attr("type", "radio");
+        statistics_a_control.attr("name", "statistics-a");
+        statistics_a_control.addClass("statistics-a");
+        // Put the layer name in so it's easy to tell which layer is A.
+        statistics_a_control.data("layer-name", layer_name);
+        statistics_holder.append(statistics_a_control);
+        
+        // And a link to un-select it if it's selected
+        var statistics_a_clear = $("<a/>").attr("href", "#").text("X");
+        statistics_a_clear.addClass("radio-clear");
+        statistics_holder.append(statistics_a_clear);
+        
+        // Label the "B" radio button.
+        var b_label = $("<span/>").addClass("radio-label").text("B");
+        statistics_holder.append(b_label);
+        
+        // Add a radio button for being the "B" group
+        var statistics_b_control = $("<input/>").attr("type", "radio");
+        statistics_b_control.attr("name", "statistics-b");
+        statistics_b_control.addClass("statistics-b");
+        // Put the layer name in so it's easy to tell which layer is A.
+        statistics_b_control.data("layer-name", layer_name);
+        statistics_holder.append(statistics_b_control);
+        
+        // And a link to un-select it if it's selected
+        var statistics_b_clear = $("<a/>").attr("href", "#").text("X");
+        statistics_b_clear.addClass("radio-clear");
+        statistics_holder.append(statistics_b_clear);
+        
+        contents.append(statistics_holder);
+        
+        // Statistics UI logic
+    
+        // Make the clear links work
+        statistics_a_clear.click(function() {
+            statistics_a_control.prop("checked", false);
+        });
+        statistics_b_clear.click(function() {
+            statistics_b_control.prop("checked", false);
+        });
+    }
+    
+    // Add a div to contain layer settings
+    var settings = $("<div/>").addClass("settings");
+    
+    // Add a slider for setting the min and max for drawing
+    var range_slider = $("<div/>").addClass("range range-slider");
+    settings.append($("<div/>").addClass("stacker").append(range_slider));
+    
+    // And a box that tells us what we have selected in the slider.
+    var range_display = $("<div/>").addClass("range range-display");
+    range_display.append($("<span/>").addClass("low"));
+    range_display.append(" to ");
+    range_display.append($("<span/>").addClass("high"));
+    settings.append($("<div/>").addClass("stacker").append(range_display));
+    
+    contents.append(settings);
+    
+    root.append(contents);
+    
+    // Handle enabling and disabling
+    checkbox.change(function() {
+        if($(this).is(":checked") && get_current_layers().length > 
+            MAX_DISPLAYED_LAYERS) {
+                
+            // Enabling this checkbox puts us over the edge, so un-check it
+            $(this).prop("checked", false);
+            
+            // Skip the redraw
+            return;
+        }
+    
+        refresh();
+    });
+    
+    // Run the removal process
+    remove_link.click(function() {
+        // Remove this layer from the shortlist
+        shortlist.splice(shortlist.indexOf(layer_name), 1);
+        
+        // Remove this from the DOM
+        root.remove();
+
+        // Make the UI match the list.
+        update_shortlist_ui();
+
+        if(checkbox.is(":checked") || filter_control.is(":checked")) {
+            // Re-draw the view since we were selected (as coloring or filter) 
+            // before removal.
+            refresh();
+        }
+
+    });
+
+    // Functionality for turning filtering on and off
+    filter_control.change(function() {
+        if(filter_control.is(":checked")) {
+            // First, figure out what kind of filter settings we take based on 
+            // what kind of layer we are.
+            with_layer(layer_name, function(layer) {
+                if(have_colormap(layer_name)) {
+                    // A discrete layer.
+                    // Show the value picker.
+                    filter_value.show();
+                    
+                    // Make sure we have all our options
+                    if(filter_value.children().length == 0) {
+                        // No options available. We have to add them.
+                        // TODO: Is there a better way to do this than asking 
+                        // the DOM?
+                        
+                        for(var i = 0; i < layer.magnitude + 1; i++) {
+                            // Make an option for each value.
+                            var option = $("<option/>").attr("value", i);
+                            
+                            if(colormaps[layer_name].hasOwnProperty(i)) {
+                                // We have a real name for this value
+                                option.text(colormaps[layer_name][i].name);
+                            } else {
+                                // No name. Use the number.
+                                option.text(i);
+                            }
+                            
+                            filter_value.append(option);
+                            
+                        }
+                        
+                        // Select the last option, so that 1 on 0/1 layers will 
+                        // be selected by default.
+                        filter_value.val(
+                            filter_value.children().last().attr("value"));
+                        
+                    }
+                } else {
+                    // Not a discrete layer, so we take a threshold.
+                    filter_threshold.show();
+                }
+                
+				save_filter.show ();
+						
+				save_filter.button().click(function() {
+				// Configure Save Filter Buttons
+
+					// Get selected value
+					var selected = filter_value.prop("selectedIndex");
+					var value = filter_value.val();
+
+					var signatures = [];
+
+					// Gather Tumor-ID Signatures with value and push to "signatures"
+					for (hex in polygons){
+						if (layer.data[hex] == value){
+								signatures.push(hex);
+						}		
+					}
+
+					// Create Layer
+					if (created == false) {
+						select_list (signatures, "user selection");	
+						created = true;
+					}
+					created = false;			
+				}); 	
+			    
+	
+				// Now that the right controls are there, assume they have 
+                refresh();
+
+            });
+        } else {
+			created = false;
+            // Hide the filtering settings
+            filter_value.hide();
+            filter_threshold.hide();
+            save_filter.hide();
+            // Draw view since we're no longer filtering on this layer.
+            refresh();
+        }
+    });
+    
+    // Respond to changes to filter configuration
+    filter_value.change(refresh);
+    
+    // TODO: Add a longer delay before refreshing here so the user can type more
+    // interactively.
+    filter_threshold.keyup(refresh);
+    
+    // Configure the range slider
+    
+    // First we need a function to update the range display, which we will run 
+    // on change and while sliding (to catch both user-initiated and 
+    //programmatic changes).
+    var update_range_display = function(event, ui) {
+        range_display.find(".low").text(ui.values[0].toFixed(3));
+        range_display.find(".high").text(ui.values[1].toFixed(3));
+    }
+    
+    range_slider.slider({
+        range: true,
+        min: -1,
+        max: 1,
+        values: [-1, 1],
+        step: 1E-9, // Ought to be fine enough
+        slide: update_range_display,
+        change: update_range_display,
+        stop: function(event, ui) {
+            // The user has finished sliding
+            // Draw the view. We will be asked for our values
+            refresh();
+        }
+    });
+    
+    // When we have time, go figure out whether the slider should be here, and 
+    // what its end values should be.
+    reset_slider(layer_name, root)
+    
+    return root;
+}
+
+// ____________________________________________________________________________
+// Replacement Set Operation Code
+// ____________________________________________________________________________
+function get_set_operation_selection () {
+	// For the new dop-down GUI for set operation selection
+	// we neeed a function to determine which set operation is selected.
+	// This way we can display the appropriate divs.	
+	
+	// Drop Down List & Index for Selected Element
+	var drop_down = document.getElementById("set-operations-list");
+	var index = drop_down.selectedIndex;
+	var selection = drop_down.options[index];
+	
+	return selection;	
+}
+
+function show_set_operation_drop_down () {
+	// Show Set Operation Drop Down Menu
+	document.getElementsByClassName("set-operation-col")[0].style.visibility="visible";
+	document.getElementsByClassName("set-operation-panel-holder")[0].style.visibility="visible";
+	document.getElementsByClassName("set-operation-panel")[0].style.visibility="visible";
+	document.getElementById("set-operations").style.visibility="visible";
+	document.getElementsByClassName("set-operation-panel-title")[0].style.visibility="visible";
+	document.getElementsByClassName("set-operation-panel-contents")[0].style.visibility="visible";
+
+}
+
+function hide_set_operation_drop_down () {
+	// Hide Set Operation Drop Down Menu
+	document.getElementsByClassName("set-operation-col")[0].style.visibility="hidden";
+	document.getElementsByClassName("set-operation-panel-holder")[0].style.visibility="hidden";
+	document.getElementsByClassName("set-operation-panel")[0].style.visibility="hidden";
+	document.getElementById("set-operations").style.visibility="hidden";
+	document.getElementsByClassName("set-operation-panel-title")[0].style.visibility="hidden";
+	document.getElementsByClassName("set-operation-panel-contents")[0].style.visibility="hidden";
+
+	// Hide the Data Values for the Selected Layers
+	var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value");
+	for (var i = 0; i < drop_downs_layer_values.length; i++) {
+				drop_downs_layer_values[i].style.visibility="hidden";
+	}
+
+	// Hide the Compute Button
+	var compute_button = document.getElementsByClassName("compute-button");
+	compute_button[0].style.visibility = "hidden";
+
+	// Set the "Select Layer" drop down to the default value
+	var list = document.getElementById("set-operations-list");
+	list.selectedIndex = 0;
+	
+	var list_value = document.getElementsByClassName("set-operation-value");
+	list_value[0].selectedIndex = 0;
+	list_value[1].selectedIndex = 0;
+
+	// Remove all elements from drop downs holding the data values for the 
+	// selected layers. This way there are no values presented when the user
+	// clicks on the set operation button to open it again.
+	var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value");
+	var length = set_operation_layer_values[0].options.length;
+	do{
+		set_operation_layer_values[0].remove(0);
+		length--;		
+	}
+	while (length > 0);
+
+	var length = set_operation_layer_values[1].options.length;
+	do{
+		set_operation_layer_values[1].remove(0);
+		length--;		
+	}
+	while (length > 0);
+	
+}
+
+function create_set_operation_ui () {
+	// Returns a Jquery element that is then prepended to the existing 
+	// set theory drop-down menu	
+
+    // This holds the root element for this set operation UI 
+    var root = $("<div/>").addClass("set-operation-entry");
+	
+	// Add Drop Downs to hold the selected layers and and selected data values 
+    var set_theory_value1 = $("<select/>").addClass("set-operation-value");
+	var set_theory_layer_value1 = $("<select/>").addClass("set-operation-layer-value");
+	var set_theory_value2 = $("<select/>").addClass("set-operation-value");	
+	var set_theory_layer_value2 = $("<select/>").addClass("set-operation-layer-value");
+
+	var compute_button = $("<input/>").attr("type", "button");
+	compute_button.addClass ("compute-button");
+
+	// Append to Root
+	root.append (set_theory_value1);
+	root.append (set_theory_layer_value1);
+	root.append (set_theory_value2);
+	root.append (set_theory_layer_value2);
+	root.append (compute_button);
+
+	return root;
+}
+
+function update_set_operation_drop_down () {
+	// This is the onchange command for the drop down displaying the 
+	// different set operation functions. It is called whenever the user changes
+	// the selected set operation.
+
+	// Get the value of the set operation selection made by the user.
+	var selection = get_set_operation_selection();
+	var value = selection.value;	
+	// Check if the selectin value is that of one of set operation functions
+	if (selection.value == 1 || selection.value == 2 
+		|| selection.value == 3 || selection.value == 4
+		|| selection.value == 5){
+			// Make the drop downs that hold layer names and data values visible
+			var drop_downs = document.getElementsByClassName("set-operation-value");
+			var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value");
+
+			for (var i = 0; i < drop_downs.length; i++) {
+				drop_downs[i].style.visibility="visible";
+			}
+			
+			for (var i = 0; i < drop_downs_layer_values.length; i++) {
+				drop_downs_layer_values[i].style.visibility="visible";
+			}
+
+			var compute_button = document.getElementsByClassName("compute-button");
+			compute_button[0].style.visibility = "visible";
+			compute_button[0].value = "Compute Set Operation";
+
+			if (first_opening == true) {
+				// Set the default value for the drop down, holding the selected layers
+				var default_value = document.createElement("option");
+				default_value.text = "Select Layer 1";
+				default_value.value = 0;
+				drop_downs[0].add(default_value);
+
+				var default_value2 = document.createElement("option");
+				default_value2.text = "Select Layer 2";
+				default_value2.value = 0;
+				drop_downs[1].add(default_value2);
+				
+				// Prevent from adding the default value again
+				first_opening = false;
+			}
+
+			// Hide the second set of drop downs if "Not:" is selected
+			if (selection.value == 5) {
+				drop_downs[1].style.visibility="hidden";
+				drop_downs_layer_values[1].style.visibility="hidden";
+			}
+	}	
+	else {
+		// If the user has the default value selected, hide all drop downs
+		var drop_downs = document.getElementsByClassName("set-operation-value");
+		for (var i = 0; i < drop_downs.length; i++) {
+			drop_downs[i].style.visibility="hidden";
+		}
+		var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value");
+		for (var i = 0; i < drop_downs_layer_values.length; i++) {
+				drop_downs_layer_values[i].style.visibility="hidden";
+		}
+		var compute_button = document.getElementsByClassName("compute-button");
+			compute_button[0].style.visibility = "hidden";
+	}
+}
+
+function update_set_operation_selections () {
+	// This function is called when the shorlist is changed.
+	// It appropriately updates the drop down containing the list of layers
+	// to match the layers found in the shortlist.
+
+	// Get the list of all layers
+	var layers = [];
+	$("#shortlist").children().each(function(index, element) {
+	 	// Get the layer name
+        var layer_name = $(element).data("layer");
+		layers.push(layer_name);
+	});
+
+	// Get a list of all drop downs that contain layer names
+	var drop_downs = document.getElementsByClassName("set-operation-value");
+
+	// Remove all existing layer names from both dropdowns
+	var length = drop_downs[0].options.length;
+	do{
+		drop_downs[0].remove(0);
+		length--;		
+	}
+	while (length > 0);
+	var length = drop_downs[1].options.length;
+	do{
+		drop_downs[1].remove(0);
+		length--;		
+	}
+	while (length > 0);
+
+	// Add the default values that were stripped in the last step.
+	var default_value = document.createElement("option");
+	default_value.text = "Select Layer 1";
+	default_value.value = 0;
+	drop_downs[0].add(default_value);
+
+	var default_value2 = document.createElement("option");
+	default_value2.text = "Select Layer 2";
+	default_value2.value = 0;
+	drop_downs[1].add(default_value2);
+	
+	first_opening = false;
+	
+	// Add the layer names from the shortlist to the drop downs that store
+	// layer names.		
+	for (var i = 0; i < drop_downs.length; i++){
+		for (var j = 0; j < layers.length; j++) {
+			var option = document.createElement("option");
+			option.text = layers[j];
+			option.value = j+1;
+			drop_downs[i].add(option);
+		}
+	}
+
+	// Remove all elements from drop downs holding the data values for the 
+	// selected layers. This way there are no values presented when the user
+	// clicks on the set operation button to open it again.
+	var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value");
+	var length = set_operation_layer_values[0].options.length;
+	do{
+		set_operation_layer_values[0].remove(0);
+		length--;		
+	}
+	while (length > 0);
+
+	var length = set_operation_layer_values[1].options.length;
+	do{
+		set_operation_layer_values[1].remove(0);
+		length--;		
+	}
+	while (length > 0);
+
+	// Call the function containing onchange commands for these dropdowns.
+	// This way the data values are updated according the the selected layer.
+	update_set_operation_data_values ();
+}
+
+function update_set_operation_data_values () {
+	// Define the onchange commands for the drop downs that hold layer names.
+	// This way the data values are updated according the the selected layer.
+
+	// Get all drop down elements
+	var selected_function = document.getElementById ("set-operations-list");
+	var drop_downs = document.getElementsByClassName("set-operation-value");
+	var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value");
+
+	// The "Select Layer1" Dropdown onchange function
+	drop_downs[0].onchange = function(){
+		// Strip current values of the data value dropdown
+		var length = set_operation_layer_values[0].options.length;
+		do{
+			set_operation_layer_values[0].remove(0);
+			length--;		
+		}
+		while (length > 0);
+	
+		// Add the data values depending on the selected layer
+		var selectedIndex = drop_downs[0].selectedIndex;
+		var layer_name = drop_downs[0].options[selectedIndex].text;
+		var set_operation_data_value_select = set_operation_layer_values[0];
+		create_set_operation_pick_list(set_operation_data_value_select, layer_name);
+	};
+
+	// The "Select Layer2" Dropdown onchange function
+	drop_downs[1].onchange = function(){
+		// Strip current values of the data value dropdown
+		var length = set_operation_layer_values[1].options.length;
+		do{
+			set_operation_layer_values[1].remove(0);
+			length--;		
+		}
+		while (length > 0);
+
+		// Add the data values depending on the selected layer
+		var selectedIndex = drop_downs[1].selectedIndex;
+		var layer_name = drop_downs[1].options[selectedIndex].text;
+		var set_operation_data_value_select = set_operation_layer_values[1];
+		create_set_operation_pick_list(set_operation_data_value_select, layer_name);
+	};
+
+}
+
+function create_set_operation_pick_list(value,layer_object) {
+
+	// We must create a drop down containing the data values for the selected
+	// layer.
+
+	// The Javascript "select" element that contains the data values
+	// is passed as "value" and the selected layer is passed as "layer_object". 
+
+	// First, figure out what kind of filter settings we take based on 
+	// what kind of layer we are.
+	with_layer(layer_object, function(layer) {
+                    
+             // No options available. We have to add them.
+             for(var i = 0; i < layer.magnitude + 1; i++) {
+             	// Make an option for each value;
+				var option = document.createElement("option");
+				option.value = i;
+                            
+                if(colormaps[layer_object].hasOwnProperty(i)) {
+                	// We have a real name for this value
+                    option.text = (colormaps[layer_object][i].name);
+                 } else {
+                     // No name. Use the number.
+                     option.text = i;
+                     }  
+                 value.add(option);
+     
+                 // Select the last option, so that 1 on 0/1 layers will 
+                 // be selected by default.
+				 var last_index = value.options.length - 1;
+                 value.selectedIndex = last_index;   
+                }                
+            // Now that the right controls are there, assume they have 
+            refresh();
+     });
+}
+
+
+function update_shortlist_ui() {
+    // Go through the shortlist and make sure each layer there has an entry in 
+    // the shortlist UI, and that each UI element has an entry in the shortlist.
+    // Also make sure the metadata for all existing layers is up to date.
+    
+    // Clear the existing UI lookup table
+    shortlist_ui = {};
+    
+    for(var i = 0; i < shortlist.length; i++) {
+        // For each shortlist entry, put a false in the lookup table
+        shortlist_ui[shortlist[i]] = false;
+    }
+    
+    
+    $("#shortlist").children().each(function(index, element) {
+        if(shortlist_ui[$(element).data("layer")] === false) {
+            // There's a space for this element: it's still in the shortlist
+            
+            // Fill it in
+            shortlist_ui[$(element).data("layer")] = $(element);
+            
+            // Update the metadata in the element. It make have changed due to
+            // statistics info coming back.
+            fill_layer_metadata($(element).find(".metadata-holder"), 
+                $(element).data("layer"));
+        } else {
+            // It wasn't in the shortlist, so get rid of it.
+            $(element).remove();
+        }
+    });
+    
+    for(var layer_name in shortlist_ui) {
+        // For each entry in the lookup table
+        if(shortlist_ui[layer_name] === false) {
+             // If it's still false, make a UI element for it.
+             shortlist_ui[layer_name] = make_shortlist_ui(layer_name);
+             $("#shortlist").prepend(shortlist_ui[layer_name]);
+             
+             // Check it's box if possible
+             shortlist_ui[layer_name].find(".layer-on").click();
+        }
+    }
+    
+    // Make things re-orderable
+    // Be sure to re-draw the view if the order changes, after the user puts 
+    // things down.
+    $("#shortlist").sortable({
+        update: refresh,
+        // Sort by the part with the lines icon, so we can still select text.
+        handle: ".shortlist-controls" 
+    });
+
+	update_set_operation_selections ();   
+}
+
+function uncheck_checkbox (checkbox_class) {
+	// Unchecks chekboxes after the function has been completed.
+	var checkboxArray = new Array ();
+	checkboxArray = document.getElementsByClassName(checkbox_class);
+	for (var i = 0; i < checkboxArray.length; i++)
+	{
+		checkboxArray[i].checked = false;
+	}
+}	
+
+function hide_values (set_theory_function) {
+	// Hides pick lists for set theory functions after function has been 
+	// completed.
+	var value_type = set_theory_function + '-value';
+
+	var values = new Array ();
+
+	values = document.getElementsByClassName(value_type);
+	
+	var length = values.length;
+
+	for (var i = 0; i < length; i++)
+	{
+		values[i].style.display = 'none';
+	}
+	refresh();
+}	
+
+function compute_intersection (values, intersection_layer_names, text) {
+	// A function that will take a list of layer names
+	// that have been selected for the intersection utility.
+	// Fetches the respective layers and list of tumor ids.
+	// Then compares data elements of the same tumor id
+	// between both layers. Adds these hexes to a new layer
+	// for visualization
+	
+	//Array of signatures that intersect 
+	var intersection_signatures = [];
+
+	with_layers (intersection_layer_names, function (intersection_layers) {
+	
+		// Gather Tumor-ID Signatures.
+		for (hex in polygons)
+		{
+			if (intersection_layers[0].data[hex] == values[0] && intersection_layers[1].data[hex] == values[1]){
+				intersection_signatures.push(hex);
+			}		
+		}
+	});
+
+	for (var i = 0; i < intersection_layer_names.length; i++){
+		intersection_layer_names[i] = intersection_layer_names[i] + " [" + text[i] + "]";
+	}
+	var intersection_function = "intersection";
+	select_list (intersection_signatures, intersection_function, intersection_layer_names);
+	uncheck_checkbox ('intersection-checkbox');
+	hide_values('intersection');
+}
+
+function compute_union (values, union_layer_names, text) {
+	// A function that will take a list of layer names
+	// that have been selected for the union utility.
+	// Fetches the respective layers and list of tumor ids.
+	// Then compares data elements of the same tumor id
+	// between both layers. Adds these hexes to a new layer
+	// for visualization
+	
+	//Array of signatures 
+	var union_signatures = [];
+
+	with_layers (union_layer_names, function (union_layers) {
+	
+		// Gather Tumor-ID Signatures.
+		for (hex in polygons)
+		{
+			// Union Function
+			if (union_layers[0].data[hex] == values[0] || union_layers[1].data[hex] == values[1]){
+				union_signatures.push(hex);
+			}		
+		}
+	});
+	
+	for (var i = 0; i < union_layer_names.length; i++){
+		union_layer_names[i] = union_layer_names[i] + " [" + text[i] + "]";
+	}
+
+	var union_function = "union";
+	select_list (union_signatures, union_function, union_layer_names);
+	uncheck_checkbox ('union-checkbox');
+	hide_values('union');
+}
+
+function compute_set_difference (values, set_difference_layer_names, text) {
+	// A function that will take a list of layer names
+	// that have been selected for the set difference utility.
+	// Fetches the respective layers and list of tumor ids.
+	// Then compares data elements of the same tumor id
+	// between both layers. Adds these hexes to a new layer
+	// for visualization
+	
+	//Array of signatures  
+	var set_difference_signatures = [];
+
+	with_layers (set_difference_layer_names, function (set_difference_layers) {
+	
+		// Gather Tumor-ID Signatures.
+		for (hex in polygons)
+		{
+			// Set Difference Function
+			if (set_difference_layers[0].data[hex] == values[0] && 
+				set_difference_layers[1].data[hex] != values[1]){
+				set_difference_signatures.push(hex);
+			}
+		}
+	});
+	
+	for (var i = 0; i < set_difference_layer_names.length; i++){
+		set_difference_layer_names[i] = set_difference_layer_names[i] + " [" + text[i] + "]";
+	}
+
+	var set_difference_function = "set difference";
+	select_list (set_difference_signatures, set_difference_function, set_difference_layer_names);
+	uncheck_checkbox ('set-difference-checkbox');
+	hide_values('set-difference');
+}
+
+function compute_symmetric_difference (values, symmetric_difference_layer_names, text) {
+	// A function that will take a list of layer names
+	// that have been selected for the set difference utility.
+	// Fetches the respective layers and list of tumor ids.
+	// Then compares data elements of the same tumor id
+	// between both layers. Adds these hexes to a new layer
+	// for visualization
+	
+	//Array of signatures 
+	var symmetric_difference_signatures = [];
+
+	with_layers (symmetric_difference_layer_names, function (symmetric_difference_layers) {
+	
+		// Gather Tumor-ID Signatures.
+		for (hex in polygons)
+		{
+			// Symmetric Difference Function
+			if (symmetric_difference_layers[0].data[hex] == values[0] && 
+				symmetric_difference_layers[1].data[hex] != values[1]){
+				symmetric_difference_signatures.push(hex);
+			}
+			if (symmetric_difference_layers[0].data[hex] != values[0] &&
+				symmetric_difference_layers[1].data[hex] == values[1]){
+				symmetric_difference_signatures.push(hex);
+			}
+		}
+	});
+	
+	for (var i = 0; i < symmetric_difference_layer_names.length; i++){
+		symmetric_difference_layer_names[i] = symmetric_difference_layer_names[i] + " [" + text[i] + "]";
+	}
+
+	var symmetric_difference_function = "symmetric difference";
+	select_list (symmetric_difference_signatures, symmetric_difference_function, symmetric_difference_layer_names);
+	uncheck_checkbox ('symmetric-difference-checkbox');
+	hide_values('symmetric-difference');
+}
+
+function compute_absolute_complement (values, absolute_complement_layer_names, text) {
+	// A function that will take a list of layer names
+	// that have been selected for the set difference utility.
+	// Fetches the respective layers and list of tumor ids.
+	// Then compares data elements of the same tumor id
+	// between both layers. Adds these hexes to a new layer
+	// for visualization
+	
+	//Array of signatures 
+	var absolute_complement_signatures = [];
+
+	with_layers (absolute_complement_layer_names, function (absolute_complement_layers) {
+	
+		// Gather Tumor-ID Signatures.
+		for (hex in polygons)
+		{
+			// Absolute Complement Function
+			if (absolute_complement_layers[0].data[hex] != values[0]) {
+				absolute_complement_signatures.push(hex);
+			}
+		}
+	});
+	
+	for (var i = 0; i < absolute_complement_layer_names.length; i++){
+		absolute_complement_layer_names[i] = absolute_complement_layer_names[i] + " [" + text[i] + "]";
+	}
+	var absolute_complement_function = "absolute complement";
+	select_list (absolute_complement_signatures, absolute_complement_function, absolute_complement_layer_names);
+	uncheck_checkbox ('absolute-complement-checkbox');
+	hide_values('absolute-complement');
+}
+
+
+function layer_sort_order(a, b) {
+    // A sort function defined on layer names.
+    // Return <0 if a belongs before b, >0 if a belongs after
+    // b, and 0 if their order doesn't matter.
+    
+    // Sort by selection status, then p_value, then clumpiness, then (for binary
+    // layers that are not selections) the frequency of the less common value,
+    // then alphabetically by name if all else fails.
+
+    // Note that we can consult the layer metadata "n" and "positives" fields to
+    // calculate the frequency of the least common value in binary layers,
+    // without downloading them.
+    
+    if(layers[a].selection && !layers[b].selection) {
+        // a is a selection and b isn't, so put a first.
+        return -1;
+    } else if(layers[b].selection && !layers[a].selection) {
+        // b is a selection and a isn't, so put b first.
+        return 1;
+    }
+    
+    if(layers[a].p_value < layers[b].p_value) {
+        // a has a lower p value, so put it first.
+        return -1;
+    } else if(layers[b].p_value < layers[a].p_value) {
+        // b has a lower p value. Put it first instead.
+        return 1;
+    } else if(isNaN(layers[b].p_value) && !isNaN(layers[a].p_value)) {
+        // a has a p value and b doesn't, so put a first
+        return -1;
+    } else if(!isNaN(layers[b].p_value) && isNaN(layers[a].p_value)) {
+        // b has a p value and a doesn't, so put b first.
+        return 1;
+    }
+    
+    if(layers[a].clumpiness < layers[b].clumpiness) {
+        // a has a lower clumpiness score, so put it first.
+        return -1;
+    } else if(layers[b].clumpiness < layers[a].clumpiness) {
+        // b has a lower clumpiness score. Put it first instead.
+        return 1;
+    } else if(isNaN(layers[b].clumpiness) && !isNaN(layers[a].clumpiness)) {
+        // a has a clumpiness score and b doesn't, so put a first
+        return -1;
+    } else if(!isNaN(layers[b].clumpiness) && isNaN(layers[a].clumpiness)) {
+        // b has a clumpiness score and a doesn't, so put b first.
+        return 1;
+    }
+    
+    
+    
+    if(!layers[a].selection && !isNaN(layers[a].positives) && layers[a].n > 0 &&
+        !layers[b].selection && !isNaN(layers[b].positives) && 
+        layers[b].n > 0) {
+        
+        // We have checked to see each layer is supposed to be bianry layer
+        // without downloading.  TODO: This is kind of a hack. Redesign the
+        // whole system with a proper concept of layer type.
+        
+        // We've also verified they both have some data in them. Otherwise we
+        // might divide by 0 trying to calculate frequency.
+            
+        // Two binary layers (not selections).
+        // Compute the frequency of the least common value for each
+        
+        // This is the frequency of the least common value in a (will be <=1/2)
+        var minor_frequency_a = layers[a].positives / layers[a].n;
+        if(minor_frequency_a > 0.5) {
+            minor_frequency_a = 1 - minor_frequency_a;
+        }
+        
+        // And this is the same frequency for the b layer
+        var minor_frequency_b = layers[b].positives / layers[b].n;
+        if(minor_frequency_b > 0.5) {
+            minor_frequency_b = 1 - minor_frequency_b;
+        }
+
+        if(minor_frequency_a > minor_frequency_b) {
+            // a is more evenly split, so put it first
+            return -1;
+        } else if(minor_frequency_a < minor_frequency_b) {
+            // b is more evenly split, so put it first
+            return 1;
+        } 
+       
+    } else if (!layers[a].selection && !isNaN(layers[a].positives) && 
+        layers[a].n > 0) {
+        
+        // a is a binary layer we can nicely sort by minor value frequency, but
+        // b isn't. Put a first so that we can avoid intransitive sort cycles.
+        
+        // Example: X and Z are binary layers, Y is a non-binary layer, Y comes
+        // after X and before Z by name ordering, but Z comes before X by minor
+        // frequency ordering. This sort is impossible.
+        
+        // The solution is to put both X and Z in front of Y, because they're
+        // more interesting.
+        
+        return -1;
+    
+    } else if (!layers[b].selection && !isNaN(layers[b].positives) && 
+        layers[b].n > 0) {
+        
+        // b is a binary layer that we can evaluate based on minor value
+        // frequency, but a isn't. Put b first.
+        
+        return 1;
+        
+    }
+    
+    // We couldn't find a difference in selection status, p-value, or clumpiness
+    // score, or the binary layer minor value frequency, or whether each layer
+    // *had* a binary layer minor value frequency, so use lexicographic ordering
+    // on the name.
+    return a.localeCompare(b);
+    
+}
+
+function sort_layers(layer_array) {
+    // Given an array of layer names, sort the array in place as we want layers
+    // to appear to the user.
+    // We should sort by p value, with NaNs at the end. But selections should be
+    // first.
+    
+    layer_array.sort(layer_sort_order);
+}
+
+function fill_layer_metadata(container, layer_name) {
+    // Empty the given jQuery container element, and fill it with layer metadata
+    // for the layer with the given name.
+    
+    // Empty the container.
+    container.html("");
+    
+    for(attribute in layers[layer_name]) {
+        // Go through everything we know about this layer
+        if(attribute == "data" || attribute == "url" || 
+            attribute == "magnitude" || attribute == "selection") {
+            
+            // Skip built-in things
+            // TODO: Ought to maybe have all metadata in its own object?
+            continue;
+        }
+        
+        // This holds the metadata value we're displaying
+        var value = layers[layer_name][attribute];
+        
+        if(typeof value == "number" && isNaN(value)) {
+            // If it's a numerical NaN (but not a string), just leave it out.
+            continue;
+        }
+        
+        // If we're still here, this is real metadata.
+        // Format it for display.
+        var value_formatted;
+        if(typeof value == "number") {
+            if(value % 1 == 0) {
+                // It's an int!
+                // Display the default way
+                value_formatted = value;
+            } else {
+                // It's a float!
+                // Format the number for easy viewing
+                value_formatted = value.toExponential(2);
+            }
+        } else {
+            // Just put the thing in as a string
+            value_formatted = value;
+        }
+        
+        // Do some transformations to make the displayed labels make more sense
+        lookup = {
+            n: "Number of non-empty values",
+            positives: "Number of ones",
+            inside_yes: "Ones in A",
+            outside_yes: "Ones in background"
+        }
+        
+        if(lookup[attribute]) {
+            // Replace a boring short name with a useful long name
+            attribute = lookup[attribute];
+        }
+        
+        // Make a spot for it in the container and put it in
+        var metadata = $("<div\>").addClass("layer-metadata");
+        metadata.text(attribute + " = " + value_formatted);
+        
+        container.append(metadata);
+        
+    }
+}
+
+function make_toggle_layout_ui(layout_name) {
+    // Returns a jQuery element to represent the layer layout the given name in 
+    // the toggle layout panel.
+    
+    // This holds a jQuery element that's the root of the structure we're
+    // building.
+    var root = $("<div/>").addClass("layout-entry");
+    root.data("layout-name", layout_name);
+    
+    // Put in the layer name in a div that makes it wrap.
+    root.append($("<div/>").addClass("layout-name").text(layout_name));
+ 
+    return root;
+}
+
+function make_browse_ui(layer_name) {
+    // Returns a jQuery element to represent the layer with the given name in 
+    // the browse panel.
+    
+    // This holds a jQuery element that's the root of the structure we're
+    // building.
+    var root = $("<div/>").addClass("layer-entry");
+    root.data("layer-name", layer_name);
+    
+    // Put in the layer name in a div that makes it wrap.
+    root.append($("<div/>").addClass("layer-name").text(layer_name));
+    
+    // Put in a layer metadata container div
+    var metadata_container = $("<div/>").addClass("layer-metadata-container");
+    
+    fill_layer_metadata(metadata_container, layer_name);
+    
+    root.append(metadata_container);
+    
+    return root;
+}
+
+function update_browse_ui() {
+    // Make the layer browse UI reflect the current list of layers in sorted
+    // order.
+    
+    // Re-sort the sorted list that we maintain
+    sort_layers(layer_names_sorted);
+    
+    // Close the select if it was open, forcing the data to refresh when it
+    // opens again.
+    $("#search").select2("close");
+}
+
+function get_slider_range(layer_name) {
+    // Given the name of a layer, get the slider range from its shortlist UI 
+    // entry.
+    // Assumes the layer has a shortlist UI entry.
+    return shortlist_ui[layer_name].find(".range-slider").slider("values");
+}
+
+function reset_slider(layer_name, shortlist_entry) {
+    // Given a layer name and a shortlist UI entry jQuery element, reset the 
+    // slider in the entry to its default values, after downloading the layer. 
+    // The default value may be invisible because we decided the layer should be
+    // a colormap.
+        
+    // We need to set its boundaries to the min and max of the data set
+    with_layer(layer_name, function(layer) {
+        if(have_colormap(layer_name)) {
+            // This is a colormap, so don't use the range slider at all.
+            // We couldn't know this before because the colormap may need to be 
+            // auto-detected upon download.
+            shortlist_entry.find(".range").hide();
+            return;
+        } else {
+            // We need the range slider
+            shortlist_entry.find(".range").show();
+        
+            // TODO: actually find max and min
+            // For now just use + and - magnitude
+            // This has the advantage of letting us have 0=black by default
+            var magnitude = layer.magnitude;
+            
+            // This holds the limit to use, which should be 1 if the magnitude 
+            // is <1. This is sort of heuristic, but it's a good guess that 
+            // nobody wants to look at a layer with values -0.2 to 0.7 on a 
+            // scale of -10 to 10, say, but they might want it on -1 to 1.
+            var range = Math.max(magnitude, 1.0)
+            
+            // Set the min and max.
+            shortlist_entry.find(".range-slider").slider("option", "min", 
+                -range);
+            shortlist_entry.find(".range-slider").slider("option", "max", 
+                range);
+            
+            // Set slider to autoscale for the magnitude.
+            shortlist_entry.find(".range-slider").slider("values", [-magnitude, 
+                magnitude]);
+                
+            print("Scaled to magnitude " + magnitude);
+                
+            // Redraw the view in case this changed anything
+            refresh();
+        }
+        
+    });
+}
+
+function get_current_layers() {
+    // Returns an array of the string names of the layers that are currently
+    // supposed to be displayed, according to the shortlist UI.
+    // Not responsible for enforcing maximum selected layers limit.
+    
+    // This holds a list of the string names of the currently selected layers,
+    // in order.
+    var current_layers = [];
+    
+    $("#shortlist").children().each(function(index, element) {
+        // This holds the checkbox that determines if we use this layer
+        var checkbox = $(element).find(".layer-on");
+        if(checkbox.is(":checked")) {
+            // Put the layer in if its checkbox is checked.
+            current_layers.push($(element).data("layer"));
+        }
+    });
+    
+    // Return things in reverse order relative to the UI.
+    // Thus, layer-added layers will be "secondary", and e.g. selecting 
+    // something with only tissue up behaves as you might expect, highlighting 
+    // those things.
+    current_layers.reverse();
+    
+    return current_layers;
+}
+
+function get_current_filters() {
+    // Returns an array of filter objects, according to the shortlist UI.
+    // Filter objects have a layer name and a boolean-valued filter function 
+    // that returns true or false, given a value from that layer.
+    var current_filters = [];
+    
+    $("#shortlist").children().each(function(index, element) {
+        // Go through all the shortlist entries.
+        // This function is also the scope used for filtering function config 
+        // variables.
+    
+        // This holds the checkbox that determines if we use this layer
+        var checkbox = $(element).find(".filter-on");
+        if(checkbox.is(":checked")) {
+            // Put the layer in if its checkbox is checked.
+            
+            // Get the layer name
+            var layer_name = $(element).data("layer");
+            
+            // This will hold our filter function. Start with a no-op filter.
+            var filter_function = function(value) {
+                return true;
+            }
+            
+            // Get the filter parameters
+            // This holds the input that specifies a filter threshold
+            var filter_threshold = $(element).find(".filter-threshold");
+            // And this the element that specifies a filter match value for 
+            // discrete layers
+            var filter_value = $(element).find(".filter-value");
+            
+            // We want to figure out which of these to use without going and 
+            // downloading the layer.
+            // So, we check to see which was left visible by the filter config
+            // setup code.
+            if(filter_threshold.is(":visible")) {
+                // Use a threshold. This holds the threshold.
+                var threshold = parseInt(filter_threshold.val());
+                
+                filter_function = function(value) {
+                    return value > threshold;
+                }
+            }
+            
+            if(filter_value.is(":visible")) {
+                // Use a discrete value match instead. This hodls the value we
+                // want to match.
+                var desired = filter_value.val();
+                
+                filter_function = function(value) {
+                    return value == desired;
+                }
+            }
+            
+            // Add a filter on this layer, with the function we've prepared.
+            current_filters.push({
+                layer_name: layer_name,
+                filter_function: filter_function
+            });
+        }
+    });
+    
+    return current_filters;
+}
+
+function get_current_layers() {
+    // Returns an array of the string names of the layers that are currently
+    // supposed to be displayed, according to the shortlist UI.
+    // Not responsible for enforcing maximum selected layers limit.
+    
+    // This holds a list of the string names of the currently selected layers,
+    // in order.
+    var current_layers = [];
+    
+    $("#shortlist").children().each(function(index, element) {
+        // This holds the checkbox that determines if we use this layer
+        var checkbox = $(element).find(".layer-on");
+        if(checkbox.is(":checked")) {
+            // Put the layer in if its checkbox is checked.
+            current_layers.push($(element).data("layer"));
+        }
+    });
+    
+    // Return things in reverse order relative to the UI.
+    // Thus, layer-added layers will be "secondary", and e.g. selecting 
+    // something with only tissue up behaves as you might expect, highlighting 
+    // those things.
+    current_layers.reverse();
+    
+    return current_layers;
+}
+
+function get_current_set_theory_layers(function_type) {
+    // Returns an array of layer names that have been selected.
+	// This function only looks at the layers that are listed on the shortlist.
+
+    var current_set_theory_layers = [];
+
+	// Initialize global variables that hold the number of checkboxes selected
+	// for set theory functions to zero so that the new number is calculated
+	// each time this function is called.
+	
+	if (function_type == "intersection"){
+	shortlist_intersection_num = 0;
+	}
+
+	if (function_type == "union"){
+	shortlist_union_num = 0;
+	}
+
+	if (function_type == "set difference"){
+	shortlist_set_difference_num = 0;
+	}
+
+	if (function_type == "symmetric difference"){
+	shortlist_symmetric_difference_num = 0;
+	}
+
+	if (function_type == "absolute complement"){
+	shortlist_absolute_complement_num = 0;
+	}
+	
+    $("#shortlist").children().each(function(index, element) {
+        // Go through all the shortlist entries.
+
+        // This holds the checkbox that determines if we use this layer
+		// The class name depends on the function_type.   
+
+		// If intersection function look for intersection-checkbox.
+		if (function_type == "intersection"){  
+		var checkbox = $(element).find(".intersection-checkbox");
+		}
+
+		// If union function look for union-checkbox.
+		if (function_type == "union"){  
+		var checkbox = $(element).find(".union-checkbox");
+		}
+
+		// If set difference function look for set-difference-checkbox.
+		if (function_type == "set difference"){  
+		var checkbox = $(element).find(".set-difference-checkbox");
+		}
+
+		// If symmetric difference function look for 
+		// symmetric-difference-checkbox.
+		if (function_type == "symmetric difference"){  
+		var checkbox = $(element).find(".symmetric-difference-checkbox");
+		}
+
+		if (function_type == "absolute complement"){  
+		var checkbox = $(element).find(".absolute-complement-checkbox");
+		}
+
+        if(checkbox.is(":checked")) {
+            // Put the layer in if its checkbox is checked.
+            
+		
+            // Get the layer name
+            var layer_name = $(element).data("layer");
+           
+            // Add the layer_name to the list of current_set_theory_layers.
+            current_set_theory_layers.push(layer_name);
+
+			// Add to the global "num" variables to keep track of the number
+			// of selected checkboxes.
+
+			if (function_type == "intersection"){
+				shortlist_intersection_num++;
+			}
+				
+			if (function_type == "union"){
+				shortlist_union_num++;
+			}
+
+			if (function_type == "set difference"){  
+				shortlist_set_difference_num++;
+			}
+
+			if (function_type == "symmetric difference"){  
+				shortlist_symmetric_difference_num++;
+			}
+
+			if (function_type == "absolute complement"){  
+				shortlist_absolute_complement_num++;
+			}
+
+        }
+    });
+    
+    return current_set_theory_layers;
+}
+
+
+function with_filtered_signatures(filters, callback) {
+    // Takes an array of filters, as produced by get_current_filters. Signatures 
+    // pass a filter if the filter's layer has a value >0 for that signature. 
+    // Computes an  array of all signatures passing all filters, and passes that
+    // to the given callback.
+    
+    // TODO: Re-organize this to do filters one at a time, recursively, like a 
+    // reasonable second-order filter.
+    
+    // Prepare a list of all the layers
+    var layer_names = [];
+    
+    for(var i = 0; i < filters.length; i++) {
+        layer_names.push(filters[i].layer_name);
+    }
+    
+    with_layers(layer_names, function(filter_layers) {
+        // filter_layers is guaranteed to be in the same order as filters.
+        
+        // This is an array of signatures that pass all the filters.
+        var passing_signatures = [];
+    
+        for(var signature in polygons) {
+            // For each signature
+            
+            // This holds whether we pass all the filters
+            var pass = true;
+            
+            for(var i = 0; i < filter_layers.length; i++) {
+                // For each filtering layer
+                if(!filters[i].filter_function(
+                    filter_layers[i].data[signature])) {
+                    
+                    // If the signature fails the filter function for the layer,
+                    // skip the signature.
+                    pass = false;
+                    break;
+                }
+            }
+            
+            if(pass) {
+                // Record that the signature passes all filters
+                passing_signatures.push(signature);
+            }
+        }
+        
+        // Now we have our list of all passing signatures, so hand it off to the
+        // callback.
+        callback(passing_signatures);
+    });
+}
+
+function select_list(to_select, function_type, layer_names) {
+    // Given an array of signature names, add a new selection layer containing
+    // just those hexes. Only looks at hexes that are not filtered out by the
+    // currently selected filters.
+	
+	// function_type is an optional parameter. If no variable is passed for the 
+	// function_type undefined then the value will be undefined and the
+	// default "selection + #" title will be assigned to the shortlist element.
+	// If layer_names is undefined, the "selection + #" will also apply as a
+	// default. However, if a value i.e. "intersection" is passed 
+	// for function_type, the layer_names will be used along with the 
+	// function_type to assign the correct title. 
+    
+    // Make the requested signature list into an object for quick membership
+    // checking. This holds true if a signature was requested, undefined
+    // otherwise.
+    var wanted = {};
+    
+    for(var i = 0; i < to_select.length; i++) {
+        wanted[to_select[i]] = true;
+    }
+    
+    // This is the data object for the layer: from signature names to 1/0
+    var data = {};
+    
+    // How many signatures will we have any mention of in this layer
+    var signatures_available = 0;
+    
+    // Start it out with 0 for each signature. Otherwise we wil have missing 
+    // data for signatures not passing the filters.
+    for(var signature in polygons) {
+        data[signature] = 0;
+        signatures_available += 1;
+    }
+    
+    // This holds the filters we're going to use to restrict our selection
+    var filters = get_current_filters();
+
+    // Go get the list of signatures passing the filters and come back.
+    with_filtered_signatures(filters, function(signatures) {   
+        // How many signatures get selected?
+        var signatures_selected = 0;
+     
+        for(var i = 0; i < signatures.length; i++) {
+            if(wanted[signatures[i]]) {
+                // This signature is both allowed by the filters and requested.
+                data[signatures[i]] = 1;
+                signatures_selected++;           
+            }
+        }
+        
+		// Make up a name for the layer
+		var layer_name;
+
+		// Default Values for Optional Parameters
+		if (function_type == undefined && layer_names == undefined){		
+        	layer_name = "Selection " + selection_next_id;
+        	selection_next_id++;
+		}
+
+		if (function_type == "user selection"){
+			 var text = prompt("Please provide a label for your selection",
+			 "Selection Label Text");
+			 if (text != null){
+			 	layer_name = text;
+			 }
+			 if (!text)
+			 {
+				return;
+			 }			
+		}
+		
+		// intersection for layer name
+		if (function_type == "intersection"){
+			layer_name = "(" + layer_names[0] + " ∩ " + layer_names[1] + ")";
+		}
+
+		// union for layer name
+		if (function_type == "union"){
+			layer_name = "(" + layer_names[0] + " U " + layer_names[1] + ")";
+		}
+
+		// set difference for layer name
+		if (function_type == "set difference"){
+			layer_name = "(" + layer_names[0] + " \\ " + layer_names[1] + ")";
+		}
+
+		// symmetric difference for layer name
+		if (function_type == "symmetric difference"){
+			layer_name = "(" + layer_names[0] + " ∆ " + layer_names[1] + ")";
+		}
+        
+		// absolute complement for layer name
+		if (function_type == "absolute complement"){
+			layer_name = "Not: " + "(" + layer_names[0] + ")";
+		}
+
+		// saved filter for layer name
+		if (function_type == "save"){
+			layer_name =  "(" + layer_names[0] + ")";
+		}
+		
+        // Add the layer. Say it is a selection
+        add_layer_data(layer_name, data, {
+            selection: true,
+            selected: signatures_selected, // Display how many hexes are in
+            n: signatures_available // And how many have a value at all
+        });
+        
+        // Update the browse UI with the new layer.
+        update_browse_ui();
+        
+        // Immediately shortlist it
+        shortlist.push(layer_name);
+        update_shortlist_ui();
+    });
+    
+}
+
+function select_rectangle(start, end) {
+    // Given two Google Maps LatLng objects (denoting arbitrary rectangle 
+    // corners), add a new selection layer containing all the hexagons 
+    // completely within that rectangle.
+    // Only looks at hexes that are not filtered out by the currently selected 
+    // filters.
+    
+    // Sort out the corners to get the rectangle limits in each dimension
+    var min_lat = Math.min(start.lat(), end.lat());
+    var max_lat = Math.max(start.lat(), end.lat());
+    var min_lng = Math.min(start.lng(), end.lng());
+    var max_lng = Math.max(start.lng(), end.lng());
+    
+    // This holds an array of all signature names in our selection box.
+    var in_box = [];
+    
+    // Start it out with 0 for each signature. Otherwise we wil have missing 
+    // data for signatures not passing the filters.
+    for(var signature in polygons) {
+         // Get the path for its hex
+        var path = polygons[signature].getPath();
+        
+        // This holds if any points of the path are outside the selection
+        // box
+        var any_outside = false;
+        
+        path.forEach(function(point, index) {
+            // Check all the points. Runs synchronously.
+            
+            if(point.lat() < min_lat || point.lat() > max_lat || 
+                point.lng() < min_lng || point.lng() > max_lng) {
+                
+                // This point is outside the rectangle
+                any_outside = true;
+                
+            }
+        });
+        
+        // Select the hex if all its corners are inside the selection
+        // rectangle.
+        if(!any_outside) {
+            in_box.push(signature);
+        }
+    }
+    
+    // Now we have an array of the signatures that ought to be in the selection
+    // (if they pass filters). Hand it off to select_list.
+    
+	var select_function_type = "user selection";
+    select_list(in_box, select_function_type);
+    
+}
+
+function recalculate_statistics(passed_filters) {
+    // Interrogate the UI to determine signatures that are "in" and "out", and
+    // run an appropriate statisical test for each layer between the "in" and
+    // "out" signatures, and update all the "p_value" fields for all the layers
+    // with the p values. Takes in a list of signatures that passed the filters,
+    // and ignores any signatures not on that list.
+    
+    // Build an efficient index of passing signatures
+    var passed = {};
+    for(var i = 0; i < passed_filters.length; i++) {
+        passed[passed_filters[i]] = true;
+    }
+    
+    // Figure out what the in-list should be (statistics group A)
+    var layer_a_name = $(".statistics-a:checked").data("layer-name");
+    var layer_b_name = $(".statistics-b:checked").data("layer-name");
+    
+    print("Running statistics between " + layer_a_name + " and " + 
+        layer_b_name);
+    
+    if(!layer_a_name) {
+        complain("Can't run statistics without an \"A\" group.");
+        
+        // Get rid of the throbber
+        // TODO: Move this UI code out of the backend code.
+        $(".recalculate-throbber").hide();
+        $("#recalculate-statistics").show();
+        
+        return;
+    }
+    
+    // We know the layers have data since they're selections, so we can just go
+    // look at them.
+    
+    // This holds the "in" list: hexes from the "A" group.
+    var in_list = [];
+    
+    for(var signature in layers[layer_a_name].data) {
+        if(passed[signature] && layers[layer_a_name].data[signature]) {
+            // Add all the signatures in the "A" layer to the in list.
+            in_list.push(signature);
+        }
+    }
+    
+    if(in_list.length == 0) {
+        complain("Can't run statistics with an empty \"A\" group.");
+        
+        // Get rid of the throbber
+        // TODO: Move this UI code out of the backend code.
+        $(".recalculate-throbber").hide();
+        $("#recalculate-statistics").show();
+        
+        return;
+    }
+    
+    // This holds the "out" list: hexes in the "B" group, or, if that's not
+    // defined, all hexes. It's a little odd to run A vs. a set that includes
+    // some members of A, but Prof. Stuart wants that and it's not too insane
+    // for a Binomial test (which is the only currently implemented test
+    // anyway).
+    var out_list = [];
+    
+    if(layer_b_name) {
+        // We have a layer B, so take everything that's on in it.
+        for(var signature in layers[layer_b_name].data) {
+            if(passed[signature] && layers[layer_b_name].data[signature]) {
+                // Add all the signatures in the "B" layer to the out list.
+                out_list.push(signature);
+            }
+        }
+    } else {
+        // The out list is all hexes
+        for(var signature in polygons) {
+            if(passed[signature]) {
+                // Put it on the out list.
+                out_list.push(signature);
+            }
+        }
+    }
+    
+    // So now we have our in_list and our out_list
+    
+    for(var layer_name in layers) {
+        // Do the stats on each layer between those lists. This only processes
+        // layers that don't have URLs. Layers with URLs are assumed to be part
+        // of the available matrices.
+        recalculate_statistics_for_layer(layer_name, in_list, out_list,
+            passed_filters);
+    }
+    
+    // Now do all the layers with URLs. They are in the available score
+    // matrices.
+    for(var i = 0; i < available_matrices.length; i++) {
+        recalculate_statistics_for_matrix(available_matrices[i], in_list, 
+            out_list, passed_filters);
+    }
+    
+    print("Statistics jobs launched.");
+    
+}
+
+function recalculate_statistics_for_layer(layer_name, in_list, out_list, all) {
+    // Re-calculate the stats for the layer with the given name, between the
+    // given in and out arrays of signatures. Store the re-calculated statistics
+    // in the layer. all is a list of "all" signatures, from which we can
+    // calculate pseudocounts.
+    
+    // All we do is send the layer data or URL (whichever is more convenient) to
+    // the workers. They independently identify the data type and run the
+    // appropriate test, returning a p value or NaN by callback.
+    
+    // This holds a callback for setting the layer's p_value to the result of
+    // the statistics.
+    var callback = function(results) {
+        
+        // The statistics code really sends back a dict of updated metadata for
+        // each layer. Copy it over.
+        for(var metadata in results) {
+            layers[layer_name][metadata] = results[metadata];
+        }
+        
+        if(jobs_running == 0) {
+            // All statistics are done!
+            // TODO: Unify this code with similar callback below.
+            // Re-sort everything and draw all the new p values.
+            update_browse_ui();
+            update_shortlist_ui();
+            
+            // Get rid of the throbber
+            $(".recalculate-throbber").hide();
+            $("#recalculate-statistics").show();
+        }
+    };
+    
+    if(layers[layer_name].data != undefined) {
+        // Already have this downloaded. A local copy to the web worker is
+        // simplest, and a URL may not exist anyway.
+        
+        rpc_call("statistics_for_layer", [layers[layer_name].data, in_list, 
+            out_list, all], callback);
+    } else if(layers[layer_name].url != undefined) {
+        // We have a URL, so the layer must be in a matrix, too.
+        // Skip it here.
+    } else {
+        // Layer has no data and no way to get data. Should never happen.
+        complain("Layer " + layer_name + " has no data and no url.");
+    }
+}
+
+function recalculate_statistics_for_matrix(matrix_url, in_list, out_list, all) {
+    // Given the URL of one of the visualizer generator's input score matrices,
+    // download the matrix, calculate statistics for each layer in the matrix
+    // between the given in and out lists, and update the layer p values. all is
+    // a list of "all" signatures, from which we can calculate pseudocounts.
+
+    rpc_call("statistics_for_matrix", [matrix_url, in_list, out_list, all], 
+        function(result) {
+        
+        // The return value is p values by layer name
+        for(var layer_name in result) {
+            // The statistics code really sends back a dict of updated metadata
+            // for each layer. Copy it over.
+            for(var metadata in result[layer_name]) {
+                layers[layer_name][metadata] = result[layer_name][metadata];
+            }
+        }
+        
+        if(jobs_running == 0) {
+            // All statistics are done!
+            // TODO: Unify this code with similar callback above.
+            // Re-sort everything and draw all the new p values.
+            update_browse_ui();
+            update_shortlist_ui();
+            
+            // Get rid of the throbber
+            $(".recalculate-throbber").hide();
+            $("#recalculate-statistics").show();
+        }
+    });    
+    
+}
+
+function rpc_initialize() {
+    // Set up the RPC system. Must be called before rpc_call is used.
+    
+    for(var i = 0; i < NUM_RPC_WORKERS; i++) {
+        // Start the statistics RPC (remote procedure call) Web Worker
+        var worker = new Worker("statistics.js");
+        
+        // Send all its messages to our reply processor
+        worker.onmessage = rpc_reply;
+        
+        // Send its error events to our error processor
+        worker.onerror = rpc_error;
+        
+        // Add it to the list of workers
+        rpc_workers.push(worker);
+    }
+}
+
+function rpc_call(function_name, function_args, callback) {
+    // Given a function name and an array of arguments, send a message to a Web 
+    // Worker thread to ask it to run the given job. When it responds with the 
+    // return value, pass it to the given callback.
+    
+    // Allocate a new call id
+    var call_id = rpc_next_id;
+    rpc_next_id++;
+    
+    // Store the callback
+    rpc_callbacks[call_id] = callback;
+    
+    // Launch the call. Pass the function name, function args, and id to send 
+    // back with the return value.
+    rpc_workers[next_free_worker].postMessage({
+        name: function_name,
+        args: function_args,
+        id: call_id
+    });
+    
+    // Next time, use the next worker on the list, wrapping if we run out.
+    // This ensures no one worker gets all the work.
+    next_free_worker = (next_free_worker + 1) % rpc_workers.length;
+    
+    // Update the UI with the number of jobs in flight. Decrement jobs_running
+    // so the callback knows if everything is done or not.
+    jobs_running++;
+    $("#jobs-running").text(jobs_running);
+    
+    // And the number of jobs total
+    $("#jobs-ever").text(rpc_next_id);
+}
+
+function rpc_reply(message) {
+    // Handle a Web Worker message, which may be an RPC response or a log entry.
+    
+    if(message.data.log != undefined) {
+        // This is really a log entry
+        print(message.data.log);
+        return;
+    }
+    
+    // This is really a job completion message (success or error).
+    
+    // Update the UI with the number of jobs in flight.
+    jobs_running--;
+    $("#jobs-running").text(jobs_running);
+    
+    if(message.data.error) {
+        // The RPC call generated an error.
+        // Inform the page.
+        print("RPC error: " + message.data.error);
+        
+        // Get rid of the callback
+        delete rpc_callbacks[message.data.id];
+        
+        return;
+    }
+    
+    // Pass the return value to the registered callback.
+    rpc_callbacks[message.data.id](message.data.return_value);
+    
+    // Get rid of the callback
+    delete rpc_callbacks[message.data.id];
+}
+
+function rpc_error(error) {
+    // Handle an error event from a web worker
+    // See http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.h
+    // tml#errorevent
+    
+    complain("Web Worker error: " + error.message);
+    print(error.message + "\n at" + error.filename + " line " + error.lineno + 
+        " column " + error.column);
+}
+
+function initialize_view(initial_zoom) {
+    // Initialize the global Google Map.
+    
+    // Configure a Google map
+    var mapOptions = {
+        // Look at the center of the map
+        center: get_LatLng(128, 128),
+        // Zoom all the way out
+        zoom: initial_zoom,
+        mapTypeId: "blank",
+        // Don't show a map type picker.
+        mapTypeControlOptions: {
+              mapTypeIds: []
+        },
+        // Or a street view man that lets you walk around various Earth places.
+        streetViewControl: false
+    };
+    
+    // Create the actual map
+    googlemap = new google.maps.Map(document.getElementById("visualization"),
+        mapOptions);
+        
+    // Attach the blank map type to the map
+    googlemap.mapTypes.set("blank", new BlankMapType());
+    
+    // Make the global info window
+    info_window = new google.maps.InfoWindow({
+        content: "No Signature Selected",
+        position: get_LatLng(0, 0)
+    });
+    
+    // Add an event to close the info window when the user clicks outside of any
+    // hexagon
+    google.maps.event.addListener(googlemap, "click", function(event) {
+        info_window.close();
+        
+        // Also make sure that the selected signature is no longer selected,
+        // so we don't pop the info_window up again.
+        selected_signature = undefined;
+        
+        // Also un-focus the search box
+        $("#search").blur();
+    });
+    
+    
+    // And an event to clear the selected hex when the info_window closes.
+    google.maps.event.addListener(info_window, "closeclick", function(event) {
+        selected_signature = undefined;
+    });
+    
+    // We also have an event listener that checks when the zoom level changes,
+    // and turns off hex borders if we zoom out far enough, and turns them on
+    // again if we come back.
+    google.maps.event.addListener(googlemap, "zoom_changed", function(event) {
+        // Get the current zoom level (low is out)
+        var zoom = googlemap.getZoom();
+        
+        // API docs say: pixelCoordinate = worldCoordinate * 2 ^ zoomLevel
+        // So this holds the number of pixels that the global length hex_size 
+        // corresponds to at this zoom level.
+        var hex_size_pixels = hex_size * Math.pow(2, zoom);
+        
+        if(hex_size_pixels < MIN_BORDER_SIZE) {
+            // We're too small for borders
+            for(var signature in polygons) {
+                set_hexagon_stroke_weight(polygons[signature], 0);
+            }
+        } else {
+            // We can fit borders on the hexes
+            for(var signature in polygons) {
+                set_hexagon_stroke_weight(polygons[signature], 
+                    HEX_STROKE_WEIGHT);
+            }
+        }
+        
+    });
+    
+    // Subscribe all the tool listeners to the map
+    subscribe_tool_listeners(googlemap);
+    
+}
+
+function add_tool(tool_name, tool_menu_option, callback) {
+    // Given a programmatic unique name for a tool, some text for the tool's
+    // button, and a callback for when the user clicks that button, add a tool
+    // to the tool menu.
+    
+    // This hodls a button to activate the tool.
+    var tool_button = $("<a/>").attr("href", "#").addClass("stacker");
+    tool_button.text(tool_menu_option);
+    tool_button.click(function() {
+        // New tool. Remove all current tool listeners
+        clear_tool_listeners();
+        
+        // Say that the select tool is selected
+        selected_tool = tool_name;
+        callback();
+        
+        // End of tool workflow must set current_tool to undefined.
+    });
+    
+    $("#toolbar").append(tool_button);
+}
+
+function add_tool_listener(name, handler, cleanup) {
+    // Add a global event listener over the Google map and everything on it. 
+    // name specifies the event to listen to, and handler is the function to be
+    // set up as an event handler. It should take a single argument: the Google 
+    // Maps event. A handle is returned that can be used to remove the event 
+    // listen with remove_tool_listener.
+    // Only events in the TOOL_EVENTS array are allowed to be passed for name.
+    // TODO: Bundle this event thing into its own object.
+    // If "cleanup" is specified, it must be a 0-argument function to call when
+    // this listener is removed.
+    
+    // Get a handle
+    var handle = tool_listener_next_id;
+    tool_listener_next_id++;
+    
+    // Add the listener for the given event under that handle.
+    // TODO: do we also need to index this for O(1) event handling?
+    tool_listeners[handle] = {
+        handler: handler,
+        event: name,
+        cleanup: cleanup
+    };
+    return handle;  
+}
+
+function remove_tool_listener(handle) {
+    // Given a handle returned by add_tool_listener, remove the listener so it
+    // will no longer fire on its event. May be called only once on a given 
+    // handle. Runs any cleanup code associated with the handle being removed.
+    
+    if(tool_listeners[handle].cleanup) {
+        // Run cleanup code if applicable
+        tool_listeners[handle].cleanup();
+    }
+    
+    // Remove the property from the object
+    delete tool_listeners[handle];
+}
+
+function clear_tool_listeners() {
+    // We're starting to use another tool. Remove all current tool listeners. 
+    // Run any associated cleanup code for each listener.
+    
+    for(var handle in tool_listeners) {
+        remove_tool_listener(handle);
+    }
+}
+
+function subscribe_tool_listeners(maps_object) {
+    // Put the given Google Maps object into the tool events system, so that 
+    // events on it will fire global tool events. This can happen before or 
+    // after the tool events themselves are enabled.
+    
+    for(var i = 0; i < TOOL_EVENTS.length; i++) {
+        // For each event name we care about,
+        // use an inline function to generate an event name specific handler,
+        // and attach that to the Maps object.
+        google.maps.event.addListener(maps_object, TOOL_EVENTS[i], 
+            function(event_name) {
+                return function(event) {
+                    // We are handling an event_name event
+                    
+                    for(var handle in tool_listeners) {
+                        if(tool_listeners[handle].event == event_name) {
+                            // The handler wants this event
+                            // Fire it with the Google Maps event args
+                            tool_listeners[handle].handler(event);
+                        }
+                    }
+                };
+        }(TOOL_EVENTS[i]));
+    }
+    
+}
+
+function have_colormap(colormap_name) {
+    // Returns true if the given string is the name of a colormap, or false if 
+    // it is only a layer.
+    
+    return !(colormaps[colormap_name] == undefined);
+}
+
+function get_range_position(score, low, high) {
+    // Given a score float, and the lower and upper bounds of an interval (which
+    // may be equal, but not backwards), return a number in the range -1 to 1
+    // that expresses the position of the score in the [low, high] interval.
+    // Positions out of bounds are clamped to -1 or 1 as appropriate.
+    
+    // This holds the length of the input interval
+    var interval_length = high - low;
+    
+    if(interval_length > 0) {
+        // First rescale 0 to 1
+        score = (score - low) / interval_length
+        
+        // Clamp
+        score = Math.min(Math.max(score, 0), 1);
+            
+        // Now re-scale to -1 to 1
+        score = 2 * score - 1;
+    } else {
+        // The interval is just a point
+        // Just use 1 if we're above the point, and 0 if below.
+        score = (score > low)? 1 : -1
+    }
+    
+    return score;
+}
+
+function refresh() {
+    // Schedule the view to be redrawn after the current event finishes.
+    
+    // Get rid of the previous redraw request, if there was one. We only want 
+    // one.
+    window.clearTimeout(redraw_handle);
+    
+    // Make a new one to happen as soon as this event finishes
+    redraw_handle = window.setTimeout(redraw_view, 0);
+}
+
+function redraw_view() {
+    // Make the view display the correct hexagons in the colors of the current 
+    // layer(s), as read from the values of the layer pickers in the global
+    // layer pickers array.
+    // All pickers must have selected layers that are in the object of 
+    // layers.
+    // Instead of calling this, you probably want to call refresh().
+    
+    // This holds a list of the string names of the currently selected layers,
+    // in order.
+    var current_layers = get_current_layers();
+    
+    // This holds arrays of the lower and upper limit we want to use for 
+    // each layer, by layer number. The lower limit corresponds to u or 
+    // v = -1, and the upper to u or v = 1. The entries we make for 
+    // colormaps are ignored.
+    // Don't do this inside the callback since the UI may have changed by then.
+    var layer_limits = []
+    for(var i = 0; i < current_layers.length; i++) {
+        layer_limits.push(get_slider_range(current_layers[i]));
+    }
+    
+    // This holds all the current filters
+    var filters = get_current_filters();
+    
+    // Obtain the layer objects (mapping from signatures/hex labels to colors)
+    with_layers(current_layers, function(retrieved_layers) {  
+        print("Redrawing view with " + retrieved_layers.length + " layers.");
+        
+        // Turn all the hexes the filtered-out color, pre-emptively
+        for(var signature in polygons) {
+            set_hexagon_color(polygons[signature], "black");
+        }
+        
+        // Go get the list of filter-passing hexes.
+        with_filtered_signatures(filters, function(signatures) {
+            for(var i = 0; i < signatures.length; i++) {
+                // For each hex passign the filter
+                // This hodls its signature label
+                var label = signatures[i];
+                
+                // This holds the color we are calculating for this hexagon.
+                // Start with the missing data color.
+                var computed_color = "grey";
+                
+                if(retrieved_layers.length >= 1) {
+                    // Two layers. We find a point in u, v cartesian space, map
+                    // it to polar, and use that to compute an HSV color.
+                    // However, we map value to the radius instead of
+                    // saturation.
+
+                    // Get the heat along u and v axes. This puts us in a square
+                    // of side length 2. Fun fact: undefined / number = NaN, but
+                    // !(NaN == NaN)
+                    var u = retrieved_layers[0].data[label];
+                    
+                    if(!have_colormap(current_layers[0])) {
+                        // Take into account the slider values and re-scale the 
+                        // layer value to express its position between them.
+                        u = get_range_position(u, layer_limits[0][0], 
+                            layer_limits[0][1]);
+                    }
+                    
+                    if(retrieved_layers.length >= 2) {
+                        // There's a second layer, so use the v axis.
+                        var v = retrieved_layers[1].data[label];
+                        
+                        if(!have_colormap(current_layers[1])) {
+                            // Take into account the slider values and re-scale
+                            // the layer value to express its position between
+                            // them.
+                            v = get_range_position(v, layer_limits[1][0], 
+                                layer_limits[1][1]);
+                        }
+                        
+                    } else {
+                        // No second layer, so v axis is unused. Don't make it 
+                        // undefined (it's not missing data), but set it to 0.
+                        var v = 0;
+                    }
+                    
+                    // Either of u or v may be undefined (or both) if the layer
+                    // did not contain an entry for this signature. But that's
+                    // OK. Compute the color that we should use to express this
+                    // combination of layer values. It's OK to pass undefined
+                    // names here for layers.
+                    computed_color = get_color(current_layers[0], u, 
+                        current_layers[1], v);
+                }
+                
+                // Set the color by the composed layers.
+                set_hexagon_color(polygons[label], computed_color);
+            }
+        });
+        
+        // Draw the color key.
+        if(retrieved_layers.length == 0) {
+            // No color key to draw
+            $(".key").hide();
+        } else {
+            // We do actually want the color key
+            $(".key").show();
+        
+            // This holds the canvas that the key gets drawn in
+            var canvas = $("#color-key")[0];
+            
+            // This holds the 2d rendering context
+            var context = canvas.getContext("2d");
+            
+            for(var i = 0; i < KEY_SIZE; i++) {
+                // We'll use i for the v coordinate (-1 to 1) (left to right)
+                var v = 0;
+                if(retrieved_layers.length >= 2) {
+                    v = i / (KEY_SIZE / 2) - 1;
+                    
+                    if(have_colormap(current_layers[1])) {
+                        // This is a color map, so do bands instead.
+                        v = Math.floor(i / KEY_SIZE * 
+                            (retrieved_layers[1].magnitude + 1));
+                    }
+                    
+                }
+                
+                for(var j = 0; j < KEY_SIZE; j++) {
+                    // And j spacifies the u coordinate (bottom to top)
+                    var u = 0;
+                    if(retrieved_layers.length >= 1) {
+                        u = 1 - j / (KEY_SIZE / 2);
+
+                        if(have_colormap(current_layers[0])) {
+                            // This is a color map, so do bands instead.
+                            // Make sure to flip sign, and have a -1 for the 
+                            // 0-based indexing.
+                            u = Math.floor((KEY_SIZE - j - 1) / KEY_SIZE * 
+                                (retrieved_layers[0].magnitude + 1));
+                        }
+                    }
+                    
+                    // Set the pixel color to the right thing for this u, v
+                    // It's OK to pass undefined names here for layers.
+                    context.fillStyle = get_color(current_layers[0], u, 
+                        current_layers[1], v);
+                    
+                    // Fill the pixel
+                    context.fillRect(i, j, 1, 1);
+                }
+            }
+        
+        }
+        
+        if(have_colormap(current_layers[0])) {
+            // We have a layer with horizontal bands
+            // Add labels to the key if we have names to use.
+            // TODO: Vertical text for vertical bands?
+        
+            // Get the colormap
+            var colormap = colormaps[current_layers[0]]
+            
+            if(colormap.length > 0) {
+                // Actually have any categories (not auto-generated)
+                print("Drawing key text for " + colormap.length + 
+                    " categories.");
+                
+                // How many pixels do we get per label, vertically
+                var pixels_per_label = KEY_SIZE / colormap.length;
+                
+                // Configure for text drawing
+                context.font = pixels_per_label + "px Arial";
+                context.textBaseline = "top";
+                
+                for(var i = 0; i < colormap.length; i++) {
+                    
+                    // This holds the pixel position where our text goes
+                    var y_position = KEY_SIZE - (i + 1) * pixels_per_label;
+                    
+                    // Get the background color here as a 1x1 ImageData
+                    var image = context.getImageData(0, y_position, 1, 1);
+                    
+                    // Get the components r, g, b, a in an array
+                    var components = image.data;
+                    
+                    // Make a Color so we can operate on it
+                    var background_color = Color({
+                        r: components[0],
+                        g: components[1],
+                        b: components[2]
+                    });
+                    
+                    if(background_color.light()) {
+                        // This color is light, so write in black.
+                        context.fillStyle = "black";
+                    } else {
+                        // It must be dark, so write in white.
+                        context.fillStyle = "white";
+                    }
+                
+                    // Draw the name on the canvas
+                    context.fillText(colormap[i].name, 0, y_position);
+                }
+            }
+        }
+        
+        // We should also set up axis labels on the color key.
+        // We need to know about colormaps to do this
+        
+        // Hide all the labels
+        $(".label").hide();
+        
+        if(current_layers.length > 0) {
+            // Show the y axis label
+            $("#y-axis").text(current_layers[0]).show();
+            
+            if(!have_colormap(current_layers[0])) {
+                // Show the low to high markers for continuous values
+                $("#low-both").show();
+                $("#high-y").show();
+            }
+        }
+        
+        if(current_layers.length > 1) {
+            // Show the x axis label
+            $("#x-axis").text(current_layers[1]).show();
+            
+            if(!have_colormap(current_layers[1])) {
+                // Show the low to high markers for continuous values
+                $("#low-both").show();
+                $("#high-x").show();
+            }
+        }
+        
+        
+    });
+    
+    // Make sure to also redraw the info window, which may be open.
+    redraw_info_window();
+}
+
+function get_color(u_name, u, v_name, v) {
+    // Given u and v, which represent the heat in each of the two currently 
+    // displayed layers, as well as u_name and v_name, which are the 
+    // corresponding layer names, return the computed CSS color.
+    // Either u or v may be undefined (or both), in which case the no-data color
+    // is returned. If a layer name is undefined, that layer dimension is 
+    // ignored.
+    
+    if(have_colormap(v_name) && !have_colormap(u_name)) {
+        // We have a colormap as our second layer, and a layer as our first.
+        // Swap everything around so colormap is our first layer instead.
+        // Now we don't need to think about drawing a layer first with a 
+        // colormap second.
+        // This is a temporary swapping variable.
+        var temp = v_name;
+        v_name = u_name;
+        u_name = temp;
+        
+        temp = v;
+        v = u;
+        u = temp;
+    }
+    
+    if(isNaN(u) || isNaN(v) || u == undefined || v == undefined) {
+        // At least one of our layers has no data for this hex.
+        return "grey";
+    }
+    
+    if(have_colormap(u_name) && have_colormap(v_name) && 
+        !colormaps[u_name].hasOwnProperty(u) && 
+        !colormaps[v_name].hasOwnProperty(v) &&
+        layers[u_name].magnitude <= 1 && layers[v_name].magnitude <= 1) {
+        
+        // Special case: two binary or unary auto-generated colormaps.
+        // Use dark grey/red/blue/purple color scheme
+    
+        if(u == 1) {
+            if(v == 1) {    
+                // Both are on
+                return "#FF00FF";
+            } else {
+                // Only the first is on
+                return "#FF0000";
+            }
+        } else {
+            if(v == 1) {
+                // Only the second is on
+                return "#0000FF";
+            } else {
+                // Neither is on
+                return "#545454";
+            }
+        }    
+        
+    }
+    
+    if(have_colormap(u_name) && !colormaps[u_name].hasOwnProperty(u) && 
+        layers[u_name].magnitude <= 1 && v_name == undefined) {
+        
+        // Special case: a single binary or unary auto-generated colormap.
+        // Use dark grey/red to make 1s stand out.
+        
+        if(u == 1) {
+            // Red for on
+            return "#FF0000";
+        } else {
+            // Dark grey for off
+            return "#545454";
+        }        
+    }
+   
+    
+    if(have_colormap(u_name)) {
+        // u is a colormap
+        if(colormaps[u_name].hasOwnProperty(u)) {
+            // And the colormap has an entry here. Use it as the base color.
+            var to_clone = colormaps[u_name][u].color;
+            
+            var base_color = Color({
+                hue: to_clone.hue(),
+                saturation: to_clone.saturationv(),
+                value: to_clone.value()
+            });
+        } else {
+            // The colormap has no entry. Assume we're calculating all the 
+            // entries. We do this by splitting the color circle evenly.
+            
+            // This holds the number of colors, which is 1 more than the largest
+            // value used (since we start at color 0), which is the magnitude.
+            // It's OK to go ask for the magnitude of this layer since it must 
+            // have already been downloaded.
+            var num_colors = layers[u_name].magnitude + 1;
+            
+            // Calculate the hue for this number.
+            var hsv_hue = u / (num_colors + 1) * 360;
+    
+            // The base color is a color at that hue, with max saturation and 
+            // value
+            var base_color = Color({
+                hue: hsv_hue, 
+                saturation: 100,
+                value: 100
+            })
+        }
+        
+        // Now that the base color is set, consult v to see what shade to use.
+        if(v_name == undefined) {
+            // No v layer is actually in use. Use whatever is in the base 
+            // color
+            // TODO: This code path is silly, clean it up.
+            var hsv_value = base_color.value();
+        } else if(have_colormap(v_name)) {
+            // Do discrete shades in v
+            // This holds the number of shades we need.
+            // It's OK to go ask for the magnitude of this layer since it must 
+            // have already been downloaded.
+            var num_shades = layers[v_name].magnitude + 1;
+            
+            // Calculate what shade we need from the nonnegative integer v
+            // We want 100 to be included (since that's full brightness), but we
+            // want to skip 0 (since no color can be seen at 0), so we add 1 to 
+            // v.
+            var hsv_value = (v + 1) / num_shades * 100;
+        } else {
+            // Calculate what shade we need from v on -1 to 1
+            var hsv_value = 50 + v * 50;
+        }
+        
+        // Set the color's value component.
+        base_color.value(hsv_value);
+        
+        // Return the shaded color
+        return base_color.hexString();
+    }
+    
+    
+    // If we get here, we only have non-colormap layers.
+    
+    // This is the polar angle (hue) in degrees, forced to be 
+    // positive.
+    var hsv_hue = Math.atan2(v, u) * 180 / Math.PI;
+    if(hsv_hue < 0) {
+        hsv_hue += 360;
+    }
+    
+    // Rotate it by 60 degrees, so that the first layer is 
+    // yellow/blue
+    hsv_hue += 60;
+    if(hsv_hue > 360) {
+        hsv_hue -= 360;
+    }
+    
+    // This is the polar radius (value). We inscribe our square
+    // of side length 2 in a circle of radius 1 by dividing by
+    // sqrt(2). So we get a value from 0 to 1
+    var hsv_value = (Math.sqrt(Math.pow(u, 2) + 
+        Math.pow(v, 2)) / Math.sqrt(2));
+        
+    // This is the HSV saturation component of the color on 0 to 1.
+    // Just fix to 1.
+    var hsv_saturation = 1.0;
+    
+    // Now scale saturation and value to percent
+    hsv_saturation *= 100;
+    hsv_value *= 100;
+    
+    // Now we have the color as HSV, but CSS doesn't support it.
+    
+    // Make a Color object and get the RGB string
+    try {
+        return Color({
+            hue: hsv_hue, 
+            saturation: hsv_saturation,
+            value: hsv_value,
+        }).hexString();
+    } catch(error) {
+        print("(" + u + "," + v + ") broke with color (" + hsv_hue +
+        "," + hsv_saturation + "," + hsv_value + ")");
+        
+        // We'll return an error color
+        return "white";
+    }
+}
+
+// Define a flat projection
+// See https://developers.google.com/maps/documentation/javascript/maptypes#Projections
+function FlatProjection() {
+}
+
+
+FlatProjection.prototype.fromLatLngToPoint = function(latLng) {
+    // Given a LatLng from -90 to 90 and -180 to 180, transform to an x, y Point 
+    // from 0 to 256 and 0 to 256   
+    var point = new google.maps.Point((latLng.lng() + 180) * 256 / 360, 
+        (latLng.lat() + 90) * 256 / 180);
+    
+    return point;
+
+}
+
+
+FlatProjection.prototype.fromPointToLatLng = function(point, noWrap) {
+    // Given a an x, y Point from 0 to 256 and 0 to 256, transform to a LatLng from
+    // -90 to 90 and -180 to 180
+    var latLng = new google.maps.LatLng(point.y * 180 / 256 - 90, 
+        point.x * 360 / 256 - 180, noWrap);
+    
+    return latLng;
+}
+
+// Define a Google Maps MapType that's all blank
+// See https://developers.google.com/maps/documentation/javascript/examples/maptype-base
+function BlankMapType() {
+}
+
+BlankMapType.prototype.tileSize = new google.maps.Size(256,256);
+BlankMapType.prototype.maxZoom = 19;
+
+BlankMapType.prototype.getTile = function(coord, zoom, ownerDocument) {
+    // This is the element representing this tile in the map
+    // It should be an empty div
+    var div = ownerDocument.createElement("div");
+    div.style.width = this.tileSize.width + "px";
+    div.style.height = this.tileSize.height + "px";
+    div.style.backgroundColor = "#000000";
+    
+    return div;
+}
+
+BlankMapType.prototype.name = "Blank";
+BlankMapType.prototype.alt = "Blank Map";
+
+BlankMapType.prototype.projection = new FlatProjection();
+
+
+
+function get_LatLng(x, y) {
+    // Given a point x, y in map space (0 to 256), get the corresponding LatLng
+    return FlatProjection.prototype.fromPointToLatLng(
+        new google.maps.Point(x, y));
+}
+
+function clearMap() {
+
+}
+
+function drl_values(layout_index) {
+
+	// Download the DrL position data, and make it into a layer
+    $.get("drl"+ layout_index +".tab", function(tsv_data) {
+        // This is an array of rows, which are arrays of values:
+        // id, x, y
+        // Only this time X and Y are Cartesian coordinates.
+        var parsed = $.tsv.parseRows(tsv_data);
+        
+        // Compute two layers: one for x position, and one for y position.
+        var layer_x = {};
+        var layer_y = {};
+        
+        for(var i = 0; i < parsed.length; i++) {
+            // Pull out the parts of the TSV entry
+            var label = parsed[i][0];
+            
+            if(label == "") {
+                // DrL ends its output with a blank line, which we skip 
+                // here.
+                continue;
+            }
+            
+            var x = parseFloat(parsed[i][1]);
+            // Invert the Y coordinate since we do that in the hex grid
+            var y = -parseFloat(parsed[i][2]);
+            
+            // Add x and y to the appropriate layers
+            layer_x[label] = x;
+            layer_y[label] = y;
+        }
+        
+        // Register the layers with no priorities. By default they are not 
+        // selections.
+        add_layer_data("DrL X Position", layer_x);
+        add_layer_data("DrL Y Position", layer_y);
+        
+        // Make sure the layer browser has the up-to-date layer list
+        update_browse_ui();
+        
+    }, "text");
+}
+
+function assignment_values (layout_index, spacing) {
+	// Download the signature assignments to hexagons and fill in the global 
+    // hexagon assignment grid.
+    $.get("assignments" + layout_index +".tab", function(tsv_data) {        
+        // This is an array of rows, which are arrays of values:
+        // id, x, y
+        var parsed = $.tsv.parseRows(tsv_data);
+
+        // This holds the maximum observed x
+        var max_x = 0;
+        // And y
+        var max_y = 0;
+        
+        // Fill in the global signature grid and ploygon grid arrays.
+        for(var i = 0; i < parsed.length; i++) {
+            // Get the label
+            var label = parsed[i][0];
+            
+            if(label == "") {
+                // Blank line
+                continue;
+            }
+            
+            // Get the x coord
+            var x = parseInt(parsed[i][1]);
+            // And the y coord
+            var y = parseInt(parsed[i][2]);
+
+			x = x * spacing;
+			y = y * spacing;			
+
+
+            // Update maxes
+            max_x = Math.max(x, max_x);
+            max_y = Math.max(y, max_y);
+           
+
+            // Make sure we have a row
+            if(signature_grid[y] == null) {
+                signature_grid[y] = [];
+                // Pre-emptively add a row to the polygon grid.
+                polygon_grid[y] = [];
+            }
+            
+            // Store the label in the global signature grid.
+            signature_grid[y][x] = label;
+        }
+        
+        // We need to fit this whole thing into a 256x256 grid.
+        // How big can we make each hexagon?
+        // TODO: Do the algrbra to make this exact. Right now we just make a 
+        // grid that we know to be small enough.
+        // Divide the space into one column per column, and calculate 
+        // side length from column width. Add an extra column for dangling
+        // corners.
+        var side_length_x = (256)/ (max_x + 2) * (2.0 / 3.0);
+        
+        print("Max hexagon side length horizontally is " + side_length_x);
+        
+        // Divide the space into rows and calculate the side length
+        // from hex height. Remember to add an extra row for wggle.
+        var side_length_y = ((256)/(max_y + 2)) / Math.sqrt(3);
+        
+        print("Max hexagon side length vertically is " + side_length_y);
+        
+        // How long is a hexagon side in world coords?
+        // Shrink it from the biggest we can have so that we don't wrap off the 
+        // edges of the map.
+        var hexagon_side_length = Math.min(side_length_x, side_length_y) / 2.0;
+
+        // Store this in the global hex_size, so we can later calculate the hex
+        // size in pixels and make borders go away if we are too zoomed out.
+        hex_size = hexagon_side_length;
+
+        // How far in should we move the whole grid from the top left corner of 
+        // the earth?
+        // Let's try leaving a 1/4 Earth gap at least, to stop wrapping in 
+        // longitude that we can't turn off.
+        // Since we already shrunk the map to half max size, this would put it 
+        // 1/4 of the 256 unit width and height away from the top left corner.
+        grid_offset = (256) / 4;
+        
+        // Loop through again and draw the polygons, now that we know how big 
+        // they have to be
+        for(var i = 0; i < parsed.length; i++) {
+            // TODO: don't re-parse this info
+            // Get the label
+            var label = parsed[i][0];
+            
+            if(label == "") {
+                // Blank line
+                continue;
+            }
+            
+            // Get the x coord
+            var x = parseInt(parsed[i][1]);
+            // And the y coord
+            var y = parseInt(parsed[i][2]);
+
+			x = x * spacing;
+			y = y * spacing;			
+
+            // Make a hexagon on the Google map and store that.
+            var hexagon = make_hexagon(y, x, hexagon_side_length, grid_offset);
+            // Store by x, y in grid
+            polygon_grid[y][x] = hexagon;
+            // Store by label
+            polygons[label] = hexagon;
+            
+            // Set the polygon's signature so we can look stuff up for it when 
+            // it's clicked.
+            set_hexagon_signature(hexagon, label);     
+            
+        }
+        
+        // Now that the ploygons exist, do the initial redraw to set all their 
+        // colors corectly. In case someone has messed with the controls.
+        // TODO: can someone yet have messed with the controlls?
+        refresh();
+        
+
+    }, "text");
+}
+
+// Function to create a new map based upon the the layout_name argument
+// Find the index of the layout_name and pass it as the index to the 
+// drl_values and assignment_values functions as these files are indexed
+// according to the appropriate layout
+function recreate_map(layout_name, spacing) {
+
+	var layout_index = layout_names.indexOf(layout_name);
+	drl_values(layout_index);
+	assignment_values(layout_index, spacing);
+
+}
+
+$(function() {
+
+    // Set up the RPC system for background statistics
+    rpc_initialize();
+
+    // Set up the Google Map
+    initialize_view(0);
+    
+    // Set up the layer search
+    $("#search").select2({
+        placeholder: "Add Attribute...",
+        query: function(query) {
+            // Given a select2 query object, call query.callback with an object
+            // with a "results" array.
+            
+            // This is the array of result objects we will be sending back.
+            var results = [];
+        
+            // Get where we should start in the layer list, from select2's
+            // infinite scrolling.
+            var start_position = 0;
+            if(query.context != undefined) {
+                start_position = query.context;
+            }
+        
+            for(var i = start_position; i < layer_names_sorted.length; i++) {
+                // For each possible result
+                if(layer_names_sorted[i].toLowerCase().indexOf(
+                    query.term.toLowerCase()) != -1) {
+                    
+                    // Query search term is in this layer's name. Add a select2
+                    // record to our results. Don't specify text: our custom
+                    // formatter looks up by ID and makes UI elements
+                    // dynamically.
+                    results.push({
+                        id: layer_names_sorted[i]
+                    });
+                    
+                    if(results.length >= SEARCH_PAGE_SIZE) {
+                        // Page is full. Send it on.
+                        break;
+                    }
+                    
+                }
+            }
+            
+            // Give the results back to select2 as the results parameter.
+            query.callback({
+                results: results,
+                // Say there's more if we broke out of the loop.
+                more: i < layer_names_sorted.length,
+                // If there are more results, start after where we left off.
+                context: i + 1
+            });
+        },
+        formatResult: function(result, container, query) {
+            // Given a select2 result record, the element that our results go
+            // in, and the query used to get the result, return a jQuery element
+            // that goes in the container to represent the result.
+            
+            // Get the layer name, and make the browse UI for it.
+            return make_browse_ui(result.id);
+        },
+        // We want our dropdown to be big enough to browse.
+        dropdownCssClass: "results-dropdown"
+    });
+
+    // Handle result selection
+    $("#search").on("select2-selecting", function(event) {
+        // The select2 id of the thing clicked (the layer's name) is event.val
+        var layer_name = event.val;
+        
+        // User chose this layer. Add it to the global shortlist.
+        
+        // Only add to the shortlist if it isn't already there
+        // Was it already there?
+        var found = false;
+        for(var j = 0; j < shortlist.length; j++) {
+            if(shortlist[j] == layer_name) {
+                found = true;
+                break;
+            }
+        }
+        
+        if(!found) {
+            // It's new. Add it to the shortlist
+            shortlist.push(layer_name);
+            
+            // Update the UI to reflect this. This may redraw the view.
+            update_shortlist_ui();
+            
+        }
+        
+        // Don't actually change the selection.
+        // This keeps the dropdown open when we click.
+        event.preventDefault();
+    });
+
+    $("#recalculate-statistics").button().click(function() {
+        // Re-calculate the statistics between the currently filtered hexes and
+        // everything else.
+        
+        // Put up the throbber instead of us.
+        $("#recalculate-statistics").hide();
+        $(".recalculate-throbber").show();
+        
+        // This holds the currently enabled filters.
+        var filters = get_current_filters();
+    
+        with_filtered_signatures(filters, function(signatures) {
+            // Find everything passing the filters and run the statistics.
+            recalculate_statistics(signatures);
+        });
+	});
+
+	// Temporary Inflate Button
+	$("#inflate").button().click(function() {
+		initialize_view (0);
+        recreate_map(current_layout_name, 2);
+		refresh ();
+	});
+
+	// Create Pop-Up UI for Set Operations
+	$("#set-operations").prepend(create_set_operation_ui ());
+
+	// Action handler for display of set operation pop-up
+	$("#set-operation").button().click(function() {
+		set_operation_clicks++;
+		if (set_operation_clicks % 2 != 0){
+			show_set_operation_drop_down ();
+			}
+		else {
+			hide_set_operation_drop_down ();
+			var drop_downs = document.getElementsByClassName("set-operation-value");
+				for (var i = 0; i < drop_downs.length; i++) {
+					drop_downs[i].style.visibility="hidden";
+				}
+		}		
+	
+	});
+	
+	// Coputation of Set Operations
+	var compute_button = document.getElementsByClassName ("compute-button");
+	compute_button[0].onclick = function () {
+		var layer_names = [];
+		var layer_values = [];
+		var layer_values_text = [];
+
+		var drop_down_layers = document.getElementsByClassName("set-operation-value");
+		var drop_down_data_values = document.getElementsByClassName("set-operation-layer-value");
+
+		var function_type = document.getElementById("set-operations-list");
+		var selected_function = function_type.selectedIndex;
+
+		var selected_index = drop_down_layers[0].selectedIndex;
+		layer_names.push(drop_down_layers[0].options[selected_index].text);	
+
+		var selected_index = drop_down_data_values[0].selectedIndex;
+		layer_values.push(drop_down_data_values[0].options[selected_index].value);	
+		layer_values_text.push(drop_down_data_values[0].options[selected_index].text);
+
+		if (selected_function != 5) {
+			var selected_index = drop_down_data_values[1].selectedIndex;
+			layer_values.push(drop_down_data_values[1].options[selected_index].value);	
+			layer_values_text.push(drop_down_data_values[1].options[selected_index].text);
+			var selected_index = drop_down_layers[1].selectedIndex;
+			layer_names.push(drop_down_layers[1].options[selected_index].text);
+		}
+		
+
+		switch (selected_function) {
+			case 1:
+				compute_intersection(layer_values, layer_names, layer_values_text);
+				break;
+			case 2:
+				compute_union(layer_values, layer_names, layer_values_text);
+				break;
+			case 3:
+				compute_set_difference(layer_values, layer_names, layer_values_text);
+				break;
+			case 4:
+				compute_symmetric_difference(layer_values, layer_names, layer_values_text);
+				break;
+			case 5:
+				compute_absolute_complement(layer_values, layer_names, layer_values_text);
+				break
+			default:
+				complain ("Set Theory Error");
+		}
+	};
+	   
+    // Download the layer index
+    $.get("layers.tab", function(tsv_data) {
+        // Layer index is <name>\t<filename>\t<clumpiness>
+        var parsed = $.tsv.parseRows(tsv_data);
+        
+        for(var i = 0; i < parsed.length; i++) {
+            // Pull out the parts of the TSV entry
+            // This is the name of the layer.
+            var layer_name = parsed[i][0];
+            
+            if(layer_name == "") {
+                // Skip any blank lines
+                continue;
+            }
+            
+            // This is the URL from which to download the TSV for the actual 
+            // layer.
+            var layer_url = parsed[i][1];
+            
+            // This is the layer's clumpiness score
+            var layer_clumpiness = parseFloat(parsed[i][2]);
+            
+            // This is the number of hexes that the layer has any values for.
+            // We need to get it from the server so we don't have to download 
+            // the layer to have it.
+            var layer_count = parseFloat(parsed[i][3]);
+            
+            // This is the number of 1s in a binary layer, or NaN in other
+            // layers
+            var layer_positives = parseFloat(parsed[i][4]);       
+                   
+            // Add this layer to our index of layers
+            add_layer_url(layer_name, layer_url, {
+                clumpiness: layer_clumpiness,
+                positives: layer_positives,
+                n: layer_count
+            });
+        }
+        
+        // Now we have added layer downloaders for all the layers in the 
+        // index. Update the UI
+        update_browse_ui();
+        
+         
+    }, "text");
+    
+    // Download full score matrix index, which we later use for statistics. Note
+    // that stats won't work unless this finishes first. TODO: enforce this.
+    $.get("matrices.tab", function(tsv_data) {
+        // Matrix index is just <filename>
+        var parsed = $.tsv.parseRows(tsv_data);
+        
+        for(var i = 0; i < parsed.length; i++) {
+            // Pull out the parts of the TSV entry
+            // This is the filename of the matrix.
+            var matrix_name = parsed[i][0];
+            
+            if(matrix_name == "") {
+                // Not a real matrix
+                continue;
+            }
+            
+            // Add it to the global list
+            available_matrices.push(matrix_name);
+        }
+    }, "text");
+    
+    // Download color map information
+    $.get("colormaps.tab", function(tsv_data) {
+        // Colormap data is <layer name>\t<value>\t<category name>\t<color>
+        // \t<value>\t<category name>\t<color>...
+        var parsed = $.tsv.parseRows(tsv_data);
+        
+        for(var i = 0; i < parsed.length; i++) {
+            // Get the name of the layer
+            var layer_name = parsed[i][0];
+            
+            // Skip blank lines
+            if(layer_name == "") {
+                continue;
+            }
+            
+            // This holds all the categories (name and color) by integer index
+            var colormap = [];
+            
+            print("Loading colormap for " + layer_name);
+            
+            for(j = 1; j < parsed[i].length; j += 3) {
+                // Store each color assignment.
+                // Doesn't run if there aren't any assignments, leaving an empty
+                // colormap object that just forces automatic color selection.
+                
+                // This holds the index of the category
+                var category_index = parseInt(parsed[i][j]);
+                
+                // The colormap gets an object with the name and color that the
+                // index number refers to. Color is stored as a color object.
+                colormap[category_index] = {
+                    name: parsed[i][j + 1],
+                    color: Color(parsed[i][j + 2])
+                };
+                
+                print( colormap[category_index].name + " -> " +  
+                    colormap[category_index].color.hexString());
+            }
+            
+            // Store the finished color map in the global object
+            colormaps[layer_name] = colormap;
+            
+            
+        }
+        
+        // We may need to redraw the view in response to having new color map 
+        // info, if it came particularly late.
+        refresh();
+            
+    }, "text");
+
+// Download the Matrix Names and pass it to the layout_names array
+	$.get("matrixnames.tab", function(tsv_data) {
+        // This is an array of rows, which are strings of matrix names
+        var parsed = $.tsv.parseRows(tsv_data);
+        
+        for(var i = 0; i < parsed.length; i++) {
+            // Pull out the parts of the TSV entry
+            var label = parsed[i][0];
+
+			if(label == "") {
+                // Skip any blank lines
+                continue;
+            }
+            // Add layout names to global array of names
+            layout_names.push(label);
+        }     
+    }, "text");
+
+	$("#layout-search").select2({
+        placeholder: "Select a Layout...",
+        query: function(query) {
+            // Given a select2 query object, call query.callback with an object
+            // with a "results" array.
+            
+            // This is the array of result objects we will be sending back.
+            var results = [];
+        
+            // Get where we should start in the layer list, from select2's
+            // infinite scrolling.
+            var start_position = 0;
+            if(query.context != undefined) {
+                start_position = query.context;
+            }
+        
+            for(var i = start_position; i < layout_names.length; i++) {
+                // For each possible result
+                if(layout_names[i].toLowerCase().indexOf(
+                    query.term.toLowerCase()) != -1) {
+                    
+                    // Query search term is in this layer's name. Add a select2
+                    // record to our results. Don't specify text: our custom
+                    // formatter looks up by ID and makes UI elements
+                    // dynamically.
+                    results.push({
+                        id: layout_names[i]
+                    });
+                    
+                    if(results.length >= SEARCH_PAGE_SIZE) {
+                        // Page is full. Send it on.
+                        break;
+                    }
+                    
+                }
+            }
+            
+            // Give the results back to select2 as the results parameter.
+            query.callback({
+                results: results,
+                // Say there's more if we broke out of the loop.
+                more: i < layout_names.length,
+                // If there are more results, start after where we left off.
+                context: i + 1
+            });
+        },
+        formatResult: function(result, container, query) {
+            // Given a select2 result record, the element that our results go
+            // in, and the query used to get the result, return a jQuery element
+            // that goes in the container to represent the result.
+            
+            // Get the layer name, and make the browse UI for it.
+            return make_toggle_layout_ui(result.id);
+        },
+        // We want our dropdown to be big enough to browse.
+        dropdownCssClass: "results-dropdown"
+    });
+
+	// Handle result selection
+    $("#layout-search").on("select2-selecting", function(event) {
+        // The select2 id of the thing clicked (the layout's name) is event.val
+        var layout_name = event.val;		
+
+		var current_layout = "Current Layout: " + layout_name;         
+	 
+		document.getElementById('current-layout').innerHTML=current_layout;
+		initialize_view (0);
+        recreate_map(layout_name, 1);
+		refresh ();
+        // Don't actually change the selection.
+        // This keeps the dropdown open when we click.
+        event.preventDefault();
+
+		current_layout_name = layout_name;
+    });
+
+	drl_values(layout_names[0]);
+	assignment_values (layout_names[0], 1);
+	current_layout_name = layout_names[0];
+
+});
+
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.py~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,1065 @@
+#!/usr/bin/env python2.7
+"""
+hexagram.py: Given a matrix of similarities, produce a hexagram visualization.
+
+This script takes in the filename of a tab-separated value file containing a
+sparse similarity matrix (with string labels) and several matrices of
+layer/score data. It produces an HTML file (and several support files) that
+provide an interactive visualization of the items clustered on a hexagonal grid.
+
+This script depends on the DrL graph alyout package, binaries for which must be
+present in your PATH.
+
+Re-uses sample code and documentation from 
+<http://users.soe.ucsc.edu/~karplus/bme205/f12/Scaffold.html>
+"""
+
+import argparse, sys, os, itertools, math, numpy, subprocess, shutil, tempfile
+import collections, scipy.stats, multiprocessing, traceback, numpy.ma
+import os.path
+import tsv
+
+def parse_args(args):
+    """
+    Takes in the command-line arguments list (args), and returns a nice argparse
+    result with fields for all the options.
+    Borrows heavily from the argparse documentation examples:
+    <http://docs.python.org/library/argparse.html>
+    """
+    
+    # The command line arguments start with the program name, which we don't
+    # want to treat as an argument for argparse. So we remove it.
+    args = args[1:]
+    
+    # Construct the parser (which is stored in parser)
+    # Module docstring lives in __doc__
+    # See http://python-forum.com/pythonforum/viewtopic.php?f=3&t=36847
+    # And a formatter class so our examples in the docstring look good. Isn't it
+    # convenient how we already wrapped it to 80 characters?
+    # See http://docs.python.org/library/argparse.html#formatter-class
+    parser = argparse.ArgumentParser(description=__doc__, 
+        formatter_class=argparse.RawDescriptionHelpFormatter)
+    
+    # Now add all the options to it
+    # Options match the ctdHeatmap tool options as much as possible.
+    parser.add_argument("similarities", type=argparse.FileType("r"), 
+        help="the TSV file with the similarities for signatures we're using")
+    parser.add_argument("--scores", type=str,
+        action="append", default=[],
+        help="a TSV to read scores for each signature from")
+    parser.add_argument("--colormaps", type=argparse.FileType("r"), 
+        default=None,
+        help="a TSV defining coloring and value names for discrete scores")
+    parser.add_argument("--html", "-H", type=str, 
+        default="index.html",
+        help="where to write HTML report")
+    parser.add_argument("--directory", "-d", type=str, default=".",
+        help="directory in which to create other output files")
+    parser.add_argument("--query", type=str, default=None,
+        help="Galaxy-escaped name of the query signature")
+    parser.add_argument("--window_size", type=int, default=20,
+        help="size of the window to use when looking for clusters")
+    parser.add_argument("--no-stats", dest="stats", action="store_false", 
+        default=True,
+        help="disable cluster-finding statistics")
+        
+    return parser.parse_args(args)
+
+def hexagon_center(x, y, scale=1.0):
+    """
+    Given a coordinate on a grid of hexagons (using wiggly rows in x), what is 
+    the 2d Euclidian coordinate of its center?
+    
+    x and y are integer column and row coordinates of the hexagon in the grid.
+    
+    scale is a float specifying hexagon side length.
+    
+    The origin in coordinate space is defined as the upper left corner of the 
+    bounding box of the hexagon wityh indices x=0 and y=0.
+    
+    Returns a tuple of floats.
+    """
+    # The grid looks like this:
+    #
+    #   /-\ /-\ /-\ /-\ 
+    # /-\-/-\-/-\-/-\-/-\
+    # \-/-\-/-\-/-\-/-\-/
+    # /-\-/-\-/-\-/-\-/-\
+    # \-/-\-/-\-/-\-/-\-/
+    # /-\-/-\-/-\-/-\-/-\
+    # \-/ \-/ \-/ \-/ \-/
+    #   
+    # Say a hexagon side has length 1
+    # It's 2 across corner to corner (x), and sqrt(3) across side to side (y)
+    # X coordinates are 1.5 per column
+    # Y coordinates (down from top) are sqrt(3) per row, -1/2 sqrt(3) if you're 
+    # in an odd column.
+    
+    center_y = math.sqrt(3) * y
+    if x % 2 == 1:
+        # Odd column: shift up
+        center_y -= 0.5 * math.sqrt(3)
+        
+    return (1.5 * x * scale + scale, center_y * scale + math.sqrt(3.0) / 2.0 * 
+        scale)
+
+def hexagon_pick(x, y, scale=1.0):
+    """
+    Given floats x and y specifying coordinates in the plane, determine which 
+    hexagon grid cell that point is in.
+    
+    scale is a float specifying hexagon side length.
+    
+    See http://blog.ruslans.com/2011/02/hexagonal-grid-math.html
+    But we flip the direction of the wiggle. Odd rows are up (-y)
+    """
+    
+    # How high is a hex?
+    hex_height = math.sqrt(3) * scale
+    
+    # First we pick a rectangular tile, from the point of one side-traingle to 
+    # the base of the other in width, and the whole hexagon height in height.
+    
+    # How wide are these tiles? Corner to line-between-far-corners distance
+    tile_width = (3.0 / 2.0 * scale)
+    
+    # Tile X index is floor(x / )
+    tile_x = int(math.floor(x / tile_width))
+    
+    # We need this intermediate value for the Y index and for tile-internal
+    # picking
+    corrected_y = y + (tile_x % 2) * hex_height / 2.0
+    
+    # Tile Y index is floor((y + (x index mod 2) * hex height/2) / hex height)
+    tile_y = int(math.floor(corrected_y / hex_height))
+    
+    # Find coordinates within the tile
+    internal_x = x - tile_x * tile_width
+    internal_y = corrected_y - tile_y * hex_height
+    
+    # Do tile-scale picking
+    # Are we in the one corner, the other corner, or the bulk of the tile?
+    if internal_x > scale * abs(0.5 - internal_y / hex_height):
+        # We're in the bulk of the tile
+        # This is the column (x) of the picked hexagon
+        hexagon_x = tile_x
+        
+        # This is the row (y) of the picked hexagon
+        hexagon_y = tile_y
+    else:
+        # We're in a corner.
+        # In an even column, the lower left is part of the next row, and the 
+        # upper left is part of the same row. In an odd column, the lower left 
+        # is part of the same row, and the upper left is part of the previous 
+        # row.
+        if internal_y > hex_height / 2.0:
+            # It's the lower left corner
+            # This is the offset in row (y) that being in this corner gives us
+            # The lower left corner is always 1 row below the upper left corner.
+            corner_y_offset = 1
+        else:
+            corner_y_offset = 0
+            
+        # TODO: verify this for correctness. It seems to be right, but I want a
+        # unit test to be sure.
+        # This is the row (y) of the picked hexagon
+        hexagon_y = tile_y - tile_x % 2 + corner_y_offset
+        
+        # This is the column (x) of the picked hexagon
+        hexagon_x = tile_x - 1
+    
+    # Now we've picked the hexagon
+    return (hexagon_x, hexagon_y)    
+
+def radial_search(center_x, center_y):
+    """
+    An iterator that yields coordinate tuples (x, y) in order of increasing 
+    hex-grid distance from the specified center position.
+    """
+    
+    # A hexagon has neighbors at the following relative coordinates:
+    # (-1, 0), (1, 0), (0, -1), (0, 1)
+    # and ((-1, 1) and (1, 1) if in an even column)
+    # or ((-1, -1) and (1, -1) if in an odd column)
+    
+    # We're going to go outwards using breadth-first search, so we need a queue 
+    # of hexes to visit and a set of already visited hexes.
+    
+    # This holds a queue (really a deque) of hexes waiting to be visited.
+    # A list has O(n) pop/insert at left.
+    queue = collections.deque()
+    # This holds a set of the (x, y) coordinate tuples of already-seen hexes,
+    # so we don't enqueue them again.
+    seen = set()
+    
+    # First place to visit is the center.
+    queue.append((center_x, center_y))
+    
+    while len(queue) > 0:
+        # We should in theory never run out of items in the queue.
+        # Get the current x and y to visit.
+        x, y = queue.popleft()
+        
+        # Yield the location we're visiting
+        yield (x, y)
+        
+        # This holds a list of all relative neighbor positions as (x, y) tuples.
+        neighbor_offsets = [(-1, 0), (1, 0), (0, -1), (0, 1)]
+        if y % 2 == 0:
+            # An even-column hex also has these neighbors
+            neighbor_offsets += [(-1, 1), (1, 1)]
+        else:
+            # An odd-column hex also has these neighbors
+            neighbor_offsets += [(-1, -1), (1, -1)]
+    
+        for x_offset, y_offset in neighbor_offsets:
+            # First calculate the absolute position of the neighbor in x
+            neighbor_x = x + x_offset
+            # And in y
+            neighbor_y = y + y_offset
+            
+            if (neighbor_x, neighbor_y) not in seen:
+                # This is a hex that has never been in the queue. Add it.
+                queue.append((neighbor_x, neighbor_y))
+                
+                # Record that it has ever been enqueued
+                seen.add((neighbor_x, neighbor_y))
+    
+    
+    
+
+def assign_hexagon(hexagons, node_x, node_y, node, scale=1.0):
+    """
+    This function assigns the given node to a hexagon in hexagons. hexagons is a
+    defaultdict from tuples of hexagon (x, y) integer indices to assigned nodes,
+    or None if a hexagon is free. node_x and node_y are the x and y coordinates 
+    of the node, adapted so that the seed node lands in the 0, 0 hexagon, and 
+    re-scaled to reduce hexagon conflicts. node is the node to be assigned. 
+    scale, if specified, is the hexagon side length in node space units.
+    
+    This function assigns nodes to their closest hexagon, reprobing outwards if 
+    already occupied.
+    
+    When the function completes, node is stored in hexagons under some (x, y) 
+    tuple.
+    
+    Returns the distance this hexagon is from its ideal location.
+    """
+    
+    # These hold the hexagon that the point falls in, which may be taken.
+    best_x, best_y = hexagon_pick(node_x, node_y, scale=scale)
+    
+    for x, y in radial_search(best_x, best_y):
+        # These hexes are enumerated in order of increasign distance from the 
+        # best one, starting with the best hex itself.
+        
+        if hexagons[(x, y)] is None:
+            # This is the closest free hex. Break out of the loop, leaving x and
+            # y pointing here.
+            break
+    
+    # Assign the node to the hexagon
+    hexagons[(x, y)] = node
+    
+    return math.sqrt((x - best_x) ** 2 + (y - best_y) ** 2)
+    
+    
+
+def assign_hexagon_local_radial(hexagons, node_x, node_y, node, scale=1.0):
+    """
+    This function assigns the given node to a hexagon in hexagons. hexagons is a
+    defaultdict from tuples of hexagon (x, y) integer indices to assigned nodes,
+    or None if a hexagon is free. node_x and node_y are the x and y coordinates 
+    of the node, adapted so that the seed node lands in the 0, 0 hexagon, and 
+    re-scaled to reduce hexagon conflicts. node is the node to be assigned. 
+    scale, if specified, is the hexagon side length in node space units.
+    
+    This function assigns nodes to their closest hexagon. If thast hexagon is 
+    full, it re-probes in the direction that the node is from the closest 
+    hexagon's center.
+    
+    When the function completes, node is stored in hexagons under some (x, y) 
+    tuple.
+    
+    Returns the distance this hexagon is from its ideal location.
+    """
+    
+    # These hold the hexagon that the point falls in, which may be taken.
+    best_x, best_y = hexagon_pick(node_x, node_y, scale=scale)
+    
+    # These hold the center of that hexagon in float space
+    center_x, center_y = hexagon_center(best_x, best_y, scale=scale)
+    
+    # This holds the distance from this point to the center of that hexagon
+    node_distance = math.sqrt((node_x - center_x) ** 2 + (node_y - center_y) **
+        2)
+    
+    # These hold the normalized direction of this point, relative to the center 
+    # of its best hexagon
+    direction_x = (node_x - center_x) / node_distance
+    direction_y = (node_y - center_y) / node_distance
+    
+    # Do a search in that direction, starting at the best hex.
+
+    # These are the hexagon indices we're considering
+    x, y = best_x, best_y
+    
+    # These are the Cartesian coordinates we're probing. Must be in the x, y hex
+    # as a loop invariant.
+    test_x, test_y = center_x, center_y
+    
+    while hexagons[(x, y)] is not None:
+        # Re-probe outwards from the best hex in scale/2-sized steps
+        # TODO: is that the right step size? Scale-sized steps seemed slightly 
+        # large.
+        test_x += direction_x * scale
+        test_y += direction_y * scale
+        
+        # Re-pick x and y for the hex containing our test point
+        x, y = hexagon_pick(test_x, test_y, scale=scale)
+        
+    # We've finally reached the edge of the cluster.
+    # Drop our hexagon
+    hexagons[(x, y)] = node
+    
+    return math.sqrt((x - best_x) ** 2 + (y - best_y) ** 2)
+
+def assign_hexagon_radial(hexagons, node_x, node_y, node, scale=1.0):
+    """
+    This function assigns the given node to a hexagon in hexagons. hexagons is a
+    defaultdict from tuples of hexagon (x, y) integer indices to assigned nodes,
+    or None if a hexagon is free. node_x and node_y are the x and y coordinates 
+    of the node, adapted so that the seed node lands in the 0, 0 hexagon, and 
+    re-scaled to reduce hexagon conflicts. node is the node to be assigned. 
+    scale, if specified, is the hexagon side length in node space units.
+    
+    This function assigns nodes to hexagons based on radial distance from 0, 0.
+    This makes hexagon assignment much more dense, but can lose spatial 
+    structure.
+    
+    When the function completes, node is stored in hexagons under some (x, y) 
+    tuple.
+    
+    Returns the distance this hexagon is from its ideal location. Unfortunately,
+    this doesn't really make sense for this assignment scheme, so it is always
+    0.
+    """
+    
+    # Compute node's distance from the origin
+    node_distance = math.sqrt(node_x ** 2 + node_y ** 2)
+    
+    # Compute normalized direction from the origin for this node
+    direction_x = node_x / node_distance
+    direction_y = node_y / node_distance
+    
+    # These are the coordinates we are testing
+    test_x = 0
+    test_y = 0
+    
+    # These are the hexagon indices that correspond to that point
+    x, y = hexagon_pick(test_x, test_y, scale=scale)
+    
+    while hexagons[(x, y)] is not None:
+        # Re-probe outwards from the origin in scale-sized steps
+        # TODO: is that the right step size?
+        test_x += direction_x * scale
+        test_y += direction_y * scale
+        
+        # Re-pick
+        x, y = hexagon_pick(test_x, test_y, scale=scale)
+        
+    # We've finally reached the edge of the cluster.
+    # Drop our hexagon
+    # TODO: this has to be N^2 if we line them all up in a line
+    hexagons[(x, y)] = node
+    
+    return 0
+
+def hexagons_in_window(hexagons, x, y, width, height):
+    """
+    Given a dict from (x, y) position to signature names, return the list of all
+    signatures in the window starting at hexagon x, y and extending width in the
+    x direction and height in the y direction on the hexagon grid.
+    """        
+    
+    # This holds the list of hexagons we've found
+    found = []
+    
+    for i in xrange(x, x + width):
+        for j in xrange(y, y + height):
+            if hexagons.has_key((i, j)):
+                # This position in the window has a hex.
+                found.append(hexagons[(i, j)])
+                
+    return found
+
+class ClusterFinder(object):
+    """
+    A class that can be invoked to find the p value of the best cluster in its 
+    layer. Instances are pickleable.
+    """
+    
+    def __init__(self, hexagons, layer, window_size=5):
+        """
+        Keep the given hexagons dict (from (x, y) to signature name) and the 
+        given layer (a dict from signature name to a value), and the given 
+        window size, in a ClusterFinder object.
+        """
+        
+        # TODO: This should probably all operate on numpy arrays that we can 
+        # slice efficiently.
+        
+        # Store the layer
+        self.hexagons = hexagons
+        # Store the hexagon assignments
+        self.layer = layer
+        
+        # Store the window size
+        self.window_size = window_size
+    
+    @staticmethod
+    def continuous_p(in_values, out_values):
+        """
+        Get the p value for in_values and out_values being distinct continuous 
+        distributions.
+        
+        in_values and out_values are both Numpy arrays. Returns the p value, or 
+        raises a ValueError if the statistical test cannot be run for some
+        reason.
+        
+        Uses the Mann-Whitney U test.
+        """
+    
+        # Do a Mann-Whitney U test to see how different the data  
+        # sets are.
+        u_statistic, p_value = scipy.stats.mannwhitneyu(in_values, 
+            out_values)
+            
+        return p_value
+    
+    @staticmethod    
+    def dichotomous_p(in_values, out_values):
+        """
+        Given two one-dimensional Numpy arrays of 0s and 1s, compute a p value 
+        for the in_values having a different probability of being 1 than the 
+        frequency of 1s in the out_values.
+        
+        This test uses the scipy.stats.binom_test function, which does not claim
+        to use the normal approximation. Therefore, this test should be valid
+        for arbitrarily small frequencies of either 0s or 1s in in_values.
+        
+        TODO: What if out_values is shorter than in_values?
+        """
+        
+        if len(out_values) == 0:
+            raise ValueError("Background group is empty!")
+        
+        # This holds the observed frequency of 1s in out_values
+        frequency = numpy.sum(out_values) / len(out_values)
+        
+        # This holds the number of 1s in in_values
+        successes = numpy.sum(in_values)
+        
+        # This holds the number of "trials" we got that many successes in
+        trials = len(in_values)
+        
+        # Return how significantly the frequency inside differs from that 
+        # outside.
+        return scipy.stats.binom_test(successes, trials, frequency)
+    
+    @staticmethod    
+    def categorical_p(in_values, out_values):
+        """
+        Given two one-dimensional Numpy arrays of integers (which may be stored
+        as floats), which represent items being assigned to different 
+        categories, return a p value for the distribution of categories observed
+        in in_values differing from that observed in out_values.
+        
+        The normal way to do this is with a chi-squared goodness of fit test. 
+        However, that test has invalid assumptions when there are fewer than 5 
+        expected and 5 observed observations in every category. 
+        See http://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.chis
+        quare.html
+        
+        However, we will use it anyway, because the tests that don't break down
+        are prohibitively slow.
+        """
+        
+        # Convert our inputs to integer arrays
+        in_values = in_values.astype(int)
+        out_values = out_values.astype(int)
+        
+        # How many categories are there (count 0 to the maximum value)
+        num_categories = max(numpy.max(in_values), numpy.max(out_values)) + 1
+        
+        # Count the number of in_values and out_values in each category
+        in_counts = numpy.array([len(in_values[in_values == i]) for i in 
+            xrange(num_categories)])
+        out_counts = numpy.array([len(out_values[out_values == i]) for i in 
+            xrange(num_categories)])
+        
+        # Get the p value for the window being from the estimated distribution
+        # None of the distribution parameters count as "estimated from data" 
+        # because they aren't estimated from the data under test.
+        _, p_value = scipy.stats.chisquare(in_counts, out_counts)
+        
+        return p_value
+        
+    def __call__(self):
+        """
+        Find the best p value for any window of size window_size. Return it.
+        """
+
+        # Calculate the bounding box where we want to look for windows.
+        # TODO: This would just be all of a numpy array
+        min_x = min(coords[0] for coords in self.hexagons.iterkeys())
+        min_y = min(coords[1] for coords in self.hexagons.iterkeys()) 
+        max_x = max(coords[0] for coords in self.hexagons.iterkeys())
+        max_y = max(coords[1] for coords in self.hexagons.iterkeys()) 
+        
+        # This holds a Numpy array of all the data by x, y
+        layer_data = numpy.empty((max_x - min_x + 1, max_y - min_y + 1))
+        
+        # Fill it with NaN so we can mask those out later
+        layer_data[:] = numpy.NAN
+        
+        for (hex_x, hex_y), name in self.hexagons.iteritems():
+            # Copy the layer values into the Numpy array
+            if self.layer.has_key(name):
+                layer_data[hex_x - min_x, hex_y - min_y] = self.layer[name]
+        
+        # This holds a masked version of the layer data
+        layer_data_masked = numpy.ma.masked_invalid(layer_data, copy=False) 
+        
+        # This holds the smallest p value we have found for this layer
+        best_p = float("+inf")
+        
+        # This holds the statistical test to use (a function from two Numpy 
+        # arrays to a p value)
+        # The most specific test is the dichotomous test (0 or 1)
+        statistical_test = self.dichotomous_p
+        
+        if numpy.sum(~layer_data_masked.mask) == 0: 
+            # There is actually no data in this layer at all.
+            # nditer complains if we try to iterate over an empty thing.
+            # So quit early and say we couldn't find anything.
+            return best_p
+ 
+        for value in numpy.nditer(layer_data_masked[~layer_data_masked.mask]):
+            # Check all the values in the layer.
+            # If this value is out of the domain of the current statistical 
+            # test, upgrade to a more general test.
+            
+            if statistical_test == self.dichotomous_p and (value > 1 or 
+                value < 0):
+                
+                # We can't use a dichotomous test on things outside 0 to 1
+                # But we haven't yet detected any non-integers
+                # Use categorical
+                statistical_test = self.categorical_p
+            
+            if value % 1 != 0:
+                # This is not an integer value
+                # So, we must use a continuous statistical test
+                statistical_test = self.continuous_p
+                
+                # This is the least specific test, so we can stop now
+                break
+        
+                
+        for i in xrange(min_x, max_x - self.window_size):
+            for j in xrange(min_y, max_y - self.window_size):
+
+                # Get the layer values for hexes in the window, as a Numpy
+                # masked array.
+                in_region = layer_data_masked[i:i + self.window_size, 
+                    j:j + self.window_size]
+                    
+                # And as a 1d Numpy array
+                in_values = numpy.reshape(in_region[~in_region.mask], -1).data
+                
+                # And out of the window (all the other hexes) as a masked array
+                out_region = numpy.ma.copy(layer_data_masked)
+                # We get this by masking out everything in the region
+                out_region.mask[i:i + self.window_size, 
+                    j:j + self.window_size] = True
+                
+                # And as a 1d Numpy array
+                out_values = numpy.reshape(out_region[~out_region.mask], 
+                    -1).data
+                 
+                    
+                if len(in_values) == 0 or len(out_values) == 0:
+                    # Can't do any stats on this window
+                    continue
+                    
+                if len(in_values) < 0.5 * self.window_size ** 2:
+                    # The window is less than half full. Skip it.
+                    # TODO: Make this threshold configurable.
+                    continue
+                
+                try:    
+                    
+                    # Get the p value for this window under the selected 
+                    # statistical test
+                    p_value = statistical_test(in_values, out_values)
+                        
+                    # If this is the best p value so far, record it
+                    best_p = min(best_p, p_value)
+                except ValueError:
+                    # Probably an all-zero layer, or something else the test 
+                    # can't handle.
+                    # But let's try all the other windows to be safe. 
+                    # Maybe one will work.
+                    pass
+                    
+                
+                
+        # We have now found the best p for any window for this layer.
+        print "Best p found: {}".format(best_p)
+        sys.stdout.flush()
+        
+        return best_p                
+
+def run_functor(functor):
+    """
+    Given a no-argument functor (like a ClusterFinder), run it and return its 
+    result. We can use this with multiprocessing.map and map it over a list of 
+    job functors to do them.
+    
+    Handles getting more than multiprocessing's pitiful exception output
+    """
+    
+    try:
+        return functor()
+    except:
+        # Put all exception text into an exception and raise that
+        raise Exception(traceback.format_exc())
+                
+def main(args):
+    """
+    Parses command line arguments, and makes visualization.
+    "args" specifies the program arguments, with args[0] being the executable
+    name. The return value should be used as the program's exit code.
+    """
+    
+    options = parse_args(args) # This holds the nicely-parsed options object
+    
+    # Test our picking
+    x, y = hexagon_center(0, 0)
+    if hexagon_pick(x, y) != (0, 0):
+        raise Exception("Picking is broken!")
+    
+    # First bit of stdout becomes annotation in Galaxy
+    
+    # Make sure our output directory exists.
+    if not os.path.exists(options.directory):
+        # makedirs is the right thing to use here: recursive
+        os.makedirs(options.directory)
+    
+    # Work in a temporary directory
+    drl_directory = tempfile.mkdtemp()
+    
+    # This is the base name for all the files that DrL uses to do the layout
+    # We're going to put it in a temporary directory.
+    drl_basename = os.path.join(drl_directory, "layout")
+    
+    # We can just pass our similarity matrix to DrL's truncate
+    # But we want to run it through our tsv parser to strip comments and ensure
+    # it's valid
+    
+    # This holds a reader for the similarity matrix
+    sim_reader = tsv.TsvReader(options.similarities)
+    
+    # This holds a writer for the sim file
+    sim_writer = tsv.TsvWriter(open(drl_basename + ".sim", "w"))
+    
+    print "Regularizing similarity matrix..."
+    sys.stdout.flush()
+    
+    for parts in sim_reader:
+        sim_writer.list_line(parts)
+        
+    sim_reader.close()
+    sim_writer.close()
+    
+    # Now our input for DrL is prepared!
+    
+    # Do DrL truncate.
+    # TODO: pass a truncation level
+    print "DrL: Truncating..."
+    sys.stdout.flush()
+    subprocess.check_call(["truncate", drl_basename]) 
+        
+    # Run the DrL layout engine.
+    print "DrL: Doing layout..."
+    sys.stdout.flush()
+    subprocess.check_call(["layout", drl_basename]) 
+    
+    # Put the string names back
+    print "DrL: Restoring names..."
+    sys.stdout.flush()
+    subprocess.check_call(["recoord", drl_basename]) 
+        
+    # Now DrL has saved its coordinates as <signature name>\t<x>\t<y> rows in 
+    # <basename>.coord
+    
+    # We want to read that.
+    # This holds a reader for the DrL output
+    coord_reader = tsv.TsvReader(open(drl_basename + ".coord", "r"))
+    
+    # This holds a dict from signature name string to (x, y) float tuple
+    nodes = {}
+    
+    print "Reading DrL output..."
+    sys.stdout.flush()
+    for parts in coord_reader:
+        nodes[parts[0]] = (float(parts[1]), float(parts[2])) 
+            
+    coord_reader.close()
+    
+    # Save the DrL coordinates in our bundle, to be displayed client-side for 
+    # debugging.
+    coord_writer = tsv.TsvWriter(open(
+        os.path.join(options.directory, "drl.tab"), "w"))
+        
+    for signature_name, (x, y) in nodes.iteritems():
+        # Write a tsv with names instead of numbers, like what DrL recoord would
+        # have written. This is what the Javascript on the client side wants.
+        coord_writer.line(signature_name, x, y)
+        
+    coord_writer.close()
+    
+    # Do the hexagon layout
+    # We do the squiggly rows setup, so express everything as integer x, y
+    
+    # This is a defaultdict from (x, y) integer tuple to id that goes there, or
+    # None if it's free.
+    hexagons = collections.defaultdict(lambda: None)
+    
+    # This holds the side length that we use
+    side_length = 1.0
+    
+    # This holds what will be a layer of how badly placed each hexagon is
+    # A dict from node name to layer value
+    placement_badnesses = {}
+    
+    for node, (node_x, node_y) in nodes.iteritems():
+        # Assign each node to a hexagon
+        # This holds the resulting placement badness for that hexagon (i.e. 
+        # distance from ideal location)
+        badness = assign_hexagon(hexagons, node_x, node_y, node,
+            scale=side_length)
+            
+        # Put the badness in the layer
+        placement_badnesses[node] = float(badness)
+   
+    # Normalize the placement badness layer
+    # This holds the max placement badness
+    max_placement_badness = max(placement_badnesses.itervalues())
+    print "Max placement badness: {}".format(max_placement_badness)
+
+    if max_placement_badness != 0:
+        # Normalize by the max if possible.
+        placement_badnesses = {node: value / max_placement_badness for node, 
+            value in placement_badnesses.iteritems()}
+   
+    # The hexagons have been assigned. Make hexagons be a dict instead of a 
+    # defaultdict, so it pickles.
+    # TODO: I should change it so I don't need to do this.
+    hexagons = dict(hexagons)  
+   
+    # Now dump the hexagon assignments as an id, x, y tsv. This will be read by
+    # the JavaScript on the static page and be used to produce the 
+    # visualization.        
+    hexagon_writer = tsv.TsvWriter(open(os.path.join(options.directory, 
+        "assignments.tab"), "w"))
+
+    # First find the x and y offsets needed to make all hexagon positions 
+    # positive
+    min_x = min(coords[0] for coords in hexagons.iterkeys())
+    min_y = min(coords[1] for coords in hexagons.iterkeys())  
+        
+    for coords, name in hexagons.iteritems():
+        # Write this hexagon assignment, converted to all-positive coordinates.
+        hexagon_writer.line(name, coords[0] - min_x, coords[1] - min_y)    
+    hexagon_writer.close()
+    
+    # Now that we have hex assignments, compute layers.
+    
+    # In addition to making per-layer files, we're going to copy all the score
+    # matrices to our output directoy. That way, the client can download layers
+    # in big chunks when it wants all layer data for statistics. We need to
+    # write a list of matrices that the client can read, which is written by
+    # this TSV writer.
+    matrix_index_writer = tsv.TsvWriter(open(os.path.join(options.directory, 
+        "matrices.tab"), "w"))
+        
+    # Read in all the layer data at once
+    # TODO: Don't read in all the layer data at once
+    
+    # This holds a dict from layer name to a dict from signature name to 
+    # score.
+    layers = {}
+    
+    # This holds the names of all layers
+    layer_names = []
+    
+    for matrix_number, score_filename in enumerate(options.scores):
+        # First, copy the whole matrix into our output. This holds its filename.
+        output_filename = "matrix_{}.tab".format(matrix_number)
+        shutil.copy2(score_filename, os.path.join(options.directory, 
+            output_filename))
+            
+        # Record were we put it
+        matrix_index_writer.line(output_filename)
+    
+        # This holds a reader for the scores TSV
+        scores_reader = tsv.TsvReader(open(score_filename, "r"))
+        
+        # This holds an iterator over lines in that file
+        # TODO: Write a proper header/data API
+        scores_iterator = scores_reader.__iter__()
+        
+        try:
+            # This holds the names of the columns (except the first, which is 
+            # labels). They also happen to be layer names
+            file_layer_names = scores_iterator.next()[1:]
+            
+            # Add all the layers in this file to the complete list of layers.
+            layer_names += file_layer_names
+            
+            # Ensure that we have a dict for every layer mentioned in the file
+            # (even the ones that have no data below). Doing it this way means
+            # all score matrices need disjoint columns, or the last one takes
+            # precedence.
+            for name in file_layer_names:
+                layers[name] = {} 
+            
+            for parts in scores_iterator:
+                # This is the signature that this line is about
+                signature_name = parts[0]
+                
+                # These are the scores for all the layers for this signature
+                layer_scores = parts[1:]
+                
+                for (layer_name, score) in itertools.izip(file_layer_names, 
+                    layer_scores):
+                    
+                    # Store all the layer scores in the appropriate
+                    # dictionaries.
+                    try:
+                        layers[layer_name][signature_name] = float(score)
+                    except ValueError:
+                        # This is not a float.
+                        # Don't set that entry for this layer.
+                        # TODO: possibly ought to complain to the user? But then
+                        # things like "N/A" won't be handled properly.
+                        continue
+                    
+        except StopIteration:
+            # We don't have any real data here. Couldn't read the header line.
+            # Skip to the next file
+            pass
+            
+        # We're done with this score file now
+        scores_reader.close()
+    
+    # We're done with all the input score matrices, so our index is done too.
+    matrix_index_writer.close()
+    
+    # We have now loaded all layer data into memory as Python objects. What
+    # could possibly go wrong?
+    
+    # Stick our placement badness layer on the end
+    layer_names.append("Placement Badness")
+    layers["Placement Badness"] = placement_badnesses
+       
+    # Now we need to write layer files.
+        
+    # Generate some filenames for layers that we can look up by layer name.
+    # We do this because layer names may not be valid filenames.
+    layer_files = {name: os.path.join(options.directory, 
+        "layer_{}.tab".format(number)) for (name, number) in itertools.izip(
+        layer_names, itertools.count())}
+        
+    for layer_name, layer in layers.iteritems():
+        # Write out all the individual layer files
+        # This holds the writer for this layer file
+        scores_writer = tsv.TsvWriter(open(layer_files[layer_name], "w"))
+        for signature_name, score in layer.iteritems():
+            # Write the score for this signature in this layer
+            scores_writer.line(signature_name, score)
+        scores_writer.close()
+    
+    # We need something to sort layers by. We have "priority" (lower is 
+    # better)
+    
+    if len(layer_names) > 0 and options.stats:
+        # We want to do this fancy parallel stats thing.
+        # We skip it when there are no layers, so we don't try to join a
+        # never-used pool, which seems to hang.
+        
+        print "Running statistics..."
+        
+        # This holds an iterator that makes ClusterFinders for all out layers
+        cluster_finders = [ClusterFinder(hexagons, layers[layer_name], 
+            window_size=options.window_size) for layer_name in layer_names]
+        
+        print "{} jobs to do.".format(len(cluster_finders))
+       
+        # This holds a multiprocessing pool for parallelization
+        pool = multiprocessing.Pool()
+       
+        # This holds all the best p values in the same order
+        best_p_values = pool.map(run_functor, cluster_finders)
+        
+        # Close down the pool so multiprocessing won't die sillily at the end
+        pool.close()
+        pool.join()
+        
+        # This holds a dict from layer name to priority (best p value)
+        # We hope the order of the dict items has not changed
+        layer_priorities = {layer_name: best_p_value for layer_name, 
+            best_p_value in itertools.izip(layer_names, best_p_values)}
+    else:
+        # We aren't doing any stats.
+        
+        print "Skipping statistics."
+        
+        # Make up priorities.
+        layer_priorities = {name: float("+inf") for name in layer_names}
+    
+    # Count how many layer entries are greater than 0 for each binary layer, and
+    # store that number in this dict by layer name. Things with the default
+    # empty string instead of a number aren't binary layers, but they can use
+    # the empty string as their TSV field value, so we can safely pull any layer
+    # out of this by name.
+    layer_positives = collections.defaultdict(str)
+    
+    for layer_name in layer_names:
+        # Assume it's a binary layer until proven otherwise
+        layer_positives[layer_name] = 0
+        for value in layers[layer_name].itervalues():
+            if value == 1:
+                # Count up all the 1s in the layer
+                layer_positives[layer_name] += 1
+            elif value != 0:
+                # It has something that isn't 1 or 0, so it can't be a binary
+                # layer. Throw it out and try the next layer.
+                layer_positives[layer_name] = ""
+                continue
+    
+    # Write an index of all the layers we have, in the form:
+    # <layer>\t<file>\t<priority>\t<number of signatures with data>\t<number of 
+    # signatures that are 1 for binary layers>
+    # This is the writer to use.
+    index_writer = tsv.TsvWriter(open(os.path.join(options.directory, 
+        "layers.tab"), "w"))
+        
+    for layer_name, layer_file in layer_files.iteritems():
+        # Write the index entry for this layer
+        index_writer.line(layer_name, os.path.basename(layer_file), 
+            layer_priorities[layer_name], len(layers[layer_name]), 
+            layer_positives[layer_name])
+        
+    index_writer.close()
+    
+    # Copy over the user-specified colormaps file, or make an empty TSV if it's
+    # not specified.
+    
+    
+    
+    # This holds a writer for the sim file. Creating it creates the file.
+    colormaps_writer = tsv.TsvWriter(open(os.path.join(options.directory, 
+        "colormaps.tab"), "w"))
+    
+    if options.colormaps is not None:
+        # The user specified colormap data, so copy it over
+        # This holds a reader for the colormaps file
+        colormaps_reader = tsv.TsvReader(options.colormaps)
+        
+        print "Regularizing colormaps file..."
+        sys.stdout.flush()
+        
+        for parts in colormaps_reader:
+            colormaps_writer.list_line(parts)
+        
+        colormaps_reader.close()
+    
+    # Close the colormaps file we wrote. It may have gotten data, or it may 
+    # still be empty.
+    colormaps_writer.close()
+    
+    # Now copy any static files from where they live next to this Python file 
+    # into the web page bundle.
+    # This holds the directory where this script lives, which also contains 
+    # static files.
+    tool_root = os.path.dirname(os.path.realpath(__file__))
+    
+    # Copy over all the static files we need for the web page
+    # This holds a list of them
+    static_files = [
+        # Static images
+        "drag.svg",
+        "filter.svg",
+        "statistics.svg",
+        "right.svg",
+        "throbber.svg",
+        
+        # jQuery itself is pulled from a CDN.
+        # We can't take everything offline since Google Maps needs to be sourced
+        # from Google, so we might as well use CDN jQuery.
+        
+        # Select2 scripts and resources:
+        "select2.css",
+        "select2.js",
+        "select2.png",
+        "select2-spinner.gif",
+        "select2x2.png",
+        
+        # The jQuery.tsv plugin
+        "jquery.tsv.js",
+        # The color library
+        "color-0.4.1.js",
+        # The jStat statistics library
+        "jstat-1.0.0.js",
+        # The Google Maps MapLabel library
+        "maplabel-compiled.js",
+        # The main CSS file
+        "hexagram.css",
+        # The main JavaScript file that runs the page
+        "hexagram.js",
+        # Web Worker for statistics
+        "statistics.js",
+        # File with all the tool code
+        "tools.js"
+    ]
+    
+    # We'd just use a directory of static files, but Galaxy needs single-level
+    # output.
+    for filename in static_files:
+        shutil.copy2(os.path.join(tool_root, filename), options.directory)
+    
+    # Copy the HTML file to our output file. It automatically knows to read
+    # assignments.tab, and does its own TSV parsing
+    shutil.copy2(os.path.join(tool_root, "hexagram.html"), options.html)
+    
+    # Delete our temporary directory.
+    shutil.rmtree(drl_directory)
+    
+    print "Visualization generation complete!"
+        
+    return 0
+
+if __name__ == "__main__" :
+    try:
+        # Get the return code to return
+        # Don't just exit with it because sys.exit works by exceptions.
+        return_code = main(sys.argv)
+    except:
+        traceback.print_exc()
+        # Return a definite number and not some unspecified error code.
+        return_code = 1
+        
+    sys.exit(return_code)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/hexagram.xml~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,114 @@
+<?xml version="1.0"?>
+<tool id="hexagram" name="Hexagram Visualization" version="0.1">
+    <description>Interactive hex grid clustering visualization</description>
+    <requirements>
+        <!--
+            Go get the drl-graph-layout package as defined in
+            tool_dependencies.xml
+        -->
+        <requirement type="package" version="1.1">drl-graph-layout</requirement>
+        <!--
+            And go get some Python modules that aren't standard.
+        -->
+        <requirement type="python-module">numpy</requirement>
+        <requirement type="python-module">scipy</requirement>
+    </requirements> 
+    <!-- 
+        This is the command to run as a Cheetah template.
+        We do fancy iteration over multiple score matrices (see
+        ../plotting/xy_plot.xml).
+    -->
+    <command interpreter="python">hexagram.py 
+		#for $i, $s in enumerate( $similarity )
+			"${s.similarity_matrix.file_name}"
+		#end for
+		 #for $i, $s in enumerate ($similarity)
+            --names "${s.similarity_matrix.name}"
+        #end for
+        #for $i, $s in enumerate( $scores )
+            --scores "${s.score_matrix.file_name}"
+        #end for
+        #if $query:
+            --query "$query"
+        #end if
+        #if $colormaps
+            --colormaps "$colormaps"
+        #end if
+        --html "$output"
+        --directory "$output.files_path"
+        --truncation_edges $edges
+        #if $singletons
+            --include-singletons
+        #end if
+        #if $nostats
+            --no-stats
+        #end if
+    </command>
+    <inputs>
+		<repeat name="similarity" title="Similarity Matrices">
+        	<param name="similarity_matrix" type="data" format="tabular" 
+            	label="Similarity matrix of signatures to visualize"/>
+		</repeat>
+        <repeat name="scores" title="Scores">
+            <param name="score_matrix" type="data" format="tabular" 
+                label="Score matrix for signatures to visualize"/>
+        </repeat>       
+        <param name="colormaps" type="data" format="text" optional="true" 
+            label="Colormap configuration file"/>
+        <param name="edges" type="integer" value="10" 
+            label="Number of edges to use per node"/>
+        <param name="query" type="text" 
+            label="Name of query signature"
+            help="A signature name, or empty for no query"/>
+        <param name="singletons" type="boolean" 
+            label="Keep unconnected singleton signatures"/>
+        <param name="nostats" type="boolean" 
+            label="Skip calculation of heatmap clumpiness statistics"/>
+    </inputs>
+    <outputs>
+        <data name="output" label="Hexagram Visualization($edges edges)" 
+            format="html" hidden="false"/>
+    </outputs>
+    <stdio>
+        <!-- 
+            The tool catches all errors and returns 1, or 0 if no errors 
+            happened. 
+        -->
+        <exit_code range="1" level="fatal" 
+            description="Error in visualization generator (see below)" />
+        <exit_code range="2:" level="fatal" description="Unhandleable error" />
+    </stdio>
+    <help>
+
+Hexagram Visualization
+======================
+
+This tool produces a "hexagram visualization": an interactive Google Maps-based two-dimensional layout of the similarity matrix data, on a hexagonal grid, with the score matrix data represented as different available colorings of the hexagons. This visualization is an HTML web page with associated support files, and appears as the tool's output in your Galaxy history; to open it, click on the eyeball icon.
+
+If, instead of a rich interactive Web application, you get a boring white page with some text on it, you probably have tool output sanitization on. Turn sanitization off in your universe_wsgi.ini, or download the visualization, unzip it, and open the HTML file it contains. If you want to use a downloaded visualization, you will need to open it in a browser other than Chrome (which, for security reasons, does not let local Web pages access local files programmatically).
+
+Input Format
+------------
+
+The tool takes three types of input files:
+
+Similarity Matrix
++++++++++++++++++
+
+The only required input file is a *similarity matrix*, which contains similarity information over a set of "samples" or "signatures". This file is a sparse matrix represented as three tab-delimited columns; the first two columns of each row contain the names of two signatures, and the last column contains a nonzero, non-negative floating-point "similarity" between them. No headers are used. Self-edges are permitted, and self-edges with a similarity of 1 will be added to every node if "Keep unconnected singleton signatures" is checked. The input similarity matrix need not describe a similarity graph that is connected, and similarity need not be transitive in any way.
+
+Score Matrices
+++++++++++++++
+
+You almost certainly want to run the tool with one or more *score matrices*, which specify some attribute data to overlay on the signature hexes once they are arranged. Any number of score matrices can be specified. Each score matrix is a TSV file; the first column contains sample names, and any other columns contain the values of attributes for those samples. Score matrices have a header line, which specifies some (unused) name for the column of sample names, and the name of the attribute described by each column of attribute data.
+
+Attribute data may be continuous (floating-point numbers), categorical (non-negative integers), or binary (0 or 1). The visualizer assumes that each column is the most restrictive type that will fit the data given; a column with all 0.0s and 1.0s will be assumed to be binary, even if the user intended it to represent a continuous value. Categories in categorical columns are assumed to be numbered from 0, even if no 0s appear in the column.
+
+Colormap Configuration File
++++++++++++++++++++++++++++
+
+If you have categorical data to display, you probably want to write a *colormap configuration file* for it. Categorical data is displayed in the visualizer with one color per category; if you want to specify particular colors for each category, or names for each category (instead of the default labels of 0 through n), you need a colormap configuration file. Each line in this file should begin with the name of a categorical attribute, followed by, for each category used in that attribute, the category number, the desired name for the category, and a CSS color (like "white" or "#FFFFFF" or "rgb(255,255,255)) to use to represent that category. Fields on each line are separated by tab characters.
+
+    </help>
+</tool>
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/layer_5.tab~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,20 @@
+GSE14206_GPL8_stage=T3A_PHENOTYPE.tab	0
+GSE18655_GPL5858_recurrence=No_Rec_PHENOTYPE.tab	0
+GSE21034_GPL10264_pathological_stage=T4_PHENOTYPE.tab	1
+TCGA_tumor_level=Middle_PHENOTYPE.tab	1
+GSE14206_GPL8_stage=T2A_PHENOTYPE.tab	1
+TCGA_clinical_spread_ct2=Induration_and+or_Nodularity_Involves___or_=__?_of_one_lobe__cT2a__PHENOTYPE.tab	0
+GSE21034_GPL10264_ERG-fusion_gex=True_PHENOTYPE.tab	1
+GSE14206_GPL8_stage=T2C_PHENOTYPE.tab	0
+GSE18655_GPL5858_age_MEAN.tab	1
+GSE18655_GPL5858_psa_MEAN.tab	1
+GSE21034_GPL10264_Gene_fusion=True_PHENOTYPE.tab	0
+GSE14206_GPL8_stage=T3B_PHENOTYPE.tab	0
+GSE21034_GPL10264_pathological_stage=NA_PHENOTYPE.tab	0
+TCGA_zone_of_origin=Peripheral_Zone_PHENOTYPE.tab	1
+TCGA_diagnostic_mri_results=Extraprostatic_Extension_Localized__e.g._seminal_vesicles__PHENOTYPE.tab	0
+TCGA_pathologic_spread_pt4=YES_PHENOTYPE.tab	1
+TCGA_shortest_dimension_MEAN.tab	1
+GSE21034_GPL10264_clint_stage=T2C_PHENOTYPE.tab	1
+TCGA_diagnostic_ct_abd_pelvis_performed=YES_PHENOTYPE.tab	1
+GSE14206_GPL887_ets_group=ESE3_Low_PHENOTYPE.tab	1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/layers.tab~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,6 @@
+metformin_0.00001	layer_0.tab	10	86	
+a_very long_long_long_name_with_no convenient_text_wrapping_breakpoints <br/> <br/> <br/>now with some extra spaced text that should already wrap fine	layer_1.tab	11	86	
+tissue	layer_2.tab	1	20	
+iCluster.k25	layer_3.tab	2	20	
+zebulon 1	layer_4.tab	2	20	12
+xylophone 2	layer_5.tab	2	20	11
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/matrix_0.tab~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,21 @@
+id	metformin_0.00001	a_very long_long_long_name_with_no convenient_text_wrapping_breakpoints <br/> <br/> <br/>now with some extra spaced text that should already wrap fine	tissue	iCluster.k25	binary 1	binary 2
+GSE14206_GPL8_stage=T3A_PHENOTYPE.tab	0.103656268715241	0.0662108455001999	0	1	1	0
+GSE18655_GPL5858_recurrence=No_Rec_PHENOTYPE.tab	0.00736496699455639	-0.0030872481570366	2	2	0	0
+GSE21034_GPL10264_pathological_stage=T4_PHENOTYPE.tab	0.212395889719257	0.104946746175943	3	3	1	1
+TCGA_tumor_level=Middle_PHENOTYPE.tab	-0.0750289367770828	-0.0160926008102573	4	4	0	1
+GSE14206_GPL8_stage=T2A_PHENOTYPE.tab	0.101591559700421	0.0581690019909618	5	5	1	1
+TCGA_clinical_spread_ct2=Induration_and+or_Nodularity_Involves___or_=__?_of_one_lobe__cT2a__PHENOTYPE.tab	-0.0448303097941127	-0.0555992149962499	6	6	1	0
+GSE21034_GPL10264_ERG-fusion_gex=True_PHENOTYPE.tab	0.0292437277400893	-0.0210673135177115	7	7	1	1
+GSE14206_GPL8_stage=T2C_PHENOTYPE.tab	-0.0942458205785588	-0.0632178924636636	8	8	1	0
+GSE18655_GPL5858_age_MEAN.tab	-0.0398766576526588	0.00359207190540213	9	9	0	1
+GSE18655_GPL5858_psa_MEAN.tab	-0.0959320347649498	-0.00882728114771138	11	10	0	1
+GSE21034_GPL10264_Gene_fusion=True_PHENOTYPE.tab	0.0292437277400893	-0.0210673135177115	1	11	0	0
+GSE14206_GPL8_stage=T3B_PHENOTYPE.tab	-0.105814702233279	-0.0740118918016848	2	12	0	0
+GSE21034_GPL10264_pathological_stage=NA_PHENOTYPE.tab	0.112017018347965	0.0251898331610073	3	13	1	0
+TCGA_zone_of_origin=Peripheral_Zone_PHENOTYPE.tab	-0.00304197273959563	-0.0207284395193551	4	14	1	1
+TCGA_diagnostic_mri_results=Extraprostatic_Extension_Localized__e.g._seminal_vesicles__PHENOTYPE.tab	0.00993944807969242	-0.0317703371649353	5	15	0	0
+TCGA_pathologic_spread_pt4=YES_PHENOTYPE.tab	-0.0724829088312745	-0.0274093321577233	6	16	0	1
+TCGA_shortest_dimension_MEAN.tab	-0.0777725626701397	-0.0166257461335536	7	17	1	1
+GSE21034_GPL10264_clint_stage=T2C_PHENOTYPE.tab	0.192148495282519	0.077324537538078	8	18	1	1
+TCGA_diagnostic_ct_abd_pelvis_performed=YES_PHENOTYPE.tab	0.098060120151555	0.0451944074068774	9	19	1	1
+GSE14206_GPL887_ets_group=ESE3_Low_PHENOTYPE.tab	-0.0777826036647061	-0.028060859132074	10	20	1	1
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/statistics.js~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,709 @@
+// statistics.js: Web Worker file to run statistical tests in the background.
+
+// Constants:
+// How many pseudocount trials should we use for the binomial test?
+var BINOMIAL_PSEUDOCOUNTS = 5;
+
+// Should we log information about suspicious p values to the console for manual
+// spot checking?
+var LOG_SUSPICIOUS = false;
+
+// Go get jStat. Hope it's happy in Worker-land.
+importScripts("jstat-1.0.0.js");
+
+// Make a fake console to catch jstat warnings, so they don't crash the script.
+console = {
+    warn: print
+}
+
+onmessage = function(message) {
+    // Handle incoming messages from the page. Each message's data is an RPC
+    // request, with "name" set to a function name, "args" set to an array of
+    // arguments, and "id" set to an ID that should be returned with the return
+    // value in a reply message. If the function call fails, an error is sent
+    // back.
+    
+    
+    try {
+        // Go get the specified global function, and apply it on the given
+        // arguments. Use the global scope ("self") as its "this".
+        var return_value = self[message.data.name].apply(self, 
+            message.data.args);
+        
+    } catch(exception) {
+    
+        // Send the error back to the page instead of a return value.
+        // Unfortunately, errors themselves can't be cloned, so we do all the
+        // message making here and send back a string.
+        
+        // First we build a string with all the parts of the error we can get.
+        var error_message = "Error in web worker doing job " + message.data.id;
+        error_message += "\n";
+        error_message += exception.name + ": " + exception.message;
+        error_message += "\n";
+        error_message += "Full details:\n";
+        for(field in exception) {
+            if(field == "name" || field == "message") {
+                // Already got these.
+                continue;
+            }
+            
+            // Copy the field into the message as a string.
+            error_message += field + ": " + exception[field] + "\n";
+        }
+        error_message += "Call: " + message.data.name + "(";
+        for(var i = 0; i < message.data.args.length; i++) {
+            error_message += message.data.args[i];
+            if(i + 1 < message.data.args.length) {
+                // Have an argument after this.
+                error_message += ", ";
+            }
+        }
+        error_message += ")";
+
+        postMessage({
+            id: message.data.id,
+            error: error_message
+        });
+        
+        return;
+    }
+    
+    
+    // Send the return value back with the id.
+    postMessage({
+        id: message.data.id,
+        return_value: return_value
+    });
+}
+
+function print(message) {
+    // Print a message to the console of the parent page.
+    postMessage({
+        log: message
+    });
+}
+
+function statistics_for_matrix(matrix_url, in_list, out_list, all_list) {
+    // Download the given score matrix, do stats between in_list and out_list
+    // for each layer in it, and return an object from layer name to p value.
+    // all_list specifies the names of all signatures that figure into the
+    // analysis at all.
+    
+    // Download the matrix synchronously. 
+    // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synch
+    // ronous_and_Asynchronous_Requests
+    // A side effect of this is that we won't have more simultaneous downloads 
+    // than workers, which is probably good.
+    // This holds the request.
+    var request = new XMLHttpRequest();
+    // Get the layer data by GET. The false makes it synchronous.
+    request.open("GET", matrix_url, false);
+    request.send(null);
+    
+    // Now we have the layer TSV
+    // But we don't have our fancy jQuery TSV parser. Parse it manually.
+    
+    // This holds an object of layer data objects (from signature to float) by
+    // layer name.
+    layers = {};
+
+    // This holds the array of lines
+    // Split on newlines (as seen in jQuery.tsv.js)
+    var lines = request.responseText.split(/\r?\n/);
+    
+    // Line 0 gives all the layer names, but the first thing isn't a layer name
+    // (since it's above the signature column).
+    var layer_names = lines[0].split(/\t/);
+    for(var i = 1; i < layer_names.length; i++) {
+        // Make sure we have an object for this layer
+        layers[layer_names[i]] = {};
+    }
+    
+    // The rest give values per layer for the hex in column 1.
+    for(var i = 1; i < lines.length; i++) {
+        // This holds the parts of each line
+        var parts = lines[i].split(/\t/);
+        
+        if(parts[0]) {
+            // We actually have data
+            
+            // Get the singature
+            var signature = parts[0];
+            
+            for(var j = 1; j < parts.length; j++) {
+                // Go through each non-signature entry and set the appropriate
+                // layer's value for this signature.
+                layers[layer_names[j]][signature] = parseFloat(parts[j]);
+            }
+        }
+    }
+    
+    // Now we've parsed the matrix.
+    // Go do stats for each layer.
+    // This holds our calculated p valued by layer name.
+    var p_values = {};
+    
+    print("Running statistics for (up to) " + layer_names.length + 
+        " layers from matrix " + matrix_url);
+    
+    for(var i = 1; i < layer_names.length; i++) {
+        // Pass the layer data to the per-layer statistics, and get the p value
+        // back. It's probably easier to do this in this worker than to go
+        // invoke more workers.
+        p_values[layer_names[i]] = statistics_for_layer(layers[layer_names[i]],
+            in_list, out_list, all_list);
+    }
+    
+    // We've now calculated a p value for every layer in the matrix. Return the
+    // calculated p values labeled by layer.
+    return p_values;
+    
+}
+
+function statistics_for_layer(layer_data, in_list, out_list, all_list) {
+    // Run the appropriate statistical test for the passed layer data, between
+    // the given in and out arrays of signatures. all_list specifies the names
+    // of all signatures that figure into the analysis at all. Return the p
+    // value for the layer, or NaN if no p value could be calculated.
+
+    // This holds whether the layer is discrete
+    var is_discrete = true;
+    
+    // This holds whether the layer is binary
+    var is_binary = true;
+    
+    for(var signature in layer_data) {
+        if(layer_data[signature] > 1 || layer_data[signature] < 0) {
+            // Not a binary layer
+            is_binary = false;
+        }
+        
+        if(layer_data[signature] % 1 !== 0) {
+            // It's a float
+            is_binary = false;
+            is_discrete = false;
+        }
+    }
+    
+    if(is_binary) {
+        // This is a binary/dichotomous layer, so run a binomial test.
+        return binomial_compare(layer_data, in_list, out_list, all_list);
+    } else if (is_discrete) {
+        // This is a multinomial/categorical layer    
+        // TODO: statistics for discrete non-binary layers
+        return NaN;
+    } else {
+        // This is a continuous layer, so run a t test
+        return t_compare(layer_data, in_list, out_list, all_list);
+    }
+
+}
+
+function statistics_for_url(layer_url, in_list, out_list, all_list) {
+    // Run the stats for the layer with the given url, between the given in and
+    // out arrays of signatures. all_list specifies the names of all signatures
+    // that figure into the analysis at all. Return the p value for the layer,
+    // or NaN if no p value could be calculated.
+    
+    print("Running statistics for individual layer " + layer_url);
+    
+    // Download the layer data synchronously. 
+    // See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synch
+    // ronous_and_Asynchronous_Requests
+    // A side effect of this is that we won't have more simultaneous downloads 
+    // than workers, which is probably good.
+    // This holds the request.
+    var request = new XMLHttpRequest();
+    // Get the layer data by GET. The false makes it synchronous.
+    request.open("GET", layer_url, false);
+    request.send(null);
+    
+    // Now we have the layer TSV
+    // But we don't have our fancy jQuery TSV parser. Parse it manually.
+    
+    // This holds the layer data (signature to float)
+    var layer_data = {}
+
+    // This holds the array of lines
+    // Split on newlines (as seen in jQuery.tsv.js)
+    var lines = request.responseText.split(/\r?\n/);
+    
+    for(var i = 0; i < lines.length; i++) {
+        // This holds the parts of each line
+        var parts = lines[i].split(/\t/);
+        
+        if(parts[0]) {
+            // We actually have data
+            // Parse the layer value for this signature
+            var value = parseFloat(parts[1]);
+            
+            // Store the value in the layer data
+            layer_data[parts[0]] = value;
+        }
+    }
+    
+    // Run stats on the downloaded data
+    return statistics_for_layer(layer_data, in_list, out_list, all_list);
+}
+
+function t_compare(layer_data, in_list, out_list, all_list) {
+    // Given the data of a continuous layer object (an object from signature
+    // name to float (or undefined)), and arrays of the names of "in" and "out"
+    // signatures, do a t test test for whether the in signatures differ from
+    // the out signatures. Returns an object of metadata, with "p_value" set to
+    // either the p value of the test (two-tailed), or NaN if the test cannot be
+    // performed (due to, e.g. fewer than 2 samples in one category).
+    
+    // Go through the in list and calculate all the summary statistics
+    // How many non-NaN values?
+    var number_in = 0;
+    // What is the sum?
+    var sum_in = 0;
+    
+    for(var i = 0; i < in_list.length; i++) {
+        if(!isNaN(layer_data[in_list[i]])) {
+            number_in++;
+            sum_in += layer_data[in_list[i]];
+        }
+    }
+    
+    // We've done one pass, so we know if we have any in list data actually
+    if(number_in < 2) {
+        // Not enough to run the t test
+        return NaN;
+    }
+    
+    // What is the mean?
+    var mean_in = sum_in / number_in;
+    
+    // What is the second moment (sum of squares of differences from the mean)
+    var second_moment_in = 0;
+    for(var i = 0; i < in_list.length; i++) {
+        if(!isNaN(layer_data[in_list[i]])) {
+            second_moment_in += Math.pow(layer_data[in_list[i]] - mean_in, 2);
+        }
+    }
+    
+    // What is the unbiased variance?
+    unbiased_variance_in = second_moment_in / (number_in - 1);
+    
+    // Now go through the same process for the out list
+    // How many non-NaN values?
+    var number_out = 0;
+    // What is the sum?
+    var sum_out = 0;
+    
+    for(var i = 0; i < out_list.length; i++) {
+        if(!isNaN(layer_data[out_list[i]])) {
+            number_out++;
+            sum_out += layer_data[out_list[i]];
+        }
+    }
+    
+    // We've done one pass, so we know if we have any out list data actually
+    if(number_out < 2) {
+        // Not enough to run the t test
+        return NaN;
+    }
+    
+    // What is the mean?
+    var mean_out = sum_out / number_out;
+    
+    // What is the second moment (sum of squares of differences from the mean)
+    var second_moment_out = 0;
+    for(var i = 0; i < out_list.length; i++) {
+        if(!isNaN(layer_data[out_list[i]])) {
+            second_moment_out += Math.pow(layer_data[out_list[i]] - mean_out, 
+                2);
+        }
+    }
+    
+    // What is the unbiased variance?
+    unbiased_variance_out = second_moment_out / (number_out - 1);
+    
+    // We can't do the test if both variances are 0
+    if(unbiased_variance_in == 0 && unbiased_variance_out == 0) {
+        return NaN;
+    }
+    
+    // Now we can calculate the t test two-tailed p value
+    var p_value = t_test(mean_in, unbiased_variance_in, number_in, mean_out, 
+        unbiased_variance_out, number_out);
+        
+    // And return it in a dict with other metadata.
+    // We don't really have any other metadata.
+    return {
+        p_value: p_value
+    };
+}
+
+function t_test(mean_in, unbiased_variance_in, number_in, mean_out, 
+    unbiased_variance_out, number_out) {
+
+    // Given the mean, unbiased variance, and number of samples for both the in
+    // group and the out group, compute the p value for the t test with unequal
+    // sample sizes and unequal variances, testing to see whether the means
+    // differ (a two-tailed "Welch's" t test). See
+    // https://en.wikipedia.org/wiki/Student%27s_t-test
+    // Assumes we have enough samples to actually perform the test.
+    
+    // First, calculate the t statistic, which is where our observations fall on
+    // the t distribution.
+    var t_statistic = (mean_in - mean_out) / Math.sqrt((unbiased_variance_in /
+        number_in) + (unbiased_variance_out / number_out));
+        
+        
+    // Calculate the degrees of freedom for the particular t distribution that
+    // we ought to compare the statistic against
+    var degrees_of_freedom = Math.pow((unbiased_variance_in / number_in) + 
+        (unbiased_variance_out / number_out), 2) / 
+        ((Math.pow(unbiased_variance_in / number_in, 2) / (number_in - 1)) + 
+        (Math.pow(unbiased_variance_out / number_out, 2) / (number_out - 1)));
+
+    // Now we have to compare the t statistic to the t test CDF available via
+    // the totally undocumented jstat.pt = function(q, df, ncp, lower_tail, log)
+    // where:
+    // q is the t statistic value to calculate the cdf at
+    // df is the degrees of freedom
+    // ncp is the "mu" parameter for the t distributiuon. I think this sets the 
+    // mean, and it's OK to leave blank.
+    // lower_tail presumably specifies if we want the lower or upper tail of the
+    // CDF. Defaults to true.
+    // Log specifies if we want the log probability. Defaults to false.
+    
+    // Make the t statistic be on the low side of the distribution, and
+    // calculate the lower tail's area using the CDF.
+    var one_tail_probability = jstat.pt(0 - Math.abs(t_statistic), 
+        degrees_of_freedom);
+        
+    // Return the two-tailed p value, which, since the t distribution is
+    // symmetric, is just twice the single-tail probability
+    return 2 * one_tail_probability;
+     
+}
+
+function binomial_compare(layer_data, in_list, out_list, all_list) {
+    // Given the data of a binary layer object (an object from signature name to
+    // 0 or 1 (or undefined)), and arrays of the names of "in" and "out"
+    // signatures, do a binomial test for whether the in signatures differ from
+    // the out signatures. Uses a number of pseudocount trials as specified in
+    // the global constant BINOMIAL_PSEUDOCOUNTS Returns an object of metadata,
+    // with "p_value" set to either the p value of the test (two-tailed), or NaN
+    // if the test cannot be performed. all_list specifies the names of all
+    // signatures that figure into the analysis at all (i.e. those which the
+    // user hasn't filtered out), which we use when calculating how many of our
+    // pseudocounts should be successes. Signature names appearing in all_list
+    // but with no data in layer_data are not counted.
+    
+    
+    // Work out the distribution from the out list
+    // How many out signatures are 1?
+    var outside_yes = 0;
+    // And are 0?
+    var outside_no = 0;
+    
+    for(var i = 0; i < out_list.length; i++) {
+        if(layer_data[out_list[i]] === 1) {
+            // This is a yes and it's outside.
+            outside_yes++;
+        } else if(layer_data[out_list[i]] === 0) {
+            // A no and outside
+            outside_no++;
+        }
+    }
+    
+    // It's OK for all the outside hexes to be 0 now. Pseudocounts can give us a
+    // p value.
+    
+    // Now work out our pseudocounts.
+    // How many signatures in all_list are successes?
+    var all_yes = 0;
+    // And how many are failures (as opposed to undef)
+    var all_no = 0;
+    
+    for(var i = 0; i < all_list.length; i++) {
+        if(layer_data[all_list[i]] === 1) {
+            // A yes anywhere
+            all_yes++;
+        } else if(layer_data[all_list[i]] === 0) {
+            // A real no (not a no-data) anywhere
+            all_no++;
+        }
+    }
+    
+    // It't not OK for there to be no hexes in the all set. Maybe they filtered
+    // out all the ones with any data?
+    if(all_yes + all_no == 0) {
+        // TODO: Sure wish we had layer names here.
+        print("No signatures were available with data for this layer.");
+        return NaN;
+    }
+    
+    // Calculate how many pseudo-yeses we should have.
+    // Match the frequency in all signatures.
+    var pseudo_yes = BINOMIAL_PSEUDOCOUNTS * (all_yes / (all_yes + all_no));
+    
+    // pseudo-trials is just BINOMIAL_PSEUDOCOUNTS
+    
+    // This holds the probability of being a 1 for the out list.
+    // We want to test if the in list differs significantly from this.
+    var background_probability = (outside_yes + pseudo_yes) / (outside_yes + 
+        outside_no + BINOMIAL_PSEUDOCOUNTS);
+
+    if(background_probability == 0) {
+        // Can't do the binomial test in this case. Somehow there were no yeses
+        // anywhere.
+        return NaN;
+    }
+    
+    // How many 1s are in the in list?
+    var inside_yes = 0;
+    // And how many 0s?
+    var inside_no = 0;
+    
+    for(var i = 0; i < in_list.length; i++) {
+        if(layer_data[in_list[i]] === 1) {
+            // This is a yes and it's inside.
+            inside_yes++;
+        } else if(layer_data[in_list[i]] === 0) {
+            // A no and it's inside
+            inside_no++;
+        }
+    }
+
+    // Return the p value for rejecting the null hypothesis that the in
+    // signatures follow the background distribution.
+    var p = binomial_test(inside_yes + inside_no, inside_yes,
+        background_probability);
+        
+    if(LOG_SUSPICIOUS && (p == 0 || p == 1)) {
+        // We got an odd p value. Complain about it.
+        print("Got suspicious p value " + p);
+        print("Was binomial test for " + inside_yes + " successes in " + 
+            (inside_yes + inside_no) + " trials at probability " + 
+            background_probability);
+        print("Background was " + outside_yes + " out of " + (outside_yes + 
+            outside_no) + " with " + pseudo_yes + " out of " + 
+            BINOMIAL_PSEUDOCOUNTS + " pseudocounts.");
+    }
+     
+    // Return our p value as "p_value", and also how many non-pseudocount
+    // successes were in the in_list and the out_list.
+    return {
+        p_value: p,
+        "1s in A": inside_yes,
+        "1s in background": outside_yes
+    };
+}    
+    
+function binomial_test(trials, successes, success_probability) {
+    if(trials < successes) {
+        print("Trying to test " + trials + " trials with " + successes + 
+            " successes!");
+    }
+
+    // Return the p value for rejecting the null hypothesis that the observed
+    // number of successes happened in the observed number of trials when the
+    // probability of success was success_probability. Does a Binomial
+    // test.
+    
+    // Calculate the P value
+    // This must be terribly complicated since nobody seems to have written up 
+    // how to do it as anything other than an arcane stats ritual.
+    // Something close: http://www.johnmyleswhite.com/notebook/2012/04/14/implem
+    // enting-the-exact-binomial-test-in-julia/
+    // How scipy.stats does it (x = successes, n = trials, p = supposed 
+    // probability):
+    // SourceForge says Scipy is BSD licensed, so we can steal this code for our
+    // comments.
+    /*
+        d = distributions.binom.pmf(x,n,p)
+        rerr = 1+1e-7
+        if (x < p*n):
+            i = np.arange(np.ceil(p*n),n+1)
+            y = np.sum(distributions.binom.pmf(i,n,p) <= d*rerr,axis=0)
+            pval = distributions.binom.cdf(x,n,p) + distributions.binom.sf(n-y,
+                n,p)
+        else:
+            i = np.arange(np.floor(p*n))
+            y = np.sum(distributions.binom.pmf(i,n,p) <= d*rerr,axis=0)
+            pval = distributions.binom.cdf(y-1,n,p) + distributions.binom.sf(
+                x-1,n,p)
+    */
+    // There is of course no justification for why this would work.
+    // What it's actually doing is a complicated Numpy vectorized operation to 
+    // find the boundary of the tail we don't have, and then adding the CDF of 
+    // the lower tail boundary and (1-CDF) of the upper tail boundary (which is 
+    // the P value by definition).
+    
+    // This holds the probability of exactly what we've observed under the null
+    // hypothesis.
+    var observed_probability = binomial_pmf(trials, successes, 
+        success_probability);
+    
+    if(successes < trials * success_probability) {
+        // We know anything with fewer successes than this is more extreme. But
+        // how many successes would we need to have an equally extreme but
+        // higher than expected number of successes?
+        // We should sum down from all successes. (We'll sum from small to large
+        // so it's OK numerically.)
+        
+        // This holds the total probability of everything more extremely
+        // successful than what we've observed.
+        var other_tail_total_probability = 0;
+        
+        // TODO: implement some better sort of search thing and use CDF
+        for(var other_tail_start = trials; other_tail_start >= 
+            Math.ceil(trials * success_probability); other_tail_start--) {
+            
+            // Get the probability for this particular case
+            var case_probability = binomial_pmf(trials, other_tail_start, 
+                success_probability);
+            
+            if(case_probability > observed_probability) {
+                // This case is actually less extreme than what we've observed, 
+                // so our summation is complete.
+                
+                break;
+            } else {
+                // This case is more extreme than what we've observed, so use it
+                other_tail_total_probability += case_probability;
+            }
+        }
+        
+        // This holds the probability in this tail
+        var this_tail_probability = binomial_cdf(trials, successes, 
+            success_probability)
+        
+        
+        // Return the total probability from both tails, clamped to 1.
+        return Math.min(this_tail_probability + other_tail_total_probability, 
+            1.0);
+    } else {
+        // We know anything with more successes than this is more extreme. But
+        // how few successes would we need to have an equally extreme but lower
+        // than expected number of successes?
+        // We will sum up from 0 successes. We really ought to use the CDF 
+        // somehow, but I can't think of how we would do it.
+        
+        // This holds the total probability of everything more extremely
+        // failureful than what we've observed.
+        var other_tail_total_probability = 0;
+        
+        for(var other_tail_end = 0; other_tail_end < 
+            Math.floor(trials * success_probability); other_tail_end++) {
+            // We only have to iterate up to the peak (most likely) value.
+        
+            // Get the probability for this particular case
+            var case_probability = binomial_pmf(trials, other_tail_end, 
+                success_probability);
+            
+            if(case_probability > observed_probability) {
+                // This case is actually less extreme than what we've observed, 
+                // so our summation is complete.
+                break;
+            } else {
+                // This case is more extreme than what we've observed, so use it
+                other_tail_total_probability += case_probability;
+            }     
+            
+        }
+        
+        // This holds the probability in this tail. It is equal to the
+        // probability up to, but not including, where this tail starts. So even
+        // if the tail starts at the highest possible number of successes, it
+        // has some probability. successes can't be 0 here (since then we'd be
+        // below any nonzero expected probability and take the other branch.
+        // Since it's a positive integer, it must be 1 or more, so we can
+        // subtract 1 safely.
+        var this_tail_probability = 1 - binomial_cdf(trials, successes - 1, 
+            success_probability);
+        
+        // Return the total probability from both tails, clamped to 1
+        return Math.min(this_tail_probability + other_tail_total_probability, 
+            1.0);
+    }
+        
+    
+}
+
+function binomial_cdf(trials, successes, success_probability) {
+    // The Binomial distribution's cumulative distribution function. Given a 
+    // number of trials, a number of successes, and a success probability, 
+    // return the probability of having observed that many successes or fewer.
+    
+    // We compute this efficiently using the "regularized incomplete beta 
+    // function", AKA the beta distribution cdf, which we get from jstat.
+    // See http://en.wikipedia.org/wiki/Binomial_distribution#Cumulative_distrib
+    // ution_function and http://en.wikipedia.org/wiki/Regularized_incomplete_be
+    // ta_function#Incomplete_beta_function
+    
+    if(trials == successes) {
+        // jStat doesn't want a 0 alpha for its beta distribution (no failures)
+        // Calculate this one by hand (it's easy)
+        return 1;
+    }
+    
+    if(trials < successes) {
+        // This should never happen. TODO: Debug when it happens.
+        print("Error: trials (" + trials + ") < successes (" + successes + 
+            ")!");
+        return NaN;
+    }
+    
+    // This is the observation that we want the beta distribution CDF before
+    var beta_observation = 1 - success_probability;
+    
+    // These are the parameters of the relavent beta distribution
+    var beta_alpha = trials - successes;
+    var beta_beta = successes + 1;
+    
+    // Return the beta distribution CDF value, which happens to also be our CDF.
+    return jstat.pbeta(beta_observation, beta_alpha, beta_beta);
+}
+
+function binomial_pmf(trials, successes, success_probability) {
+    // The Binomial distribution's probability mass function. Given a number of
+    // trials, a number of successes, and the probability of success on each
+    // trial, calculate the probability of observing that many successes in that
+    // many trials with the given success rate.
+    
+    // The probability of this many successes in this many trials at this
+    // success rate is the probability of succeeding so many times and failing
+    // so many times, summed over all the mutually exclusive arrangements of
+    // successes and failures.
+    return (choose(trials, successes) * 
+        Math.pow(success_probability, successes) * 
+        Math.pow(1 - success_probability, trials - successes));
+    
+}
+
+function choose(available, selected) {
+    // The choose function: from available distinct objects, how many ways are 
+    // there to select selected of them. Returns "available choose selected". 
+    // Works with large input numbers that are too big to take the factorials 
+    // of.
+    
+    // We use a neat overflow-robust algorithm that eliminates the factorials 
+    // and makes the computation a multiplication of numbers greater than one.
+    // So, no overflow unless the result itself is too big.
+    // See http://arantxa.ii.uam.es/~ssantini/writing/notes/s667_binomial.pdf
+    
+    if(selected < available - selected) {
+        // It would be faster to think about choosing what we don't include. So
+        // do that instead.
+        return choose(available, available - selected);
+    }
+    
+    // This holds the result we are accumulating. Initialize to the 
+    // multiplicative identity.
+    var result = 1;
+    
+    for(var i = 1; i < available - selected + 1; i++) {
+        result *= (1 + (selected / i));
+    }
+    
+    // TODO: The result ought always to be an integer. Ensure this.
+    return result;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/tool_dependencies.xml~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,40 @@
+<?xml version="1.0"?>
+<!--
+Defines how to install the binaries that this tool depends on (in this case, DrL).
+Based on the examples at http://wiki.galaxyproject.org/ToolShedToolFeatures
+and http://toolshed.g2.bx.psu.edu/repos/jjohnson/defuse/file/f65857c1b92e/tool_dependencies.xml
+-->
+<tool_dependency>
+    <package name="drl-graph-layout" version="1.1">
+        <install version="1.0"><!-- This is the install tag version, not the package version -->
+            <actions>
+                <action type="shell_command">hg clone https://bitbucket.org/adam_novak/drl-graph-layout</action>
+                <!-- 
+                    TODO: We're supposed to copy the right Configuration.mk 
+                    file. Not doing so assumes our system is GNU.
+                -->
+                <action type="shell_command">hg up -r drl-graph-layout-1.1</action>
+                <action type="shell_command">make</action>
+                <action type="move_directory_files">
+                    <source_directory>bin</source_directory>
+                    <destination_directory>$INSTALL_DIR/bin</destination_directory>
+                </action>
+                <!-- 
+                    Now we can access DrL tools like truncate (at the expense of
+                    GNU truncate)
+                -->
+                <!-- 
+                    TODO: report to Galaxy that comments as the last element of 
+                    a set_enviromnent action atag are not properly handeled.
+                    See install_util.py line 435 in revision 9d42f1e32efb
+                -->
+                <action type="set_environment">
+                    <environment_variable name="PATH" action="prepend_to">$INSTALL_DIR/bin</environment_variable>
+                </action>
+            </actions>
+        </install>
+        <readme>
+        This installs the latest DrL Graph Layout tool from Adam Novak's Bitbucket, because Shawn Martin has stopped maintaining it.
+        </readme>
+    </package>
+</tool_dependency>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hexagram/tools.js~	Fri Oct 18 18:40:35 2013 -0400
@@ -0,0 +1,373 @@
+// tools.js: Code to run all the tools in the menu bar.
+// References globals in hexagram.js to actually do the tools' work.
+
+// To add a tool:
+// * Make a $(function() {...}); block to hold your code.
+// * Add a tool with add_tool with your tool code as the callback.
+// * Add at least one tool listener with add_tool_listener. Give it cleanup code
+//   if necessary to remove temporary UI elements.
+// * Make sure to set selected_tool to undefined when your tool's normal 
+//   workflow completes, so that the infowindow can use click events again.
+//   (it got set to your tool's name by the code prepended to your callback).
+
+$(function() {
+    // Set up the add text control
+    add_tool("add-text", "Add Text...", function() {
+        
+        // We'll prompt the user for some text, and then put a label where they 
+        // next click.
+        
+        var text = prompt("Enter some text, and click anywhere on the " +
+            "visualization to place it there", "Label Text");
+            
+        if(!text) {
+            // They don't want to put a label
+            print("Not putting any text");
+            selected_tool = undefined;
+            return;
+        }
+        
+        // Add a tool listenerr that places the label. It fires on a click 
+        // anywhere on anything on the map, including the background. We keep a 
+        // handle to it so we can remove it when it fires, ensuring we get just 
+        // one label. See http://stackoverflow.com/a/1544185
+        var handle = add_tool_listener("click", function(event) {
+            
+            // Make a new MapLabel at the click position
+            // See http://bit.ly/18MbLhR (the MapLabel library example page)
+            var map_label = new MapLabel({
+                text: text,
+                position: event.latLng,
+                map: googlemap,
+                fontSize: 10,
+                align: "left"
+            });
+            
+            // Subscribe tool listeners to the label
+            subscribe_tool_listeners(map_label);
+            
+            // Don't trigger again
+            remove_tool_listener(handle);
+        }, function() {
+            // Cleanup: de-select ourselves.
+            selected_tool = undefined;
+        });
+    });
+});
+
+$(function() {
+    // Set up the selection tool
+    add_tool("select", "Select", function() {
+    
+        // Turn on a crosshair cursor
+        googlemap.setOptions({
+            draggableCursor:"crosshair"
+        });
+    
+        // Add a listener to start the selection where the user clicks
+        var start_handle = add_tool_listener("click",
+            function(event) {
+            
+            // Don't trigger again
+            remove_tool_listener(start_handle);
+            
+            // Turn on a crosshair cursor again
+            googlemap.setOptions({
+                draggableCursor:"crosshair"
+            });
+            
+            // Store the start of the selection
+            var selection_start = event.latLng;
+            
+            print("Selection started at " + selection_start);
+            
+            // Make a rectangle for the selection
+            var rectangle = new google.maps.Rectangle({
+                fillColor: "#FFFFFF",
+                strokeColor: "#FFFFFF",
+                strokeWeight: 2,
+                strokeOpacity: 1.0,
+                fillOpacity: 0.5,
+                // Don't give us a clickable cursor, or take mouse events.
+                clickable: false, 
+                map: googlemap,
+                bounds: new google.maps.LatLngBounds(selection_start, 
+                    selection_start)
+            });
+            
+            // This holds a selection preview event handler that should happen
+            // when we mouse over the map or the rectangle.
+            var preview = function(event) {
+                
+                // Store the end of the selection (provisionally)
+                var selection_end = event.latLng;
+                
+                
+                if(selection_end.lng() < selection_start.lng()) {
+                    // The user has selected a backwards rectangle, which wraps
+                    // across the place where the globe is cut. None of our 
+                    // selections ever need to do this.
+                    
+                    // Make the rectangle backwards
+                    rectangle.setBounds(new google.maps.LatLngBounds(
+                        selection_end, selection_start));    
+                    
+                } else {
+                    // Make the rectangle forwards
+                    rectangle.setBounds(new google.maps.LatLngBounds(
+                        selection_start, selection_end));    
+                }
+            }
+            
+            // This holds a cleanup function to get rid of the rectangle when 
+            // the resizing listener goes away.
+            var preview_cleanup = function() {
+                // Remove the rectangle
+                rectangle.setMap(undefined);
+                
+                // Remove the crosshair cursor
+                googlemap.setOptions({
+                    draggableCursor: undefined
+                });
+            };
+            
+            // Add a mouse move listener for interactivity
+            // Works over the map, hexes, or the rectangle.
+            var move_handle = add_tool_listener("mousemove", preview, 
+                preview_cleanup);
+            
+            // We need a listener to finish the selection
+            var finish = function(event) {
+                // Don't trigger again
+                remove_tool_listener(stop_handle);
+                
+                // Also stop the dynamic updates. This removes the rectangle.
+                remove_tool_listener(move_handle);
+
+                // Store the end of the selection
+                var selection_end = event.latLng;
+                
+                print("Selection ended at " + selection_end);
+                    
+                // Select the rectangle by arbitrary corners.
+                select_rectangle(selection_start, selection_end);    
+            };
+            
+            // Attach the listener.
+            // The listener can still use its own handle because variable 
+            // references are resolved at runtime.
+            var stop_handle = add_tool_listener("click", finish, function() {
+                // Cleanup: say this tool is no longer selected
+                selected_tool = undefined;
+            });
+            
+        }, function() {
+            // Remove the crosshair cursor
+            googlemap.setOptions({
+                draggableCursor: undefined
+            });
+        });
+    });
+});
+
+// A tool for importing a list of hexes as a selection
+$(function() {
+    add_tool("import", "Import...", function() {
+        // Make the import form
+        var import_form = $("<form/>").attr("title", 
+            "Import List As Selection");
+        
+        import_form.append($("<div/>").text("Input names, one per line:"));
+        
+        // A big text box
+        var text_area = $("<textarea/>").addClass("import");
+        import_form.append(text_area);
+        
+        import_form.append($("<div/>").text(
+            "Open a file:"));
+            
+        // This holds a file form element
+        var file_picker = $("<input/>").attr("type", "file").addClass("import");
+        
+        import_form.append(file_picker);
+        
+        file_picker.change(function(event) {
+            // When a file is selected, read it in and populate the text box.
+            
+            // What file do we really want to read?
+            var file = event.target.files[0];
+            
+            // Make a FileReader to read the file
+            var reader = new FileReader();
+            
+            reader.onload = function(read_event) {  
+                // When we read with readAsText, we get a string. Just stuff it
+                // in the text box for the user to see.
+                text_area.text(reader.result);
+            };
+            
+            // Read the file, and, when it comes in, stick it in the textbox.
+            reader.readAsText(file);
+        });
+        
+        import_form.dialog({
+            modal: true,
+            buttons: {
+                "Import": function() {
+                    // Do the import of the data. The data in question is always
+                    // in the textbox.
+                    
+                    // Select all the entered hexes
+                    select_string(text_area.val());
+                    
+                    // Finally, close the dialog
+                    $(this).dialog("close");
+                    
+                    // Done with the tool
+                    selected_tool = undefined;
+                }   
+            },
+            close: function() {
+                // They didn't want to use this tool.
+                selected_tool = undefined;
+            }
+        });
+    });
+});
+
+// The actual text to selection import function used by that tool
+function select_string(string) {
+    // Given a string of hex names, one per line, make a selection of all those
+    // hexes.
+    
+    // This is an array of signature names entered.
+    var to_select = [];
+    
+    // This holds the array of lines. Split on newlines (as seen in
+    // jQuery.tsv.js)
+    var lines = string.split(/\r?\n/);
+    
+    for(var i = 0; i < lines.length; i++) {
+        // Trim and add to our requested selection
+        to_select.push(lines[i].trim());
+    }
+    
+    // Add a selection with as many of the requested hexes as actually exist and
+    // pass the current filters.
+    select_list(to_select);
+}
+
+// And a tool for exporting selections as lists of hexes
+$(function() {
+    add_tool("export", "Export...", function() {
+        // Make the export form
+        var export_form = $("<form/>").attr("title", 
+            "Export Selection As List");
+        
+        export_form.append($("<div/>").text("Select a selection to export:"));
+        
+        // Make a select box for picking from all selections.
+        var select_box = $("<select/>");
+        
+        // Populate it with all existing selections
+        for(var layer_name in layers) {
+            if(layers[layer_name].selection) {
+                // This is a selection, so add it to the dropdown.
+                select_box.append($("<option/>").text(layer_name).attr("value",
+                    layer_name));
+            }
+        }
+        
+        export_form.append(select_box);
+        
+        export_form.append($("<div/>").text("Exported data:"));
+        
+        // A big text box
+        var text_area = $("<textarea/>").addClass("export");
+        text_area.prop("readonly", true);
+        export_form.append(text_area);
+        
+        // Add a download as file link. The "download" attribute makes the
+        // browser save it, and the href data URI holds the data.
+        var download_link = $("<a/>").attr("download", "selection.txt");
+        download_link.attr("href", "data:text/plain;base64,");
+        download_link.text("Download As Text");
+        
+        export_form.append(download_link);
+        
+        text_area.focus(function() {
+            // Select all on focus.
+            
+            $(this).select();
+        });
+        
+        text_area.mouseup(function(event) {
+            // Don't change selection on mouseup. See
+            // http://stackoverflow.com/a/5797700/402891 and
+            // http://stackoverflow.com/q/3380458/402891
+            event.preventDefault();
+        });
+        
+        select_box.change(function() {
+            // Update the text area with the list of hexes in the selected
+            // layer.
+            
+            // Get the layer name.
+            var layer_name = select_box.val();
+            if(!have_layer(layer_name)) {
+                // Not a real layer.
+                // Probably just an empty select or something
+                return;
+            }
+            
+            // This holds our list. We build it in a string so we can escape it
+            // with one .text() call when adding it to the page.
+            var exported = "";
+            
+            // Get the layer data to export
+            var layer_data = layers[layer_name].data;
+            for(var signature in layer_data) {
+                if(layer_data[signature]) {
+                    // It's selected, put it in
+                    
+                    if(exported != "") {
+                        // If there's already text, put a newline first.
+                        exported += "\n";
+                    }
+                    
+                    exported += signature;
+                }
+            }
+            
+            // Now we know all the signatures from the selection, so tell the
+            // page.
+            text_area.text(exported);
+            
+            // Also fill in the data URI for saving. We use the handy
+            // window.bota encoding function.
+            download_link.attr("href", "data:text/plain;base64," + 
+                window.btoa(exported));
+        });
+        
+        // Trigger the change event on the select box for the first selected
+        // thing, if any.
+        select_box.change();
+        
+        export_form.dialog({
+            modal: true,
+            buttons: {
+                "Done": function() {
+                    // First, close the dialog
+                    $(this).dialog("close");
+                    
+                    // Done with the tool
+                    selected_tool = undefined;
+                }   
+            },
+            close: function() {
+                // They didn't want to use this tool.
+                selected_tool = undefined;
+            }
+        });
+    });
+});