Mercurial > repos > adam-novak > hexagram
view hexagram-6ae12361157c/hexagram/hexagram.js @ 0:1407e3634bcf draft default tip
Uploaded r11 from test tool shed.
author | adam-novak |
---|---|
date | Tue, 22 Oct 2013 14:17:59 -0400 |
parents | |
children |
line wrap: on
line source
// hexagram.js // Run the hexagram visualizer client. // Globals // This is a mapping from coordinates [x][y] in the global hex grid to signature // name var signature_grid = []; // This holds a global list of layer pickers in layer order. It is also the // authority on what layers are currently selected. var layer_pickers = []; // This holds a list of layer objects by name. // Layer objects have: // A downloading function "downloader" // A data object (from hex name to float) "data" // A magnitude "magnitude" // A boolean "selection" that specifies whether this is a user selection or not. // (This may be absent, which is the same as false.) // Various optional metadata fields var layers = {}; // This is a list of layer names maintained in sorted order. var layer_names_sorted = []; // This is a list of the map-layour names mantained in order of entry var layout_names = []; // This holds an array of layer names that the user has added to the "shortlist" // They can be quickly selected for display. var shortlist = []; // This holds an object form shortlisted layer names to jQuery shortlist UI // elements, so we can efficiently tell if e.g. one is selected. var shortlist_ui = {}; // This is a list of layer names whose intersection checkbox has been selected. var shortlist_intersection = []; //This is the number of intersection checkboxes that have been selected. var shortlist_intersection_num = 0; // This is a list of layer names whose union checkbox has been selected. var shortlist_union = []; //This is the number of union checkboxes that have been selected. var shortlist_union_num = 0; //This is a list of layer names whose set difference checkbox has been selected. var shortlist_set_difference = []; // This is the number of set difference checkboxes that have been selected. var shortlist_set_difference_num = 0; // This is a list of the layer names whose symmetric difference checkbox // has been selected. var shortlist_symmetric_difference = []; // This is the number of symmetric difference checkboxes that have been // selected. var shortlist_symmetric_difference_num = 0; // This is an array containing the layer whose absolute complement checkbox // has been selected. var shortlist_absolute_complement = []; // This is the number of absolute complement checkboxes that have been selected. var shortlist_absolute_complement_num = 0; // Records number of set-operation clicks var set_operation_clicks = 0; // Boolean stating whether this is the first time the set operation popup // has been created so that "Select Layer" Default is added only once var first_opening = true; // Boolean for Creating Layer from Filter var created = false; // Stores the Name of Current Layer Displayed var current_layout_name; // This holds colormaps (objects from layer values to category objects with a // name and color). They are stored under the name of the layer they apply to. var colormaps = {} // This holds an array of the available score matrix filenames var available_matrices = []; // This holds the Google Map that we use for visualization var googlemap = null; // This is the global Google Maps info window. We only want one hex to have its // info open at a time. var info_window = null; // This holds the signature name of the hex that the info window is currently // about. var selected_signature = undefined; // Which tool is the user currently using (string name or undefined for no tool) // TODO: This is a horrible hack, replace it with a unified tool system at once. var selected_tool = undefined; // This holds the grid of hexagon polygons on that Google Map. var polygon_grid = []; // This holds an object of polygons by signature name var polygons = {}; // How big is a hexagon in google maps units? This gets filled in once we have // the hex assignment data. (This is really the side length.) var hex_size; // This holds a handle for the currently enqueued view redrawing timeout. var redraw_handle; // This holds all the currently active tool event listeners. // They are indexed by handle, and are objects with a "handler" and an "event". var tool_listeners = {}; // This holds the next tool listener handle to give out var tool_listener_next_id = 0; // This holds the next selection number to use. Start at 1 since the user sees // these. var selection_next_id = 1; // This is a pool of statistics Web Workers. var rpc_workers = []; // This holds which RPC worker we ought to give work to next. // TODO: Better scheduling, and wrap all this into an RPC object. var next_free_worker = 0; // This holds how namy RPC jobs are currently running var jobs_running = 0; // This is the object of pending callbacks by RPC id var rpc_callbacks = {}; // This is the next unallocated RPC id var rpc_next_id = 0; // How many statistics Web Workers should we start? var NUM_RPC_WORKERS = 10; // What's the minimum number of pixels that hex_size must represent at the // current zoom level before we start drawing hex borders? var MIN_BORDER_SIZE = 10; // And how thick should the border be when drawn? var HEX_STROKE_WEIGHT = 2; // How many layers do we know how to draw at once? var MAX_DISPLAYED_LAYERS = 2; // How many layer search results should we display at once? var SEARCH_PAGE_SIZE = 10; // How big is our color key in pixels? var KEY_SIZE = 100; // This is an array of all Google Maps events that tools can use. var TOOL_EVENTS = [ "click", "mousemove" ]; // This is a global variable that keeps track of the current Goolge Map zoom // This is needed to keep viewing consistent across layouts var global_zoom = 0; function print(text) { // Print some logging text to the browser console if(console && console.log) { // We know the console exists, and we can log to it. console.log(text); } } function complain(text) { // Display a temporary error message to the user. $("#error-notification").text(text); $(".error").show().delay(1250).fadeOut(1000); if(console && console.error) { // Inform the browser console of this problem.as console.error(text); } } function make_hexagon(row, column, hex_side_length, grid_offset) { // Make a new hexagon representing the hexagon at the given grid coordinates. // hex_side_length is the side length of hexagons in Google Maps world // coordinate units. grid_offset specifies a distance to shift the whole // grid down and right from the top left corner of the map. This lets us // keep the whole thing away from the edges of the "earth", where Google // Maps likes to wrap. // Returns the Google Maps polygon. // How much horizontal space is needed per hex on average, stacked the // way we stack them (wiggly)? var hex_column_width = 3.0/2.0 * hex_side_length; // How tall is a hexagon? var hex_height = Math.sqrt(3) * hex_side_length; // How far apart are hexagons on our grid, horizontally (world coordinate units)? var hex_padding_horizontal = 0; // And vertically (world coordinate units)? var hex_padding_veritcal = 0; // First, what are x and y in 0-256 world coordinates fo this grid position? var x = column * (hex_column_width + hex_padding_horizontal); var y = row * (hex_height + hex_padding_veritcal); if(column % 2 == 1) { // Odd columns go up y -= hex_height / 2; } // Apply the grid offset to this hex x += grid_offset; y += grid_offset; // That got X and Y for the top left corner of the bounding box. Shift to // the center. x += hex_side_length; y += hex_height / 2; // Offset the whole thing so no hexes end up off the map when they wiggle up y += hex_height / 2; // This holds an array of all the hexagon corners var coords = [ get_LatLng(x - hex_side_length, y), get_LatLng(x - hex_side_length / 2, y - hex_height / 2), get_LatLng(x + hex_side_length / 2, y - hex_height / 2), get_LatLng(x + hex_side_length, y), get_LatLng(x + hex_side_length / 2, y + hex_height / 2), get_LatLng(x - hex_side_length / 2, y + hex_height / 2), ]; // We don't know whether the hex should start with a stroke or not without // looking at the current zoom level. // Get the current zoom level (low is out) var zoom = googlemap.getZoom(); // API docs say: pixelCoordinate = worldCoordinate * 2 ^ zoomLevel // So this holds the number of pixels that the global length hex_size // corresponds to at this zoom level. var hex_size_pixels = hex_size * Math.pow(2, zoom); // Construct the Polygon var hexagon = new google.maps.Polygon({ paths: coords, strokeColor: "#000000", strokeOpacity: 1.0, // Only turn on the border if we're big enough strokeWeight: hex_size_pixels < MIN_BORDER_SIZE ? 0 : HEX_STROKE_WEIGHT, fillColor: "#FF0000", fillOpacity: 1.0 }); // Attach the hexagon to the global map hexagon.setMap(googlemap); // Set up the click listener to move the global info window to this hexagon // and display the hexagon's information google.maps.event.addListener(hexagon, "click", function(event) { if(selected_tool == undefined) { // The user isn't trying to use a tool currently, so we can use // their clicks for the infowindow. // Remove the window from where it currently is info_window.close(); // Place the window in the center of this hexagon. info_window.setPosition(get_LatLng(x, y)); // Record that this signature is selected now selected_signature = hexagon.signature; // Calculate the window's contents and make it display them. redraw_info_window(); } }); // Subscribe the tool listeners to events on this hexagon subscribe_tool_listeners(hexagon); return hexagon; } function set_hexagon_signature(hexagon, text) { // Given a polygon representing a hexagon, set the signature that the // hexagon represents. hexagon.signature = text; } function set_hexagon_color(hexagon, color) { // Given a polygon, set the hexagon's current background // color. hexagon.setOptions({ fillColor: color }); } function set_hexagon_stroke_weight(hexagon, weight) { // Given a polygon, set the weight of hexagon's border stroke, in number of // screen pixels. hexagon.setOptions({ strokeWeight: weight }); } function redraw_info_window() { // Set the contents of the global info window to reflect the currently // visible information about the global selected signature. if(selected_signature == undefined) { // No need to update anything return; } // Go get the infocard that goes in the info_window and, when it's // prepared, display it. with_infocard(selected_signature, function(infocard) { // The [0] is supposed to get the DOM element from the jQuery // element. info_window.setContent(infocard[0]); // Open the window. It may already be open, or it may be closed but // properly positioned and waiting for its initial contents before // opening. info_window.open(googlemap); }); } function with_infocard(signature, callback) { // Given a signature, call the callback with a jQuery element representing // an "info card" about that signature. It's the contents of the infowindow // that we want to appear when the user clicks on the hex representing this // signature, and it includes things like the signature name and its values // under any displayed layers (with category names if applicable). // We return by callback because preparing the infocard requires reading // from the layers, which are retrieved by callback. // TODO: Can we say that we will never have to download a layer here and // just directly access them? Is that neater or less neat? // Using jQuery to build this saves us from HTML injection by making jQuery // do all the escaping work (we only ever set text). function row(key, value) { // Small helper function that returns a jQuery element that displays the // given key being the given value. // This holds the root element of the row var root = $("<div/>").addClass("info-row"); // Add the key and value elements root.append($("<div/>").addClass("info-key").text(key)); root.append($("<div/>").addClass("info-value").text(value)); return root; } // This holds a list of the string names of the currently selected layers, // in order. // Just use everything on the shortlist. var current_layers = shortlist; // Obtain the layer objects (mapping from signatures/hex labels to colors) with_layers(current_layers, function(retrieved_layers) { // This holds the root element of the card. var infocard = $("<div/>").addClass("infocard"); infocard.append(row("Name", signature).addClass("info-name")); for(var i = 0; i < current_layers.length; i++) { // This holds the layer's value for this signature var layer_value = retrieved_layers[i].data[signature]; if(have_colormap(current_layers[i])) { // This is a color map // This holds the category object for this category number, or // undefined if there isn't one. var category = colormaps[current_layers[i]][layer_value]; if(category != undefined) { // There's a specific entry for this category, with a // human-specified name and color. // Use the name as the layer value layer_value = category.name; } } if(layer_value == undefined) { // Let the user know that there's nothing there in this layer. layer_value = "<undefined>"; } // Make a listing for this layer's value infocard.append(row(current_layers[i], layer_value)); } // Return the infocard by callback callback(infocard); }); } function add_layer_url(layer_name, layer_url, attributes) { // Add a layer with the given name, to be downloaded from the given URL, to // the list of available layers. // Attributes is an object of attributes to copy into the layer. // Store the layer. Just keep the URL, since with_layer knows what to do // with it. layers[layer_name] = { url: layer_url, data: undefined, magnitude: undefined }; for(var name in attributes) { // Copy over each specified attribute layers[layer_name][name] = attributes[name]; } // Add it to the sorted layer list. layer_names_sorted.push(layer_name); // Don't sort because our caller does that when they're done adding layers. } function add_layer_data(layer_name, data, attributes) { // Add a layer with the given name, with the given data to the list of // available layers. // Attributes is an object of attributes to copy into the layer. // Store the layer. Just put in the data. with_layer knows what to do if the // magnitude isn't filled in. layers[layer_name] = { url: undefined, data: data, magnitude: undefined }; var check_layer_exists = layers[layer_name]; for(var name in attributes) { // Copy over each specified attribute layers[layer_name][name] = attributes[name]; } // Add it to the sorted layer list and sort layer_names_sorted.push(layer_name); // Don't sort because our caller does that when they're done adding layers. } function with_layer(layer_name, callback) { // Run the callback, passing it the layer (object from hex label/signature // to float) with the given name. // This is how you get layers, and allows for layers to be downloaded // dynamically. // have_layer must return true for the given name. // First get what we have stored for the layer var layer = layers[layer_name]; var data_val = layer.data; if(layer.data == undefined) { // We need to download the layer. print("Downloading \"" + layer.url + "\""); // Go get it (as text!) $.get(layer.url, function(layer_tsv_data) { // This is the TSV as parsed by our TSV-parsing plugin var layer_parsed = $.tsv.parseRows(layer_tsv_data); // This is the layer we'll be passing out. Maps from // signatures to floats on -1 to 1. var layer_data = {}; for(var j = 0; j < layer_parsed.length; j++) { // This is the label of the hex var label = layer_parsed[j][0]; if(label == "") { // Skip blank lines continue; } // This is the heat level (-1 to 1) var heat = parseFloat(layer_parsed[j][1]); // Store in the layer layer_data[label] = heat; } // Save the layer data locally layers[layer_name].data = layer_data; // Now the layer has been properly downloaded, but it may not have // metadata. Recurse with the same callback to get metadata. with_layer(layer_name, callback); }, "text"); } else if(layer.magnitude == undefined) { // We've downloaded it already, or generated it locally, but we don't // know the magnitude. Compute that and check if it's a colormap. // Grab the data, which we know is defined. var layer_data = layers[layer_name].data; // Store the maximum magnitude in the layer // -1 is a good starting value since this always comes out positive var magnitude = -1; // We also want to know if all layer entries are non-negative // integers (and it is thus valid as a colormap). // If so, we want to display it as a colormap, so we will add an // empty entry to the colormaps object (meaning we should // auto-generate the colors on demand). // This stores whether the layer is all integrs all_nonnegative_integers = true; for(var signature_name in layer_data) { // Take the new max if it's bigger (and thus not something silly // like NaN). // This holds the potential new max magnitude. var new_magnitude = Math.abs(layer_data[signature_name]); if(new_magnitude > magnitude) { magnitude = new_magnitude; } if(layer_data[signature_name] % 1 !== 0 || layer_data[signature_name] < 0 ) { // If we have an illegal value for a colormap, record that // fact // See http://stackoverflow.com/a/3886106 all_nonnegative_integers = false; } } // Save the layer magnitude for later. layer.magnitude = magnitude; if(!have_colormap(layer_name) && all_nonnegative_integers) { // Add an empty colormap for this layer, so that // auto-generated discrete colors will be used. // TODO: Provide some way to override this if you really do want // to see integers as a heatmap? // The only overlap with the -1 to 1 restricted actual layers // is if you have a data set with only 0s and 1s. Is it a // heatmap layer or a colormap layer? colormaps[layer_name] = {}; print("Inferring that " + layer_name + " is really a colormap"); } // Now layer metadata has been filled in. Call the callback. callback(layer); } else { // It's already downloaded, and already has metadata. // Pass it to our callback callback(layer); } } function with_layers(layer_list, callback) { // Given an array of layer names, call the callback with an array of the // corresponding layer objects (objects from signatures to floats). // Conceptually it's like calling with_layer several times in a loop, only // because the whole thing is continuation-based we have to phrase it in // terms of recursion. // See http://marijnhaverbeke.nl/cps/ // "So, we've created code that does exactly the same as the earlier // version, but is twice as confusing." if(layer_list.length == 0) { // Base case: run the callback with an empty list callback([]); } else { // Recursive case: handle the last thing in the list with_layers(layer_list.slice(0, layer_list.length - 1), function(rest) { // We've recursively gotten all but the last layer // Go get the last one, and pass the complete array to our callback. with_layer(layer_list[layer_list.length - 1], function(last) { // Mutate the array. Shouldn't matter because it won't matter // for us if callback does it. rest.push(last); // Send the complete array to the callback. callback(rest); }); }); } } function have_layer(layer_name) { // Returns true if a layer exists with the given name, false otherwise. return layers.hasOwnProperty(layer_name); } function make_shortlist_ui(layer_name) { // Return a jQuery element representing the layer with the given name in the // shortlist UI. // This holds the root element for this shortlist UI entry var root = $("<div/>").addClass("shortlist-entry"); root.data("layer", layer_name); // If this is a selection, give the layer a special class // TODO: Justify not having to use with_layer because this is always known // client-side if(layers[layer_name].selection) { root.addClass("selection"); } // We have some configuration stuff and then the div from the dropdown // This holds all the config stuff var controls = $("<div/>").addClass("shortlist-controls"); // Add a remove link var remove_link = $("<a/>").addClass("remove").attr("href", "#").text("X"); controls.append(remove_link); // Add a checkbox for whether this is enabled or not var checkbox = $("<input/>").attr("type", "checkbox").addClass("layer-on"); controls.append(checkbox); root.append(controls); var contents = $("<div/>").addClass("shortlist-contents"); // Add the layer name contents.append($("<span/>").text(layer_name)); // Add all of the metadata. This is a div to hold it var metadata_holder = $("<div/>").addClass("metadata-holder"); // Fill it in fill_layer_metadata(metadata_holder, layer_name); contents.append(metadata_holder); // Add a div to hold the filtering stuff so it wraps together. var filter_holder = $("<div/>").addClass("filter-holder"); // Add an image label for the filter control. // TODO: put this in a label var filter_image = $("<img/>").attr("src", "filter.svg"); filter_image.addClass("control-icon"); filter_image.addClass("filter-image"); filter_image.attr("title", "Filter on Layer"); filter_image.addClass("filter"); // Add a control for filtering var filter_control = $("<input/>").attr("type", "checkbox"); filter_control.addClass("filter-on"); filter_holder.append(filter_image); filter_holder.append(filter_control); // Add a text input to specify a filtering threshold for continuous layers var filter_threshold = $("<input/>").addClass("filter-threshold"); // Initialize to a reasonable value. filter_threshold.val(0); filter_holder.append(filter_threshold); // Add a select input to pick from a discrete list of values to filter on var filter_value = $("<select/>").addClass("filter-value"); filter_holder.append(filter_value); // Add a image for the save function var save_filter = $("<img/>").attr("src", "save.svg"); save_filter.addClass("save-filter"); save_filter.attr("title", "Save Filter as Layer"); contents.append(filter_holder); contents.append(save_filter); if(layers[layer_name].selection) { // We can do statistics on this layer. // Add a div to hold the statistics stuff so it wraps together. var statistics_holder = $("<div/>").addClass("statistics-holder"); // Add an icon var statistics_image = $("<img/>").attr("src", "statistics.svg"); statistics_image.addClass("control-icon"); statistics_image.attr("title", "Statistics Group"); statistics_holder.append(statistics_image); // Label the "A" radio button. var a_label = $("<span/>").addClass("radio-label").text("A"); statistics_holder.append(a_label); // Add a radio button for being the "A" group var statistics_a_control = $("<input/>").attr("type", "radio"); statistics_a_control.attr("name", "statistics-a"); statistics_a_control.addClass("statistics-a"); // Put the layer name in so it's easy to tell which layer is A. statistics_a_control.data("layer-name", layer_name); statistics_holder.append(statistics_a_control); // And a link to un-select it if it's selected var statistics_a_clear = $("<a/>").attr("href", "#").text("X"); statistics_a_clear.addClass("radio-clear"); statistics_holder.append(statistics_a_clear); // Label the "B" radio button. var b_label = $("<span/>").addClass("radio-label").text("B"); statistics_holder.append(b_label); // Add a radio button for being the "B" group var statistics_b_control = $("<input/>").attr("type", "radio"); statistics_b_control.attr("name", "statistics-b"); statistics_b_control.addClass("statistics-b"); // Put the layer name in so it's easy to tell which layer is A. statistics_b_control.data("layer-name", layer_name); statistics_holder.append(statistics_b_control); // And a link to un-select it if it's selected var statistics_b_clear = $("<a/>").attr("href", "#").text("X"); statistics_b_clear.addClass("radio-clear"); statistics_holder.append(statistics_b_clear); contents.append(statistics_holder); // Statistics UI logic // Make the clear links work statistics_a_clear.click(function() { statistics_a_control.prop("checked", false); }); statistics_b_clear.click(function() { statistics_b_control.prop("checked", false); }); } // Add a div to contain layer settings var settings = $("<div/>").addClass("settings"); // Add a slider for setting the min and max for drawing var range_slider = $("<div/>").addClass("range range-slider"); settings.append($("<div/>").addClass("stacker").append(range_slider)); // And a box that tells us what we have selected in the slider. var range_display = $("<div/>").addClass("range range-display"); range_display.append($("<span/>").addClass("low")); range_display.append(" to "); range_display.append($("<span/>").addClass("high")); settings.append($("<div/>").addClass("stacker").append(range_display)); contents.append(settings); root.append(contents); // Handle enabling and disabling checkbox.change(function() { if($(this).is(":checked") && get_current_layers().length > MAX_DISPLAYED_LAYERS) { // Enabling this checkbox puts us over the edge, so un-check it $(this).prop("checked", false); // Skip the redraw return; } refresh(); }); // Run the removal process remove_link.click(function() { // Remove this layer from the shortlist shortlist.splice(shortlist.indexOf(layer_name), 1); // Remove this from the DOM root.remove(); // Make the UI match the list. update_shortlist_ui(); if(checkbox.is(":checked") || filter_control.is(":checked")) { // Re-draw the view since we were selected (as coloring or filter) // before removal. refresh(); } }); // Functionality for turning filtering on and off filter_control.change(function() { if(filter_control.is(":checked")) { // First, figure out what kind of filter settings we take based on // what kind of layer we are. with_layer(layer_name, function(layer) { if(have_colormap(layer_name)) { // A discrete layer. // Show the value picker. filter_value.show(); // Make sure we have all our options if(filter_value.children().length == 0) { // No options available. We have to add them. // TODO: Is there a better way to do this than asking // the DOM? for(var i = 0; i < layer.magnitude + 1; i++) { // Make an option for each value. var option = $("<option/>").attr("value", i); if(colormaps[layer_name].hasOwnProperty(i)) { // We have a real name for this value option.text(colormaps[layer_name][i].name); } else { // No name. Use the number. option.text(i); } filter_value.append(option); } // Select the last option, so that 1 on 0/1 layers will // be selected by default. filter_value.val( filter_value.children().last().attr("value")); } } else { // Not a discrete layer, so we take a threshold. filter_threshold.show(); } save_filter.show (); save_filter.button().click(function() { // Configure Save Filter Buttons // Get selected value var selected = filter_value.prop("selectedIndex"); var value = filter_value.val(); var signatures = []; // Gather Tumor-ID Signatures with value and push to "signatures" for (hex in polygons){ if (layer.data[hex] == value){ signatures.push(hex); } } // Create Layer if (created == false) { select_list (signatures, "user selection"); created = true; } created = false; }); // Now that the right controls are there, assume they have refresh(); }); } else { created = false; // Hide the filtering settings filter_value.hide(); filter_threshold.hide(); save_filter.hide(); // Draw view since we're no longer filtering on this layer. refresh(); } }); // Respond to changes to filter configuration filter_value.change(refresh); // TODO: Add a longer delay before refreshing here so the user can type more // interactively. filter_threshold.keyup(refresh); // Configure the range slider // First we need a function to update the range display, which we will run // on change and while sliding (to catch both user-initiated and //programmatic changes). var update_range_display = function(event, ui) { range_display.find(".low").text(ui.values[0].toFixed(3)); range_display.find(".high").text(ui.values[1].toFixed(3)); } range_slider.slider({ range: true, min: -1, max: 1, values: [-1, 1], step: 1E-9, // Ought to be fine enough slide: update_range_display, change: update_range_display, stop: function(event, ui) { // The user has finished sliding // Draw the view. We will be asked for our values refresh(); } }); // When we have time, go figure out whether the slider should be here, and // what its end values should be. reset_slider(layer_name, root) return root; } // ____________________________________________________________________________ // Replacement Set Operation Code // ____________________________________________________________________________ function get_set_operation_selection () { // For the new dop-down GUI for set operation selection // we neeed a function to determine which set operation is selected. // This way we can display the appropriate divs. // Drop Down List & Index for Selected Element var drop_down = document.getElementById("set-operations-list"); var index = drop_down.selectedIndex; var selection = drop_down.options[index]; return selection; } function show_set_operation_drop_down () { // Show Set Operation Drop Down Menu document.getElementsByClassName("set-operation-col")[0].style.visibility="visible"; document.getElementsByClassName("set-operation-panel-holder")[0].style.visibility="visible"; document.getElementsByClassName("set-operation-panel")[0].style.visibility="visible"; document.getElementById("set-operations").style.visibility="visible"; document.getElementsByClassName("set-operation-panel-title")[0].style.visibility="visible"; document.getElementsByClassName("set-operation-panel-contents")[0].style.visibility="visible"; } function hide_set_operation_drop_down () { // Hide Set Operation Drop Down Menu document.getElementsByClassName("set-operation-col")[0].style.visibility="hidden"; document.getElementsByClassName("set-operation-panel-holder")[0].style.visibility="hidden"; document.getElementsByClassName("set-operation-panel")[0].style.visibility="hidden"; document.getElementById("set-operations").style.visibility="hidden"; document.getElementsByClassName("set-operation-panel-title")[0].style.visibility="hidden"; document.getElementsByClassName("set-operation-panel-contents")[0].style.visibility="hidden"; // Hide the Data Values for the Selected Layers var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value"); for (var i = 0; i < drop_downs_layer_values.length; i++) { drop_downs_layer_values[i].style.visibility="hidden"; } // Hide the Compute Button var compute_button = document.getElementsByClassName("compute-button"); compute_button[0].style.visibility = "hidden"; // Set the "Select Layer" drop down to the default value var list = document.getElementById("set-operations-list"); list.selectedIndex = 0; var list_value = document.getElementsByClassName("set-operation-value"); list_value[0].selectedIndex = 0; list_value[1].selectedIndex = 0; // Remove all elements from drop downs holding the data values for the // selected layers. This way there are no values presented when the user // clicks on the set operation button to open it again. var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value"); var length = set_operation_layer_values[0].options.length; do{ set_operation_layer_values[0].remove(0); length--; } while (length > 0); var length = set_operation_layer_values[1].options.length; do{ set_operation_layer_values[1].remove(0); length--; } while (length > 0); } function create_set_operation_ui () { // Returns a Jquery element that is then prepended to the existing // set theory drop-down menu // This holds the root element for this set operation UI var root = $("<div/>").addClass("set-operation-entry"); // Add Drop Downs to hold the selected layers and and selected data values var set_theory_value1 = $("<select/>").addClass("set-operation-value"); var set_theory_layer_value1 = $("<select/>").addClass("set-operation-layer-value"); var set_theory_value2 = $("<select/>").addClass("set-operation-value"); var set_theory_layer_value2 = $("<select/>").addClass("set-operation-layer-value"); var compute_button = $("<input/>").attr("type", "button"); compute_button.addClass ("compute-button"); // Append to Root root.append (set_theory_value1); root.append (set_theory_layer_value1); root.append (set_theory_value2); root.append (set_theory_layer_value2); root.append (compute_button); return root; } function update_set_operation_drop_down () { // This is the onchange command for the drop down displaying the // different set operation functions. It is called whenever the user changes // the selected set operation. // Get the value of the set operation selection made by the user. var selection = get_set_operation_selection(); var value = selection.value; // Check if the selectin value is that of one of set operation functions if (selection.value == 1 || selection.value == 2 || selection.value == 3 || selection.value == 4 || selection.value == 5){ // Make the drop downs that hold layer names and data values visible var drop_downs = document.getElementsByClassName("set-operation-value"); var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value"); for (var i = 0; i < drop_downs.length; i++) { drop_downs[i].style.visibility="visible"; } for (var i = 0; i < drop_downs_layer_values.length; i++) { drop_downs_layer_values[i].style.visibility="visible"; } var compute_button = document.getElementsByClassName("compute-button"); compute_button[0].style.visibility = "visible"; compute_button[0].value = "Compute Set Operation"; if (first_opening == true) { // Set the default value for the drop down, holding the selected layers var default_value = document.createElement("option"); default_value.text = "Select Layer 1"; default_value.value = 0; drop_downs[0].add(default_value); var default_value2 = document.createElement("option"); default_value2.text = "Select Layer 2"; default_value2.value = 0; drop_downs[1].add(default_value2); // Prevent from adding the default value again first_opening = false; } // Hide the second set of drop downs if "Not:" is selected if (selection.value == 5) { drop_downs[1].style.visibility="hidden"; drop_downs_layer_values[1].style.visibility="hidden"; } } else { // If the user has the default value selected, hide all drop downs var drop_downs = document.getElementsByClassName("set-operation-value"); for (var i = 0; i < drop_downs.length; i++) { drop_downs[i].style.visibility="hidden"; } var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value"); for (var i = 0; i < drop_downs_layer_values.length; i++) { drop_downs_layer_values[i].style.visibility="hidden"; } var compute_button = document.getElementsByClassName("compute-button"); compute_button[0].style.visibility = "hidden"; } } function update_set_operation_selections () { // This function is called when the shorlist is changed. // It appropriately updates the drop down containing the list of layers // to match the layers found in the shortlist. // Get the list of all layers var layers = []; $("#shortlist").children().each(function(index, element) { // Get the layer name var layer_name = $(element).data("layer"); layers.push(layer_name); }); // Get a list of all drop downs that contain layer names var drop_downs = document.getElementsByClassName("set-operation-value"); // Remove all existing layer names from both dropdowns var length = drop_downs[0].options.length; do{ drop_downs[0].remove(0); length--; } while (length > 0); var length = drop_downs[1].options.length; do{ drop_downs[1].remove(0); length--; } while (length > 0); // Add the default values that were stripped in the last step. var default_value = document.createElement("option"); default_value.text = "Select Layer 1"; default_value.value = 0; drop_downs[0].add(default_value); var default_value2 = document.createElement("option"); default_value2.text = "Select Layer 2"; default_value2.value = 0; drop_downs[1].add(default_value2); first_opening = false; // Add the layer names from the shortlist to the drop downs that store // layer names. for (var i = 0; i < drop_downs.length; i++){ for (var j = 0; j < layers.length; j++) { var option = document.createElement("option"); option.text = layers[j]; option.value = j+1; drop_downs[i].add(option); } } // Remove all elements from drop downs holding the data values for the // selected layers. This way there are no values presented when the user // clicks on the set operation button to open it again. var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value"); var length = set_operation_layer_values[0].options.length; do{ set_operation_layer_values[0].remove(0); length--; } while (length > 0); var length = set_operation_layer_values[1].options.length; do{ set_operation_layer_values[1].remove(0); length--; } while (length > 0); // Call the function containing onchange commands for these dropdowns. // This way the data values are updated according the the selected layer. update_set_operation_data_values (); } function update_set_operation_data_values () { // Define the onchange commands for the drop downs that hold layer names. // This way the data values are updated according the the selected layer. // Get all drop down elements var selected_function = document.getElementById ("set-operations-list"); var drop_downs = document.getElementsByClassName("set-operation-value"); var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value"); // The "Select Layer1" Dropdown onchange function drop_downs[0].onchange = function(){ // Strip current values of the data value dropdown var length = set_operation_layer_values[0].options.length; do{ set_operation_layer_values[0].remove(0); length--; } while (length > 0); // Add the data values depending on the selected layer var selectedIndex = drop_downs[0].selectedIndex; var layer_name = drop_downs[0].options[selectedIndex].text; var set_operation_data_value_select = set_operation_layer_values[0]; create_set_operation_pick_list(set_operation_data_value_select, layer_name); }; // The "Select Layer2" Dropdown onchange function drop_downs[1].onchange = function(){ // Strip current values of the data value dropdown var length = set_operation_layer_values[1].options.length; do{ set_operation_layer_values[1].remove(0); length--; } while (length > 0); // Add the data values depending on the selected layer var selectedIndex = drop_downs[1].selectedIndex; var layer_name = drop_downs[1].options[selectedIndex].text; var set_operation_data_value_select = set_operation_layer_values[1]; create_set_operation_pick_list(set_operation_data_value_select, layer_name); }; } function create_set_operation_pick_list(value,layer_object) { // We must create a drop down containing the data values for the selected // layer. // The Javascript "select" element that contains the data values // is passed as "value" and the selected layer is passed as "layer_object". // First, figure out what kind of filter settings we take based on // what kind of layer we are. with_layer(layer_object, function(layer) { // No options available. We have to add them. for(var i = 0; i < layer.magnitude + 1; i++) { // Make an option for each value; var option = document.createElement("option"); option.value = i; if(colormaps[layer_object].hasOwnProperty(i)) { // We have a real name for this value option.text = (colormaps[layer_object][i].name); } else { // No name. Use the number. option.text = i; } value.add(option); // Select the last option, so that 1 on 0/1 layers will // be selected by default. var last_index = value.options.length - 1; value.selectedIndex = last_index; } // Now that the right controls are there, assume they have refresh(); }); } function update_shortlist_ui() { // Go through the shortlist and make sure each layer there has an entry in // the shortlist UI, and that each UI element has an entry in the shortlist. // Also make sure the metadata for all existing layers is up to date. // Clear the existing UI lookup table shortlist_ui = {}; for(var i = 0; i < shortlist.length; i++) { // For each shortlist entry, put a false in the lookup table shortlist_ui[shortlist[i]] = false; } $("#shortlist").children().each(function(index, element) { if(shortlist_ui[$(element).data("layer")] === false) { // There's a space for this element: it's still in the shortlist // Fill it in shortlist_ui[$(element).data("layer")] = $(element); // Update the metadata in the element. It make have changed due to // statistics info coming back. fill_layer_metadata($(element).find(".metadata-holder"), $(element).data("layer")); } else { // It wasn't in the shortlist, so get rid of it. $(element).remove(); } }); for(var layer_name in shortlist_ui) { // For each entry in the lookup table if(shortlist_ui[layer_name] === false) { // If it's still false, make a UI element for it. shortlist_ui[layer_name] = make_shortlist_ui(layer_name); $("#shortlist").prepend(shortlist_ui[layer_name]); // Check it's box if possible shortlist_ui[layer_name].find(".layer-on").click(); } } // Make things re-orderable // Be sure to re-draw the view if the order changes, after the user puts // things down. $("#shortlist").sortable({ update: refresh, // Sort by the part with the lines icon, so we can still select text. handle: ".shortlist-controls" }); update_set_operation_selections (); } function uncheck_checkbox (checkbox_class) { // Unchecks chekboxes after the function has been completed. var checkboxArray = new Array (); checkboxArray = document.getElementsByClassName(checkbox_class); for (var i = 0; i < checkboxArray.length; i++) { checkboxArray[i].checked = false; } } function hide_values (set_theory_function) { // Hides pick lists for set theory functions after function has been // completed. var value_type = set_theory_function + '-value'; var values = new Array (); values = document.getElementsByClassName(value_type); var length = values.length; for (var i = 0; i < length; i++) { values[i].style.display = 'none'; } refresh(); } function compute_intersection (values, intersection_layer_names, text) { // A function that will take a list of layer names // that have been selected for the intersection utility. // Fetches the respective layers and list of tumor ids. // Then compares data elements of the same tumor id // between both layers. Adds these hexes to a new layer // for visualization //Array of signatures that intersect var intersection_signatures = []; with_layers (intersection_layer_names, function (intersection_layers) { // Gather Tumor-ID Signatures. for (hex in polygons) { if (intersection_layers[0].data[hex] == values[0] && intersection_layers[1].data[hex] == values[1]){ intersection_signatures.push(hex); } } }); for (var i = 0; i < intersection_layer_names.length; i++){ intersection_layer_names[i] = intersection_layer_names[i] + " [" + text[i] + "]"; } var intersection_function = "intersection"; select_list (intersection_signatures, intersection_function, intersection_layer_names); uncheck_checkbox ('intersection-checkbox'); hide_values('intersection'); } function compute_union (values, union_layer_names, text) { // A function that will take a list of layer names // that have been selected for the union utility. // Fetches the respective layers and list of tumor ids. // Then compares data elements of the same tumor id // between both layers. Adds these hexes to a new layer // for visualization //Array of signatures var union_signatures = []; with_layers (union_layer_names, function (union_layers) { // Gather Tumor-ID Signatures. for (hex in polygons) { // Union Function if (union_layers[0].data[hex] == values[0] || union_layers[1].data[hex] == values[1]){ union_signatures.push(hex); } } }); for (var i = 0; i < union_layer_names.length; i++){ union_layer_names[i] = union_layer_names[i] + " [" + text[i] + "]"; } var union_function = "union"; select_list (union_signatures, union_function, union_layer_names); uncheck_checkbox ('union-checkbox'); hide_values('union'); } function compute_set_difference (values, set_difference_layer_names, text) { // A function that will take a list of layer names // that have been selected for the set difference utility. // Fetches the respective layers and list of tumor ids. // Then compares data elements of the same tumor id // between both layers. Adds these hexes to a new layer // for visualization //Array of signatures var set_difference_signatures = []; with_layers (set_difference_layer_names, function (set_difference_layers) { // Gather Tumor-ID Signatures. for (hex in polygons) { // Set Difference Function if (set_difference_layers[0].data[hex] == values[0] && set_difference_layers[1].data[hex] != values[1]){ set_difference_signatures.push(hex); } } }); for (var i = 0; i < set_difference_layer_names.length; i++){ set_difference_layer_names[i] = set_difference_layer_names[i] + " [" + text[i] + "]"; } var set_difference_function = "set difference"; select_list (set_difference_signatures, set_difference_function, set_difference_layer_names); uncheck_checkbox ('set-difference-checkbox'); hide_values('set-difference'); } function compute_symmetric_difference (values, symmetric_difference_layer_names, text) { // A function that will take a list of layer names // that have been selected for the set difference utility. // Fetches the respective layers and list of tumor ids. // Then compares data elements of the same tumor id // between both layers. Adds these hexes to a new layer // for visualization //Array of signatures var symmetric_difference_signatures = []; with_layers (symmetric_difference_layer_names, function (symmetric_difference_layers) { // Gather Tumor-ID Signatures. for (hex in polygons) { // Symmetric Difference Function if (symmetric_difference_layers[0].data[hex] == values[0] && symmetric_difference_layers[1].data[hex] != values[1]){ symmetric_difference_signatures.push(hex); } if (symmetric_difference_layers[0].data[hex] != values[0] && symmetric_difference_layers[1].data[hex] == values[1]){ symmetric_difference_signatures.push(hex); } } }); for (var i = 0; i < symmetric_difference_layer_names.length; i++){ symmetric_difference_layer_names[i] = symmetric_difference_layer_names[i] + " [" + text[i] + "]"; } var symmetric_difference_function = "symmetric difference"; select_list (symmetric_difference_signatures, symmetric_difference_function, symmetric_difference_layer_names); uncheck_checkbox ('symmetric-difference-checkbox'); hide_values('symmetric-difference'); } function compute_absolute_complement (values, absolute_complement_layer_names, text) { // A function that will take a list of layer names // that have been selected for the set difference utility. // Fetches the respective layers and list of tumor ids. // Then compares data elements of the same tumor id // between both layers. Adds these hexes to a new layer // for visualization //Array of signatures var absolute_complement_signatures = []; with_layers (absolute_complement_layer_names, function (absolute_complement_layers) { // Gather Tumor-ID Signatures. for (hex in polygons) { // Absolute Complement Function if (absolute_complement_layers[0].data[hex] != values[0]) { absolute_complement_signatures.push(hex); } } }); for (var i = 0; i < absolute_complement_layer_names.length; i++){ absolute_complement_layer_names[i] = absolute_complement_layer_names[i] + " [" + text[i] + "]"; } var absolute_complement_function = "absolute complement"; select_list (absolute_complement_signatures, absolute_complement_function, absolute_complement_layer_names); uncheck_checkbox ('absolute-complement-checkbox'); hide_values('absolute-complement'); } function layer_sort_order(a, b) { // A sort function defined on layer names. // Return <0 if a belongs before b, >0 if a belongs after // b, and 0 if their order doesn't matter. // Sort by selection status, then p_value, then clumpiness, then (for binary // layers that are not selections) the frequency of the less common value, // then alphabetically by name if all else fails. // Note that we can consult the layer metadata "n" and "positives" fields to // calculate the frequency of the least common value in binary layers, // without downloading them. if(layers[a].selection && !layers[b].selection) { // a is a selection and b isn't, so put a first. return -1; } else if(layers[b].selection && !layers[a].selection) { // b is a selection and a isn't, so put b first. return 1; } if(layers[a].p_value < layers[b].p_value) { // a has a lower p value, so put it first. return -1; } else if(layers[b].p_value < layers[a].p_value) { // b has a lower p value. Put it first instead. return 1; } else if(isNaN(layers[b].p_value) && !isNaN(layers[a].p_value)) { // a has a p value and b doesn't, so put a first return -1; } else if(!isNaN(layers[b].p_value) && isNaN(layers[a].p_value)) { // b has a p value and a doesn't, so put b first. return 1; } if(layers[a].clumpiness < layers[b].clumpiness) { // a has a lower clumpiness score, so put it first. return -1; } else if(layers[b].clumpiness < layers[a].clumpiness) { // b has a lower clumpiness score. Put it first instead. return 1; } else if(isNaN(layers[b].clumpiness) && !isNaN(layers[a].clumpiness)) { // a has a clumpiness score and b doesn't, so put a first return -1; } else if(!isNaN(layers[b].clumpiness) && isNaN(layers[a].clumpiness)) { // b has a clumpiness score and a doesn't, so put b first. return 1; } if(!layers[a].selection && !isNaN(layers[a].positives) && layers[a].n > 0 && !layers[b].selection && !isNaN(layers[b].positives) && layers[b].n > 0) { // We have checked to see each layer is supposed to be bianry layer // without downloading. TODO: This is kind of a hack. Redesign the // whole system with a proper concept of layer type. // We've also verified they both have some data in them. Otherwise we // might divide by 0 trying to calculate frequency. // Two binary layers (not selections). // Compute the frequency of the least common value for each // This is the frequency of the least common value in a (will be <=1/2) var minor_frequency_a = layers[a].positives / layers[a].n; if(minor_frequency_a > 0.5) { minor_frequency_a = 1 - minor_frequency_a; } // And this is the same frequency for the b layer var minor_frequency_b = layers[b].positives / layers[b].n; if(minor_frequency_b > 0.5) { minor_frequency_b = 1 - minor_frequency_b; } if(minor_frequency_a > minor_frequency_b) { // a is more evenly split, so put it first return -1; } else if(minor_frequency_a < minor_frequency_b) { // b is more evenly split, so put it first return 1; } } else if (!layers[a].selection && !isNaN(layers[a].positives) && layers[a].n > 0) { // a is a binary layer we can nicely sort by minor value frequency, but // b isn't. Put a first so that we can avoid intransitive sort cycles. // Example: X and Z are binary layers, Y is a non-binary layer, Y comes // after X and before Z by name ordering, but Z comes before X by minor // frequency ordering. This sort is impossible. // The solution is to put both X and Z in front of Y, because they're // more interesting. return -1; } else if (!layers[b].selection && !isNaN(layers[b].positives) && layers[b].n > 0) { // b is a binary layer that we can evaluate based on minor value // frequency, but a isn't. Put b first. return 1; } // We couldn't find a difference in selection status, p-value, or clumpiness // score, or the binary layer minor value frequency, or whether each layer // *had* a binary layer minor value frequency, so use lexicographic ordering // on the name. return a.localeCompare(b); } function sort_layers(layer_array) { // Given an array of layer names, sort the array in place as we want layers // to appear to the user. // We should sort by p value, with NaNs at the end. But selections should be // first. layer_array.sort(layer_sort_order); } function fill_layer_metadata(container, layer_name) { // Empty the given jQuery container element, and fill it with layer metadata // for the layer with the given name. // Empty the container. container.html(""); for(attribute in layers[layer_name]) { // Go through everything we know about this layer if(attribute == "data" || attribute == "url" || attribute == "magnitude" || attribute == "selection") { // Skip built-in things // TODO: Ought to maybe have all metadata in its own object? continue; } // This holds the metadata value we're displaying var value = layers[layer_name][attribute]; if(typeof value == "number" && isNaN(value)) { // If it's a numerical NaN (but not a string), just leave it out. continue; } // If we're still here, this is real metadata. // Format it for display. var value_formatted; if(typeof value == "number") { if(value % 1 == 0) { // It's an int! // Display the default way value_formatted = value; } else { // It's a float! // Format the number for easy viewing value_formatted = value.toExponential(2); } } else { // Just put the thing in as a string value_formatted = value; } // Do some transformations to make the displayed labels make more sense lookup = { n: "Number of non-empty values", positives: "Number of ones", inside_yes: "Ones in A", outside_yes: "Ones in background" } if(lookup[attribute]) { // Replace a boring short name with a useful long name attribute = lookup[attribute]; } // Make a spot for it in the container and put it in var metadata = $("<div\>").addClass("layer-metadata"); metadata.text(attribute + " = " + value_formatted); container.append(metadata); } } function make_toggle_layout_ui(layout_name) { // Returns a jQuery element to represent the layer layout the given name in // the toggle layout panel. // This holds a jQuery element that's the root of the structure we're // building. var root = $("<div/>").addClass("layout-entry"); root.data("layout-name", layout_name); // Put in the layer name in a div that makes it wrap. root.append($("<div/>").addClass("layout-name").text(layout_name)); return root; } function make_browse_ui(layer_name) { // Returns a jQuery element to represent the layer with the given name in // the browse panel. // This holds a jQuery element that's the root of the structure we're // building. var root = $("<div/>").addClass("layer-entry"); root.data("layer-name", layer_name); // Put in the layer name in a div that makes it wrap. root.append($("<div/>").addClass("layer-name").text(layer_name)); // Put in a layer metadata container div var metadata_container = $("<div/>").addClass("layer-metadata-container"); fill_layer_metadata(metadata_container, layer_name); root.append(metadata_container); return root; } function update_browse_ui() { // Make the layer browse UI reflect the current list of layers in sorted // order. // Re-sort the sorted list that we maintain sort_layers(layer_names_sorted); // Close the select if it was open, forcing the data to refresh when it // opens again. $("#search").select2("close"); } function get_slider_range(layer_name) { // Given the name of a layer, get the slider range from its shortlist UI // entry. // Assumes the layer has a shortlist UI entry. return shortlist_ui[layer_name].find(".range-slider").slider("values"); } function reset_slider(layer_name, shortlist_entry) { // Given a layer name and a shortlist UI entry jQuery element, reset the // slider in the entry to its default values, after downloading the layer. // The default value may be invisible because we decided the layer should be // a colormap. // We need to set its boundaries to the min and max of the data set with_layer(layer_name, function(layer) { if(have_colormap(layer_name)) { // This is a colormap, so don't use the range slider at all. // We couldn't know this before because the colormap may need to be // auto-detected upon download. shortlist_entry.find(".range").hide(); return; } else { // We need the range slider shortlist_entry.find(".range").show(); // TODO: actually find max and min // For now just use + and - magnitude // This has the advantage of letting us have 0=black by default var magnitude = layer.magnitude; // This holds the limit to use, which should be 1 if the magnitude // is <1. This is sort of heuristic, but it's a good guess that // nobody wants to look at a layer with values -0.2 to 0.7 on a // scale of -10 to 10, say, but they might want it on -1 to 1. var range = Math.max(magnitude, 1.0) // Set the min and max. shortlist_entry.find(".range-slider").slider("option", "min", -range); shortlist_entry.find(".range-slider").slider("option", "max", range); // Set slider to autoscale for the magnitude. shortlist_entry.find(".range-slider").slider("values", [-magnitude, magnitude]); print("Scaled to magnitude " + magnitude); // Redraw the view in case this changed anything refresh(); } }); } function get_current_layers() { // Returns an array of the string names of the layers that are currently // supposed to be displayed, according to the shortlist UI. // Not responsible for enforcing maximum selected layers limit. // This holds a list of the string names of the currently selected layers, // in order. var current_layers = []; $("#shortlist").children().each(function(index, element) { // This holds the checkbox that determines if we use this layer var checkbox = $(element).find(".layer-on"); if(checkbox.is(":checked")) { // Put the layer in if its checkbox is checked. current_layers.push($(element).data("layer")); } }); // Return things in reverse order relative to the UI. // Thus, layer-added layers will be "secondary", and e.g. selecting // something with only tissue up behaves as you might expect, highlighting // those things. current_layers.reverse(); return current_layers; } function get_current_filters() { // Returns an array of filter objects, according to the shortlist UI. // Filter objects have a layer name and a boolean-valued filter function // that returns true or false, given a value from that layer. var current_filters = []; $("#shortlist").children().each(function(index, element) { // Go through all the shortlist entries. // This function is also the scope used for filtering function config // variables. // This holds the checkbox that determines if we use this layer var checkbox = $(element).find(".filter-on"); if(checkbox.is(":checked")) { // Put the layer in if its checkbox is checked. // Get the layer name var layer_name = $(element).data("layer"); // This will hold our filter function. Start with a no-op filter. var filter_function = function(value) { return true; } // Get the filter parameters // This holds the input that specifies a filter threshold var filter_threshold = $(element).find(".filter-threshold"); // And this the element that specifies a filter match value for // discrete layers var filter_value = $(element).find(".filter-value"); // We want to figure out which of these to use without going and // downloading the layer. // So, we check to see which was left visible by the filter config // setup code. if(filter_threshold.is(":visible")) { // Use a threshold. This holds the threshold. var threshold = parseInt(filter_threshold.val()); filter_function = function(value) { return value > threshold; } } if(filter_value.is(":visible")) { // Use a discrete value match instead. This hodls the value we // want to match. var desired = filter_value.val(); filter_function = function(value) { return value == desired; } } // Add a filter on this layer, with the function we've prepared. current_filters.push({ layer_name: layer_name, filter_function: filter_function }); } }); return current_filters; } function get_current_layers() { // Returns an array of the string names of the layers that are currently // supposed to be displayed, according to the shortlist UI. // Not responsible for enforcing maximum selected layers limit. // This holds a list of the string names of the currently selected layers, // in order. var current_layers = []; $("#shortlist").children().each(function(index, element) { // This holds the checkbox that determines if we use this layer var checkbox = $(element).find(".layer-on"); if(checkbox.is(":checked")) { // Put the layer in if its checkbox is checked. current_layers.push($(element).data("layer")); } }); // Return things in reverse order relative to the UI. // Thus, layer-added layers will be "secondary", and e.g. selecting // something with only tissue up behaves as you might expect, highlighting // those things. current_layers.reverse(); return current_layers; } function get_current_set_theory_layers(function_type) { // Returns an array of layer names that have been selected. // This function only looks at the layers that are listed on the shortlist. var current_set_theory_layers = []; // Initialize global variables that hold the number of checkboxes selected // for set theory functions to zero so that the new number is calculated // each time this function is called. if (function_type == "intersection"){ shortlist_intersection_num = 0; } if (function_type == "union"){ shortlist_union_num = 0; } if (function_type == "set difference"){ shortlist_set_difference_num = 0; } if (function_type == "symmetric difference"){ shortlist_symmetric_difference_num = 0; } if (function_type == "absolute complement"){ shortlist_absolute_complement_num = 0; } $("#shortlist").children().each(function(index, element) { // Go through all the shortlist entries. // This holds the checkbox that determines if we use this layer // The class name depends on the function_type. // If intersection function look for intersection-checkbox. if (function_type == "intersection"){ var checkbox = $(element).find(".intersection-checkbox"); } // If union function look for union-checkbox. if (function_type == "union"){ var checkbox = $(element).find(".union-checkbox"); } // If set difference function look for set-difference-checkbox. if (function_type == "set difference"){ var checkbox = $(element).find(".set-difference-checkbox"); } // If symmetric difference function look for // symmetric-difference-checkbox. if (function_type == "symmetric difference"){ var checkbox = $(element).find(".symmetric-difference-checkbox"); } if (function_type == "absolute complement"){ var checkbox = $(element).find(".absolute-complement-checkbox"); } if(checkbox.is(":checked")) { // Put the layer in if its checkbox is checked. // Get the layer name var layer_name = $(element).data("layer"); // Add the layer_name to the list of current_set_theory_layers. current_set_theory_layers.push(layer_name); // Add to the global "num" variables to keep track of the number // of selected checkboxes. if (function_type == "intersection"){ shortlist_intersection_num++; } if (function_type == "union"){ shortlist_union_num++; } if (function_type == "set difference"){ shortlist_set_difference_num++; } if (function_type == "symmetric difference"){ shortlist_symmetric_difference_num++; } if (function_type == "absolute complement"){ shortlist_absolute_complement_num++; } } }); return current_set_theory_layers; } function with_filtered_signatures(filters, callback) { // Takes an array of filters, as produced by get_current_filters. Signatures // pass a filter if the filter's layer has a value >0 for that signature. // Computes an array of all signatures passing all filters, and passes that // to the given callback. // TODO: Re-organize this to do filters one at a time, recursively, like a // reasonable second-order filter. // Prepare a list of all the layers var layer_names = []; for(var i = 0; i < filters.length; i++) { layer_names.push(filters[i].layer_name); } with_layers(layer_names, function(filter_layers) { // filter_layers is guaranteed to be in the same order as filters. // This is an array of signatures that pass all the filters. var passing_signatures = []; for(var signature in polygons) { // For each signature // This holds whether we pass all the filters var pass = true; for(var i = 0; i < filter_layers.length; i++) { // For each filtering layer if(!filters[i].filter_function( filter_layers[i].data[signature])) { // If the signature fails the filter function for the layer, // skip the signature. pass = false; break; } } if(pass) { // Record that the signature passes all filters passing_signatures.push(signature); } } // Now we have our list of all passing signatures, so hand it off to the // callback. callback(passing_signatures); }); } function select_list(to_select, function_type, layer_names) { // Given an array of signature names, add a new selection layer containing // just those hexes. Only looks at hexes that are not filtered out by the // currently selected filters. // function_type is an optional parameter. If no variable is passed for the // function_type undefined then the value will be undefined and the // default "selection + #" title will be assigned to the shortlist element. // If layer_names is undefined, the "selection + #" will also apply as a // default. However, if a value i.e. "intersection" is passed // for function_type, the layer_names will be used along with the // function_type to assign the correct title. // Make the requested signature list into an object for quick membership // checking. This holds true if a signature was requested, undefined // otherwise. var wanted = {}; for(var i = 0; i < to_select.length; i++) { wanted[to_select[i]] = true; } // This is the data object for the layer: from signature names to 1/0 var data = {}; // How many signatures will we have any mention of in this layer var signatures_available = 0; // Start it out with 0 for each signature. Otherwise we wil have missing // data for signatures not passing the filters. for(var signature in polygons) { data[signature] = 0; signatures_available += 1; } // This holds the filters we're going to use to restrict our selection var filters = get_current_filters(); // Go get the list of signatures passing the filters and come back. with_filtered_signatures(filters, function(signatures) { // How many signatures get selected? var signatures_selected = 0; for(var i = 0; i < signatures.length; i++) { if(wanted[signatures[i]]) { // This signature is both allowed by the filters and requested. data[signatures[i]] = 1; signatures_selected++; } } // Make up a name for the layer var layer_name; // Default Values for Optional Parameters if (function_type == undefined && layer_names == undefined){ layer_name = "Selection " + selection_next_id; selection_next_id++; } if (function_type == "user selection"){ var text = prompt("Please provide a label for your selection", "Selection Label Text"); if (text != null){ layer_name = text; } if (!text) { return; } } // intersection for layer name if (function_type == "intersection"){ layer_name = "(" + layer_names[0] + " ∩ " + layer_names[1] + ")"; } // union for layer name if (function_type == "union"){ layer_name = "(" + layer_names[0] + " U " + layer_names[1] + ")"; } // set difference for layer name if (function_type == "set difference"){ layer_name = "(" + layer_names[0] + " \\ " + layer_names[1] + ")"; } // symmetric difference for layer name if (function_type == "symmetric difference"){ layer_name = "(" + layer_names[0] + " ∆ " + layer_names[1] + ")"; } // absolute complement for layer name if (function_type == "absolute complement"){ layer_name = "Not: " + "(" + layer_names[0] + ")"; } // saved filter for layer name if (function_type == "save"){ layer_name = "(" + layer_names[0] + ")"; } // Add the layer. Say it is a selection add_layer_data(layer_name, data, { selection: true, selected: signatures_selected, // Display how many hexes are in n: signatures_available // And how many have a value at all }); // Update the browse UI with the new layer. update_browse_ui(); // Immediately shortlist it shortlist.push(layer_name); update_shortlist_ui(); }); } function select_rectangle(start, end) { // Given two Google Maps LatLng objects (denoting arbitrary rectangle // corners), add a new selection layer containing all the hexagons // completely within that rectangle. // Only looks at hexes that are not filtered out by the currently selected // filters. // Sort out the corners to get the rectangle limits in each dimension var min_lat = Math.min(start.lat(), end.lat()); var max_lat = Math.max(start.lat(), end.lat()); var min_lng = Math.min(start.lng(), end.lng()); var max_lng = Math.max(start.lng(), end.lng()); // This holds an array of all signature names in our selection box. var in_box = []; // Start it out with 0 for each signature. Otherwise we wil have missing // data for signatures not passing the filters. for(var signature in polygons) { // Get the path for its hex var path = polygons[signature].getPath(); // This holds if any points of the path are outside the selection // box var any_outside = false; path.forEach(function(point, index) { // Check all the points. Runs synchronously. if(point.lat() < min_lat || point.lat() > max_lat || point.lng() < min_lng || point.lng() > max_lng) { // This point is outside the rectangle any_outside = true; } }); // Select the hex if all its corners are inside the selection // rectangle. if(!any_outside) { in_box.push(signature); } } // Now we have an array of the signatures that ought to be in the selection // (if they pass filters). Hand it off to select_list. var select_function_type = "user selection"; select_list(in_box, select_function_type); } function recalculate_statistics(passed_filters) { // Interrogate the UI to determine signatures that are "in" and "out", and // run an appropriate statisical test for each layer between the "in" and // "out" signatures, and update all the "p_value" fields for all the layers // with the p values. Takes in a list of signatures that passed the filters, // and ignores any signatures not on that list. // Build an efficient index of passing signatures var passed = {}; for(var i = 0; i < passed_filters.length; i++) { passed[passed_filters[i]] = true; } // Figure out what the in-list should be (statistics group A) var layer_a_name = $(".statistics-a:checked").data("layer-name"); var layer_b_name = $(".statistics-b:checked").data("layer-name"); print("Running statistics between " + layer_a_name + " and " + layer_b_name); if(!layer_a_name) { complain("Can't run statistics without an \"A\" group."); // Get rid of the throbber // TODO: Move this UI code out of the backend code. $(".recalculate-throbber").hide(); $("#recalculate-statistics").show(); return; } // We know the layers have data since they're selections, so we can just go // look at them. // This holds the "in" list: hexes from the "A" group. var in_list = []; for(var signature in layers[layer_a_name].data) { if(passed[signature] && layers[layer_a_name].data[signature]) { // Add all the signatures in the "A" layer to the in list. in_list.push(signature); } } if(in_list.length == 0) { complain("Can't run statistics with an empty \"A\" group."); // Get rid of the throbber // TODO: Move this UI code out of the backend code. $(".recalculate-throbber").hide(); $("#recalculate-statistics").show(); return; } // This holds the "out" list: hexes in the "B" group, or, if that's not // defined, all hexes. It's a little odd to run A vs. a set that includes // some members of A, but Prof. Stuart wants that and it's not too insane // for a Binomial test (which is the only currently implemented test // anyway). var out_list = []; if(layer_b_name) { // We have a layer B, so take everything that's on in it. for(var signature in layers[layer_b_name].data) { if(passed[signature] && layers[layer_b_name].data[signature]) { // Add all the signatures in the "B" layer to the out list. out_list.push(signature); } } } else { // The out list is all hexes for(var signature in polygons) { if(passed[signature]) { // Put it on the out list. out_list.push(signature); } } } // So now we have our in_list and our out_list for(var layer_name in layers) { // Do the stats on each layer between those lists. This only processes // layers that don't have URLs. Layers with URLs are assumed to be part // of the available matrices. recalculate_statistics_for_layer(layer_name, in_list, out_list, passed_filters); } // Now do all the layers with URLs. They are in the available score // matrices. for(var i = 0; i < available_matrices.length; i++) { recalculate_statistics_for_matrix(available_matrices[i], in_list, out_list, passed_filters); } print("Statistics jobs launched."); } function recalculate_statistics_for_layer(layer_name, in_list, out_list, all) { // Re-calculate the stats for the layer with the given name, between the // given in and out arrays of signatures. Store the re-calculated statistics // in the layer. all is a list of "all" signatures, from which we can // calculate pseudocounts. // All we do is send the layer data or URL (whichever is more convenient) to // the workers. They independently identify the data type and run the // appropriate test, returning a p value or NaN by callback. // This holds a callback for setting the layer's p_value to the result of // the statistics. var callback = function(results) { // The statistics code really sends back a dict of updated metadata for // each layer. Copy it over. for(var metadata in results) { layers[layer_name][metadata] = results[metadata]; } if(jobs_running == 0) { // All statistics are done! // TODO: Unify this code with similar callback below. // Re-sort everything and draw all the new p values. update_browse_ui(); update_shortlist_ui(); // Get rid of the throbber $(".recalculate-throbber").hide(); $("#recalculate-statistics").show(); } }; if(layers[layer_name].data != undefined) { // Already have this downloaded. A local copy to the web worker is // simplest, and a URL may not exist anyway. rpc_call("statistics_for_layer", [layers[layer_name].data, in_list, out_list, all], callback); } else if(layers[layer_name].url != undefined) { // We have a URL, so the layer must be in a matrix, too. // Skip it here. } else { // Layer has no data and no way to get data. Should never happen. complain("Layer " + layer_name + " has no data and no url."); } } function recalculate_statistics_for_matrix(matrix_url, in_list, out_list, all) { // Given the URL of one of the visualizer generator's input score matrices, // download the matrix, calculate statistics for each layer in the matrix // between the given in and out lists, and update the layer p values. all is // a list of "all" signatures, from which we can calculate pseudocounts. rpc_call("statistics_for_matrix", [matrix_url, in_list, out_list, all], function(result) { // The return value is p values by layer name for(var layer_name in result) { // The statistics code really sends back a dict of updated metadata // for each layer. Copy it over. for(var metadata in result[layer_name]) { layers[layer_name][metadata] = result[layer_name][metadata]; } } if(jobs_running == 0) { // All statistics are done! // TODO: Unify this code with similar callback above. // Re-sort everything and draw all the new p values. update_browse_ui(); update_shortlist_ui(); // Get rid of the throbber $(".recalculate-throbber").hide(); $("#recalculate-statistics").show(); } }); } function rpc_initialize() { // Set up the RPC system. Must be called before rpc_call is used. for(var i = 0; i < NUM_RPC_WORKERS; i++) { // Start the statistics RPC (remote procedure call) Web Worker var worker = new Worker("statistics.js"); // Send all its messages to our reply processor worker.onmessage = rpc_reply; // Send its error events to our error processor worker.onerror = rpc_error; // Add it to the list of workers rpc_workers.push(worker); } } function rpc_call(function_name, function_args, callback) { // Given a function name and an array of arguments, send a message to a Web // Worker thread to ask it to run the given job. When it responds with the // return value, pass it to the given callback. // Allocate a new call id var call_id = rpc_next_id; rpc_next_id++; // Store the callback rpc_callbacks[call_id] = callback; // Launch the call. Pass the function name, function args, and id to send // back with the return value. rpc_workers[next_free_worker].postMessage({ name: function_name, args: function_args, id: call_id }); // Next time, use the next worker on the list, wrapping if we run out. // This ensures no one worker gets all the work. next_free_worker = (next_free_worker + 1) % rpc_workers.length; // Update the UI with the number of jobs in flight. Decrement jobs_running // so the callback knows if everything is done or not. jobs_running++; $("#jobs-running").text(jobs_running); // And the number of jobs total $("#jobs-ever").text(rpc_next_id); } function rpc_reply(message) { // Handle a Web Worker message, which may be an RPC response or a log entry. if(message.data.log != undefined) { // This is really a log entry print(message.data.log); return; } // This is really a job completion message (success or error). // Update the UI with the number of jobs in flight. jobs_running--; $("#jobs-running").text(jobs_running); if(message.data.error) { // The RPC call generated an error. // Inform the page. print("RPC error: " + message.data.error); // Get rid of the callback delete rpc_callbacks[message.data.id]; return; } // Pass the return value to the registered callback. rpc_callbacks[message.data.id](message.data.return_value); // Get rid of the callback delete rpc_callbacks[message.data.id]; } function rpc_error(error) { // Handle an error event from a web worker // See http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.h // tml#errorevent complain("Web Worker error: " + error.message); print(error.message + "\n at" + error.filename + " line " + error.lineno + " column " + error.column); } function initialize_view(initial_zoom) { // Initialize the global Google Map. // Configure a Google map var mapOptions = { // Look at the center of the map center: get_LatLng(128, 128), // Zoom all the way out zoom: initial_zoom, mapTypeId: "blank", // Don't show a map type picker. mapTypeControlOptions: { mapTypeIds: [] }, // Or a street view man that lets you walk around various Earth places. streetViewControl: false }; // Create the actual map googlemap = new google.maps.Map(document.getElementById("visualization"), mapOptions); // Attach the blank map type to the map googlemap.mapTypes.set("blank", new BlankMapType()); // Make the global info window info_window = new google.maps.InfoWindow({ content: "No Signature Selected", position: get_LatLng(0, 0) }); // Add an event to close the info window when the user clicks outside of any // hexagon google.maps.event.addListener(googlemap, "click", function(event) { info_window.close(); // Also make sure that the selected signature is no longer selected, // so we don't pop the info_window up again. selected_signature = undefined; // Also un-focus the search box $("#search").blur(); }); // And an event to clear the selected hex when the info_window closes. google.maps.event.addListener(info_window, "closeclick", function(event) { selected_signature = undefined; }); // We also have an event listener that checks when the zoom level changes, // and turns off hex borders if we zoom out far enough, and turns them on // again if we come back. google.maps.event.addListener(googlemap, "zoom_changed", function(event) { // Get the current zoom level (low is out) var zoom = googlemap.getZoom(); // API docs say: pixelCoordinate = worldCoordinate * 2 ^ zoomLevel // So this holds the number of pixels that the global length hex_size // corresponds to at this zoom level. var hex_size_pixels = hex_size * Math.pow(2, zoom); if(hex_size_pixels < MIN_BORDER_SIZE) { // We're too small for borders for(var signature in polygons) { set_hexagon_stroke_weight(polygons[signature], 0); } } else { // We can fit borders on the hexes for(var signature in polygons) { set_hexagon_stroke_weight(polygons[signature], HEX_STROKE_WEIGHT); } } }); // Subscribe all the tool listeners to the map subscribe_tool_listeners(googlemap); } function add_tool(tool_name, tool_menu_option, callback) { // Given a programmatic unique name for a tool, some text for the tool's // button, and a callback for when the user clicks that button, add a tool // to the tool menu. // This hodls a button to activate the tool. var tool_button = $("<a/>").attr("href", "#").addClass("stacker"); tool_button.text(tool_menu_option); tool_button.click(function() { // New tool. Remove all current tool listeners clear_tool_listeners(); // Say that the select tool is selected selected_tool = tool_name; callback(); // End of tool workflow must set current_tool to undefined. }); $("#toolbar").append(tool_button); } function add_tool_listener(name, handler, cleanup) { // Add a global event listener over the Google map and everything on it. // name specifies the event to listen to, and handler is the function to be // set up as an event handler. It should take a single argument: the Google // Maps event. A handle is returned that can be used to remove the event // listen with remove_tool_listener. // Only events in the TOOL_EVENTS array are allowed to be passed for name. // TODO: Bundle this event thing into its own object. // If "cleanup" is specified, it must be a 0-argument function to call when // this listener is removed. // Get a handle var handle = tool_listener_next_id; tool_listener_next_id++; // Add the listener for the given event under that handle. // TODO: do we also need to index this for O(1) event handling? tool_listeners[handle] = { handler: handler, event: name, cleanup: cleanup }; return handle; } function remove_tool_listener(handle) { // Given a handle returned by add_tool_listener, remove the listener so it // will no longer fire on its event. May be called only once on a given // handle. Runs any cleanup code associated with the handle being removed. if(tool_listeners[handle].cleanup) { // Run cleanup code if applicable tool_listeners[handle].cleanup(); } // Remove the property from the object delete tool_listeners[handle]; } function clear_tool_listeners() { // We're starting to use another tool. Remove all current tool listeners. // Run any associated cleanup code for each listener. for(var handle in tool_listeners) { remove_tool_listener(handle); } } function subscribe_tool_listeners(maps_object) { // Put the given Google Maps object into the tool events system, so that // events on it will fire global tool events. This can happen before or // after the tool events themselves are enabled. for(var i = 0; i < TOOL_EVENTS.length; i++) { // For each event name we care about, // use an inline function to generate an event name specific handler, // and attach that to the Maps object. google.maps.event.addListener(maps_object, TOOL_EVENTS[i], function(event_name) { return function(event) { // We are handling an event_name event for(var handle in tool_listeners) { if(tool_listeners[handle].event == event_name) { // The handler wants this event // Fire it with the Google Maps event args tool_listeners[handle].handler(event); } } }; }(TOOL_EVENTS[i])); } } function have_colormap(colormap_name) { // Returns true if the given string is the name of a colormap, or false if // it is only a layer. return !(colormaps[colormap_name] == undefined); } function get_range_position(score, low, high) { // Given a score float, and the lower and upper bounds of an interval (which // may be equal, but not backwards), return a number in the range -1 to 1 // that expresses the position of the score in the [low, high] interval. // Positions out of bounds are clamped to -1 or 1 as appropriate. // This holds the length of the input interval var interval_length = high - low; if(interval_length > 0) { // First rescale 0 to 1 score = (score - low) / interval_length // Clamp score = Math.min(Math.max(score, 0), 1); // Now re-scale to -1 to 1 score = 2 * score - 1; } else { // The interval is just a point // Just use 1 if we're above the point, and 0 if below. score = (score > low)? 1 : -1 } return score; } function refresh() { // Schedule the view to be redrawn after the current event finishes. // Get rid of the previous redraw request, if there was one. We only want // one. window.clearTimeout(redraw_handle); // Make a new one to happen as soon as this event finishes redraw_handle = window.setTimeout(redraw_view, 0); } function redraw_view() { // Make the view display the correct hexagons in the colors of the current // layer(s), as read from the values of the layer pickers in the global // layer pickers array. // All pickers must have selected layers that are in the object of // layers. // Instead of calling this, you probably want to call refresh(). // This holds a list of the string names of the currently selected layers, // in order. var current_layers = get_current_layers(); // This holds arrays of the lower and upper limit we want to use for // each layer, by layer number. The lower limit corresponds to u or // v = -1, and the upper to u or v = 1. The entries we make for // colormaps are ignored. // Don't do this inside the callback since the UI may have changed by then. var layer_limits = [] for(var i = 0; i < current_layers.length; i++) { layer_limits.push(get_slider_range(current_layers[i])); } // This holds all the current filters var filters = get_current_filters(); // Obtain the layer objects (mapping from signatures/hex labels to colors) with_layers(current_layers, function(retrieved_layers) { print("Redrawing view with " + retrieved_layers.length + " layers."); // Turn all the hexes the filtered-out color, pre-emptively for(var signature in polygons) { set_hexagon_color(polygons[signature], "black"); } // Go get the list of filter-passing hexes. with_filtered_signatures(filters, function(signatures) { for(var i = 0; i < signatures.length; i++) { // For each hex passign the filter // This hodls its signature label var label = signatures[i]; // This holds the color we are calculating for this hexagon. // Start with the missing data color. var computed_color = "grey"; if(retrieved_layers.length >= 1) { // Two layers. We find a point in u, v cartesian space, map // it to polar, and use that to compute an HSV color. // However, we map value to the radius instead of // saturation. // Get the heat along u and v axes. This puts us in a square // of side length 2. Fun fact: undefined / number = NaN, but // !(NaN == NaN) var u = retrieved_layers[0].data[label]; if(!have_colormap(current_layers[0])) { // Take into account the slider values and re-scale the // layer value to express its position between them. u = get_range_position(u, layer_limits[0][0], layer_limits[0][1]); } if(retrieved_layers.length >= 2) { // There's a second layer, so use the v axis. var v = retrieved_layers[1].data[label]; if(!have_colormap(current_layers[1])) { // Take into account the slider values and re-scale // the layer value to express its position between // them. v = get_range_position(v, layer_limits[1][0], layer_limits[1][1]); } } else { // No second layer, so v axis is unused. Don't make it // undefined (it's not missing data), but set it to 0. var v = 0; } // Either of u or v may be undefined (or both) if the layer // did not contain an entry for this signature. But that's // OK. Compute the color that we should use to express this // combination of layer values. It's OK to pass undefined // names here for layers. computed_color = get_color(current_layers[0], u, current_layers[1], v); } // Set the color by the composed layers. set_hexagon_color(polygons[label], computed_color); } }); // Draw the color key. if(retrieved_layers.length == 0) { // No color key to draw $(".key").hide(); } else { // We do actually want the color key $(".key").show(); // This holds the canvas that the key gets drawn in var canvas = $("#color-key")[0]; // This holds the 2d rendering context var context = canvas.getContext("2d"); for(var i = 0; i < KEY_SIZE; i++) { // We'll use i for the v coordinate (-1 to 1) (left to right) var v = 0; if(retrieved_layers.length >= 2) { v = i / (KEY_SIZE / 2) - 1; if(have_colormap(current_layers[1])) { // This is a color map, so do bands instead. v = Math.floor(i / KEY_SIZE * (retrieved_layers[1].magnitude + 1)); } } for(var j = 0; j < KEY_SIZE; j++) { // And j spacifies the u coordinate (bottom to top) var u = 0; if(retrieved_layers.length >= 1) { u = 1 - j / (KEY_SIZE / 2); if(have_colormap(current_layers[0])) { // This is a color map, so do bands instead. // Make sure to flip sign, and have a -1 for the // 0-based indexing. u = Math.floor((KEY_SIZE - j - 1) / KEY_SIZE * (retrieved_layers[0].magnitude + 1)); } } // Set the pixel color to the right thing for this u, v // It's OK to pass undefined names here for layers. context.fillStyle = get_color(current_layers[0], u, current_layers[1], v); // Fill the pixel context.fillRect(i, j, 1, 1); } } } if(have_colormap(current_layers[0])) { // We have a layer with horizontal bands // Add labels to the key if we have names to use. // TODO: Vertical text for vertical bands? // Get the colormap var colormap = colormaps[current_layers[0]] if(colormap.length > 0) { // Actually have any categories (not auto-generated) print("Drawing key text for " + colormap.length + " categories."); // How many pixels do we get per label, vertically var pixels_per_label = KEY_SIZE / colormap.length; // Configure for text drawing context.font = pixels_per_label + "px Arial"; context.textBaseline = "top"; for(var i = 0; i < colormap.length; i++) { // This holds the pixel position where our text goes var y_position = KEY_SIZE - (i + 1) * pixels_per_label; // Get the background color here as a 1x1 ImageData var image = context.getImageData(0, y_position, 1, 1); // Get the components r, g, b, a in an array var components = image.data; // Make a Color so we can operate on it var background_color = Color({ r: components[0], g: components[1], b: components[2] }); if(background_color.light()) { // This color is light, so write in black. context.fillStyle = "black"; } else { // It must be dark, so write in white. context.fillStyle = "white"; } // Draw the name on the canvas context.fillText(colormap[i].name, 0, y_position); } } } // We should also set up axis labels on the color key. // We need to know about colormaps to do this // Hide all the labels $(".label").hide(); if(current_layers.length > 0) { // Show the y axis label $("#y-axis").text(current_layers[0]).show(); if(!have_colormap(current_layers[0])) { // Show the low to high markers for continuous values $("#low-both").show(); $("#high-y").show(); } } if(current_layers.length > 1) { // Show the x axis label $("#x-axis").text(current_layers[1]).show(); if(!have_colormap(current_layers[1])) { // Show the low to high markers for continuous values $("#low-both").show(); $("#high-x").show(); } } }); // Make sure to also redraw the info window, which may be open. redraw_info_window(); } function get_color(u_name, u, v_name, v) { // Given u and v, which represent the heat in each of the two currently // displayed layers, as well as u_name and v_name, which are the // corresponding layer names, return the computed CSS color. // Either u or v may be undefined (or both), in which case the no-data color // is returned. If a layer name is undefined, that layer dimension is // ignored. if(have_colormap(v_name) && !have_colormap(u_name)) { // We have a colormap as our second layer, and a layer as our first. // Swap everything around so colormap is our first layer instead. // Now we don't need to think about drawing a layer first with a // colormap second. // This is a temporary swapping variable. var temp = v_name; v_name = u_name; u_name = temp; temp = v; v = u; u = temp; } if(isNaN(u) || isNaN(v) || u == undefined || v == undefined) { // At least one of our layers has no data for this hex. return "grey"; } if(have_colormap(u_name) && have_colormap(v_name) && !colormaps[u_name].hasOwnProperty(u) && !colormaps[v_name].hasOwnProperty(v) && layers[u_name].magnitude <= 1 && layers[v_name].magnitude <= 1) { // Special case: two binary or unary auto-generated colormaps. // Use dark grey/red/blue/purple color scheme if(u == 1) { if(v == 1) { // Both are on return "#FF00FF"; } else { // Only the first is on return "#FF0000"; } } else { if(v == 1) { // Only the second is on return "#0000FF"; } else { // Neither is on return "#545454"; } } } if(have_colormap(u_name) && !colormaps[u_name].hasOwnProperty(u) && layers[u_name].magnitude <= 1 && v_name == undefined) { // Special case: a single binary or unary auto-generated colormap. // Use dark grey/red to make 1s stand out. if(u == 1) { // Red for on return "#FF0000"; } else { // Dark grey for off return "#545454"; } } if(have_colormap(u_name)) { // u is a colormap if(colormaps[u_name].hasOwnProperty(u)) { // And the colormap has an entry here. Use it as the base color. var to_clone = colormaps[u_name][u].color; var base_color = Color({ hue: to_clone.hue(), saturation: to_clone.saturationv(), value: to_clone.value() }); } else { // The colormap has no entry. Assume we're calculating all the // entries. We do this by splitting the color circle evenly. // This holds the number of colors, which is 1 more than the largest // value used (since we start at color 0), which is the magnitude. // It's OK to go ask for the magnitude of this layer since it must // have already been downloaded. var num_colors = layers[u_name].magnitude + 1; // Calculate the hue for this number. var hsv_hue = u / (num_colors + 1) * 360; // The base color is a color at that hue, with max saturation and // value var base_color = Color({ hue: hsv_hue, saturation: 100, value: 100 }) } // Now that the base color is set, consult v to see what shade to use. if(v_name == undefined) { // No v layer is actually in use. Use whatever is in the base // color // TODO: This code path is silly, clean it up. var hsv_value = base_color.value(); } else if(have_colormap(v_name)) { // Do discrete shades in v // This holds the number of shades we need. // It's OK to go ask for the magnitude of this layer since it must // have already been downloaded. var num_shades = layers[v_name].magnitude + 1; // Calculate what shade we need from the nonnegative integer v // We want 100 to be included (since that's full brightness), but we // want to skip 0 (since no color can be seen at 0), so we add 1 to // v. var hsv_value = (v + 1) / num_shades * 100; } else { // Calculate what shade we need from v on -1 to 1 var hsv_value = 50 + v * 50; } // Set the color's value component. base_color.value(hsv_value); // Return the shaded color return base_color.hexString(); } // If we get here, we only have non-colormap layers. // This is the polar angle (hue) in degrees, forced to be // positive. var hsv_hue = Math.atan2(v, u) * 180 / Math.PI; if(hsv_hue < 0) { hsv_hue += 360; } // Rotate it by 60 degrees, so that the first layer is // yellow/blue hsv_hue += 60; if(hsv_hue > 360) { hsv_hue -= 360; } // This is the polar radius (value). We inscribe our square // of side length 2 in a circle of radius 1 by dividing by // sqrt(2). So we get a value from 0 to 1 var hsv_value = (Math.sqrt(Math.pow(u, 2) + Math.pow(v, 2)) / Math.sqrt(2)); // This is the HSV saturation component of the color on 0 to 1. // Just fix to 1. var hsv_saturation = 1.0; // Now scale saturation and value to percent hsv_saturation *= 100; hsv_value *= 100; // Now we have the color as HSV, but CSS doesn't support it. // Make a Color object and get the RGB string try { return Color({ hue: hsv_hue, saturation: hsv_saturation, value: hsv_value, }).hexString(); } catch(error) { print("(" + u + "," + v + ") broke with color (" + hsv_hue + "," + hsv_saturation + "," + hsv_value + ")"); // We'll return an error color return "white"; } } // Define a flat projection // See https://developers.google.com/maps/documentation/javascript/maptypes#Projections function FlatProjection() { } FlatProjection.prototype.fromLatLngToPoint = function(latLng) { // Given a LatLng from -90 to 90 and -180 to 180, transform to an x, y Point // from 0 to 256 and 0 to 256 var point = new google.maps.Point((latLng.lng() + 180) * 256 / 360, (latLng.lat() + 90) * 256 / 180); return point; } FlatProjection.prototype.fromPointToLatLng = function(point, noWrap) { // Given a an x, y Point from 0 to 256 and 0 to 256, transform to a LatLng from // -90 to 90 and -180 to 180 var latLng = new google.maps.LatLng(point.y * 180 / 256 - 90, point.x * 360 / 256 - 180, noWrap); return latLng; } // Define a Google Maps MapType that's all blank // See https://developers.google.com/maps/documentation/javascript/examples/maptype-base function BlankMapType() { } BlankMapType.prototype.tileSize = new google.maps.Size(256,256); BlankMapType.prototype.maxZoom = 19; BlankMapType.prototype.getTile = function(coord, zoom, ownerDocument) { // This is the element representing this tile in the map // It should be an empty div var div = ownerDocument.createElement("div"); div.style.width = this.tileSize.width + "px"; div.style.height = this.tileSize.height + "px"; div.style.backgroundColor = "#000000"; return div; } BlankMapType.prototype.name = "Blank"; BlankMapType.prototype.alt = "Blank Map"; BlankMapType.prototype.projection = new FlatProjection(); function get_LatLng(x, y) { // Given a point x, y in map space (0 to 256), get the corresponding LatLng return FlatProjection.prototype.fromPointToLatLng( new google.maps.Point(x, y)); } function clearMap() { } function drl_values(layout_index) { // Download the DrL position data, and make it into a layer $.get("drl"+ layout_index +".tab", function(tsv_data) { // This is an array of rows, which are arrays of values: // id, x, y // Only this time X and Y are Cartesian coordinates. var parsed = $.tsv.parseRows(tsv_data); // Compute two layers: one for x position, and one for y position. var layer_x = {}; var layer_y = {}; for(var i = 0; i < parsed.length; i++) { // Pull out the parts of the TSV entry var label = parsed[i][0]; if(label == "") { // DrL ends its output with a blank line, which we skip // here. continue; } var x = parseFloat(parsed[i][1]); // Invert the Y coordinate since we do that in the hex grid var y = -parseFloat(parsed[i][2]); // Add x and y to the appropriate layers layer_x[label] = x; layer_y[label] = y; } // Register the layers with no priorities. By default they are not // selections. add_layer_data("DrL X Position", layer_x); add_layer_data("DrL Y Position", layer_y); // Make sure the layer browser has the up-to-date layer list update_browse_ui(); }, "text"); } function assignment_values (layout_index, spacing) { // Download the signature assignments to hexagons and fill in the global // hexagon assignment grid. $.get("assignments" + layout_index +".tab", function(tsv_data) { // This is an array of rows, which are arrays of values: // id, x, y var parsed = $.tsv.parseRows(tsv_data); // This holds the maximum observed x var max_x = 0; // And y var max_y = 0; // Fill in the global signature grid and ploygon grid arrays. for(var i = 0; i < parsed.length; i++) { // Get the label var label = parsed[i][0]; if(label == "") { // Blank line continue; } // Get the x coord var x = parseInt(parsed[i][1]); // And the y coord var y = parseInt(parsed[i][2]); x = x * spacing; y = y * spacing; // Update maxes max_x = Math.max(x, max_x); max_y = Math.max(y, max_y); // Make sure we have a row if(signature_grid[y] == null) { signature_grid[y] = []; // Pre-emptively add a row to the polygon grid. polygon_grid[y] = []; } // Store the label in the global signature grid. signature_grid[y][x] = label; } // We need to fit this whole thing into a 256x256 grid. // How big can we make each hexagon? // TODO: Do the algrbra to make this exact. Right now we just make a // grid that we know to be small enough. // Divide the space into one column per column, and calculate // side length from column width. Add an extra column for dangling // corners. var side_length_x = (256)/ (max_x + 2) * (2.0 / 3.0); print("Max hexagon side length horizontally is " + side_length_x); // Divide the space into rows and calculate the side length // from hex height. Remember to add an extra row for wggle. var side_length_y = ((256)/(max_y + 2)) / Math.sqrt(3); print("Max hexagon side length vertically is " + side_length_y); // How long is a hexagon side in world coords? // Shrink it from the biggest we can have so that we don't wrap off the // edges of the map. var hexagon_side_length = Math.min(side_length_x, side_length_y) / 2.0; // Store this in the global hex_size, so we can later calculate the hex // size in pixels and make borders go away if we are too zoomed out. hex_size = hexagon_side_length; // How far in should we move the whole grid from the top left corner of // the earth? // Let's try leaving a 1/4 Earth gap at least, to stop wrapping in // longitude that we can't turn off. // Since we already shrunk the map to half max size, this would put it // 1/4 of the 256 unit width and height away from the top left corner. grid_offset = (256) / 4; // Loop through again and draw the polygons, now that we know how big // they have to be for(var i = 0; i < parsed.length; i++) { // TODO: don't re-parse this info // Get the label var label = parsed[i][0]; if(label == "") { // Blank line continue; } // Get the x coord var x = parseInt(parsed[i][1]); // And the y coord var y = parseInt(parsed[i][2]); x = x * spacing; y = y * spacing; // Make a hexagon on the Google map and store that. var hexagon = make_hexagon(y, x, hexagon_side_length, grid_offset); // Store by x, y in grid polygon_grid[y][x] = hexagon; // Store by label polygons[label] = hexagon; // Set the polygon's signature so we can look stuff up for it when // it's clicked. set_hexagon_signature(hexagon, label); } // Now that the ploygons exist, do the initial redraw to set all their // colors corectly. In case someone has messed with the controls. // TODO: can someone yet have messed with the controlls? refresh(); }, "text"); } // Function to create a new map based upon the the layout_name argument // Find the index of the layout_name and pass it as the index to the // drl_values and assignment_values functions as these files are indexed // according to the appropriate layout function recreate_map(layout_name, spacing) { var layout_index = layout_names.indexOf(layout_name); drl_values(layout_index); assignment_values(layout_index, spacing); } $(function() { // Set up the RPC system for background statistics rpc_initialize(); // Set up the Google Map initialize_view(0); // Set up the layer search $("#search").select2({ placeholder: "Add Attribute...", query: function(query) { // Given a select2 query object, call query.callback with an object // with a "results" array. // This is the array of result objects we will be sending back. var results = []; // Get where we should start in the layer list, from select2's // infinite scrolling. var start_position = 0; if(query.context != undefined) { start_position = query.context; } for(var i = start_position; i < layer_names_sorted.length; i++) { // For each possible result if(layer_names_sorted[i].toLowerCase().indexOf( query.term.toLowerCase()) != -1) { // Query search term is in this layer's name. Add a select2 // record to our results. Don't specify text: our custom // formatter looks up by ID and makes UI elements // dynamically. results.push({ id: layer_names_sorted[i] }); if(results.length >= SEARCH_PAGE_SIZE) { // Page is full. Send it on. break; } } } // Give the results back to select2 as the results parameter. query.callback({ results: results, // Say there's more if we broke out of the loop. more: i < layer_names_sorted.length, // If there are more results, start after where we left off. context: i + 1 }); }, formatResult: function(result, container, query) { // Given a select2 result record, the element that our results go // in, and the query used to get the result, return a jQuery element // that goes in the container to represent the result. // Get the layer name, and make the browse UI for it. return make_browse_ui(result.id); }, // We want our dropdown to be big enough to browse. dropdownCssClass: "results-dropdown" }); // Handle result selection $("#search").on("select2-selecting", function(event) { // The select2 id of the thing clicked (the layer's name) is event.val var layer_name = event.val; // User chose this layer. Add it to the global shortlist. // Only add to the shortlist if it isn't already there // Was it already there? var found = false; for(var j = 0; j < shortlist.length; j++) { if(shortlist[j] == layer_name) { found = true; break; } } if(!found) { // It's new. Add it to the shortlist shortlist.push(layer_name); // Update the UI to reflect this. This may redraw the view. update_shortlist_ui(); } // Don't actually change the selection. // This keeps the dropdown open when we click. event.preventDefault(); }); $("#recalculate-statistics").button().click(function() { // Re-calculate the statistics between the currently filtered hexes and // everything else. // Put up the throbber instead of us. $("#recalculate-statistics").hide(); $(".recalculate-throbber").show(); // This holds the currently enabled filters. var filters = get_current_filters(); with_filtered_signatures(filters, function(signatures) { // Find everything passing the filters and run the statistics. recalculate_statistics(signatures); }); }); // Temporary Inflate Button $("#inflate").button().click(function() { initialize_view (0); recreate_map(current_layout_name, 2); refresh (); }); // Create Pop-Up UI for Set Operations $("#set-operations").prepend(create_set_operation_ui ()); // Action handler for display of set operation pop-up $("#set-operation").button().click(function() { set_operation_clicks++; if (set_operation_clicks % 2 != 0){ show_set_operation_drop_down (); } else { hide_set_operation_drop_down (); var drop_downs = document.getElementsByClassName("set-operation-value"); for (var i = 0; i < drop_downs.length; i++) { drop_downs[i].style.visibility="hidden"; } } }); // Coputation of Set Operations var compute_button = document.getElementsByClassName ("compute-button"); compute_button[0].onclick = function () { var layer_names = []; var layer_values = []; var layer_values_text = []; var drop_down_layers = document.getElementsByClassName("set-operation-value"); var drop_down_data_values = document.getElementsByClassName("set-operation-layer-value"); var function_type = document.getElementById("set-operations-list"); var selected_function = function_type.selectedIndex; var selected_index = drop_down_layers[0].selectedIndex; layer_names.push(drop_down_layers[0].options[selected_index].text); var selected_index = drop_down_data_values[0].selectedIndex; layer_values.push(drop_down_data_values[0].options[selected_index].value); layer_values_text.push(drop_down_data_values[0].options[selected_index].text); if (selected_function != 5) { var selected_index = drop_down_data_values[1].selectedIndex; layer_values.push(drop_down_data_values[1].options[selected_index].value); layer_values_text.push(drop_down_data_values[1].options[selected_index].text); var selected_index = drop_down_layers[1].selectedIndex; layer_names.push(drop_down_layers[1].options[selected_index].text); } switch (selected_function) { case 1: compute_intersection(layer_values, layer_names, layer_values_text); break; case 2: compute_union(layer_values, layer_names, layer_values_text); break; case 3: compute_set_difference(layer_values, layer_names, layer_values_text); break; case 4: compute_symmetric_difference(layer_values, layer_names, layer_values_text); break; case 5: compute_absolute_complement(layer_values, layer_names, layer_values_text); break default: complain ("Set Theory Error"); } }; // Download the layer index $.get("layers.tab", function(tsv_data) { // Layer index is <name>\t<filename>\t<clumpiness> var parsed = $.tsv.parseRows(tsv_data); for(var i = 0; i < parsed.length; i++) { // Pull out the parts of the TSV entry // This is the name of the layer. var layer_name = parsed[i][0]; if(layer_name == "") { // Skip any blank lines continue; } // This is the URL from which to download the TSV for the actual // layer. var layer_url = parsed[i][1]; // This is the layer's clumpiness score var layer_clumpiness = parseFloat(parsed[i][2]); // This is the number of hexes that the layer has any values for. // We need to get it from the server so we don't have to download // the layer to have it. var layer_count = parseFloat(parsed[i][3]); // This is the number of 1s in a binary layer, or NaN in other // layers var layer_positives = parseFloat(parsed[i][4]); // Add this layer to our index of layers add_layer_url(layer_name, layer_url, { clumpiness: layer_clumpiness, positives: layer_positives, n: layer_count }); } // Now we have added layer downloaders for all the layers in the // index. Update the UI update_browse_ui(); }, "text"); // Download full score matrix index, which we later use for statistics. Note // that stats won't work unless this finishes first. TODO: enforce this. $.get("matrices.tab", function(tsv_data) { // Matrix index is just <filename> var parsed = $.tsv.parseRows(tsv_data); for(var i = 0; i < parsed.length; i++) { // Pull out the parts of the TSV entry // This is the filename of the matrix. var matrix_name = parsed[i][0]; if(matrix_name == "") { // Not a real matrix continue; } // Add it to the global list available_matrices.push(matrix_name); } }, "text"); // Download color map information $.get("colormaps.tab", function(tsv_data) { // Colormap data is <layer name>\t<value>\t<category name>\t<color> // \t<value>\t<category name>\t<color>... var parsed = $.tsv.parseRows(tsv_data); for(var i = 0; i < parsed.length; i++) { // Get the name of the layer var layer_name = parsed[i][0]; // Skip blank lines if(layer_name == "") { continue; } // This holds all the categories (name and color) by integer index var colormap = []; print("Loading colormap for " + layer_name); for(j = 1; j < parsed[i].length; j += 3) { // Store each color assignment. // Doesn't run if there aren't any assignments, leaving an empty // colormap object that just forces automatic color selection. // This holds the index of the category var category_index = parseInt(parsed[i][j]); // The colormap gets an object with the name and color that the // index number refers to. Color is stored as a color object. colormap[category_index] = { name: parsed[i][j + 1], color: Color(parsed[i][j + 2]) }; print( colormap[category_index].name + " -> " + colormap[category_index].color.hexString()); } // Store the finished color map in the global object colormaps[layer_name] = colormap; } // We may need to redraw the view in response to having new color map // info, if it came particularly late. refresh(); }, "text"); // Download the Matrix Names and pass it to the layout_names array $.get("matrixnames.tab", function(tsv_data) { // This is an array of rows, which are strings of matrix names var parsed = $.tsv.parseRows(tsv_data); for(var i = 0; i < parsed.length; i++) { // Pull out the parts of the TSV entry var label = parsed[i][0]; if(label == "") { // Skip any blank lines continue; } // Add layout names to global array of names layout_names.push(label); if(layout_names.length == 1) { // This is the very first layout. Pull it up. // TODO: We don't go through the normal change event since we // never change the dropdown value actually. But we duplicate // user selection hode here. var current_layout = "Current Layout: " + layout_names[0]; $("#current-layout").text(current_layout); initialize_view (0); recreate_map(layout_names[0], 1); refresh (); current_layout_name = layout_names[0]; } } }, "text"); $("#layout-search").select2({ placeholder: "Select a Layout...", query: function(query) { // Given a select2 query object, call query.callback with an object // with a "results" array. // This is the array of result objects we will be sending back. var results = []; // Get where we should start in the layer list, from select2's // infinite scrolling. var start_position = 0; if(query.context != undefined) { start_position = query.context; } for(var i = start_position; i < layout_names.length; i++) { // For each possible result if(layout_names[i].toLowerCase().indexOf( query.term.toLowerCase()) != -1) { // Query search term is in this layer's name. Add a select2 // record to our results. Don't specify text: our custom // formatter looks up by ID and makes UI elements // dynamically. results.push({ id: layout_names[i] }); if(results.length >= SEARCH_PAGE_SIZE) { // Page is full. Send it on. break; } } } // Give the results back to select2 as the results parameter. query.callback({ results: results, // Say there's more if we broke out of the loop. more: i < layout_names.length, // If there are more results, start after where we left off. context: i + 1 }); }, formatResult: function(result, container, query) { // Given a select2 result record, the element that our results go // in, and the query used to get the result, return a jQuery element // that goes in the container to represent the result. // Get the layer name, and make the browse UI for it. return make_toggle_layout_ui(result.id); }, // We want our dropdown to be big enough to browse. dropdownCssClass: "results-dropdown" }); // Handle result selection $("#layout-search").on("select2-selecting", function(event) { // The select2 id of the thing clicked (the layout's name) is event.val var layout_name = event.val; var current_layout = "Current Layout: " + layout_name; $("#current-layout").text(current_layout); initialize_view (0); recreate_map(layout_name, 1); refresh (); // Don't actually change the selection. // This keeps the dropdown open when we click. event.preventDefault(); current_layout_name = layout_name; }); drl_values(layout_names[0]); assignment_values (layout_names[0], 1); current_layout_name = layout_names[0]; });