Mercurial > repos > adam-novak > hexagram
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hexagram-6ae12361157c/hexagram/hexagram.js~ Tue Oct 22 14:17:59 2013 -0400 @@ -0,0 +1,3848 @@ +// 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; + + document.getElementById('current-layout').innerHTML=current_layout; + initialize_view (0); + recreate_map(layout_name, 1); + refresh (); + // Don't actually change the selection. + // This keeps the dropdown open when we click. + event.preventDefault(); + + current_layout_name = layout_name; + }); + + drl_values(layout_names[0]); + assignment_values (layout_names[0], 1); + current_layout_name = layout_names[0]; + +}); + + +