view hexagram-6ae12361157c/hexagram/hexagram.js @ 0:1407e3634bcf draft default tip

Uploaded r11 from test tool shed.
author adam-novak
date Tue, 22 Oct 2013 14:17:59 -0400
parents
children
line wrap: on
line source

// 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);
            
            if(layout_names.length == 1) {
                // This is the very first layout. Pull it up.
                    
                // TODO: We don't go through the normal change event since we
                // never change the dropdown value actually. But we duplicate
                // user selection hode here.
                var current_layout = "Current Layout: " + layout_names[0];         
	 
		        $("#current-layout").text(current_layout);
		        initialize_view (0);
                recreate_map(layout_names[0], 1);
		        refresh ();
		        current_layout_name = layout_names[0];
                
            }
        }     
    }, "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;         
	 
		$("#current-layout").text(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];

});