comparison 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
comparison
equal deleted inserted replaced
-1:000000000000 0:1407e3634bcf
1 // hexagram.js
2 // Run the hexagram visualizer client.
3
4 // Globals
5 // This is a mapping from coordinates [x][y] in the global hex grid to signature
6 // name
7 var signature_grid = [];
8
9 // This holds a global list of layer pickers in layer order. It is also the
10 // authority on what layers are currently selected.
11 var layer_pickers = [];
12
13 // This holds a list of layer objects by name.
14 // Layer objects have:
15 // A downloading function "downloader"
16 // A data object (from hex name to float) "data"
17 // A magnitude "magnitude"
18 // A boolean "selection" that specifies whether this is a user selection or not.
19 // (This may be absent, which is the same as false.)
20 // Various optional metadata fields
21 var layers = {};
22
23 // This is a list of layer names maintained in sorted order.
24 var layer_names_sorted = [];
25
26 // This is a list of the map-layour names mantained in order of entry
27 var layout_names = [];
28
29 // This holds an array of layer names that the user has added to the "shortlist"
30 // They can be quickly selected for display.
31 var shortlist = [];
32
33 // This holds an object form shortlisted layer names to jQuery shortlist UI
34 // elements, so we can efficiently tell if e.g. one is selected.
35 var shortlist_ui = {};
36
37 // This is a list of layer names whose intersection checkbox has been selected.
38 var shortlist_intersection = [];
39
40 //This is the number of intersection checkboxes that have been selected.
41 var shortlist_intersection_num = 0;
42
43 // This is a list of layer names whose union checkbox has been selected.
44 var shortlist_union = [];
45
46 //This is the number of union checkboxes that have been selected.
47 var shortlist_union_num = 0;
48
49 //This is a list of layer names whose set difference checkbox has been selected.
50 var shortlist_set_difference = [];
51
52 // This is the number of set difference checkboxes that have been selected.
53 var shortlist_set_difference_num = 0;
54
55 // This is a list of the layer names whose symmetric difference checkbox
56 // has been selected.
57 var shortlist_symmetric_difference = [];
58
59 // This is the number of symmetric difference checkboxes that have been
60 // selected.
61 var shortlist_symmetric_difference_num = 0;
62
63 // This is an array containing the layer whose absolute complement checkbox
64 // has been selected.
65 var shortlist_absolute_complement = [];
66
67 // This is the number of absolute complement checkboxes that have been selected.
68 var shortlist_absolute_complement_num = 0;
69
70 // Records number of set-operation clicks
71 var set_operation_clicks = 0;
72
73 // Boolean stating whether this is the first time the set operation popup
74 // has been created so that "Select Layer" Default is added only once
75 var first_opening = true;
76
77 // Boolean for Creating Layer from Filter
78 var created = false;
79
80 // Stores the Name of Current Layer Displayed
81 var current_layout_name;
82
83 // This holds colormaps (objects from layer values to category objects with a
84 // name and color). They are stored under the name of the layer they apply to.
85 var colormaps = {}
86
87 // This holds an array of the available score matrix filenames
88 var available_matrices = [];
89
90 // This holds the Google Map that we use for visualization
91 var googlemap = null;
92
93 // This is the global Google Maps info window. We only want one hex to have its
94 // info open at a time.
95 var info_window = null;
96
97 // This holds the signature name of the hex that the info window is currently
98 // about.
99 var selected_signature = undefined;
100
101 // Which tool is the user currently using (string name or undefined for no tool)
102 // TODO: This is a horrible hack, replace it with a unified tool system at once.
103 var selected_tool = undefined;
104
105 // This holds the grid of hexagon polygons on that Google Map.
106 var polygon_grid = [];
107
108 // This holds an object of polygons by signature name
109 var polygons = {};
110
111 // How big is a hexagon in google maps units? This gets filled in once we have
112 // the hex assignment data. (This is really the side length.)
113 var hex_size;
114
115 // This holds a handle for the currently enqueued view redrawing timeout.
116 var redraw_handle;
117
118 // This holds all the currently active tool event listeners.
119 // They are indexed by handle, and are objects with a "handler" and an "event".
120 var tool_listeners = {};
121
122 // This holds the next tool listener handle to give out
123 var tool_listener_next_id = 0;
124
125 // This holds the next selection number to use. Start at 1 since the user sees
126 // these.
127 var selection_next_id = 1;
128
129 // This is a pool of statistics Web Workers.
130 var rpc_workers = [];
131
132 // This holds which RPC worker we ought to give work to next.
133 // TODO: Better scheduling, and wrap all this into an RPC object.
134 var next_free_worker = 0;
135
136 // This holds how namy RPC jobs are currently running
137 var jobs_running = 0;
138
139 // This is the object of pending callbacks by RPC id
140 var rpc_callbacks = {};
141
142 // This is the next unallocated RPC id
143 var rpc_next_id = 0;
144
145 // How many statistics Web Workers should we start?
146 var NUM_RPC_WORKERS = 10;
147
148 // What's the minimum number of pixels that hex_size must represent at the
149 // current zoom level before we start drawing hex borders?
150 var MIN_BORDER_SIZE = 10;
151
152 // And how thick should the border be when drawn?
153 var HEX_STROKE_WEIGHT = 2;
154
155 // How many layers do we know how to draw at once?
156 var MAX_DISPLAYED_LAYERS = 2;
157
158 // How many layer search results should we display at once?
159 var SEARCH_PAGE_SIZE = 10;
160
161 // How big is our color key in pixels?
162 var KEY_SIZE = 100;
163
164 // This is an array of all Google Maps events that tools can use.
165 var TOOL_EVENTS = [
166 "click",
167 "mousemove"
168 ];
169
170 // This is a global variable that keeps track of the current Goolge Map zoom
171 // This is needed to keep viewing consistent across layouts
172 var global_zoom = 0;
173
174 function print(text) {
175 // Print some logging text to the browser console
176
177 if(console && console.log) {
178 // We know the console exists, and we can log to it.
179 console.log(text);
180 }
181 }
182
183 function complain(text) {
184 // Display a temporary error message to the user.
185 $("#error-notification").text(text);
186 $(".error").show().delay(1250).fadeOut(1000);
187
188 if(console && console.error) {
189 // Inform the browser console of this problem.as
190 console.error(text);
191 }
192 }
193
194 function make_hexagon(row, column, hex_side_length, grid_offset) {
195 // Make a new hexagon representing the hexagon at the given grid coordinates.
196 // hex_side_length is the side length of hexagons in Google Maps world
197 // coordinate units. grid_offset specifies a distance to shift the whole
198 // grid down and right from the top left corner of the map. This lets us
199 // keep the whole thing away from the edges of the "earth", where Google
200 // Maps likes to wrap.
201 // Returns the Google Maps polygon.
202
203 // How much horizontal space is needed per hex on average, stacked the
204 // way we stack them (wiggly)?
205 var hex_column_width = 3.0/2.0 * hex_side_length;
206
207 // How tall is a hexagon?
208 var hex_height = Math.sqrt(3) * hex_side_length;
209
210 // How far apart are hexagons on our grid, horizontally (world coordinate units)?
211 var hex_padding_horizontal = 0;
212
213 // And vertically (world coordinate units)?
214 var hex_padding_veritcal = 0;
215
216 // First, what are x and y in 0-256 world coordinates fo this grid position?
217 var x = column * (hex_column_width + hex_padding_horizontal);
218 var y = row * (hex_height + hex_padding_veritcal);
219 if(column % 2 == 1) {
220 // Odd columns go up
221 y -= hex_height / 2;
222 }
223
224 // Apply the grid offset to this hex
225 x += grid_offset;
226 y += grid_offset;
227
228 // That got X and Y for the top left corner of the bounding box. Shift to
229 // the center.
230 x += hex_side_length;
231 y += hex_height / 2;
232
233 // Offset the whole thing so no hexes end up off the map when they wiggle up
234 y += hex_height / 2;
235
236 // This holds an array of all the hexagon corners
237 var coords = [
238 get_LatLng(x - hex_side_length, y),
239 get_LatLng(x - hex_side_length / 2, y - hex_height / 2),
240 get_LatLng(x + hex_side_length / 2, y - hex_height / 2),
241 get_LatLng(x + hex_side_length, y),
242 get_LatLng(x + hex_side_length / 2, y + hex_height / 2),
243 get_LatLng(x - hex_side_length / 2, y + hex_height / 2),
244 ];
245
246 // We don't know whether the hex should start with a stroke or not without
247 // looking at the current zoom level.
248 // Get the current zoom level (low is out)
249 var zoom = googlemap.getZoom();
250
251 // API docs say: pixelCoordinate = worldCoordinate * 2 ^ zoomLevel
252 // So this holds the number of pixels that the global length hex_size
253 // corresponds to at this zoom level.
254 var hex_size_pixels = hex_size * Math.pow(2, zoom);
255
256 // Construct the Polygon
257 var hexagon = new google.maps.Polygon({
258 paths: coords,
259 strokeColor: "#000000",
260 strokeOpacity: 1.0,
261 // Only turn on the border if we're big enough
262 strokeWeight: hex_size_pixels < MIN_BORDER_SIZE ? 0 : HEX_STROKE_WEIGHT,
263 fillColor: "#FF0000",
264 fillOpacity: 1.0
265 });
266
267 // Attach the hexagon to the global map
268 hexagon.setMap(googlemap);
269
270 // Set up the click listener to move the global info window to this hexagon
271 // and display the hexagon's information
272 google.maps.event.addListener(hexagon, "click", function(event) {
273 if(selected_tool == undefined) {
274 // The user isn't trying to use a tool currently, so we can use
275 // their clicks for the infowindow.
276
277 // Remove the window from where it currently is
278 info_window.close();
279
280 // Place the window in the center of this hexagon.
281 info_window.setPosition(get_LatLng(x, y));
282
283 // Record that this signature is selected now
284 selected_signature = hexagon.signature;
285
286 // Calculate the window's contents and make it display them.
287 redraw_info_window();
288 }
289 });
290
291 // Subscribe the tool listeners to events on this hexagon
292 subscribe_tool_listeners(hexagon);
293
294 return hexagon;
295 }
296
297 function set_hexagon_signature(hexagon, text) {
298 // Given a polygon representing a hexagon, set the signature that the
299 // hexagon represents.
300 hexagon.signature = text;
301 }
302
303 function set_hexagon_color(hexagon, color) {
304 // Given a polygon, set the hexagon's current background
305 // color.
306
307 hexagon.setOptions({
308 fillColor: color
309 });
310 }
311
312 function set_hexagon_stroke_weight(hexagon, weight) {
313 // Given a polygon, set the weight of hexagon's border stroke, in number of
314 // screen pixels.
315
316 hexagon.setOptions({
317 strokeWeight: weight
318 });
319 }
320
321 function redraw_info_window() {
322 // Set the contents of the global info window to reflect the currently
323 // visible information about the global selected signature.
324
325 if(selected_signature == undefined) {
326 // No need to update anything
327 return;
328 }
329
330 // Go get the infocard that goes in the info_window and, when it's
331 // prepared, display it.
332 with_infocard(selected_signature, function(infocard) {
333 // The [0] is supposed to get the DOM element from the jQuery
334 // element.
335 info_window.setContent(infocard[0]);
336
337 // Open the window. It may already be open, or it may be closed but
338 // properly positioned and waiting for its initial contents before
339 // opening.
340 info_window.open(googlemap);
341 });
342 }
343
344 function with_infocard(signature, callback) {
345 // Given a signature, call the callback with a jQuery element representing
346 // an "info card" about that signature. It's the contents of the infowindow
347 // that we want to appear when the user clicks on the hex representing this
348 // signature, and it includes things like the signature name and its values
349 // under any displayed layers (with category names if applicable).
350 // We return by callback because preparing the infocard requires reading
351 // from the layers, which are retrieved by callback.
352 // TODO: Can we say that we will never have to download a layer here and
353 // just directly access them? Is that neater or less neat?
354
355 // Using jQuery to build this saves us from HTML injection by making jQuery
356 // do all the escaping work (we only ever set text).
357
358 function row(key, value) {
359 // Small helper function that returns a jQuery element that displays the
360 // given key being the given value.
361
362 // This holds the root element of the row
363 var root = $("<div/>").addClass("info-row");
364
365 // Add the key and value elements
366 root.append($("<div/>").addClass("info-key").text(key));
367 root.append($("<div/>").addClass("info-value").text(value));
368
369 return root;
370 }
371
372 // This holds a list of the string names of the currently selected layers,
373 // in order.
374 // Just use everything on the shortlist.
375 var current_layers = shortlist;
376
377 // Obtain the layer objects (mapping from signatures/hex labels to colors)
378 with_layers(current_layers, function(retrieved_layers) {
379
380 // This holds the root element of the card.
381 var infocard = $("<div/>").addClass("infocard");
382
383 infocard.append(row("Name", signature).addClass("info-name"));
384
385 for(var i = 0; i < current_layers.length; i++) {
386 // This holds the layer's value for this signature
387 var layer_value = retrieved_layers[i].data[signature];
388
389 if(have_colormap(current_layers[i])) {
390 // This is a color map
391
392 // This holds the category object for this category number, or
393 // undefined if there isn't one.
394 var category = colormaps[current_layers[i]][layer_value];
395
396 if(category != undefined) {
397 // There's a specific entry for this category, with a
398 // human-specified name and color.
399 // Use the name as the layer value
400 layer_value = category.name;
401 }
402 }
403
404 if(layer_value == undefined) {
405 // Let the user know that there's nothing there in this layer.
406 layer_value = "<undefined>";
407 }
408
409 // Make a listing for this layer's value
410 infocard.append(row(current_layers[i], layer_value));
411 }
412
413 // Return the infocard by callback
414 callback(infocard);
415 });
416
417 }
418
419 function add_layer_url(layer_name, layer_url, attributes) {
420 // Add a layer with the given name, to be downloaded from the given URL, to
421 // the list of available layers.
422 // Attributes is an object of attributes to copy into the layer.
423
424 // Store the layer. Just keep the URL, since with_layer knows what to do
425 // with it.
426 layers[layer_name] = {
427 url: layer_url,
428 data: undefined,
429 magnitude: undefined
430 };
431
432 for(var name in attributes) {
433 // Copy over each specified attribute
434 layers[layer_name][name] = attributes[name];
435 }
436
437 // Add it to the sorted layer list.
438 layer_names_sorted.push(layer_name);
439
440 // Don't sort because our caller does that when they're done adding layers.
441
442 }
443
444 function add_layer_data(layer_name, data, attributes) {
445 // Add a layer with the given name, with the given data to the list of
446 // available layers.
447 // Attributes is an object of attributes to copy into the layer.
448
449 // Store the layer. Just put in the data. with_layer knows what to do if the
450 // magnitude isn't filled in.
451 layers[layer_name] = {
452 url: undefined,
453 data: data,
454 magnitude: undefined
455 };
456
457 var check_layer_exists = layers[layer_name];
458
459 for(var name in attributes) {
460 // Copy over each specified attribute
461 layers[layer_name][name] = attributes[name];
462 }
463
464 // Add it to the sorted layer list and sort
465 layer_names_sorted.push(layer_name);
466
467 // Don't sort because our caller does that when they're done adding layers.
468 }
469
470 function with_layer(layer_name, callback) {
471 // Run the callback, passing it the layer (object from hex label/signature
472 // to float) with the given name.
473 // This is how you get layers, and allows for layers to be downloaded
474 // dynamically.
475 // have_layer must return true for the given name.
476
477 // First get what we have stored for the layer
478 var layer = layers[layer_name];
479
480 var data_val = layer.data;
481 if(layer.data == undefined) {
482 // We need to download the layer.
483 print("Downloading \"" + layer.url + "\"");
484
485 // Go get it (as text!)
486 $.get(layer.url, function(layer_tsv_data) {
487
488 // This is the TSV as parsed by our TSV-parsing plugin
489 var layer_parsed = $.tsv.parseRows(layer_tsv_data);
490
491 // This is the layer we'll be passing out. Maps from
492 // signatures to floats on -1 to 1.
493 var layer_data = {};
494
495 for(var j = 0; j < layer_parsed.length; j++) {
496 // This is the label of the hex
497 var label = layer_parsed[j][0];
498
499 if(label == "") {
500 // Skip blank lines
501 continue;
502 }
503
504 // This is the heat level (-1 to 1)
505 var heat = parseFloat(layer_parsed[j][1]);
506
507 // Store in the layer
508 layer_data[label] = heat;
509 }
510
511 // Save the layer data locally
512 layers[layer_name].data = layer_data;
513
514 // Now the layer has been properly downloaded, but it may not have
515 // metadata. Recurse with the same callback to get metadata.
516 with_layer(layer_name, callback);
517 }, "text");
518 } else if(layer.magnitude == undefined) {
519 // We've downloaded it already, or generated it locally, but we don't
520 // know the magnitude. Compute that and check if it's a colormap.
521
522 // Grab the data, which we know is defined.
523 var layer_data = layers[layer_name].data;
524
525 // Store the maximum magnitude in the layer
526 // -1 is a good starting value since this always comes out positive
527 var magnitude = -1;
528
529 // We also want to know if all layer entries are non-negative
530 // integers (and it is thus valid as a colormap).
531 // If so, we want to display it as a colormap, so we will add an
532 // empty entry to the colormaps object (meaning we should
533 // auto-generate the colors on demand).
534 // This stores whether the layer is all integrs
535 all_nonnegative_integers = true;
536
537 for(var signature_name in layer_data) {
538 // Take the new max if it's bigger (and thus not something silly
539 // like NaN).
540 // This holds the potential new max magnitude.
541 var new_magnitude = Math.abs(layer_data[signature_name]);
542 if(new_magnitude > magnitude) {
543 magnitude = new_magnitude;
544 }
545
546 if(layer_data[signature_name] % 1 !== 0 ||
547 layer_data[signature_name] < 0 ) {
548
549 // If we have an illegal value for a colormap, record that
550 // fact
551 // See http://stackoverflow.com/a/3886106
552
553 all_nonnegative_integers = false;
554 }
555 }
556
557 // Save the layer magnitude for later.
558 layer.magnitude = magnitude;
559
560 if(!have_colormap(layer_name) && all_nonnegative_integers) {
561 // Add an empty colormap for this layer, so that
562 // auto-generated discrete colors will be used.
563 // TODO: Provide some way to override this if you really do want
564 // to see integers as a heatmap?
565 // The only overlap with the -1 to 1 restricted actual layers
566 // is if you have a data set with only 0s and 1s. Is it a
567 // heatmap layer or a colormap layer?
568 colormaps[layer_name] = {};
569 print("Inferring that " + layer_name +
570 " is really a colormap");
571 }
572
573 // Now layer metadata has been filled in. Call the callback.
574 callback(layer);
575 } else {
576 // It's already downloaded, and already has metadata.
577 // Pass it to our callback
578 callback(layer);
579 }
580 }
581
582 function with_layers(layer_list, callback) {
583 // Given an array of layer names, call the callback with an array of the
584 // corresponding layer objects (objects from signatures to floats).
585 // Conceptually it's like calling with_layer several times in a loop, only
586 // because the whole thing is continuation-based we have to phrase it in
587 // terms of recursion.
588
589 // See http://marijnhaverbeke.nl/cps/
590 // "So, we've created code that does exactly the same as the earlier
591 // version, but is twice as confusing."
592
593 if(layer_list.length == 0) {
594 // Base case: run the callback with an empty list
595 callback([]);
596 } else {
597 // Recursive case: handle the last thing in the list
598 with_layers(layer_list.slice(0, layer_list.length - 1),
599 function(rest) {
600
601 // We've recursively gotten all but the last layer
602 // Go get the last one, and pass the complete array to our callback.
603
604 with_layer(layer_list[layer_list.length - 1],
605 function(last) {
606
607 // Mutate the array. Shouldn't matter because it won't matter
608 // for us if callback does it.
609 rest.push(last);
610
611 // Send the complete array to the callback.
612 callback(rest);
613
614 });
615
616 });
617
618 }
619 }
620
621 function have_layer(layer_name) {
622 // Returns true if a layer exists with the given name, false otherwise.
623 return layers.hasOwnProperty(layer_name);
624 }
625
626 function make_shortlist_ui(layer_name) {
627 // Return a jQuery element representing the layer with the given name in the
628 // shortlist UI.
629
630
631 // This holds the root element for this shortlist UI entry
632 var root = $("<div/>").addClass("shortlist-entry");
633 root.data("layer", layer_name);
634
635 // If this is a selection, give the layer a special class
636 // TODO: Justify not having to use with_layer because this is always known
637 // client-side
638 if(layers[layer_name].selection) {
639 root.addClass("selection");
640 }
641
642 // We have some configuration stuff and then the div from the dropdown
643 // This holds all the config stuff
644 var controls = $("<div/>").addClass("shortlist-controls");
645
646 // Add a remove link
647 var remove_link = $("<a/>").addClass("remove").attr("href", "#").text("X");
648
649 controls.append(remove_link);
650
651 // Add a checkbox for whether this is enabled or not
652 var checkbox = $("<input/>").attr("type", "checkbox").addClass("layer-on");
653
654 controls.append(checkbox);
655
656 root.append(controls);
657
658 var contents = $("<div/>").addClass("shortlist-contents");
659
660 // Add the layer name
661 contents.append($("<span/>").text(layer_name));
662
663 // Add all of the metadata. This is a div to hold it
664 var metadata_holder = $("<div/>").addClass("metadata-holder");
665
666 // Fill it in
667 fill_layer_metadata(metadata_holder, layer_name);
668
669 contents.append(metadata_holder);
670
671 // Add a div to hold the filtering stuff so it wraps together.
672 var filter_holder = $("<div/>").addClass("filter-holder");
673
674 // Add an image label for the filter control.
675 // TODO: put this in a label
676 var filter_image = $("<img/>").attr("src", "filter.svg");
677 filter_image.addClass("control-icon");
678 filter_image.addClass("filter-image");
679 filter_image.attr("title", "Filter on Layer");
680 filter_image.addClass("filter");
681
682 // Add a control for filtering
683 var filter_control = $("<input/>").attr("type", "checkbox");
684 filter_control.addClass("filter-on");
685
686 filter_holder.append(filter_image);
687 filter_holder.append(filter_control);
688
689 // Add a text input to specify a filtering threshold for continuous layers
690 var filter_threshold = $("<input/>").addClass("filter-threshold");
691 // Initialize to a reasonable value.
692 filter_threshold.val(0);
693 filter_holder.append(filter_threshold);
694
695 // Add a select input to pick from a discrete list of values to filter on
696 var filter_value = $("<select/>").addClass("filter-value");
697 filter_holder.append(filter_value);
698
699 // Add a image for the save function
700 var save_filter = $("<img/>").attr("src", "save.svg");
701 save_filter.addClass("save-filter");
702 save_filter.attr("title", "Save Filter as Layer");
703
704 contents.append(filter_holder);
705 contents.append(save_filter);
706
707 if(layers[layer_name].selection) {
708 // We can do statistics on this layer.
709
710 // Add a div to hold the statistics stuff so it wraps together.
711 var statistics_holder = $("<div/>").addClass("statistics-holder");
712
713 // Add an icon
714 var statistics_image = $("<img/>").attr("src", "statistics.svg");
715 statistics_image.addClass("control-icon");
716 statistics_image.attr("title", "Statistics Group");
717 statistics_holder.append(statistics_image);
718
719 // Label the "A" radio button.
720 var a_label = $("<span/>").addClass("radio-label").text("A");
721 statistics_holder.append(a_label);
722
723 // Add a radio button for being the "A" group
724 var statistics_a_control = $("<input/>").attr("type", "radio");
725 statistics_a_control.attr("name", "statistics-a");
726 statistics_a_control.addClass("statistics-a");
727 // Put the layer name in so it's easy to tell which layer is A.
728 statistics_a_control.data("layer-name", layer_name);
729 statistics_holder.append(statistics_a_control);
730
731 // And a link to un-select it if it's selected
732 var statistics_a_clear = $("<a/>").attr("href", "#").text("X");
733 statistics_a_clear.addClass("radio-clear");
734 statistics_holder.append(statistics_a_clear);
735
736 // Label the "B" radio button.
737 var b_label = $("<span/>").addClass("radio-label").text("B");
738 statistics_holder.append(b_label);
739
740 // Add a radio button for being the "B" group
741 var statistics_b_control = $("<input/>").attr("type", "radio");
742 statistics_b_control.attr("name", "statistics-b");
743 statistics_b_control.addClass("statistics-b");
744 // Put the layer name in so it's easy to tell which layer is A.
745 statistics_b_control.data("layer-name", layer_name);
746 statistics_holder.append(statistics_b_control);
747
748 // And a link to un-select it if it's selected
749 var statistics_b_clear = $("<a/>").attr("href", "#").text("X");
750 statistics_b_clear.addClass("radio-clear");
751 statistics_holder.append(statistics_b_clear);
752
753 contents.append(statistics_holder);
754
755 // Statistics UI logic
756
757 // Make the clear links work
758 statistics_a_clear.click(function() {
759 statistics_a_control.prop("checked", false);
760 });
761 statistics_b_clear.click(function() {
762 statistics_b_control.prop("checked", false);
763 });
764 }
765
766 // Add a div to contain layer settings
767 var settings = $("<div/>").addClass("settings");
768
769 // Add a slider for setting the min and max for drawing
770 var range_slider = $("<div/>").addClass("range range-slider");
771 settings.append($("<div/>").addClass("stacker").append(range_slider));
772
773 // And a box that tells us what we have selected in the slider.
774 var range_display = $("<div/>").addClass("range range-display");
775 range_display.append($("<span/>").addClass("low"));
776 range_display.append(" to ");
777 range_display.append($("<span/>").addClass("high"));
778 settings.append($("<div/>").addClass("stacker").append(range_display));
779
780 contents.append(settings);
781
782 root.append(contents);
783
784 // Handle enabling and disabling
785 checkbox.change(function() {
786 if($(this).is(":checked") && get_current_layers().length >
787 MAX_DISPLAYED_LAYERS) {
788
789 // Enabling this checkbox puts us over the edge, so un-check it
790 $(this).prop("checked", false);
791
792 // Skip the redraw
793 return;
794 }
795
796 refresh();
797 });
798
799 // Run the removal process
800 remove_link.click(function() {
801 // Remove this layer from the shortlist
802 shortlist.splice(shortlist.indexOf(layer_name), 1);
803
804 // Remove this from the DOM
805 root.remove();
806
807 // Make the UI match the list.
808 update_shortlist_ui();
809
810 if(checkbox.is(":checked") || filter_control.is(":checked")) {
811 // Re-draw the view since we were selected (as coloring or filter)
812 // before removal.
813 refresh();
814 }
815
816 });
817
818 // Functionality for turning filtering on and off
819 filter_control.change(function() {
820 if(filter_control.is(":checked")) {
821 // First, figure out what kind of filter settings we take based on
822 // what kind of layer we are.
823 with_layer(layer_name, function(layer) {
824 if(have_colormap(layer_name)) {
825 // A discrete layer.
826 // Show the value picker.
827 filter_value.show();
828
829 // Make sure we have all our options
830 if(filter_value.children().length == 0) {
831 // No options available. We have to add them.
832 // TODO: Is there a better way to do this than asking
833 // the DOM?
834
835 for(var i = 0; i < layer.magnitude + 1; i++) {
836 // Make an option for each value.
837 var option = $("<option/>").attr("value", i);
838
839 if(colormaps[layer_name].hasOwnProperty(i)) {
840 // We have a real name for this value
841 option.text(colormaps[layer_name][i].name);
842 } else {
843 // No name. Use the number.
844 option.text(i);
845 }
846
847 filter_value.append(option);
848
849 }
850
851 // Select the last option, so that 1 on 0/1 layers will
852 // be selected by default.
853 filter_value.val(
854 filter_value.children().last().attr("value"));
855
856 }
857 } else {
858 // Not a discrete layer, so we take a threshold.
859 filter_threshold.show();
860 }
861
862 save_filter.show ();
863
864 save_filter.button().click(function() {
865 // Configure Save Filter Buttons
866
867 // Get selected value
868 var selected = filter_value.prop("selectedIndex");
869 var value = filter_value.val();
870
871 var signatures = [];
872
873 // Gather Tumor-ID Signatures with value and push to "signatures"
874 for (hex in polygons){
875 if (layer.data[hex] == value){
876 signatures.push(hex);
877 }
878 }
879
880 // Create Layer
881 if (created == false) {
882 select_list (signatures, "user selection");
883 created = true;
884 }
885 created = false;
886 });
887
888
889 // Now that the right controls are there, assume they have
890 refresh();
891
892 });
893 } else {
894 created = false;
895 // Hide the filtering settings
896 filter_value.hide();
897 filter_threshold.hide();
898 save_filter.hide();
899 // Draw view since we're no longer filtering on this layer.
900 refresh();
901 }
902 });
903
904 // Respond to changes to filter configuration
905 filter_value.change(refresh);
906
907 // TODO: Add a longer delay before refreshing here so the user can type more
908 // interactively.
909 filter_threshold.keyup(refresh);
910
911 // Configure the range slider
912
913 // First we need a function to update the range display, which we will run
914 // on change and while sliding (to catch both user-initiated and
915 //programmatic changes).
916 var update_range_display = function(event, ui) {
917 range_display.find(".low").text(ui.values[0].toFixed(3));
918 range_display.find(".high").text(ui.values[1].toFixed(3));
919 }
920
921 range_slider.slider({
922 range: true,
923 min: -1,
924 max: 1,
925 values: [-1, 1],
926 step: 1E-9, // Ought to be fine enough
927 slide: update_range_display,
928 change: update_range_display,
929 stop: function(event, ui) {
930 // The user has finished sliding
931 // Draw the view. We will be asked for our values
932 refresh();
933 }
934 });
935
936 // When we have time, go figure out whether the slider should be here, and
937 // what its end values should be.
938 reset_slider(layer_name, root)
939
940 return root;
941 }
942
943 // ____________________________________________________________________________
944 // Replacement Set Operation Code
945 // ____________________________________________________________________________
946 function get_set_operation_selection () {
947 // For the new dop-down GUI for set operation selection
948 // we neeed a function to determine which set operation is selected.
949 // This way we can display the appropriate divs.
950
951 // Drop Down List & Index for Selected Element
952 var drop_down = document.getElementById("set-operations-list");
953 var index = drop_down.selectedIndex;
954 var selection = drop_down.options[index];
955
956 return selection;
957 }
958
959 function show_set_operation_drop_down () {
960 // Show Set Operation Drop Down Menu
961 document.getElementsByClassName("set-operation-col")[0].style.visibility="visible";
962 document.getElementsByClassName("set-operation-panel-holder")[0].style.visibility="visible";
963 document.getElementsByClassName("set-operation-panel")[0].style.visibility="visible";
964 document.getElementById("set-operations").style.visibility="visible";
965 document.getElementsByClassName("set-operation-panel-title")[0].style.visibility="visible";
966 document.getElementsByClassName("set-operation-panel-contents")[0].style.visibility="visible";
967
968 }
969
970 function hide_set_operation_drop_down () {
971 // Hide Set Operation Drop Down Menu
972 document.getElementsByClassName("set-operation-col")[0].style.visibility="hidden";
973 document.getElementsByClassName("set-operation-panel-holder")[0].style.visibility="hidden";
974 document.getElementsByClassName("set-operation-panel")[0].style.visibility="hidden";
975 document.getElementById("set-operations").style.visibility="hidden";
976 document.getElementsByClassName("set-operation-panel-title")[0].style.visibility="hidden";
977 document.getElementsByClassName("set-operation-panel-contents")[0].style.visibility="hidden";
978
979 // Hide the Data Values for the Selected Layers
980 var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value");
981 for (var i = 0; i < drop_downs_layer_values.length; i++) {
982 drop_downs_layer_values[i].style.visibility="hidden";
983 }
984
985 // Hide the Compute Button
986 var compute_button = document.getElementsByClassName("compute-button");
987 compute_button[0].style.visibility = "hidden";
988
989 // Set the "Select Layer" drop down to the default value
990 var list = document.getElementById("set-operations-list");
991 list.selectedIndex = 0;
992
993 var list_value = document.getElementsByClassName("set-operation-value");
994 list_value[0].selectedIndex = 0;
995 list_value[1].selectedIndex = 0;
996
997 // Remove all elements from drop downs holding the data values for the
998 // selected layers. This way there are no values presented when the user
999 // clicks on the set operation button to open it again.
1000 var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value");
1001 var length = set_operation_layer_values[0].options.length;
1002 do{
1003 set_operation_layer_values[0].remove(0);
1004 length--;
1005 }
1006 while (length > 0);
1007
1008 var length = set_operation_layer_values[1].options.length;
1009 do{
1010 set_operation_layer_values[1].remove(0);
1011 length--;
1012 }
1013 while (length > 0);
1014
1015 }
1016
1017 function create_set_operation_ui () {
1018 // Returns a Jquery element that is then prepended to the existing
1019 // set theory drop-down menu
1020
1021 // This holds the root element for this set operation UI
1022 var root = $("<div/>").addClass("set-operation-entry");
1023
1024 // Add Drop Downs to hold the selected layers and and selected data values
1025 var set_theory_value1 = $("<select/>").addClass("set-operation-value");
1026 var set_theory_layer_value1 = $("<select/>").addClass("set-operation-layer-value");
1027 var set_theory_value2 = $("<select/>").addClass("set-operation-value");
1028 var set_theory_layer_value2 = $("<select/>").addClass("set-operation-layer-value");
1029
1030 var compute_button = $("<input/>").attr("type", "button");
1031 compute_button.addClass ("compute-button");
1032
1033 // Append to Root
1034 root.append (set_theory_value1);
1035 root.append (set_theory_layer_value1);
1036 root.append (set_theory_value2);
1037 root.append (set_theory_layer_value2);
1038 root.append (compute_button);
1039
1040 return root;
1041 }
1042
1043 function update_set_operation_drop_down () {
1044 // This is the onchange command for the drop down displaying the
1045 // different set operation functions. It is called whenever the user changes
1046 // the selected set operation.
1047
1048 // Get the value of the set operation selection made by the user.
1049 var selection = get_set_operation_selection();
1050 var value = selection.value;
1051 // Check if the selectin value is that of one of set operation functions
1052 if (selection.value == 1 || selection.value == 2
1053 || selection.value == 3 || selection.value == 4
1054 || selection.value == 5){
1055 // Make the drop downs that hold layer names and data values visible
1056 var drop_downs = document.getElementsByClassName("set-operation-value");
1057 var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value");
1058
1059 for (var i = 0; i < drop_downs.length; i++) {
1060 drop_downs[i].style.visibility="visible";
1061 }
1062
1063 for (var i = 0; i < drop_downs_layer_values.length; i++) {
1064 drop_downs_layer_values[i].style.visibility="visible";
1065 }
1066
1067 var compute_button = document.getElementsByClassName("compute-button");
1068 compute_button[0].style.visibility = "visible";
1069 compute_button[0].value = "Compute Set Operation";
1070
1071 if (first_opening == true) {
1072 // Set the default value for the drop down, holding the selected layers
1073 var default_value = document.createElement("option");
1074 default_value.text = "Select Layer 1";
1075 default_value.value = 0;
1076 drop_downs[0].add(default_value);
1077
1078 var default_value2 = document.createElement("option");
1079 default_value2.text = "Select Layer 2";
1080 default_value2.value = 0;
1081 drop_downs[1].add(default_value2);
1082
1083 // Prevent from adding the default value again
1084 first_opening = false;
1085 }
1086
1087 // Hide the second set of drop downs if "Not:" is selected
1088 if (selection.value == 5) {
1089 drop_downs[1].style.visibility="hidden";
1090 drop_downs_layer_values[1].style.visibility="hidden";
1091 }
1092 }
1093 else {
1094 // If the user has the default value selected, hide all drop downs
1095 var drop_downs = document.getElementsByClassName("set-operation-value");
1096 for (var i = 0; i < drop_downs.length; i++) {
1097 drop_downs[i].style.visibility="hidden";
1098 }
1099 var drop_downs_layer_values = document.getElementsByClassName("set-operation-layer-value");
1100 for (var i = 0; i < drop_downs_layer_values.length; i++) {
1101 drop_downs_layer_values[i].style.visibility="hidden";
1102 }
1103 var compute_button = document.getElementsByClassName("compute-button");
1104 compute_button[0].style.visibility = "hidden";
1105 }
1106 }
1107
1108 function update_set_operation_selections () {
1109 // This function is called when the shorlist is changed.
1110 // It appropriately updates the drop down containing the list of layers
1111 // to match the layers found in the shortlist.
1112
1113 // Get the list of all layers
1114 var layers = [];
1115 $("#shortlist").children().each(function(index, element) {
1116 // Get the layer name
1117 var layer_name = $(element).data("layer");
1118 layers.push(layer_name);
1119 });
1120
1121 // Get a list of all drop downs that contain layer names
1122 var drop_downs = document.getElementsByClassName("set-operation-value");
1123
1124 // Remove all existing layer names from both dropdowns
1125 var length = drop_downs[0].options.length;
1126 do{
1127 drop_downs[0].remove(0);
1128 length--;
1129 }
1130 while (length > 0);
1131 var length = drop_downs[1].options.length;
1132 do{
1133 drop_downs[1].remove(0);
1134 length--;
1135 }
1136 while (length > 0);
1137
1138 // Add the default values that were stripped in the last step.
1139 var default_value = document.createElement("option");
1140 default_value.text = "Select Layer 1";
1141 default_value.value = 0;
1142 drop_downs[0].add(default_value);
1143
1144 var default_value2 = document.createElement("option");
1145 default_value2.text = "Select Layer 2";
1146 default_value2.value = 0;
1147 drop_downs[1].add(default_value2);
1148
1149 first_opening = false;
1150
1151 // Add the layer names from the shortlist to the drop downs that store
1152 // layer names.
1153 for (var i = 0; i < drop_downs.length; i++){
1154 for (var j = 0; j < layers.length; j++) {
1155 var option = document.createElement("option");
1156 option.text = layers[j];
1157 option.value = j+1;
1158 drop_downs[i].add(option);
1159 }
1160 }
1161
1162 // Remove all elements from drop downs holding the data values for the
1163 // selected layers. This way there are no values presented when the user
1164 // clicks on the set operation button to open it again.
1165 var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value");
1166 var length = set_operation_layer_values[0].options.length;
1167 do{
1168 set_operation_layer_values[0].remove(0);
1169 length--;
1170 }
1171 while (length > 0);
1172
1173 var length = set_operation_layer_values[1].options.length;
1174 do{
1175 set_operation_layer_values[1].remove(0);
1176 length--;
1177 }
1178 while (length > 0);
1179
1180 // Call the function containing onchange commands for these dropdowns.
1181 // This way the data values are updated according the the selected layer.
1182 update_set_operation_data_values ();
1183 }
1184
1185 function update_set_operation_data_values () {
1186 // Define the onchange commands for the drop downs that hold layer names.
1187 // This way the data values are updated according the the selected layer.
1188
1189 // Get all drop down elements
1190 var selected_function = document.getElementById ("set-operations-list");
1191 var drop_downs = document.getElementsByClassName("set-operation-value");
1192 var set_operation_layer_values = document.getElementsByClassName("set-operation-layer-value");
1193
1194 // The "Select Layer1" Dropdown onchange function
1195 drop_downs[0].onchange = function(){
1196 // Strip current values of the data value dropdown
1197 var length = set_operation_layer_values[0].options.length;
1198 do{
1199 set_operation_layer_values[0].remove(0);
1200 length--;
1201 }
1202 while (length > 0);
1203
1204 // Add the data values depending on the selected layer
1205 var selectedIndex = drop_downs[0].selectedIndex;
1206 var layer_name = drop_downs[0].options[selectedIndex].text;
1207 var set_operation_data_value_select = set_operation_layer_values[0];
1208 create_set_operation_pick_list(set_operation_data_value_select, layer_name);
1209 };
1210
1211 // The "Select Layer2" Dropdown onchange function
1212 drop_downs[1].onchange = function(){
1213 // Strip current values of the data value dropdown
1214 var length = set_operation_layer_values[1].options.length;
1215 do{
1216 set_operation_layer_values[1].remove(0);
1217 length--;
1218 }
1219 while (length > 0);
1220
1221 // Add the data values depending on the selected layer
1222 var selectedIndex = drop_downs[1].selectedIndex;
1223 var layer_name = drop_downs[1].options[selectedIndex].text;
1224 var set_operation_data_value_select = set_operation_layer_values[1];
1225 create_set_operation_pick_list(set_operation_data_value_select, layer_name);
1226 };
1227
1228 }
1229
1230 function create_set_operation_pick_list(value,layer_object) {
1231
1232 // We must create a drop down containing the data values for the selected
1233 // layer.
1234
1235 // The Javascript "select" element that contains the data values
1236 // is passed as "value" and the selected layer is passed as "layer_object".
1237
1238 // First, figure out what kind of filter settings we take based on
1239 // what kind of layer we are.
1240 with_layer(layer_object, function(layer) {
1241
1242 // No options available. We have to add them.
1243 for(var i = 0; i < layer.magnitude + 1; i++) {
1244 // Make an option for each value;
1245 var option = document.createElement("option");
1246 option.value = i;
1247
1248 if(colormaps[layer_object].hasOwnProperty(i)) {
1249 // We have a real name for this value
1250 option.text = (colormaps[layer_object][i].name);
1251 } else {
1252 // No name. Use the number.
1253 option.text = i;
1254 }
1255 value.add(option);
1256
1257 // Select the last option, so that 1 on 0/1 layers will
1258 // be selected by default.
1259 var last_index = value.options.length - 1;
1260 value.selectedIndex = last_index;
1261 }
1262 // Now that the right controls are there, assume they have
1263 refresh();
1264 });
1265 }
1266
1267
1268 function update_shortlist_ui() {
1269 // Go through the shortlist and make sure each layer there has an entry in
1270 // the shortlist UI, and that each UI element has an entry in the shortlist.
1271 // Also make sure the metadata for all existing layers is up to date.
1272
1273 // Clear the existing UI lookup table
1274 shortlist_ui = {};
1275
1276 for(var i = 0; i < shortlist.length; i++) {
1277 // For each shortlist entry, put a false in the lookup table
1278 shortlist_ui[shortlist[i]] = false;
1279 }
1280
1281
1282 $("#shortlist").children().each(function(index, element) {
1283 if(shortlist_ui[$(element).data("layer")] === false) {
1284 // There's a space for this element: it's still in the shortlist
1285
1286 // Fill it in
1287 shortlist_ui[$(element).data("layer")] = $(element);
1288
1289 // Update the metadata in the element. It make have changed due to
1290 // statistics info coming back.
1291 fill_layer_metadata($(element).find(".metadata-holder"),
1292 $(element).data("layer"));
1293 } else {
1294 // It wasn't in the shortlist, so get rid of it.
1295 $(element).remove();
1296 }
1297 });
1298
1299 for(var layer_name in shortlist_ui) {
1300 // For each entry in the lookup table
1301 if(shortlist_ui[layer_name] === false) {
1302 // If it's still false, make a UI element for it.
1303 shortlist_ui[layer_name] = make_shortlist_ui(layer_name);
1304 $("#shortlist").prepend(shortlist_ui[layer_name]);
1305
1306 // Check it's box if possible
1307 shortlist_ui[layer_name].find(".layer-on").click();
1308 }
1309 }
1310
1311 // Make things re-orderable
1312 // Be sure to re-draw the view if the order changes, after the user puts
1313 // things down.
1314 $("#shortlist").sortable({
1315 update: refresh,
1316 // Sort by the part with the lines icon, so we can still select text.
1317 handle: ".shortlist-controls"
1318 });
1319
1320 update_set_operation_selections ();
1321 }
1322
1323 function uncheck_checkbox (checkbox_class) {
1324 // Unchecks chekboxes after the function has been completed.
1325 var checkboxArray = new Array ();
1326 checkboxArray = document.getElementsByClassName(checkbox_class);
1327 for (var i = 0; i < checkboxArray.length; i++)
1328 {
1329 checkboxArray[i].checked = false;
1330 }
1331 }
1332
1333 function hide_values (set_theory_function) {
1334 // Hides pick lists for set theory functions after function has been
1335 // completed.
1336 var value_type = set_theory_function + '-value';
1337
1338 var values = new Array ();
1339
1340 values = document.getElementsByClassName(value_type);
1341
1342 var length = values.length;
1343
1344 for (var i = 0; i < length; i++)
1345 {
1346 values[i].style.display = 'none';
1347 }
1348 refresh();
1349 }
1350
1351 function compute_intersection (values, intersection_layer_names, text) {
1352 // A function that will take a list of layer names
1353 // that have been selected for the intersection utility.
1354 // Fetches the respective layers and list of tumor ids.
1355 // Then compares data elements of the same tumor id
1356 // between both layers. Adds these hexes to a new layer
1357 // for visualization
1358
1359 //Array of signatures that intersect
1360 var intersection_signatures = [];
1361
1362 with_layers (intersection_layer_names, function (intersection_layers) {
1363
1364 // Gather Tumor-ID Signatures.
1365 for (hex in polygons)
1366 {
1367 if (intersection_layers[0].data[hex] == values[0] && intersection_layers[1].data[hex] == values[1]){
1368 intersection_signatures.push(hex);
1369 }
1370 }
1371 });
1372
1373 for (var i = 0; i < intersection_layer_names.length; i++){
1374 intersection_layer_names[i] = intersection_layer_names[i] + " [" + text[i] + "]";
1375 }
1376 var intersection_function = "intersection";
1377 select_list (intersection_signatures, intersection_function, intersection_layer_names);
1378 uncheck_checkbox ('intersection-checkbox');
1379 hide_values('intersection');
1380 }
1381
1382 function compute_union (values, union_layer_names, text) {
1383 // A function that will take a list of layer names
1384 // that have been selected for the union utility.
1385 // Fetches the respective layers and list of tumor ids.
1386 // Then compares data elements of the same tumor id
1387 // between both layers. Adds these hexes to a new layer
1388 // for visualization
1389
1390 //Array of signatures
1391 var union_signatures = [];
1392
1393 with_layers (union_layer_names, function (union_layers) {
1394
1395 // Gather Tumor-ID Signatures.
1396 for (hex in polygons)
1397 {
1398 // Union Function
1399 if (union_layers[0].data[hex] == values[0] || union_layers[1].data[hex] == values[1]){
1400 union_signatures.push(hex);
1401 }
1402 }
1403 });
1404
1405 for (var i = 0; i < union_layer_names.length; i++){
1406 union_layer_names[i] = union_layer_names[i] + " [" + text[i] + "]";
1407 }
1408
1409 var union_function = "union";
1410 select_list (union_signatures, union_function, union_layer_names);
1411 uncheck_checkbox ('union-checkbox');
1412 hide_values('union');
1413 }
1414
1415 function compute_set_difference (values, set_difference_layer_names, text) {
1416 // A function that will take a list of layer names
1417 // that have been selected for the set difference utility.
1418 // Fetches the respective layers and list of tumor ids.
1419 // Then compares data elements of the same tumor id
1420 // between both layers. Adds these hexes to a new layer
1421 // for visualization
1422
1423 //Array of signatures
1424 var set_difference_signatures = [];
1425
1426 with_layers (set_difference_layer_names, function (set_difference_layers) {
1427
1428 // Gather Tumor-ID Signatures.
1429 for (hex in polygons)
1430 {
1431 // Set Difference Function
1432 if (set_difference_layers[0].data[hex] == values[0] &&
1433 set_difference_layers[1].data[hex] != values[1]){
1434 set_difference_signatures.push(hex);
1435 }
1436 }
1437 });
1438
1439 for (var i = 0; i < set_difference_layer_names.length; i++){
1440 set_difference_layer_names[i] = set_difference_layer_names[i] + " [" + text[i] + "]";
1441 }
1442
1443 var set_difference_function = "set difference";
1444 select_list (set_difference_signatures, set_difference_function, set_difference_layer_names);
1445 uncheck_checkbox ('set-difference-checkbox');
1446 hide_values('set-difference');
1447 }
1448
1449 function compute_symmetric_difference (values, symmetric_difference_layer_names, text) {
1450 // A function that will take a list of layer names
1451 // that have been selected for the set difference utility.
1452 // Fetches the respective layers and list of tumor ids.
1453 // Then compares data elements of the same tumor id
1454 // between both layers. Adds these hexes to a new layer
1455 // for visualization
1456
1457 //Array of signatures
1458 var symmetric_difference_signatures = [];
1459
1460 with_layers (symmetric_difference_layer_names, function (symmetric_difference_layers) {
1461
1462 // Gather Tumor-ID Signatures.
1463 for (hex in polygons)
1464 {
1465 // Symmetric Difference Function
1466 if (symmetric_difference_layers[0].data[hex] == values[0] &&
1467 symmetric_difference_layers[1].data[hex] != values[1]){
1468 symmetric_difference_signatures.push(hex);
1469 }
1470 if (symmetric_difference_layers[0].data[hex] != values[0] &&
1471 symmetric_difference_layers[1].data[hex] == values[1]){
1472 symmetric_difference_signatures.push(hex);
1473 }
1474 }
1475 });
1476
1477 for (var i = 0; i < symmetric_difference_layer_names.length; i++){
1478 symmetric_difference_layer_names[i] = symmetric_difference_layer_names[i] + " [" + text[i] + "]";
1479 }
1480
1481 var symmetric_difference_function = "symmetric difference";
1482 select_list (symmetric_difference_signatures, symmetric_difference_function, symmetric_difference_layer_names);
1483 uncheck_checkbox ('symmetric-difference-checkbox');
1484 hide_values('symmetric-difference');
1485 }
1486
1487 function compute_absolute_complement (values, absolute_complement_layer_names, text) {
1488 // A function that will take a list of layer names
1489 // that have been selected for the set difference utility.
1490 // Fetches the respective layers and list of tumor ids.
1491 // Then compares data elements of the same tumor id
1492 // between both layers. Adds these hexes to a new layer
1493 // for visualization
1494
1495 //Array of signatures
1496 var absolute_complement_signatures = [];
1497
1498 with_layers (absolute_complement_layer_names, function (absolute_complement_layers) {
1499
1500 // Gather Tumor-ID Signatures.
1501 for (hex in polygons)
1502 {
1503 // Absolute Complement Function
1504 if (absolute_complement_layers[0].data[hex] != values[0]) {
1505 absolute_complement_signatures.push(hex);
1506 }
1507 }
1508 });
1509
1510 for (var i = 0; i < absolute_complement_layer_names.length; i++){
1511 absolute_complement_layer_names[i] = absolute_complement_layer_names[i] + " [" + text[i] + "]";
1512 }
1513 var absolute_complement_function = "absolute complement";
1514 select_list (absolute_complement_signatures, absolute_complement_function, absolute_complement_layer_names);
1515 uncheck_checkbox ('absolute-complement-checkbox');
1516 hide_values('absolute-complement');
1517 }
1518
1519
1520 function layer_sort_order(a, b) {
1521 // A sort function defined on layer names.
1522 // Return <0 if a belongs before b, >0 if a belongs after
1523 // b, and 0 if their order doesn't matter.
1524
1525 // Sort by selection status, then p_value, then clumpiness, then (for binary
1526 // layers that are not selections) the frequency of the less common value,
1527 // then alphabetically by name if all else fails.
1528
1529 // Note that we can consult the layer metadata "n" and "positives" fields to
1530 // calculate the frequency of the least common value in binary layers,
1531 // without downloading them.
1532
1533 if(layers[a].selection && !layers[b].selection) {
1534 // a is a selection and b isn't, so put a first.
1535 return -1;
1536 } else if(layers[b].selection && !layers[a].selection) {
1537 // b is a selection and a isn't, so put b first.
1538 return 1;
1539 }
1540
1541 if(layers[a].p_value < layers[b].p_value) {
1542 // a has a lower p value, so put it first.
1543 return -1;
1544 } else if(layers[b].p_value < layers[a].p_value) {
1545 // b has a lower p value. Put it first instead.
1546 return 1;
1547 } else if(isNaN(layers[b].p_value) && !isNaN(layers[a].p_value)) {
1548 // a has a p value and b doesn't, so put a first
1549 return -1;
1550 } else if(!isNaN(layers[b].p_value) && isNaN(layers[a].p_value)) {
1551 // b has a p value and a doesn't, so put b first.
1552 return 1;
1553 }
1554
1555 if(layers[a].clumpiness < layers[b].clumpiness) {
1556 // a has a lower clumpiness score, so put it first.
1557 return -1;
1558 } else if(layers[b].clumpiness < layers[a].clumpiness) {
1559 // b has a lower clumpiness score. Put it first instead.
1560 return 1;
1561 } else if(isNaN(layers[b].clumpiness) && !isNaN(layers[a].clumpiness)) {
1562 // a has a clumpiness score and b doesn't, so put a first
1563 return -1;
1564 } else if(!isNaN(layers[b].clumpiness) && isNaN(layers[a].clumpiness)) {
1565 // b has a clumpiness score and a doesn't, so put b first.
1566 return 1;
1567 }
1568
1569
1570
1571 if(!layers[a].selection && !isNaN(layers[a].positives) && layers[a].n > 0 &&
1572 !layers[b].selection && !isNaN(layers[b].positives) &&
1573 layers[b].n > 0) {
1574
1575 // We have checked to see each layer is supposed to be bianry layer
1576 // without downloading. TODO: This is kind of a hack. Redesign the
1577 // whole system with a proper concept of layer type.
1578
1579 // We've also verified they both have some data in them. Otherwise we
1580 // might divide by 0 trying to calculate frequency.
1581
1582 // Two binary layers (not selections).
1583 // Compute the frequency of the least common value for each
1584
1585 // This is the frequency of the least common value in a (will be <=1/2)
1586 var minor_frequency_a = layers[a].positives / layers[a].n;
1587 if(minor_frequency_a > 0.5) {
1588 minor_frequency_a = 1 - minor_frequency_a;
1589 }
1590
1591 // And this is the same frequency for the b layer
1592 var minor_frequency_b = layers[b].positives / layers[b].n;
1593 if(minor_frequency_b > 0.5) {
1594 minor_frequency_b = 1 - minor_frequency_b;
1595 }
1596
1597 if(minor_frequency_a > minor_frequency_b) {
1598 // a is more evenly split, so put it first
1599 return -1;
1600 } else if(minor_frequency_a < minor_frequency_b) {
1601 // b is more evenly split, so put it first
1602 return 1;
1603 }
1604
1605 } else if (!layers[a].selection && !isNaN(layers[a].positives) &&
1606 layers[a].n > 0) {
1607
1608 // a is a binary layer we can nicely sort by minor value frequency, but
1609 // b isn't. Put a first so that we can avoid intransitive sort cycles.
1610
1611 // Example: X and Z are binary layers, Y is a non-binary layer, Y comes
1612 // after X and before Z by name ordering, but Z comes before X by minor
1613 // frequency ordering. This sort is impossible.
1614
1615 // The solution is to put both X and Z in front of Y, because they're
1616 // more interesting.
1617
1618 return -1;
1619
1620 } else if (!layers[b].selection && !isNaN(layers[b].positives) &&
1621 layers[b].n > 0) {
1622
1623 // b is a binary layer that we can evaluate based on minor value
1624 // frequency, but a isn't. Put b first.
1625
1626 return 1;
1627
1628 }
1629
1630 // We couldn't find a difference in selection status, p-value, or clumpiness
1631 // score, or the binary layer minor value frequency, or whether each layer
1632 // *had* a binary layer minor value frequency, so use lexicographic ordering
1633 // on the name.
1634 return a.localeCompare(b);
1635
1636 }
1637
1638 function sort_layers(layer_array) {
1639 // Given an array of layer names, sort the array in place as we want layers
1640 // to appear to the user.
1641 // We should sort by p value, with NaNs at the end. But selections should be
1642 // first.
1643
1644 layer_array.sort(layer_sort_order);
1645 }
1646
1647 function fill_layer_metadata(container, layer_name) {
1648 // Empty the given jQuery container element, and fill it with layer metadata
1649 // for the layer with the given name.
1650
1651 // Empty the container.
1652 container.html("");
1653
1654 for(attribute in layers[layer_name]) {
1655 // Go through everything we know about this layer
1656 if(attribute == "data" || attribute == "url" ||
1657 attribute == "magnitude" || attribute == "selection") {
1658
1659 // Skip built-in things
1660 // TODO: Ought to maybe have all metadata in its own object?
1661 continue;
1662 }
1663
1664 // This holds the metadata value we're displaying
1665 var value = layers[layer_name][attribute];
1666
1667 if(typeof value == "number" && isNaN(value)) {
1668 // If it's a numerical NaN (but not a string), just leave it out.
1669 continue;
1670 }
1671
1672 // If we're still here, this is real metadata.
1673 // Format it for display.
1674 var value_formatted;
1675 if(typeof value == "number") {
1676 if(value % 1 == 0) {
1677 // It's an int!
1678 // Display the default way
1679 value_formatted = value;
1680 } else {
1681 // It's a float!
1682 // Format the number for easy viewing
1683 value_formatted = value.toExponential(2);
1684 }
1685 } else {
1686 // Just put the thing in as a string
1687 value_formatted = value;
1688 }
1689
1690 // Do some transformations to make the displayed labels make more sense
1691 lookup = {
1692 n: "Number of non-empty values",
1693 positives: "Number of ones",
1694 inside_yes: "Ones in A",
1695 outside_yes: "Ones in background"
1696 }
1697
1698 if(lookup[attribute]) {
1699 // Replace a boring short name with a useful long name
1700 attribute = lookup[attribute];
1701 }
1702
1703 // Make a spot for it in the container and put it in
1704 var metadata = $("<div\>").addClass("layer-metadata");
1705 metadata.text(attribute + " = " + value_formatted);
1706
1707 container.append(metadata);
1708
1709 }
1710 }
1711
1712 function make_toggle_layout_ui(layout_name) {
1713 // Returns a jQuery element to represent the layer layout the given name in
1714 // the toggle layout panel.
1715
1716 // This holds a jQuery element that's the root of the structure we're
1717 // building.
1718 var root = $("<div/>").addClass("layout-entry");
1719 root.data("layout-name", layout_name);
1720
1721 // Put in the layer name in a div that makes it wrap.
1722 root.append($("<div/>").addClass("layout-name").text(layout_name));
1723
1724 return root;
1725 }
1726
1727 function make_browse_ui(layer_name) {
1728 // Returns a jQuery element to represent the layer with the given name in
1729 // the browse panel.
1730
1731 // This holds a jQuery element that's the root of the structure we're
1732 // building.
1733 var root = $("<div/>").addClass("layer-entry");
1734 root.data("layer-name", layer_name);
1735
1736 // Put in the layer name in a div that makes it wrap.
1737 root.append($("<div/>").addClass("layer-name").text(layer_name));
1738
1739 // Put in a layer metadata container div
1740 var metadata_container = $("<div/>").addClass("layer-metadata-container");
1741
1742 fill_layer_metadata(metadata_container, layer_name);
1743
1744 root.append(metadata_container);
1745
1746 return root;
1747 }
1748
1749 function update_browse_ui() {
1750 // Make the layer browse UI reflect the current list of layers in sorted
1751 // order.
1752
1753 // Re-sort the sorted list that we maintain
1754 sort_layers(layer_names_sorted);
1755
1756 // Close the select if it was open, forcing the data to refresh when it
1757 // opens again.
1758 $("#search").select2("close");
1759 }
1760
1761 function get_slider_range(layer_name) {
1762 // Given the name of a layer, get the slider range from its shortlist UI
1763 // entry.
1764 // Assumes the layer has a shortlist UI entry.
1765 return shortlist_ui[layer_name].find(".range-slider").slider("values");
1766 }
1767
1768 function reset_slider(layer_name, shortlist_entry) {
1769 // Given a layer name and a shortlist UI entry jQuery element, reset the
1770 // slider in the entry to its default values, after downloading the layer.
1771 // The default value may be invisible because we decided the layer should be
1772 // a colormap.
1773
1774 // We need to set its boundaries to the min and max of the data set
1775 with_layer(layer_name, function(layer) {
1776 if(have_colormap(layer_name)) {
1777 // This is a colormap, so don't use the range slider at all.
1778 // We couldn't know this before because the colormap may need to be
1779 // auto-detected upon download.
1780 shortlist_entry.find(".range").hide();
1781 return;
1782 } else {
1783 // We need the range slider
1784 shortlist_entry.find(".range").show();
1785
1786 // TODO: actually find max and min
1787 // For now just use + and - magnitude
1788 // This has the advantage of letting us have 0=black by default
1789 var magnitude = layer.magnitude;
1790
1791 // This holds the limit to use, which should be 1 if the magnitude
1792 // is <1. This is sort of heuristic, but it's a good guess that
1793 // nobody wants to look at a layer with values -0.2 to 0.7 on a
1794 // scale of -10 to 10, say, but they might want it on -1 to 1.
1795 var range = Math.max(magnitude, 1.0)
1796
1797 // Set the min and max.
1798 shortlist_entry.find(".range-slider").slider("option", "min",
1799 -range);
1800 shortlist_entry.find(".range-slider").slider("option", "max",
1801 range);
1802
1803 // Set slider to autoscale for the magnitude.
1804 shortlist_entry.find(".range-slider").slider("values", [-magnitude,
1805 magnitude]);
1806
1807 print("Scaled to magnitude " + magnitude);
1808
1809 // Redraw the view in case this changed anything
1810 refresh();
1811 }
1812
1813 });
1814 }
1815
1816 function get_current_layers() {
1817 // Returns an array of the string names of the layers that are currently
1818 // supposed to be displayed, according to the shortlist UI.
1819 // Not responsible for enforcing maximum selected layers limit.
1820
1821 // This holds a list of the string names of the currently selected layers,
1822 // in order.
1823 var current_layers = [];
1824
1825 $("#shortlist").children().each(function(index, element) {
1826 // This holds the checkbox that determines if we use this layer
1827 var checkbox = $(element).find(".layer-on");
1828 if(checkbox.is(":checked")) {
1829 // Put the layer in if its checkbox is checked.
1830 current_layers.push($(element).data("layer"));
1831 }
1832 });
1833
1834 // Return things in reverse order relative to the UI.
1835 // Thus, layer-added layers will be "secondary", and e.g. selecting
1836 // something with only tissue up behaves as you might expect, highlighting
1837 // those things.
1838 current_layers.reverse();
1839
1840 return current_layers;
1841 }
1842
1843 function get_current_filters() {
1844 // Returns an array of filter objects, according to the shortlist UI.
1845 // Filter objects have a layer name and a boolean-valued filter function
1846 // that returns true or false, given a value from that layer.
1847 var current_filters = [];
1848
1849 $("#shortlist").children().each(function(index, element) {
1850 // Go through all the shortlist entries.
1851 // This function is also the scope used for filtering function config
1852 // variables.
1853
1854 // This holds the checkbox that determines if we use this layer
1855 var checkbox = $(element).find(".filter-on");
1856 if(checkbox.is(":checked")) {
1857 // Put the layer in if its checkbox is checked.
1858
1859 // Get the layer name
1860 var layer_name = $(element).data("layer");
1861
1862 // This will hold our filter function. Start with a no-op filter.
1863 var filter_function = function(value) {
1864 return true;
1865 }
1866
1867 // Get the filter parameters
1868 // This holds the input that specifies a filter threshold
1869 var filter_threshold = $(element).find(".filter-threshold");
1870 // And this the element that specifies a filter match value for
1871 // discrete layers
1872 var filter_value = $(element).find(".filter-value");
1873
1874 // We want to figure out which of these to use without going and
1875 // downloading the layer.
1876 // So, we check to see which was left visible by the filter config
1877 // setup code.
1878 if(filter_threshold.is(":visible")) {
1879 // Use a threshold. This holds the threshold.
1880 var threshold = parseInt(filter_threshold.val());
1881
1882 filter_function = function(value) {
1883 return value > threshold;
1884 }
1885 }
1886
1887 if(filter_value.is(":visible")) {
1888 // Use a discrete value match instead. This hodls the value we
1889 // want to match.
1890 var desired = filter_value.val();
1891
1892 filter_function = function(value) {
1893 return value == desired;
1894 }
1895 }
1896
1897 // Add a filter on this layer, with the function we've prepared.
1898 current_filters.push({
1899 layer_name: layer_name,
1900 filter_function: filter_function
1901 });
1902 }
1903 });
1904
1905 return current_filters;
1906 }
1907
1908 function get_current_layers() {
1909 // Returns an array of the string names of the layers that are currently
1910 // supposed to be displayed, according to the shortlist UI.
1911 // Not responsible for enforcing maximum selected layers limit.
1912
1913 // This holds a list of the string names of the currently selected layers,
1914 // in order.
1915 var current_layers = [];
1916
1917 $("#shortlist").children().each(function(index, element) {
1918 // This holds the checkbox that determines if we use this layer
1919 var checkbox = $(element).find(".layer-on");
1920 if(checkbox.is(":checked")) {
1921 // Put the layer in if its checkbox is checked.
1922 current_layers.push($(element).data("layer"));
1923 }
1924 });
1925
1926 // Return things in reverse order relative to the UI.
1927 // Thus, layer-added layers will be "secondary", and e.g. selecting
1928 // something with only tissue up behaves as you might expect, highlighting
1929 // those things.
1930 current_layers.reverse();
1931
1932 return current_layers;
1933 }
1934
1935 function get_current_set_theory_layers(function_type) {
1936 // Returns an array of layer names that have been selected.
1937 // This function only looks at the layers that are listed on the shortlist.
1938
1939 var current_set_theory_layers = [];
1940
1941 // Initialize global variables that hold the number of checkboxes selected
1942 // for set theory functions to zero so that the new number is calculated
1943 // each time this function is called.
1944
1945 if (function_type == "intersection"){
1946 shortlist_intersection_num = 0;
1947 }
1948
1949 if (function_type == "union"){
1950 shortlist_union_num = 0;
1951 }
1952
1953 if (function_type == "set difference"){
1954 shortlist_set_difference_num = 0;
1955 }
1956
1957 if (function_type == "symmetric difference"){
1958 shortlist_symmetric_difference_num = 0;
1959 }
1960
1961 if (function_type == "absolute complement"){
1962 shortlist_absolute_complement_num = 0;
1963 }
1964
1965 $("#shortlist").children().each(function(index, element) {
1966 // Go through all the shortlist entries.
1967
1968 // This holds the checkbox that determines if we use this layer
1969 // The class name depends on the function_type.
1970
1971 // If intersection function look for intersection-checkbox.
1972 if (function_type == "intersection"){
1973 var checkbox = $(element).find(".intersection-checkbox");
1974 }
1975
1976 // If union function look for union-checkbox.
1977 if (function_type == "union"){
1978 var checkbox = $(element).find(".union-checkbox");
1979 }
1980
1981 // If set difference function look for set-difference-checkbox.
1982 if (function_type == "set difference"){
1983 var checkbox = $(element).find(".set-difference-checkbox");
1984 }
1985
1986 // If symmetric difference function look for
1987 // symmetric-difference-checkbox.
1988 if (function_type == "symmetric difference"){
1989 var checkbox = $(element).find(".symmetric-difference-checkbox");
1990 }
1991
1992 if (function_type == "absolute complement"){
1993 var checkbox = $(element).find(".absolute-complement-checkbox");
1994 }
1995
1996 if(checkbox.is(":checked")) {
1997 // Put the layer in if its checkbox is checked.
1998
1999
2000 // Get the layer name
2001 var layer_name = $(element).data("layer");
2002
2003 // Add the layer_name to the list of current_set_theory_layers.
2004 current_set_theory_layers.push(layer_name);
2005
2006 // Add to the global "num" variables to keep track of the number
2007 // of selected checkboxes.
2008
2009 if (function_type == "intersection"){
2010 shortlist_intersection_num++;
2011 }
2012
2013 if (function_type == "union"){
2014 shortlist_union_num++;
2015 }
2016
2017 if (function_type == "set difference"){
2018 shortlist_set_difference_num++;
2019 }
2020
2021 if (function_type == "symmetric difference"){
2022 shortlist_symmetric_difference_num++;
2023 }
2024
2025 if (function_type == "absolute complement"){
2026 shortlist_absolute_complement_num++;
2027 }
2028
2029 }
2030 });
2031
2032 return current_set_theory_layers;
2033 }
2034
2035
2036 function with_filtered_signatures(filters, callback) {
2037 // Takes an array of filters, as produced by get_current_filters. Signatures
2038 // pass a filter if the filter's layer has a value >0 for that signature.
2039 // Computes an array of all signatures passing all filters, and passes that
2040 // to the given callback.
2041
2042 // TODO: Re-organize this to do filters one at a time, recursively, like a
2043 // reasonable second-order filter.
2044
2045 // Prepare a list of all the layers
2046 var layer_names = [];
2047
2048 for(var i = 0; i < filters.length; i++) {
2049 layer_names.push(filters[i].layer_name);
2050 }
2051
2052 with_layers(layer_names, function(filter_layers) {
2053 // filter_layers is guaranteed to be in the same order as filters.
2054
2055 // This is an array of signatures that pass all the filters.
2056 var passing_signatures = [];
2057
2058 for(var signature in polygons) {
2059 // For each signature
2060
2061 // This holds whether we pass all the filters
2062 var pass = true;
2063
2064 for(var i = 0; i < filter_layers.length; i++) {
2065 // For each filtering layer
2066 if(!filters[i].filter_function(
2067 filter_layers[i].data[signature])) {
2068
2069 // If the signature fails the filter function for the layer,
2070 // skip the signature.
2071 pass = false;
2072 break;
2073 }
2074 }
2075
2076 if(pass) {
2077 // Record that the signature passes all filters
2078 passing_signatures.push(signature);
2079 }
2080 }
2081
2082 // Now we have our list of all passing signatures, so hand it off to the
2083 // callback.
2084 callback(passing_signatures);
2085 });
2086 }
2087
2088 function select_list(to_select, function_type, layer_names) {
2089 // Given an array of signature names, add a new selection layer containing
2090 // just those hexes. Only looks at hexes that are not filtered out by the
2091 // currently selected filters.
2092
2093 // function_type is an optional parameter. If no variable is passed for the
2094 // function_type undefined then the value will be undefined and the
2095 // default "selection + #" title will be assigned to the shortlist element.
2096 // If layer_names is undefined, the "selection + #" will also apply as a
2097 // default. However, if a value i.e. "intersection" is passed
2098 // for function_type, the layer_names will be used along with the
2099 // function_type to assign the correct title.
2100
2101 // Make the requested signature list into an object for quick membership
2102 // checking. This holds true if a signature was requested, undefined
2103 // otherwise.
2104 var wanted = {};
2105
2106 for(var i = 0; i < to_select.length; i++) {
2107 wanted[to_select[i]] = true;
2108 }
2109
2110 // This is the data object for the layer: from signature names to 1/0
2111 var data = {};
2112
2113 // How many signatures will we have any mention of in this layer
2114 var signatures_available = 0;
2115
2116 // Start it out with 0 for each signature. Otherwise we wil have missing
2117 // data for signatures not passing the filters.
2118 for(var signature in polygons) {
2119 data[signature] = 0;
2120 signatures_available += 1;
2121 }
2122
2123 // This holds the filters we're going to use to restrict our selection
2124 var filters = get_current_filters();
2125
2126 // Go get the list of signatures passing the filters and come back.
2127 with_filtered_signatures(filters, function(signatures) {
2128 // How many signatures get selected?
2129 var signatures_selected = 0;
2130
2131 for(var i = 0; i < signatures.length; i++) {
2132 if(wanted[signatures[i]]) {
2133 // This signature is both allowed by the filters and requested.
2134 data[signatures[i]] = 1;
2135 signatures_selected++;
2136 }
2137 }
2138
2139 // Make up a name for the layer
2140 var layer_name;
2141
2142 // Default Values for Optional Parameters
2143 if (function_type == undefined && layer_names == undefined){
2144 layer_name = "Selection " + selection_next_id;
2145 selection_next_id++;
2146 }
2147
2148 if (function_type == "user selection"){
2149 var text = prompt("Please provide a label for your selection",
2150 "Selection Label Text");
2151 if (text != null){
2152 layer_name = text;
2153 }
2154 if (!text)
2155 {
2156 return;
2157 }
2158 }
2159
2160 // intersection for layer name
2161 if (function_type == "intersection"){
2162 layer_name = "(" + layer_names[0] + " ∩ " + layer_names[1] + ")";
2163 }
2164
2165 // union for layer name
2166 if (function_type == "union"){
2167 layer_name = "(" + layer_names[0] + " U " + layer_names[1] + ")";
2168 }
2169
2170 // set difference for layer name
2171 if (function_type == "set difference"){
2172 layer_name = "(" + layer_names[0] + " \\ " + layer_names[1] + ")";
2173 }
2174
2175 // symmetric difference for layer name
2176 if (function_type == "symmetric difference"){
2177 layer_name = "(" + layer_names[0] + " ∆ " + layer_names[1] + ")";
2178 }
2179
2180 // absolute complement for layer name
2181 if (function_type == "absolute complement"){
2182 layer_name = "Not: " + "(" + layer_names[0] + ")";
2183 }
2184
2185 // saved filter for layer name
2186 if (function_type == "save"){
2187 layer_name = "(" + layer_names[0] + ")";
2188 }
2189
2190 // Add the layer. Say it is a selection
2191 add_layer_data(layer_name, data, {
2192 selection: true,
2193 selected: signatures_selected, // Display how many hexes are in
2194 n: signatures_available // And how many have a value at all
2195 });
2196
2197 // Update the browse UI with the new layer.
2198 update_browse_ui();
2199
2200 // Immediately shortlist it
2201 shortlist.push(layer_name);
2202 update_shortlist_ui();
2203 });
2204
2205 }
2206
2207 function select_rectangle(start, end) {
2208 // Given two Google Maps LatLng objects (denoting arbitrary rectangle
2209 // corners), add a new selection layer containing all the hexagons
2210 // completely within that rectangle.
2211 // Only looks at hexes that are not filtered out by the currently selected
2212 // filters.
2213
2214 // Sort out the corners to get the rectangle limits in each dimension
2215 var min_lat = Math.min(start.lat(), end.lat());
2216 var max_lat = Math.max(start.lat(), end.lat());
2217 var min_lng = Math.min(start.lng(), end.lng());
2218 var max_lng = Math.max(start.lng(), end.lng());
2219
2220 // This holds an array of all signature names in our selection box.
2221 var in_box = [];
2222
2223 // Start it out with 0 for each signature. Otherwise we wil have missing
2224 // data for signatures not passing the filters.
2225 for(var signature in polygons) {
2226 // Get the path for its hex
2227 var path = polygons[signature].getPath();
2228
2229 // This holds if any points of the path are outside the selection
2230 // box
2231 var any_outside = false;
2232
2233 path.forEach(function(point, index) {
2234 // Check all the points. Runs synchronously.
2235
2236 if(point.lat() < min_lat || point.lat() > max_lat ||
2237 point.lng() < min_lng || point.lng() > max_lng) {
2238
2239 // This point is outside the rectangle
2240 any_outside = true;
2241
2242 }
2243 });
2244
2245 // Select the hex if all its corners are inside the selection
2246 // rectangle.
2247 if(!any_outside) {
2248 in_box.push(signature);
2249 }
2250 }
2251
2252 // Now we have an array of the signatures that ought to be in the selection
2253 // (if they pass filters). Hand it off to select_list.
2254
2255 var select_function_type = "user selection";
2256 select_list(in_box, select_function_type);
2257
2258 }
2259
2260 function recalculate_statistics(passed_filters) {
2261 // Interrogate the UI to determine signatures that are "in" and "out", and
2262 // run an appropriate statisical test for each layer between the "in" and
2263 // "out" signatures, and update all the "p_value" fields for all the layers
2264 // with the p values. Takes in a list of signatures that passed the filters,
2265 // and ignores any signatures not on that list.
2266
2267 // Build an efficient index of passing signatures
2268 var passed = {};
2269 for(var i = 0; i < passed_filters.length; i++) {
2270 passed[passed_filters[i]] = true;
2271 }
2272
2273 // Figure out what the in-list should be (statistics group A)
2274 var layer_a_name = $(".statistics-a:checked").data("layer-name");
2275 var layer_b_name = $(".statistics-b:checked").data("layer-name");
2276
2277 print("Running statistics between " + layer_a_name + " and " +
2278 layer_b_name);
2279
2280 if(!layer_a_name) {
2281 complain("Can't run statistics without an \"A\" group.");
2282
2283 // Get rid of the throbber
2284 // TODO: Move this UI code out of the backend code.
2285 $(".recalculate-throbber").hide();
2286 $("#recalculate-statistics").show();
2287
2288 return;
2289 }
2290
2291 // We know the layers have data since they're selections, so we can just go
2292 // look at them.
2293
2294 // This holds the "in" list: hexes from the "A" group.
2295 var in_list = [];
2296
2297 for(var signature in layers[layer_a_name].data) {
2298 if(passed[signature] && layers[layer_a_name].data[signature]) {
2299 // Add all the signatures in the "A" layer to the in list.
2300 in_list.push(signature);
2301 }
2302 }
2303
2304 if(in_list.length == 0) {
2305 complain("Can't run statistics with an empty \"A\" group.");
2306
2307 // Get rid of the throbber
2308 // TODO: Move this UI code out of the backend code.
2309 $(".recalculate-throbber").hide();
2310 $("#recalculate-statistics").show();
2311
2312 return;
2313 }
2314
2315 // This holds the "out" list: hexes in the "B" group, or, if that's not
2316 // defined, all hexes. It's a little odd to run A vs. a set that includes
2317 // some members of A, but Prof. Stuart wants that and it's not too insane
2318 // for a Binomial test (which is the only currently implemented test
2319 // anyway).
2320 var out_list = [];
2321
2322 if(layer_b_name) {
2323 // We have a layer B, so take everything that's on in it.
2324 for(var signature in layers[layer_b_name].data) {
2325 if(passed[signature] && layers[layer_b_name].data[signature]) {
2326 // Add all the signatures in the "B" layer to the out list.
2327 out_list.push(signature);
2328 }
2329 }
2330 } else {
2331 // The out list is all hexes
2332 for(var signature in polygons) {
2333 if(passed[signature]) {
2334 // Put it on the out list.
2335 out_list.push(signature);
2336 }
2337 }
2338 }
2339
2340 // So now we have our in_list and our out_list
2341
2342 for(var layer_name in layers) {
2343 // Do the stats on each layer between those lists. This only processes
2344 // layers that don't have URLs. Layers with URLs are assumed to be part
2345 // of the available matrices.
2346 recalculate_statistics_for_layer(layer_name, in_list, out_list,
2347 passed_filters);
2348 }
2349
2350 // Now do all the layers with URLs. They are in the available score
2351 // matrices.
2352 for(var i = 0; i < available_matrices.length; i++) {
2353 recalculate_statistics_for_matrix(available_matrices[i], in_list,
2354 out_list, passed_filters);
2355 }
2356
2357 print("Statistics jobs launched.");
2358
2359 }
2360
2361 function recalculate_statistics_for_layer(layer_name, in_list, out_list, all) {
2362 // Re-calculate the stats for the layer with the given name, between the
2363 // given in and out arrays of signatures. Store the re-calculated statistics
2364 // in the layer. all is a list of "all" signatures, from which we can
2365 // calculate pseudocounts.
2366
2367 // All we do is send the layer data or URL (whichever is more convenient) to
2368 // the workers. They independently identify the data type and run the
2369 // appropriate test, returning a p value or NaN by callback.
2370
2371 // This holds a callback for setting the layer's p_value to the result of
2372 // the statistics.
2373 var callback = function(results) {
2374
2375 // The statistics code really sends back a dict of updated metadata for
2376 // each layer. Copy it over.
2377 for(var metadata in results) {
2378 layers[layer_name][metadata] = results[metadata];
2379 }
2380
2381 if(jobs_running == 0) {
2382 // All statistics are done!
2383 // TODO: Unify this code with similar callback below.
2384 // Re-sort everything and draw all the new p values.
2385 update_browse_ui();
2386 update_shortlist_ui();
2387
2388 // Get rid of the throbber
2389 $(".recalculate-throbber").hide();
2390 $("#recalculate-statistics").show();
2391 }
2392 };
2393
2394 if(layers[layer_name].data != undefined) {
2395 // Already have this downloaded. A local copy to the web worker is
2396 // simplest, and a URL may not exist anyway.
2397
2398 rpc_call("statistics_for_layer", [layers[layer_name].data, in_list,
2399 out_list, all], callback);
2400 } else if(layers[layer_name].url != undefined) {
2401 // We have a URL, so the layer must be in a matrix, too.
2402 // Skip it here.
2403 } else {
2404 // Layer has no data and no way to get data. Should never happen.
2405 complain("Layer " + layer_name + " has no data and no url.");
2406 }
2407 }
2408
2409 function recalculate_statistics_for_matrix(matrix_url, in_list, out_list, all) {
2410 // Given the URL of one of the visualizer generator's input score matrices,
2411 // download the matrix, calculate statistics for each layer in the matrix
2412 // between the given in and out lists, and update the layer p values. all is
2413 // a list of "all" signatures, from which we can calculate pseudocounts.
2414
2415 rpc_call("statistics_for_matrix", [matrix_url, in_list, out_list, all],
2416 function(result) {
2417
2418 // The return value is p values by layer name
2419 for(var layer_name in result) {
2420 // The statistics code really sends back a dict of updated metadata
2421 // for each layer. Copy it over.
2422 for(var metadata in result[layer_name]) {
2423 layers[layer_name][metadata] = result[layer_name][metadata];
2424 }
2425 }
2426
2427 if(jobs_running == 0) {
2428 // All statistics are done!
2429 // TODO: Unify this code with similar callback above.
2430 // Re-sort everything and draw all the new p values.
2431 update_browse_ui();
2432 update_shortlist_ui();
2433
2434 // Get rid of the throbber
2435 $(".recalculate-throbber").hide();
2436 $("#recalculate-statistics").show();
2437 }
2438 });
2439
2440 }
2441
2442 function rpc_initialize() {
2443 // Set up the RPC system. Must be called before rpc_call is used.
2444
2445 for(var i = 0; i < NUM_RPC_WORKERS; i++) {
2446 // Start the statistics RPC (remote procedure call) Web Worker
2447 var worker = new Worker("statistics.js");
2448
2449 // Send all its messages to our reply processor
2450 worker.onmessage = rpc_reply;
2451
2452 // Send its error events to our error processor
2453 worker.onerror = rpc_error;
2454
2455 // Add it to the list of workers
2456 rpc_workers.push(worker);
2457 }
2458 }
2459
2460 function rpc_call(function_name, function_args, callback) {
2461 // Given a function name and an array of arguments, send a message to a Web
2462 // Worker thread to ask it to run the given job. When it responds with the
2463 // return value, pass it to the given callback.
2464
2465 // Allocate a new call id
2466 var call_id = rpc_next_id;
2467 rpc_next_id++;
2468
2469 // Store the callback
2470 rpc_callbacks[call_id] = callback;
2471
2472 // Launch the call. Pass the function name, function args, and id to send
2473 // back with the return value.
2474 rpc_workers[next_free_worker].postMessage({
2475 name: function_name,
2476 args: function_args,
2477 id: call_id
2478 });
2479
2480 // Next time, use the next worker on the list, wrapping if we run out.
2481 // This ensures no one worker gets all the work.
2482 next_free_worker = (next_free_worker + 1) % rpc_workers.length;
2483
2484 // Update the UI with the number of jobs in flight. Decrement jobs_running
2485 // so the callback knows if everything is done or not.
2486 jobs_running++;
2487 $("#jobs-running").text(jobs_running);
2488
2489 // And the number of jobs total
2490 $("#jobs-ever").text(rpc_next_id);
2491 }
2492
2493 function rpc_reply(message) {
2494 // Handle a Web Worker message, which may be an RPC response or a log entry.
2495
2496 if(message.data.log != undefined) {
2497 // This is really a log entry
2498 print(message.data.log);
2499 return;
2500 }
2501
2502 // This is really a job completion message (success or error).
2503
2504 // Update the UI with the number of jobs in flight.
2505 jobs_running--;
2506 $("#jobs-running").text(jobs_running);
2507
2508 if(message.data.error) {
2509 // The RPC call generated an error.
2510 // Inform the page.
2511 print("RPC error: " + message.data.error);
2512
2513 // Get rid of the callback
2514 delete rpc_callbacks[message.data.id];
2515
2516 return;
2517 }
2518
2519 // Pass the return value to the registered callback.
2520 rpc_callbacks[message.data.id](message.data.return_value);
2521
2522 // Get rid of the callback
2523 delete rpc_callbacks[message.data.id];
2524 }
2525
2526 function rpc_error(error) {
2527 // Handle an error event from a web worker
2528 // See http://www.whatwg.org/specs/web-apps/current-work/multipage/workers.h
2529 // tml#errorevent
2530
2531 complain("Web Worker error: " + error.message);
2532 print(error.message + "\n at" + error.filename + " line " + error.lineno +
2533 " column " + error.column);
2534 }
2535
2536 function initialize_view(initial_zoom) {
2537 // Initialize the global Google Map.
2538
2539 // Configure a Google map
2540 var mapOptions = {
2541 // Look at the center of the map
2542 center: get_LatLng(128, 128),
2543 // Zoom all the way out
2544 zoom: initial_zoom,
2545 mapTypeId: "blank",
2546 // Don't show a map type picker.
2547 mapTypeControlOptions: {
2548 mapTypeIds: []
2549 },
2550 // Or a street view man that lets you walk around various Earth places.
2551 streetViewControl: false
2552 };
2553
2554 // Create the actual map
2555 googlemap = new google.maps.Map(document.getElementById("visualization"),
2556 mapOptions);
2557
2558 // Attach the blank map type to the map
2559 googlemap.mapTypes.set("blank", new BlankMapType());
2560
2561 // Make the global info window
2562 info_window = new google.maps.InfoWindow({
2563 content: "No Signature Selected",
2564 position: get_LatLng(0, 0)
2565 });
2566
2567 // Add an event to close the info window when the user clicks outside of any
2568 // hexagon
2569 google.maps.event.addListener(googlemap, "click", function(event) {
2570 info_window.close();
2571
2572 // Also make sure that the selected signature is no longer selected,
2573 // so we don't pop the info_window up again.
2574 selected_signature = undefined;
2575
2576 // Also un-focus the search box
2577 $("#search").blur();
2578 });
2579
2580
2581 // And an event to clear the selected hex when the info_window closes.
2582 google.maps.event.addListener(info_window, "closeclick", function(event) {
2583 selected_signature = undefined;
2584 });
2585
2586 // We also have an event listener that checks when the zoom level changes,
2587 // and turns off hex borders if we zoom out far enough, and turns them on
2588 // again if we come back.
2589 google.maps.event.addListener(googlemap, "zoom_changed", function(event) {
2590 // Get the current zoom level (low is out)
2591 var zoom = googlemap.getZoom();
2592
2593 // API docs say: pixelCoordinate = worldCoordinate * 2 ^ zoomLevel
2594 // So this holds the number of pixels that the global length hex_size
2595 // corresponds to at this zoom level.
2596 var hex_size_pixels = hex_size * Math.pow(2, zoom);
2597
2598 if(hex_size_pixels < MIN_BORDER_SIZE) {
2599 // We're too small for borders
2600 for(var signature in polygons) {
2601 set_hexagon_stroke_weight(polygons[signature], 0);
2602 }
2603 } else {
2604 // We can fit borders on the hexes
2605 for(var signature in polygons) {
2606 set_hexagon_stroke_weight(polygons[signature],
2607 HEX_STROKE_WEIGHT);
2608 }
2609 }
2610
2611 });
2612
2613 // Subscribe all the tool listeners to the map
2614 subscribe_tool_listeners(googlemap);
2615
2616 }
2617
2618 function add_tool(tool_name, tool_menu_option, callback) {
2619 // Given a programmatic unique name for a tool, some text for the tool's
2620 // button, and a callback for when the user clicks that button, add a tool
2621 // to the tool menu.
2622
2623 // This hodls a button to activate the tool.
2624 var tool_button = $("<a/>").attr("href", "#").addClass("stacker");
2625 tool_button.text(tool_menu_option);
2626 tool_button.click(function() {
2627 // New tool. Remove all current tool listeners
2628 clear_tool_listeners();
2629
2630 // Say that the select tool is selected
2631 selected_tool = tool_name;
2632 callback();
2633
2634 // End of tool workflow must set current_tool to undefined.
2635 });
2636
2637 $("#toolbar").append(tool_button);
2638 }
2639
2640 function add_tool_listener(name, handler, cleanup) {
2641 // Add a global event listener over the Google map and everything on it.
2642 // name specifies the event to listen to, and handler is the function to be
2643 // set up as an event handler. It should take a single argument: the Google
2644 // Maps event. A handle is returned that can be used to remove the event
2645 // listen with remove_tool_listener.
2646 // Only events in the TOOL_EVENTS array are allowed to be passed for name.
2647 // TODO: Bundle this event thing into its own object.
2648 // If "cleanup" is specified, it must be a 0-argument function to call when
2649 // this listener is removed.
2650
2651 // Get a handle
2652 var handle = tool_listener_next_id;
2653 tool_listener_next_id++;
2654
2655 // Add the listener for the given event under that handle.
2656 // TODO: do we also need to index this for O(1) event handling?
2657 tool_listeners[handle] = {
2658 handler: handler,
2659 event: name,
2660 cleanup: cleanup
2661 };
2662 return handle;
2663 }
2664
2665 function remove_tool_listener(handle) {
2666 // Given a handle returned by add_tool_listener, remove the listener so it
2667 // will no longer fire on its event. May be called only once on a given
2668 // handle. Runs any cleanup code associated with the handle being removed.
2669
2670 if(tool_listeners[handle].cleanup) {
2671 // Run cleanup code if applicable
2672 tool_listeners[handle].cleanup();
2673 }
2674
2675 // Remove the property from the object
2676 delete tool_listeners[handle];
2677 }
2678
2679 function clear_tool_listeners() {
2680 // We're starting to use another tool. Remove all current tool listeners.
2681 // Run any associated cleanup code for each listener.
2682
2683 for(var handle in tool_listeners) {
2684 remove_tool_listener(handle);
2685 }
2686 }
2687
2688 function subscribe_tool_listeners(maps_object) {
2689 // Put the given Google Maps object into the tool events system, so that
2690 // events on it will fire global tool events. This can happen before or
2691 // after the tool events themselves are enabled.
2692
2693 for(var i = 0; i < TOOL_EVENTS.length; i++) {
2694 // For each event name we care about,
2695 // use an inline function to generate an event name specific handler,
2696 // and attach that to the Maps object.
2697 google.maps.event.addListener(maps_object, TOOL_EVENTS[i],
2698 function(event_name) {
2699 return function(event) {
2700 // We are handling an event_name event
2701
2702 for(var handle in tool_listeners) {
2703 if(tool_listeners[handle].event == event_name) {
2704 // The handler wants this event
2705 // Fire it with the Google Maps event args
2706 tool_listeners[handle].handler(event);
2707 }
2708 }
2709 };
2710 }(TOOL_EVENTS[i]));
2711 }
2712
2713 }
2714
2715 function have_colormap(colormap_name) {
2716 // Returns true if the given string is the name of a colormap, or false if
2717 // it is only a layer.
2718
2719 return !(colormaps[colormap_name] == undefined);
2720 }
2721
2722 function get_range_position(score, low, high) {
2723 // Given a score float, and the lower and upper bounds of an interval (which
2724 // may be equal, but not backwards), return a number in the range -1 to 1
2725 // that expresses the position of the score in the [low, high] interval.
2726 // Positions out of bounds are clamped to -1 or 1 as appropriate.
2727
2728 // This holds the length of the input interval
2729 var interval_length = high - low;
2730
2731 if(interval_length > 0) {
2732 // First rescale 0 to 1
2733 score = (score - low) / interval_length
2734
2735 // Clamp
2736 score = Math.min(Math.max(score, 0), 1);
2737
2738 // Now re-scale to -1 to 1
2739 score = 2 * score - 1;
2740 } else {
2741 // The interval is just a point
2742 // Just use 1 if we're above the point, and 0 if below.
2743 score = (score > low)? 1 : -1
2744 }
2745
2746 return score;
2747 }
2748
2749 function refresh() {
2750 // Schedule the view to be redrawn after the current event finishes.
2751
2752 // Get rid of the previous redraw request, if there was one. We only want
2753 // one.
2754 window.clearTimeout(redraw_handle);
2755
2756 // Make a new one to happen as soon as this event finishes
2757 redraw_handle = window.setTimeout(redraw_view, 0);
2758 }
2759
2760 function redraw_view() {
2761 // Make the view display the correct hexagons in the colors of the current
2762 // layer(s), as read from the values of the layer pickers in the global
2763 // layer pickers array.
2764 // All pickers must have selected layers that are in the object of
2765 // layers.
2766 // Instead of calling this, you probably want to call refresh().
2767
2768 // This holds a list of the string names of the currently selected layers,
2769 // in order.
2770 var current_layers = get_current_layers();
2771
2772 // This holds arrays of the lower and upper limit we want to use for
2773 // each layer, by layer number. The lower limit corresponds to u or
2774 // v = -1, and the upper to u or v = 1. The entries we make for
2775 // colormaps are ignored.
2776 // Don't do this inside the callback since the UI may have changed by then.
2777 var layer_limits = []
2778 for(var i = 0; i < current_layers.length; i++) {
2779 layer_limits.push(get_slider_range(current_layers[i]));
2780 }
2781
2782 // This holds all the current filters
2783 var filters = get_current_filters();
2784
2785 // Obtain the layer objects (mapping from signatures/hex labels to colors)
2786 with_layers(current_layers, function(retrieved_layers) {
2787 print("Redrawing view with " + retrieved_layers.length + " layers.");
2788
2789 // Turn all the hexes the filtered-out color, pre-emptively
2790 for(var signature in polygons) {
2791 set_hexagon_color(polygons[signature], "black");
2792 }
2793
2794 // Go get the list of filter-passing hexes.
2795 with_filtered_signatures(filters, function(signatures) {
2796 for(var i = 0; i < signatures.length; i++) {
2797 // For each hex passign the filter
2798 // This hodls its signature label
2799 var label = signatures[i];
2800
2801 // This holds the color we are calculating for this hexagon.
2802 // Start with the missing data color.
2803 var computed_color = "grey";
2804
2805 if(retrieved_layers.length >= 1) {
2806 // Two layers. We find a point in u, v cartesian space, map
2807 // it to polar, and use that to compute an HSV color.
2808 // However, we map value to the radius instead of
2809 // saturation.
2810
2811 // Get the heat along u and v axes. This puts us in a square
2812 // of side length 2. Fun fact: undefined / number = NaN, but
2813 // !(NaN == NaN)
2814 var u = retrieved_layers[0].data[label];
2815
2816 if(!have_colormap(current_layers[0])) {
2817 // Take into account the slider values and re-scale the
2818 // layer value to express its position between them.
2819 u = get_range_position(u, layer_limits[0][0],
2820 layer_limits[0][1]);
2821 }
2822
2823 if(retrieved_layers.length >= 2) {
2824 // There's a second layer, so use the v axis.
2825 var v = retrieved_layers[1].data[label];
2826
2827 if(!have_colormap(current_layers[1])) {
2828 // Take into account the slider values and re-scale
2829 // the layer value to express its position between
2830 // them.
2831 v = get_range_position(v, layer_limits[1][0],
2832 layer_limits[1][1]);
2833 }
2834
2835 } else {
2836 // No second layer, so v axis is unused. Don't make it
2837 // undefined (it's not missing data), but set it to 0.
2838 var v = 0;
2839 }
2840
2841 // Either of u or v may be undefined (or both) if the layer
2842 // did not contain an entry for this signature. But that's
2843 // OK. Compute the color that we should use to express this
2844 // combination of layer values. It's OK to pass undefined
2845 // names here for layers.
2846 computed_color = get_color(current_layers[0], u,
2847 current_layers[1], v);
2848 }
2849
2850 // Set the color by the composed layers.
2851 set_hexagon_color(polygons[label], computed_color);
2852 }
2853 });
2854
2855 // Draw the color key.
2856 if(retrieved_layers.length == 0) {
2857 // No color key to draw
2858 $(".key").hide();
2859 } else {
2860 // We do actually want the color key
2861 $(".key").show();
2862
2863 // This holds the canvas that the key gets drawn in
2864 var canvas = $("#color-key")[0];
2865
2866 // This holds the 2d rendering context
2867 var context = canvas.getContext("2d");
2868
2869 for(var i = 0; i < KEY_SIZE; i++) {
2870 // We'll use i for the v coordinate (-1 to 1) (left to right)
2871 var v = 0;
2872 if(retrieved_layers.length >= 2) {
2873 v = i / (KEY_SIZE / 2) - 1;
2874
2875 if(have_colormap(current_layers[1])) {
2876 // This is a color map, so do bands instead.
2877 v = Math.floor(i / KEY_SIZE *
2878 (retrieved_layers[1].magnitude + 1));
2879 }
2880
2881 }
2882
2883 for(var j = 0; j < KEY_SIZE; j++) {
2884 // And j spacifies the u coordinate (bottom to top)
2885 var u = 0;
2886 if(retrieved_layers.length >= 1) {
2887 u = 1 - j / (KEY_SIZE / 2);
2888
2889 if(have_colormap(current_layers[0])) {
2890 // This is a color map, so do bands instead.
2891 // Make sure to flip sign, and have a -1 for the
2892 // 0-based indexing.
2893 u = Math.floor((KEY_SIZE - j - 1) / KEY_SIZE *
2894 (retrieved_layers[0].magnitude + 1));
2895 }
2896 }
2897
2898 // Set the pixel color to the right thing for this u, v
2899 // It's OK to pass undefined names here for layers.
2900 context.fillStyle = get_color(current_layers[0], u,
2901 current_layers[1], v);
2902
2903 // Fill the pixel
2904 context.fillRect(i, j, 1, 1);
2905 }
2906 }
2907
2908 }
2909
2910 if(have_colormap(current_layers[0])) {
2911 // We have a layer with horizontal bands
2912 // Add labels to the key if we have names to use.
2913 // TODO: Vertical text for vertical bands?
2914
2915 // Get the colormap
2916 var colormap = colormaps[current_layers[0]]
2917
2918 if(colormap.length > 0) {
2919 // Actually have any categories (not auto-generated)
2920 print("Drawing key text for " + colormap.length +
2921 " categories.");
2922
2923 // How many pixels do we get per label, vertically
2924 var pixels_per_label = KEY_SIZE / colormap.length;
2925
2926 // Configure for text drawing
2927 context.font = pixels_per_label + "px Arial";
2928 context.textBaseline = "top";
2929
2930 for(var i = 0; i < colormap.length; i++) {
2931
2932 // This holds the pixel position where our text goes
2933 var y_position = KEY_SIZE - (i + 1) * pixels_per_label;
2934
2935 // Get the background color here as a 1x1 ImageData
2936 var image = context.getImageData(0, y_position, 1, 1);
2937
2938 // Get the components r, g, b, a in an array
2939 var components = image.data;
2940
2941 // Make a Color so we can operate on it
2942 var background_color = Color({
2943 r: components[0],
2944 g: components[1],
2945 b: components[2]
2946 });
2947
2948 if(background_color.light()) {
2949 // This color is light, so write in black.
2950 context.fillStyle = "black";
2951 } else {
2952 // It must be dark, so write in white.
2953 context.fillStyle = "white";
2954 }
2955
2956 // Draw the name on the canvas
2957 context.fillText(colormap[i].name, 0, y_position);
2958 }
2959 }
2960 }
2961
2962 // We should also set up axis labels on the color key.
2963 // We need to know about colormaps to do this
2964
2965 // Hide all the labels
2966 $(".label").hide();
2967
2968 if(current_layers.length > 0) {
2969 // Show the y axis label
2970 $("#y-axis").text(current_layers[0]).show();
2971
2972 if(!have_colormap(current_layers[0])) {
2973 // Show the low to high markers for continuous values
2974 $("#low-both").show();
2975 $("#high-y").show();
2976 }
2977 }
2978
2979 if(current_layers.length > 1) {
2980 // Show the x axis label
2981 $("#x-axis").text(current_layers[1]).show();
2982
2983 if(!have_colormap(current_layers[1])) {
2984 // Show the low to high markers for continuous values
2985 $("#low-both").show();
2986 $("#high-x").show();
2987 }
2988 }
2989
2990
2991 });
2992
2993 // Make sure to also redraw the info window, which may be open.
2994 redraw_info_window();
2995 }
2996
2997 function get_color(u_name, u, v_name, v) {
2998 // Given u and v, which represent the heat in each of the two currently
2999 // displayed layers, as well as u_name and v_name, which are the
3000 // corresponding layer names, return the computed CSS color.
3001 // Either u or v may be undefined (or both), in which case the no-data color
3002 // is returned. If a layer name is undefined, that layer dimension is
3003 // ignored.
3004
3005 if(have_colormap(v_name) && !have_colormap(u_name)) {
3006 // We have a colormap as our second layer, and a layer as our first.
3007 // Swap everything around so colormap is our first layer instead.
3008 // Now we don't need to think about drawing a layer first with a
3009 // colormap second.
3010 // This is a temporary swapping variable.
3011 var temp = v_name;
3012 v_name = u_name;
3013 u_name = temp;
3014
3015 temp = v;
3016 v = u;
3017 u = temp;
3018 }
3019
3020 if(isNaN(u) || isNaN(v) || u == undefined || v == undefined) {
3021 // At least one of our layers has no data for this hex.
3022 return "grey";
3023 }
3024
3025 if(have_colormap(u_name) && have_colormap(v_name) &&
3026 !colormaps[u_name].hasOwnProperty(u) &&
3027 !colormaps[v_name].hasOwnProperty(v) &&
3028 layers[u_name].magnitude <= 1 && layers[v_name].magnitude <= 1) {
3029
3030 // Special case: two binary or unary auto-generated colormaps.
3031 // Use dark grey/red/blue/purple color scheme
3032
3033 if(u == 1) {
3034 if(v == 1) {
3035 // Both are on
3036 return "#FF00FF";
3037 } else {
3038 // Only the first is on
3039 return "#FF0000";
3040 }
3041 } else {
3042 if(v == 1) {
3043 // Only the second is on
3044 return "#0000FF";
3045 } else {
3046 // Neither is on
3047 return "#545454";
3048 }
3049 }
3050
3051 }
3052
3053 if(have_colormap(u_name) && !colormaps[u_name].hasOwnProperty(u) &&
3054 layers[u_name].magnitude <= 1 && v_name == undefined) {
3055
3056 // Special case: a single binary or unary auto-generated colormap.
3057 // Use dark grey/red to make 1s stand out.
3058
3059 if(u == 1) {
3060 // Red for on
3061 return "#FF0000";
3062 } else {
3063 // Dark grey for off
3064 return "#545454";
3065 }
3066 }
3067
3068
3069 if(have_colormap(u_name)) {
3070 // u is a colormap
3071 if(colormaps[u_name].hasOwnProperty(u)) {
3072 // And the colormap has an entry here. Use it as the base color.
3073 var to_clone = colormaps[u_name][u].color;
3074
3075 var base_color = Color({
3076 hue: to_clone.hue(),
3077 saturation: to_clone.saturationv(),
3078 value: to_clone.value()
3079 });
3080 } else {
3081 // The colormap has no entry. Assume we're calculating all the
3082 // entries. We do this by splitting the color circle evenly.
3083
3084 // This holds the number of colors, which is 1 more than the largest
3085 // value used (since we start at color 0), which is the magnitude.
3086 // It's OK to go ask for the magnitude of this layer since it must
3087 // have already been downloaded.
3088 var num_colors = layers[u_name].magnitude + 1;
3089
3090 // Calculate the hue for this number.
3091 var hsv_hue = u / (num_colors + 1) * 360;
3092
3093 // The base color is a color at that hue, with max saturation and
3094 // value
3095 var base_color = Color({
3096 hue: hsv_hue,
3097 saturation: 100,
3098 value: 100
3099 })
3100 }
3101
3102 // Now that the base color is set, consult v to see what shade to use.
3103 if(v_name == undefined) {
3104 // No v layer is actually in use. Use whatever is in the base
3105 // color
3106 // TODO: This code path is silly, clean it up.
3107 var hsv_value = base_color.value();
3108 } else if(have_colormap(v_name)) {
3109 // Do discrete shades in v
3110 // This holds the number of shades we need.
3111 // It's OK to go ask for the magnitude of this layer since it must
3112 // have already been downloaded.
3113 var num_shades = layers[v_name].magnitude + 1;
3114
3115 // Calculate what shade we need from the nonnegative integer v
3116 // We want 100 to be included (since that's full brightness), but we
3117 // want to skip 0 (since no color can be seen at 0), so we add 1 to
3118 // v.
3119 var hsv_value = (v + 1) / num_shades * 100;
3120 } else {
3121 // Calculate what shade we need from v on -1 to 1
3122 var hsv_value = 50 + v * 50;
3123 }
3124
3125 // Set the color's value component.
3126 base_color.value(hsv_value);
3127
3128 // Return the shaded color
3129 return base_color.hexString();
3130 }
3131
3132
3133 // If we get here, we only have non-colormap layers.
3134
3135 // This is the polar angle (hue) in degrees, forced to be
3136 // positive.
3137 var hsv_hue = Math.atan2(v, u) * 180 / Math.PI;
3138 if(hsv_hue < 0) {
3139 hsv_hue += 360;
3140 }
3141
3142 // Rotate it by 60 degrees, so that the first layer is
3143 // yellow/blue
3144 hsv_hue += 60;
3145 if(hsv_hue > 360) {
3146 hsv_hue -= 360;
3147 }
3148
3149 // This is the polar radius (value). We inscribe our square
3150 // of side length 2 in a circle of radius 1 by dividing by
3151 // sqrt(2). So we get a value from 0 to 1
3152 var hsv_value = (Math.sqrt(Math.pow(u, 2) +
3153 Math.pow(v, 2)) / Math.sqrt(2));
3154
3155 // This is the HSV saturation component of the color on 0 to 1.
3156 // Just fix to 1.
3157 var hsv_saturation = 1.0;
3158
3159 // Now scale saturation and value to percent
3160 hsv_saturation *= 100;
3161 hsv_value *= 100;
3162
3163 // Now we have the color as HSV, but CSS doesn't support it.
3164
3165 // Make a Color object and get the RGB string
3166 try {
3167 return Color({
3168 hue: hsv_hue,
3169 saturation: hsv_saturation,
3170 value: hsv_value,
3171 }).hexString();
3172 } catch(error) {
3173 print("(" + u + "," + v + ") broke with color (" + hsv_hue +
3174 "," + hsv_saturation + "," + hsv_value + ")");
3175
3176 // We'll return an error color
3177 return "white";
3178 }
3179 }
3180
3181 // Define a flat projection
3182 // See https://developers.google.com/maps/documentation/javascript/maptypes#Projections
3183 function FlatProjection() {
3184 }
3185
3186
3187 FlatProjection.prototype.fromLatLngToPoint = function(latLng) {
3188 // Given a LatLng from -90 to 90 and -180 to 180, transform to an x, y Point
3189 // from 0 to 256 and 0 to 256
3190 var point = new google.maps.Point((latLng.lng() + 180) * 256 / 360,
3191 (latLng.lat() + 90) * 256 / 180);
3192
3193 return point;
3194
3195 }
3196
3197
3198 FlatProjection.prototype.fromPointToLatLng = function(point, noWrap) {
3199 // Given a an x, y Point from 0 to 256 and 0 to 256, transform to a LatLng from
3200 // -90 to 90 and -180 to 180
3201 var latLng = new google.maps.LatLng(point.y * 180 / 256 - 90,
3202 point.x * 360 / 256 - 180, noWrap);
3203
3204 return latLng;
3205 }
3206
3207 // Define a Google Maps MapType that's all blank
3208 // See https://developers.google.com/maps/documentation/javascript/examples/maptype-base
3209 function BlankMapType() {
3210 }
3211
3212 BlankMapType.prototype.tileSize = new google.maps.Size(256,256);
3213 BlankMapType.prototype.maxZoom = 19;
3214
3215 BlankMapType.prototype.getTile = function(coord, zoom, ownerDocument) {
3216 // This is the element representing this tile in the map
3217 // It should be an empty div
3218 var div = ownerDocument.createElement("div");
3219 div.style.width = this.tileSize.width + "px";
3220 div.style.height = this.tileSize.height + "px";
3221 div.style.backgroundColor = "#000000";
3222
3223 return div;
3224 }
3225
3226 BlankMapType.prototype.name = "Blank";
3227 BlankMapType.prototype.alt = "Blank Map";
3228
3229 BlankMapType.prototype.projection = new FlatProjection();
3230
3231
3232
3233 function get_LatLng(x, y) {
3234 // Given a point x, y in map space (0 to 256), get the corresponding LatLng
3235 return FlatProjection.prototype.fromPointToLatLng(
3236 new google.maps.Point(x, y));
3237 }
3238
3239 function clearMap() {
3240
3241 }
3242
3243 function drl_values(layout_index) {
3244
3245 // Download the DrL position data, and make it into a layer
3246 $.get("drl"+ layout_index +".tab", function(tsv_data) {
3247 // This is an array of rows, which are arrays of values:
3248 // id, x, y
3249 // Only this time X and Y are Cartesian coordinates.
3250 var parsed = $.tsv.parseRows(tsv_data);
3251
3252 // Compute two layers: one for x position, and one for y position.
3253 var layer_x = {};
3254 var layer_y = {};
3255
3256 for(var i = 0; i < parsed.length; i++) {
3257 // Pull out the parts of the TSV entry
3258 var label = parsed[i][0];
3259
3260 if(label == "") {
3261 // DrL ends its output with a blank line, which we skip
3262 // here.
3263 continue;
3264 }
3265
3266 var x = parseFloat(parsed[i][1]);
3267 // Invert the Y coordinate since we do that in the hex grid
3268 var y = -parseFloat(parsed[i][2]);
3269
3270 // Add x and y to the appropriate layers
3271 layer_x[label] = x;
3272 layer_y[label] = y;
3273 }
3274
3275 // Register the layers with no priorities. By default they are not
3276 // selections.
3277 add_layer_data("DrL X Position", layer_x);
3278 add_layer_data("DrL Y Position", layer_y);
3279
3280 // Make sure the layer browser has the up-to-date layer list
3281 update_browse_ui();
3282
3283 }, "text");
3284 }
3285
3286 function assignment_values (layout_index, spacing) {
3287 // Download the signature assignments to hexagons and fill in the global
3288 // hexagon assignment grid.
3289 $.get("assignments" + layout_index +".tab", function(tsv_data) {
3290 // This is an array of rows, which are arrays of values:
3291 // id, x, y
3292 var parsed = $.tsv.parseRows(tsv_data);
3293
3294 // This holds the maximum observed x
3295 var max_x = 0;
3296 // And y
3297 var max_y = 0;
3298
3299 // Fill in the global signature grid and ploygon grid arrays.
3300 for(var i = 0; i < parsed.length; i++) {
3301 // Get the label
3302 var label = parsed[i][0];
3303
3304 if(label == "") {
3305 // Blank line
3306 continue;
3307 }
3308
3309 // Get the x coord
3310 var x = parseInt(parsed[i][1]);
3311 // And the y coord
3312 var y = parseInt(parsed[i][2]);
3313
3314 x = x * spacing;
3315 y = y * spacing;
3316
3317
3318 // Update maxes
3319 max_x = Math.max(x, max_x);
3320 max_y = Math.max(y, max_y);
3321
3322
3323 // Make sure we have a row
3324 if(signature_grid[y] == null) {
3325 signature_grid[y] = [];
3326 // Pre-emptively add a row to the polygon grid.
3327 polygon_grid[y] = [];
3328 }
3329
3330 // Store the label in the global signature grid.
3331 signature_grid[y][x] = label;
3332 }
3333
3334 // We need to fit this whole thing into a 256x256 grid.
3335 // How big can we make each hexagon?
3336 // TODO: Do the algrbra to make this exact. Right now we just make a
3337 // grid that we know to be small enough.
3338 // Divide the space into one column per column, and calculate
3339 // side length from column width. Add an extra column for dangling
3340 // corners.
3341 var side_length_x = (256)/ (max_x + 2) * (2.0 / 3.0);
3342
3343 print("Max hexagon side length horizontally is " + side_length_x);
3344
3345 // Divide the space into rows and calculate the side length
3346 // from hex height. Remember to add an extra row for wggle.
3347 var side_length_y = ((256)/(max_y + 2)) / Math.sqrt(3);
3348
3349 print("Max hexagon side length vertically is " + side_length_y);
3350
3351 // How long is a hexagon side in world coords?
3352 // Shrink it from the biggest we can have so that we don't wrap off the
3353 // edges of the map.
3354 var hexagon_side_length = Math.min(side_length_x, side_length_y) / 2.0;
3355
3356 // Store this in the global hex_size, so we can later calculate the hex
3357 // size in pixels and make borders go away if we are too zoomed out.
3358 hex_size = hexagon_side_length;
3359
3360 // How far in should we move the whole grid from the top left corner of
3361 // the earth?
3362 // Let's try leaving a 1/4 Earth gap at least, to stop wrapping in
3363 // longitude that we can't turn off.
3364 // Since we already shrunk the map to half max size, this would put it
3365 // 1/4 of the 256 unit width and height away from the top left corner.
3366 grid_offset = (256) / 4;
3367
3368 // Loop through again and draw the polygons, now that we know how big
3369 // they have to be
3370 for(var i = 0; i < parsed.length; i++) {
3371 // TODO: don't re-parse this info
3372 // Get the label
3373 var label = parsed[i][0];
3374
3375 if(label == "") {
3376 // Blank line
3377 continue;
3378 }
3379
3380 // Get the x coord
3381 var x = parseInt(parsed[i][1]);
3382 // And the y coord
3383 var y = parseInt(parsed[i][2]);
3384
3385 x = x * spacing;
3386 y = y * spacing;
3387
3388 // Make a hexagon on the Google map and store that.
3389 var hexagon = make_hexagon(y, x, hexagon_side_length, grid_offset);
3390 // Store by x, y in grid
3391 polygon_grid[y][x] = hexagon;
3392 // Store by label
3393 polygons[label] = hexagon;
3394
3395 // Set the polygon's signature so we can look stuff up for it when
3396 // it's clicked.
3397 set_hexagon_signature(hexagon, label);
3398
3399 }
3400
3401 // Now that the ploygons exist, do the initial redraw to set all their
3402 // colors corectly. In case someone has messed with the controls.
3403 // TODO: can someone yet have messed with the controlls?
3404 refresh();
3405
3406
3407 }, "text");
3408 }
3409
3410 // Function to create a new map based upon the the layout_name argument
3411 // Find the index of the layout_name and pass it as the index to the
3412 // drl_values and assignment_values functions as these files are indexed
3413 // according to the appropriate layout
3414 function recreate_map(layout_name, spacing) {
3415
3416 var layout_index = layout_names.indexOf(layout_name);
3417 drl_values(layout_index);
3418 assignment_values(layout_index, spacing);
3419
3420 }
3421
3422 $(function() {
3423
3424 // Set up the RPC system for background statistics
3425 rpc_initialize();
3426
3427 // Set up the Google Map
3428 initialize_view(0);
3429
3430 // Set up the layer search
3431 $("#search").select2({
3432 placeholder: "Add Attribute...",
3433 query: function(query) {
3434 // Given a select2 query object, call query.callback with an object
3435 // with a "results" array.
3436
3437 // This is the array of result objects we will be sending back.
3438 var results = [];
3439
3440 // Get where we should start in the layer list, from select2's
3441 // infinite scrolling.
3442 var start_position = 0;
3443 if(query.context != undefined) {
3444 start_position = query.context;
3445 }
3446
3447 for(var i = start_position; i < layer_names_sorted.length; i++) {
3448 // For each possible result
3449 if(layer_names_sorted[i].toLowerCase().indexOf(
3450 query.term.toLowerCase()) != -1) {
3451
3452 // Query search term is in this layer's name. Add a select2
3453 // record to our results. Don't specify text: our custom
3454 // formatter looks up by ID and makes UI elements
3455 // dynamically.
3456 results.push({
3457 id: layer_names_sorted[i]
3458 });
3459
3460 if(results.length >= SEARCH_PAGE_SIZE) {
3461 // Page is full. Send it on.
3462 break;
3463 }
3464
3465 }
3466 }
3467
3468 // Give the results back to select2 as the results parameter.
3469 query.callback({
3470 results: results,
3471 // Say there's more if we broke out of the loop.
3472 more: i < layer_names_sorted.length,
3473 // If there are more results, start after where we left off.
3474 context: i + 1
3475 });
3476 },
3477 formatResult: function(result, container, query) {
3478 // Given a select2 result record, the element that our results go
3479 // in, and the query used to get the result, return a jQuery element
3480 // that goes in the container to represent the result.
3481
3482 // Get the layer name, and make the browse UI for it.
3483 return make_browse_ui(result.id);
3484 },
3485 // We want our dropdown to be big enough to browse.
3486 dropdownCssClass: "results-dropdown"
3487 });
3488
3489 // Handle result selection
3490 $("#search").on("select2-selecting", function(event) {
3491 // The select2 id of the thing clicked (the layer's name) is event.val
3492 var layer_name = event.val;
3493
3494 // User chose this layer. Add it to the global shortlist.
3495
3496 // Only add to the shortlist if it isn't already there
3497 // Was it already there?
3498 var found = false;
3499 for(var j = 0; j < shortlist.length; j++) {
3500 if(shortlist[j] == layer_name) {
3501 found = true;
3502 break;
3503 }
3504 }
3505
3506 if(!found) {
3507 // It's new. Add it to the shortlist
3508 shortlist.push(layer_name);
3509
3510 // Update the UI to reflect this. This may redraw the view.
3511 update_shortlist_ui();
3512
3513 }
3514
3515 // Don't actually change the selection.
3516 // This keeps the dropdown open when we click.
3517 event.preventDefault();
3518 });
3519
3520 $("#recalculate-statistics").button().click(function() {
3521 // Re-calculate the statistics between the currently filtered hexes and
3522 // everything else.
3523
3524 // Put up the throbber instead of us.
3525 $("#recalculate-statistics").hide();
3526 $(".recalculate-throbber").show();
3527
3528 // This holds the currently enabled filters.
3529 var filters = get_current_filters();
3530
3531 with_filtered_signatures(filters, function(signatures) {
3532 // Find everything passing the filters and run the statistics.
3533 recalculate_statistics(signatures);
3534 });
3535 });
3536
3537 // Temporary Inflate Button
3538 $("#inflate").button().click(function() {
3539 initialize_view (0);
3540 recreate_map(current_layout_name, 2);
3541 refresh ();
3542 });
3543
3544 // Create Pop-Up UI for Set Operations
3545 $("#set-operations").prepend(create_set_operation_ui ());
3546
3547 // Action handler for display of set operation pop-up
3548 $("#set-operation").button().click(function() {
3549 set_operation_clicks++;
3550 if (set_operation_clicks % 2 != 0){
3551 show_set_operation_drop_down ();
3552 }
3553 else {
3554 hide_set_operation_drop_down ();
3555 var drop_downs = document.getElementsByClassName("set-operation-value");
3556 for (var i = 0; i < drop_downs.length; i++) {
3557 drop_downs[i].style.visibility="hidden";
3558 }
3559 }
3560
3561 });
3562
3563 // Coputation of Set Operations
3564 var compute_button = document.getElementsByClassName ("compute-button");
3565 compute_button[0].onclick = function () {
3566 var layer_names = [];
3567 var layer_values = [];
3568 var layer_values_text = [];
3569
3570 var drop_down_layers = document.getElementsByClassName("set-operation-value");
3571 var drop_down_data_values = document.getElementsByClassName("set-operation-layer-value");
3572
3573 var function_type = document.getElementById("set-operations-list");
3574 var selected_function = function_type.selectedIndex;
3575
3576 var selected_index = drop_down_layers[0].selectedIndex;
3577 layer_names.push(drop_down_layers[0].options[selected_index].text);
3578
3579 var selected_index = drop_down_data_values[0].selectedIndex;
3580 layer_values.push(drop_down_data_values[0].options[selected_index].value);
3581 layer_values_text.push(drop_down_data_values[0].options[selected_index].text);
3582
3583 if (selected_function != 5) {
3584 var selected_index = drop_down_data_values[1].selectedIndex;
3585 layer_values.push(drop_down_data_values[1].options[selected_index].value);
3586 layer_values_text.push(drop_down_data_values[1].options[selected_index].text);
3587 var selected_index = drop_down_layers[1].selectedIndex;
3588 layer_names.push(drop_down_layers[1].options[selected_index].text);
3589 }
3590
3591
3592 switch (selected_function) {
3593 case 1:
3594 compute_intersection(layer_values, layer_names, layer_values_text);
3595 break;
3596 case 2:
3597 compute_union(layer_values, layer_names, layer_values_text);
3598 break;
3599 case 3:
3600 compute_set_difference(layer_values, layer_names, layer_values_text);
3601 break;
3602 case 4:
3603 compute_symmetric_difference(layer_values, layer_names, layer_values_text);
3604 break;
3605 case 5:
3606 compute_absolute_complement(layer_values, layer_names, layer_values_text);
3607 break
3608 default:
3609 complain ("Set Theory Error");
3610 }
3611 };
3612
3613 // Download the layer index
3614 $.get("layers.tab", function(tsv_data) {
3615 // Layer index is <name>\t<filename>\t<clumpiness>
3616 var parsed = $.tsv.parseRows(tsv_data);
3617
3618 for(var i = 0; i < parsed.length; i++) {
3619 // Pull out the parts of the TSV entry
3620 // This is the name of the layer.
3621 var layer_name = parsed[i][0];
3622
3623 if(layer_name == "") {
3624 // Skip any blank lines
3625 continue;
3626 }
3627
3628 // This is the URL from which to download the TSV for the actual
3629 // layer.
3630 var layer_url = parsed[i][1];
3631
3632 // This is the layer's clumpiness score
3633 var layer_clumpiness = parseFloat(parsed[i][2]);
3634
3635 // This is the number of hexes that the layer has any values for.
3636 // We need to get it from the server so we don't have to download
3637 // the layer to have it.
3638 var layer_count = parseFloat(parsed[i][3]);
3639
3640 // This is the number of 1s in a binary layer, or NaN in other
3641 // layers
3642 var layer_positives = parseFloat(parsed[i][4]);
3643
3644 // Add this layer to our index of layers
3645 add_layer_url(layer_name, layer_url, {
3646 clumpiness: layer_clumpiness,
3647 positives: layer_positives,
3648 n: layer_count
3649 });
3650 }
3651
3652 // Now we have added layer downloaders for all the layers in the
3653 // index. Update the UI
3654 update_browse_ui();
3655
3656
3657 }, "text");
3658
3659 // Download full score matrix index, which we later use for statistics. Note
3660 // that stats won't work unless this finishes first. TODO: enforce this.
3661 $.get("matrices.tab", function(tsv_data) {
3662 // Matrix index is just <filename>
3663 var parsed = $.tsv.parseRows(tsv_data);
3664
3665 for(var i = 0; i < parsed.length; i++) {
3666 // Pull out the parts of the TSV entry
3667 // This is the filename of the matrix.
3668 var matrix_name = parsed[i][0];
3669
3670 if(matrix_name == "") {
3671 // Not a real matrix
3672 continue;
3673 }
3674
3675 // Add it to the global list
3676 available_matrices.push(matrix_name);
3677 }
3678 }, "text");
3679
3680 // Download color map information
3681 $.get("colormaps.tab", function(tsv_data) {
3682 // Colormap data is <layer name>\t<value>\t<category name>\t<color>
3683 // \t<value>\t<category name>\t<color>...
3684 var parsed = $.tsv.parseRows(tsv_data);
3685
3686 for(var i = 0; i < parsed.length; i++) {
3687 // Get the name of the layer
3688 var layer_name = parsed[i][0];
3689
3690 // Skip blank lines
3691 if(layer_name == "") {
3692 continue;
3693 }
3694
3695 // This holds all the categories (name and color) by integer index
3696 var colormap = [];
3697
3698 print("Loading colormap for " + layer_name);
3699
3700 for(j = 1; j < parsed[i].length; j += 3) {
3701 // Store each color assignment.
3702 // Doesn't run if there aren't any assignments, leaving an empty
3703 // colormap object that just forces automatic color selection.
3704
3705 // This holds the index of the category
3706 var category_index = parseInt(parsed[i][j]);
3707
3708 // The colormap gets an object with the name and color that the
3709 // index number refers to. Color is stored as a color object.
3710 colormap[category_index] = {
3711 name: parsed[i][j + 1],
3712 color: Color(parsed[i][j + 2])
3713 };
3714
3715 print( colormap[category_index].name + " -> " +
3716 colormap[category_index].color.hexString());
3717 }
3718
3719 // Store the finished color map in the global object
3720 colormaps[layer_name] = colormap;
3721
3722
3723 }
3724
3725 // We may need to redraw the view in response to having new color map
3726 // info, if it came particularly late.
3727 refresh();
3728
3729 }, "text");
3730
3731 // Download the Matrix Names and pass it to the layout_names array
3732 $.get("matrixnames.tab", function(tsv_data) {
3733 // This is an array of rows, which are strings of matrix names
3734 var parsed = $.tsv.parseRows(tsv_data);
3735
3736 for(var i = 0; i < parsed.length; i++) {
3737 // Pull out the parts of the TSV entry
3738 var label = parsed[i][0];
3739
3740 if(label == "") {
3741 // Skip any blank lines
3742 continue;
3743 }
3744 // Add layout names to global array of names
3745 layout_names.push(label);
3746
3747 if(layout_names.length == 1) {
3748 // This is the very first layout. Pull it up.
3749
3750 // TODO: We don't go through the normal change event since we
3751 // never change the dropdown value actually. But we duplicate
3752 // user selection hode here.
3753 var current_layout = "Current Layout: " + layout_names[0];
3754
3755 $("#current-layout").text(current_layout);
3756 initialize_view (0);
3757 recreate_map(layout_names[0], 1);
3758 refresh ();
3759 current_layout_name = layout_names[0];
3760
3761 }
3762 }
3763 }, "text");
3764
3765 $("#layout-search").select2({
3766 placeholder: "Select a Layout...",
3767 query: function(query) {
3768 // Given a select2 query object, call query.callback with an object
3769 // with a "results" array.
3770
3771 // This is the array of result objects we will be sending back.
3772 var results = [];
3773
3774 // Get where we should start in the layer list, from select2's
3775 // infinite scrolling.
3776 var start_position = 0;
3777 if(query.context != undefined) {
3778 start_position = query.context;
3779 }
3780
3781 for(var i = start_position; i < layout_names.length; i++) {
3782 // For each possible result
3783 if(layout_names[i].toLowerCase().indexOf(
3784 query.term.toLowerCase()) != -1) {
3785
3786 // Query search term is in this layer's name. Add a select2
3787 // record to our results. Don't specify text: our custom
3788 // formatter looks up by ID and makes UI elements
3789 // dynamically.
3790 results.push({
3791 id: layout_names[i]
3792 });
3793
3794 if(results.length >= SEARCH_PAGE_SIZE) {
3795 // Page is full. Send it on.
3796 break;
3797 }
3798
3799 }
3800 }
3801
3802 // Give the results back to select2 as the results parameter.
3803 query.callback({
3804 results: results,
3805 // Say there's more if we broke out of the loop.
3806 more: i < layout_names.length,
3807 // If there are more results, start after where we left off.
3808 context: i + 1
3809 });
3810 },
3811 formatResult: function(result, container, query) {
3812 // Given a select2 result record, the element that our results go
3813 // in, and the query used to get the result, return a jQuery element
3814 // that goes in the container to represent the result.
3815
3816 // Get the layer name, and make the browse UI for it.
3817 return make_toggle_layout_ui(result.id);
3818 },
3819 // We want our dropdown to be big enough to browse.
3820 dropdownCssClass: "results-dropdown"
3821 });
3822
3823 // Handle result selection
3824 $("#layout-search").on("select2-selecting", function(event) {
3825 // The select2 id of the thing clicked (the layout's name) is event.val
3826 var layout_name = event.val;
3827
3828 var current_layout = "Current Layout: " + layout_name;
3829
3830 document.getElementById('current-layout').innerHTML=current_layout;
3831 initialize_view (0);
3832 recreate_map(layout_name, 1);
3833 refresh ();
3834 // Don't actually change the selection.
3835 // This keeps the dropdown open when we click.
3836 event.preventDefault();
3837
3838 current_layout_name = layout_name;
3839 });
3840
3841 drl_values(layout_names[0]);
3842 assignment_values (layout_names[0], 1);
3843 current_layout_name = layout_names[0];
3844
3845 });
3846
3847
3848