0
|
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 $("#current-layout").text(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
|