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