blob: 4edbdb4d0b6d2d900722f3e221e31b94f46fcb3a [file] [log] [blame]
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001$(function() {
2
3 // Connection to AsterixDB - Just one needed!
4 A = new AsterixDBConnection().dataverse("twitter");
5
6 // Following this is some stuff specific to the Black Cherry demo
7 // This is not necessary for working with AsterixDB
8 APIqueryTracker = {};
9 drilldown_data_map = {};
10 drilldown_data_map_vals = {};
11 asyncQueryManager = {};
12
13 review_mode_tweetbooks = [];
14 review_mode_handles = [];
15
16 map_cells = [];
17 map_tweet_markers = [];
18 param_placeholder = {};
19
20 // UI Elements - Modals & perspective tabs
21 $('#drilldown_modal').modal({ show: false});
22 $('#explore-mode').click( onLaunchExploreMode );
23 $('#review-mode').click( onLaunchReviewMode );
24
25 // UI Elements - A button to clear current map and query data
26 $("#clear-button").button().click(function () {
27 mapWidgetClearMap();
28 param_placeholder = {};
29
30 map.setZoom(4);
31 map.setCenter(new google.maps.LatLng(38.89, 77.03));
32
33 $('#query-preview-window').html('');
34 $("#metatweetzone").html('');
35 });
36
37 // UI Elements - Query setup
38 $("#selection-button").button('toggle');
39
40 var dialog = $("#dialog").dialog({
41 width: "auto",
42 title: "AQL Query"
43 }).dialog("close");
44 $("#show-query-button")
45 .button()
46 .attr("disabled", true)
47 .click(function (event) {
48 $("#dialog").dialog("open");
49 });
50
51 // UI Element - Grid sliders
52 var updateSliderDisplay = function(event, ui) {
53 if (event.target.id == "grid-lat-slider") {
54 $("#gridlat").text(""+ui.value);
55 } else {
56 $("#gridlng").text(""+ui.value);
57 }
58 };
59
60 sliderOptions = {
61 max: 10,
62 min: 1.5,
63 step: .1,
64 value: 2.0,
65 slidechange: updateSliderDisplay,
66 slide: updateSliderDisplay,
67 start: updateSliderDisplay,
68 stop: updateSliderDisplay
69 };
70
71 $("#gridlat").text(""+sliderOptions.value);
72 $("#gridlng").text(""+sliderOptions.value);
73 $(".grid-slider").slider(sliderOptions);
74
75 // UI Elements - Date Pickers
76 var dateOptions = {
77 dateFormat: "yy-mm-dd",
78 defaultDate: "2012-01-02",
79 navigationAsDateFormat: true,
80 constrainInput: true
81 };
82 var start_dp = $("#start-date").datepicker(dateOptions);
83 start_dp.val(dateOptions.defaultDate);
84 dateOptions['defaultDate'] = "2012-12-31";
85 var end_dp= $("#end-date").datepicker(dateOptions);
86 end_dp.val(dateOptions.defaultDate);
87
88 // This little bit of code manages period checks of the asynchronous query manager,
89 // which holds onto handles asynchornously received. We can set the handle update
90 // frequency using seconds, and it will let us know when it is ready.
91 var intervalID = setInterval(
92 function() {
93 asynchronousQueryIntervalUpdate();
94 },
95 asynchronousQueryGetInterval()
96 );
97
98 // UI Elements - Creates map and location auto-complete
99 onOpenExploreMap();
100 var mapOptions = {
101 center: new google.maps.LatLng(38.89, 77.03),
102 zoom: 4,
103 mapTypeId: google.maps.MapTypeId.ROADMAP, // Could also be SATELLITE
104 streetViewControl: false,
105 draggable : false
106 };
107 map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions);
108
109 var input = document.getElementById('location-text-box');
110 var autocomplete = new google.maps.places.Autocomplete(input);
111 autocomplete.bindTo('bounds', map);
112
113 google.maps.event.addListener(autocomplete, 'place_changed', function() {
114 var place = autocomplete.getPlace();
115 if (place.geometry.viewport) {
116 map.fitBounds(place.geometry.viewport);
117 } else {
118 map.setCenter(place.geometry.location);
119 map.setZoom(17); // Why 17? Because it looks good.
120 }
121 var address = '';
122 if (place.address_components) {
123 address = [(place.address_components[0] && place.address_components[0].short_name || ''),
124 (place.address_components[1] && place.address_components[1].short_name || ''),
125 (place.address_components[2] && place.address_components[2].short_name || '') ].join(' ');
126 }
127 });
128
129 // UI Elements - Selection Rectangle Drawing
130 shouldDraw = false;
131 var startLatLng;
132 selectionRect = null;
133 var selectionRadio = $("#selection-button");
134 var firstClick = true;
135
136 google.maps.event.addListener(map, 'mousedown', function (event) {
137 // only allow drawing if selection is selected
138 if (selectionRadio.hasClass("active")) {
139 startLatLng = event.latLng;
140 shouldDraw = true;
141 }
142 });
143
144 google.maps.event.addListener(map, 'mousemove', drawRect);
145 function drawRect (event) {
146 if (shouldDraw) {
147 if (!selectionRect) {
148 var selectionRectOpts = {
149 bounds: new google.maps.LatLngBounds(startLatLng, event.latLng),
150 map: map,
151 strokeWeight: 1,
152 strokeColor: "2b3f8c",
153 fillColor: "2b3f8c"
154 };
155 selectionRect = new google.maps.Rectangle(selectionRectOpts);
156 google.maps.event.addListener(selectionRect, 'mouseup', function () {
157 shouldDraw = false;
158 //submitQuery();
159 });
160 } else {
161 if (startLatLng.lng() < event.latLng.lng()) {
162 selectionRect.setBounds(new google.maps.LatLngBounds(startLatLng, event.latLng));
163 } else {
164 selectionRect.setBounds(new google.maps.LatLngBounds(event.latLng, startLatLng));
165 }
166 }
167 }
168 };
169
170 // UI Elements - Toggle location search style by location or by map selection
171 $('#selection-button').on('click', function (e) {
172 $("#location-text-box").attr("disabled", "disabled");
173 if (selectionRect) {
174 selectionRect.setMap(map);
175 }
176 });
177 $('#location-button').on('click', function (e) {
178 $("#location-text-box").removeAttr("disabled");
179 if (selectionRect) {
180 selectionRect.setMap(null);
181 }
182 });
183
184 // UI Elements - Tweetbook Management
185 $('.dropdown-menu a.holdmenu').click(function(e) {
186 e.stopPropagation();
187 });
188
189 $('#new-tweetbook-button').on('click', function (e) {
190 onCreateNewTweetBook($('#new-tweetbook-entry').val());
191
192 $('#new-tweetbook-entry').val($('#new-tweetbook-entry').attr('placeholder'));
193 });
194
195 // UI Element - Query Submission
196 $("#submit-button").button().click(function () {
197 // Clear current map on trigger
198 mapWidgetClearMap();
199
200 // gather all of the data from the inputs
201 var kwterm = $("#keyword-textbox").val();
202 var startdp = $("#start-date").datepicker("getDate");
203 var enddp = $("#end-date").datepicker("getDate");
204 var startdt = $.datepicker.formatDate("yy-mm-dd", startdp)+"T00:00:00Z";
205 var enddt = $.datepicker.formatDate("yy-mm-dd", enddp)+"T23:59:59Z";
206
207 var formData = {
208 "keyword": kwterm,
209 "startdt": startdt,
210 "enddt": enddt,
211 "gridlat": $("#grid-lat-slider").slider("value"),
212 "gridlng": $("#grid-lng-slider").slider("value")
213 };
214
215 // Get Map Bounds
216 var bounds;
217 if ($('#selection-button').hasClass("active") && selectionRect) {
218 bounds = selectionRect.getBounds();
219 } else {
220 bounds = map.getBounds();
221 }
222
223 formData["swLat"] = Math.abs(bounds.getSouthWest().lat());
224 formData["swLng"] = Math.abs(bounds.getSouthWest().lng());
225 formData["neLat"] = Math.abs(bounds.getNorthEast().lat());
226 formData["neLng"] = Math.abs(bounds.getNorthEast().lng());
227
228 var build_cherry_mode = "synchronous";
229
230 if ($('#asbox').is(":checked")) {
231 build_cherry_mode = "asynchronous";
232 }
233
234 var f = buildAQLQueryFromForm(formData);
235 param_placeholder["payload"] = formData;
236 param_placeholder["query_string"] = "use dataverse twitter;\n" + f.val();
237
238 if (build_cherry_mode == "synchronous") {
239 A.query(f.val(), cherryQuerySyncCallback, build_cherry_mode);
240 } else {
241 A.query(f.val(), cherryQueryAsyncCallback, build_cherry_mode);
242 }
243
244 APIqueryTracker = {
245 "query" : "use dataverse twitter;\n" + f.val(),
246 "data" : formData
247 };
248
249 $('#dialog').html(APIqueryTracker["query"]);
250
251 if (!$('#asbox').is(":checked")) {
252 $('#show-query-button').attr("disabled", false);
253 } else {
254 $('#show-query-button').attr("disabled", true);
255 }
256 });
257});
258
259
260function buildAQLQueryFromForm(parameters) {
261 var bounds = {
262 "ne" : { "lat" : parameters["neLat"], "lng" : parameters["neLng"]},
263 "sw" : { "lat" : parameters["swLat"], "lng" : parameters["swLng"]}
264 };
265
266 var rectangle =
267 new FunctionExpression("create-rectangle",
268 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
269 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
270
271
272 var aql = new FLWOGRExpression()
273 .ForClause("$t", new AExpression("dataset TweetMessages"))
274 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
275 .LetClause("$region", rectangle)
276 .WhereClause().and(
277 new FunctionExpression("spatial-intersect", "$t.sender-location", "$region"),
278 new AExpression('$t.send-time > datetime("' + parameters["startdt"] + '")'),
279 new AExpression('$t.send-time < datetime("' + parameters["enddt"] + '")'),
280 new FunctionExpression("contains", "$t.message-text", "$keyword")
281 )
282 .GroupClause(
283 "$c",
284 new FunctionExpression("spatial-cell", "$t.sender-location",
285 new FunctionExpression("create-point", "24.5", "-125.5"),
286 parameters["gridlat"].toFixed(1), parameters["gridlng"].toFixed(1)),
287 "with",
288 "$t"
289 )
290 .ReturnClause({ "cell" : "$c", "count" : "count($t)" });
291
292 return aql;
293}
294
295/** Asynchronous Query Management **/
296
297
298/**
299* Checks through each asynchronous query to see if they are ready yet
300*/
301function asynchronousQueryIntervalUpdate() {
302 for (var handle_key in asyncQueryManager) {
303 if (!asyncQueryManager[handle_key].hasOwnProperty("ready")) {
304 asynchronousQueryGetAPIQueryStatus( asyncQueryManager[handle_key]["handle"], handle_key );
305 }
306 }
307}
308
309
310/**
311* Returns current time interval to check for asynchronous query readiness
312* @returns {number} milliseconds between asychronous query checks
313*/
314function asynchronousQueryGetInterval() {
315 var seconds = 10;
316 return seconds * 1000;
317}
318
319
320/**
321* Retrieves status of an asynchronous query, using an opaque result handle from API
322* @param {Object} handle, an object previously returned from an async call
323* @param {number} handle_id, the integer ID parsed from the handle object
324*/
325function asynchronousQueryGetAPIQueryStatus (handle, handle_id) {
326
327 // FIXME query status call should disable other functions while it
328 // loads...for simplicity, really...
329 A.query_status(
330 {
331 "handle" : JSON.stringify(handle)
332 },
333 function (res) {
334 if (res["status"] == "SUCCESS") {
335 // We don't need to check if this one is ready again, it's not going anywhere...
336 // Unless the life cycle of handles has changed drastically
337 asyncQueryManager[handle_id]["ready"] = true;
338
339 // Indicate success.
340 $('#handle_' + handle_id).addClass("label-success");
341 }
342 }
343 );
344}
345
346
347/**
348* On-success callback after async API query
349* @param {object} res, a result object containing an opaque result handle to Asterix
350*/
351function cherryQueryAsyncCallback(res) {
352
353 // Parse handle, handle id and query from async call result
354 var handle_query = param_placeholder["query_string"];
355 var handle = res;
356 var handle_id = res["handle"].toString().split(',')[0];
357
358 // Add to stored map of existing handles
359 asyncQueryManager[handle_id] = {
360 "handle" : handle,
361 "query" : handle_query,
362 "data" : param_placeholder["payload"],
363 };
364
365 $('#review-handles-dropdown').append('<a href="#" class="holdmenu"><span class="label" id="handle_' + handle_id + '">Handle ' + handle_id + '</span></a><br/>');
366
367 $('#handle_' + handle_id).hover(
368 function(){
369 $('#query-preview-window').html('');
370 $('#query-preview-window').html('<br/><br/>' + asyncQueryManager[handle_id]["query"]);
371 },
372 function() {
373 $('#query-preview-window').html('');
374 }
375 );
376
377 $('#handle_' + handle_id).on('click', function (e) {
378
379 // make sure query is ready to be run
380 if (asyncQueryManager[handle_id]["ready"]) {
381
382 // Update API Query Tracker and view to reflect this query
383 $('#query-preview-window').html('<br/><br/>' + asyncQueryManager[handle_id]["query"]);
384 APIqueryTracker = {
385 "query" : asyncQueryManager[handle_id]["query"],
386 "data" : asyncQueryManager[handle_id]["data"]
387 };
388 $('#dialog').html(APIqueryTracker["query"]);
389
390 // Generate new Asterix Core API Query
391 A.query_result(
392 { "handle" : JSON.stringify(asyncQueryManager[handle_id]["handle"]) },
393 cherryQuerySyncCallback
394 );
395 }
396 });
397}
398
399
400/** Core Query Management and Drilldown
401
402/**
403* Utility Method for parsing a record of this form:
404* { "cell": rectangle("22.5,64.5 24.5,66.5"), "count": 5 }
405* returns a json object with keys: weight, latSW, lngSW, latNE, lngNE
406*/
407function getRecord(cell_count_record) {
408 var record_representation = {};
409
410 var rectangle = cell_count_record.split('")')[0].split('("')[1];
411 record_representation["latSW"] = parseFloat(rectangle.split(" ")[0].split(',')[0]);
412 record_representation["lngSW"] = parseFloat(rectangle.split(" ")[0].split(',')[1]);
413 record_representation["latNE"] = parseFloat(rectangle.split(" ")[1].split(',')[0]);
414 record_representation["lngNE"] = parseFloat(rectangle.split(" ")[1].split(',')[1]);
415 record_representation["weight"] = parseInt(cell_count_record.split('count": ')[1].split(" ")[0]);
416
417 return record_representation;
418}
419
420/**
421* A spatial data cleaning and mapping call
422* @param {Object} res, a result object from a cherry geospatial query
423*/
424function cherryQuerySyncCallback(res) {
425
426 records = res["results"];
427 if (typeof res["results"][0] == "object") {
428 records = res["results"][0];
429 }
430
431 var coordinates = [];
432 var weights = [];
433
434 for (var subrecord in records) {
435 var coordinate = getRecord(records[subrecord]);
436 weights.push(coordinate["weight"]);
437 coordinates.push(coordinate);
438 }
439
440 triggerUIUpdate(coordinates, param_placeholder["payload"], weights);
441}
442
443/**
444* Triggers a map update based on a set of spatial query result cells
445* @param [Array] mapPlotData, an array of coordinate and weight objects
446* @param [Array] params, an object containing original query parameters [LEGACY]
447* @param [Array] plotWeights, a list of weights of the spatial cells - e.g., number of tweets
448*/
449function triggerUIUpdate(mapPlotData, params, plotWeights) {
450 /** Clear anything currently on the map **/
451 mapWidgetClearMap();
452 param_placeholder = params;
453
454 // Compute data point spread
455 var dataBreakpoints = mapWidgetLegendComputeNaturalBreaks(plotWeights);
456
457 $.each(mapPlotData, function (m, val) {
458
459 // Only map points in data range of top 4 natural breaks
460 if (mapPlotData[m].weight > dataBreakpoints[0]) {
461
462 // Get color value of legend
463 var mapColor = mapWidgetLegendGetHeatValue(mapPlotData[m].weight, dataBreakpoints);
464 var markerRadius = mapWidgetComputeCircleRadius(mapPlotData[m], dataBreakpoints);
465 var point_opacity = 1.0;
466
467 var point_center = new google.maps.LatLng(
468 (mapPlotData[m].latSW + mapPlotData[m].latNE)/2.0,
469 (mapPlotData[m].lngSW + mapPlotData[m].lngNE)/2.0);
470
471 // Create and plot marker
472 var map_circle_options = {
473 center: point_center,
474 radius: markerRadius,
475 map: map,
476 fillOpacity: point_opacity,
477 fillColor: mapColor,
478 clickable: true
479 };
480 var map_circle = new google.maps.Circle(map_circle_options);
481 map_circle.val = mapPlotData[m];
482 /*var map_circle_info = new google.maps.InfoWindow({
483 content: mapPlotData[m].weight + " "
484 });*/
485
486 // Clicking on a circle drills down map to that value
487 google.maps.event.addListener(map_circle, 'click', function (event) {
488
489 onMapPointDrillDown(map_circle.val);
490 });
491
492 // Add this marker to global marker cells
493 map_cells.push(map_circle);
494 }
495 });
496
497 // Add a legend to the map
498 mapControlWidgetAddLegend(dataBreakpoints);
499}
500
501/**
502* prepares an Asterix API query to drill down in a rectangular spatial zone
503*
504* @params {object} marker_borders [LEGACY] a set of bounds for a region from a previous api result
505*/
506function onMapPointDrillDown(marker_borders) {
507 var zoneData = APIqueryTracker["data"];
508
509 var zswBounds = new google.maps.LatLng(marker_borders.latSW, marker_borders.lngSW);
510 var zneBounds = new google.maps.LatLng(marker_borders.latNE, marker_borders.lngNE);
511
512 var zoneBounds = new google.maps.LatLngBounds(zswBounds, zneBounds);
513 zoneData["swLat"] = zoneBounds.getSouthWest().lat();
514 zoneData["swLng"] = zoneBounds.getSouthWest().lng();
515 zoneData["neLat"] = zoneBounds.getNorthEast().lat();
516 zoneData["neLng"] = zoneBounds.getNorthEast().lng();
517 var zB = {
518 "sw" : {
519 "lat" : zoneBounds.getSouthWest().lat(),
520 "lng" : zoneBounds.getSouthWest().lng()
521 },
522 "ne" : {
523 "lat" : zoneBounds.getNorthEast().lat(),
524 "lng" : zoneBounds.getNorthEast().lng()
525 }
526 };
527
528 mapWidgetClearMap();
529
530 var customBounds = new google.maps.LatLngBounds();
531 var zoomSWBounds = new google.maps.LatLng(zoneData["swLat"], zoneData["swLng"]);
532 var zoomNEBounds = new google.maps.LatLng(zoneData["neLat"], zoneData["neLng"]);
533 customBounds.extend(zoomSWBounds);
534 customBounds.extend(zoomNEBounds);
535 map.fitBounds(customBounds);
536
537 var df = getDrillDownQuery(zoneData, zB);
538
539 param_placeholder = {
540 "query_string" : "use dataverse twitter;\n" + df.val(),
541 "marker_path" : "../img/mobile2.png",
542 "on_click_marker" : onClickTweetbookMapMarker,
543 "on_clean_result" : onCleanTweetbookDrilldown,
544 "payload" : zoneData
545 };
546
547 A.query(df.val(), onTweetbookQuerySuccessPlot);
548}
549
550function getDrillDownQuery(parameters, bounds) {
551
552 var zoomRectangle = new FunctionExpression("create-rectangle",
553 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
554 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
555
556 var drillDown = new FLWOGRExpression()
557 .ForClause("$t", new AExpression("dataset TweetMessages"))
558 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
559 .LetClause("$region", zoomRectangle)
560 .WhereClause().and(
561 new FunctionExpression('spatial-intersect', '$t.sender-location', '$region'),
562 new AExpression().set('$t.send-time > datetime("' + parameters["startdt"] + '")'),
563 new AExpression().set('$t.send-time < datetime("' + parameters["enddt"] + '")'),
564 new FunctionExpression('contains', '$t.message-text', '$keyword')
565 )
566 .ReturnClause({
567 "tweetId" : "$t.tweetid",
568 "tweetText" : "$t.message-text",
569 "tweetLoc" : "$t.sender-location"
570 });
571
572 return drillDown;
573}
574
575
576function addTweetbookCommentDropdown(appendToDiv) {
577
578 // Creates a div to manage a radio button set of chosen tweetbooks
579 $('<div/>')
580 .attr("class","btn-group chosen-tweetbooks")
581 .attr("data-toggle", "buttons-radio")
582 .css("margin-bottom", "10px")
583 .attr("id", "metacomment-tweetbooks")
584 .appendTo(appendToDiv);
585
586 // For each existing tweetbook from review mode, adds a radio button option.
587 $('#metacomment-tweetbooks').append('<input type="hidden" id="target-tweetbook" value="" />');
588 for (var rmt in review_mode_tweetbooks) {
589 var tweetbook_option = '<button type="button" class="btn">' + review_mode_tweetbooks[rmt] + '</button>';
590
591 $('#metacomment-tweetbooks').append(tweetbook_option + '<br/>');
592 }
593
594 // Creates a button + input combination to add tweet comment to new tweetbook
595 var new_tweetbook_option = '<button type="button" class="btn" id="new-tweetbook-target-m"></button>' +
596 '<input type="text" id="new-tweetbook-entry-m" placeholder="Add to new tweetbook..."><br/>';
597 $('#metacomment-tweetbooks').append(new_tweetbook_option);
598
599 $("#new-tweetbook-entry-m").keyup(function() {
600 $("#new-tweetbook-target-m").val($("#new-tweetbook-entry-m").val());
601 $("#new-tweetbook-target-m").text($("#new-tweetbook-entry-m").val());
602 });
603
604 // There is a hidden input (id = target-tweetbook) which is used to track the value
605 // of the tweetbook to which the comment on this tweet will be added.
606 $(".chosen-tweetbooks .btn").click(function() {
607 $("#target-tweetbook").val($(this).text());
608 });
609}
610
611function onDrillDownAtLocation(tO) {
612
613 var tweetId = tO["tweetEntryId"];
614 var tweetText = tO["tweetText"];
615
616 var tweetContainerId = '#drilldown_modal_body';
617 var tweetDiv = '<div id="drilltweetobj' + tweetId + '"></div>';
618
619 $(tweetContainerId).empty();
620 $(tweetContainerId).append(tweetDiv);
621 $('#drilltweetobj' + tweetId).append('<p>Tweet #' + tweetId + ": " + tweetText + '</p>');
622
623 // Add comment field
624 $('#drilltweetobj' + tweetId).append('<input class="textbox" type="text" id="metacomment' + tweetId + '"><br/>');
625
626 if (tO.hasOwnProperty("tweetComment")) {
627 $("#metacomment" + tweetId).val(tO["tweetComment"]);
628 }
629
630 addTweetbookCommentDropdown('#drilltweetobj' + tweetId);
631 $('#drilltweetobj' + tweetId).append('<br/><button type="button" class="btn" id="add-metacomment">Save Comment</button>');
632
633 $('#add-metacomment').button().click(function () {
634 var save_metacomment_target_tweetbook = $("#target-tweetbook").val();
635 var save_metacomment_target_comment = '"' + $("#metacomment" + tweetId).val() + '"';
636 var save_metacomment_target_tweet = '"' + tweetId + '"';
637
638 if (save_metacomment_target_tweetbook.length == 0) {
639 alert("Please choose a tweetbook.");
640
641 // TODO Indicate failure message
642 } else {
643 // If necessary, add new tweetbook
644 // TODO existsTargetTweetbook method
645 if (!(existsTweetbook(save_metacomment_target_tweetbook))) {
646 onCreateNewTweetBook(save_metacomment_target_tweetbook);
647 }
648
649 var toDelete = new DeleteStatement(
650 "$mt",
651 save_metacomment_target_tweetbook,
652 new AExpression("$mt.tweetid = " + save_metacomment_target_tweet.toString())
653 );
654
655 A.update(toDelete.val());
656
657 var toInsert = new InsertStatement(
658 save_metacomment_target_tweetbook,
659 {
660 "tweetid" : save_metacomment_target_tweet.toString(),
661 "comment-text" : save_metacomment_target_comment
662 }
663 );
664
665 // Insert query to add metacomment to said tweetbook dataset
666 A.update(toInsert.val());
667
668 // TODO Indicate success
669
670 }
671 });
672
673 // Set width of tweetbook buttons
674 $(".chosen-tweetbooks .btn").css("width", "200px");
675}
676
677
678/**
679* Adds a new tweetbook entry to the menu and creates a dataset of type TweetbookEntry.
680*/
681function onCreateNewTweetBook(tweetbook_title) {
682
683 var tweetbook_title = tweetbook_title.split(' ').join('_');
684
685 A.ddl(
686 "create dataset " + tweetbook_title + "(TweetbookEntry) primary key tweetid;",
687 function () {}
688 );
689
690 if (!(existsTweetbook(tweetbook_title))) {
691 review_mode_tweetbooks.push(tweetbook_title);
692 addTweetBookDropdownItem(tweetbook_title);
693 }
694}
695
696
697function onDropTweetBook(tweetbook_title) {
698
699 // AQL Call
700 A.ddl(
701 "drop dataset " + tweetbook_title + " if exists;",
702 function () {}
703 );
704
705 // Removes tweetbook from review_mode_tweetbooks
706 var remove_position = $.inArray(tweetbook_title, review_mode_tweetbooks);
707 if (remove_position >= 0) review_mode_tweetbooks.splice(remove_position, 1);
708
709 // Clear UI with review tweetbook titles
710 $('#review-tweetbook-titles').html('');
711 for (r in review_mode_tweetbooks) {
712 addTweetBookDropdownItem(review_mode_tweetbooks[r]);
713 }
714}
715
716
717function addTweetBookDropdownItem(tweetbook) {
718 // Add placeholder for this tweetbook
719 $('<div/>')
720 .css("padding-left", "1em")
721 .attr({
722 "class" : "btn-group",
723 "id" : "rm_holder_" + tweetbook
724 }).appendTo("#review-tweetbook-titles");
725 $("#review-tweetbook-titles").append('<br/>');
726
727 // Add plotting button for this tweetbook
728 var plot_button = '<button class="btn" id="rm_plotbook_' + tweetbook + '">' + tweetbook + '</button>';
729 $("#rm_holder_" + tweetbook).append(plot_button);
730 $("#rm_plotbook_" + tweetbook).on('click', function(e) {
731 onPlotTweetbook(tweetbook);
732 });
733
734 // Add trash button for this tweetbook
735 var trash_button = '<button class="btn" id="rm_trashbook_' + tweetbook + '"><i class="icon-trash"></i></button>';
736 $("#rm_holder_" + tweetbook).append(trash_button);
737 $('#rm_trashbook_' + tweetbook).on('click', function(e) {
738 onDropTweetBook(tweetbook)
739 });
740
741 /*.success(onTweetbookQuerySuccessPlot, true)
742 .add_extra("on_click_marker", onClickTweetbookMapMarker)
743 .add_extra("on_clean_result", onCleanPlotTweetbook)*/
744}
745
746
747function onPlotTweetbook(tweetbook) {
748 var plotTweetQuery = new FLWOGRExpression()
749 .ForClause("$t", new AExpression("dataset TweetMessages"))
750 .ForClause("$m", new AExpression("dataset " + tweetbook))
751 .WhereClause(new AExpression("$m.tweetid = $t.tweetid"))
752 .ReturnClause({
753 "tweetId" : "$m.tweetid",
754 "tweetText" : "$t.message-text",
755 "tweetLoc" : "$t.sender-location",
756 "tweetCom" : "$m.comment-text"
757 });
758
759 param_placeholder = {
760 "query_string" : "use dataverse twitter;\n" + plotTweetQuery.val(),
761 "marker_path" : "../img/mobile_green2.png",
762 "on_click_marker" : onClickTweetbookMapMarker,
763 "on_clean_result" : onCleanPlotTweetbook,
764 };
765
766 A.query(plotTweetQuery.val(), onTweetbookQuerySuccessPlot);
767}
768
769
770function onTweetbookQuerySuccessPlot (res) {
771
772 var records = res["results"];
773
774 var coordinates = [];
775 map_tweet_markers = [];
776 map_tweet_overlays = [];
777 drilldown_data_map = {};
778 drilldown_data_map_vals = {};
779
780 var micon = param_placeholder["marker_path"];
781 var marker_click_function = param_placeholder["on_click_marker"];
782 var clean_result_function = param_placeholder["on_clean_result"];
783
784 coordinates = clean_result_function(records);
785
786 for (var dm in coordinates) {
787 var keyLat = coordinates[dm].tweetLat.toString();
788 var keyLng = coordinates[dm].tweetLng.toString();
789 if (!drilldown_data_map.hasOwnProperty(keyLat)) {
790 drilldown_data_map[keyLat] = {};
791 }
792 if (!drilldown_data_map[keyLat].hasOwnProperty(keyLng)) {
793 drilldown_data_map[keyLat][keyLng] = [];
794 }
795 drilldown_data_map[keyLat][keyLng].push(coordinates[dm]);
796 drilldown_data_map_vals[coordinates[dm].tweetEntryId.toString()] = coordinates[dm];
797 }
798
799 $.each(drilldown_data_map, function(drillKeyLat, valuesAtLat) {
800 $.each(drilldown_data_map[drillKeyLat], function (drillKeyLng, valueAtLng) {
801
802 // Get subset of drilldown position on map
803 var cposition = new google.maps.LatLng(parseFloat(drillKeyLat), parseFloat(drillKeyLng));
804
805 // Create a marker using the snazzy phone icon
806 var map_tweet_m = new google.maps.Marker({
807 position: cposition,
808 map: map,
809 icon: micon,
810 clickable: true,
811 });
812
813 // Open Tweet exploration window on click
814 google.maps.event.addListener(map_tweet_m, 'click', function (event) {
815 marker_click_function(drilldown_data_map[drillKeyLat][drillKeyLng]);
816 });
817
818 // Add marker to index of tweets
819 map_tweet_markers.push(map_tweet_m);
820
821 });
822 });
823}
824
825
826function existsTweetbook(tweetbook) {
827 if (parseInt($.inArray(tweetbook, review_mode_tweetbooks)) == -1) {
828 return false;
829 } else {
830 return true;
831 }
832}
833
834
835function onCleanPlotTweetbook(records) {
836 var toPlot = [];
837 for (var entry in records) {
838 var tweetbook_element = {
839 "tweetEntryId" : parseInt(records[entry].split(",")[0].split(":")[1].split('"')[1]),
840 "tweetText" : records[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
841 "tweetLat" : parseFloat(records[entry].split("tweetLoc\": point(\"")[1].split(",")[0]),
842 "tweetLng" : parseFloat(records[entry].split("tweetLoc\": point(\"")[1].split(",")[1].split("\"")[0]),
843 "tweetComment" : records[entry].split("tweetCom\": \"")[1].split("\"")[0]
844 };
845 toPlot.push(tweetbook_element);
846 }
847
848 return toPlot;
849}
850
851function onCleanTweetbookDrilldown (rec) {
852
853 var drilldown_cleaned = [];
854
855 for (var entry = 0; entry < rec.length; entry++) {
856
857 var drill_element = {
858 "tweetEntryId" : parseInt(rec[entry].split(",")[0].split(":")[1].split('"')[1]),
859 "tweetText" : rec[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
860 "tweetLat" : parseFloat(rec[entry].split("tweetLoc\": point(\"")[1].split(",")[0]),
861 "tweetLng" : parseFloat(rec[entry].split("tweetLoc\": point(\"")[1].split(",")[1].split("\"")[0])
862 };
863 drilldown_cleaned.push(drill_element);
864 }
865 return drilldown_cleaned;
866}
867
868function onClickTweetbookMapMarker(tweet_arr) {
869 $('#drilldown_modal_body').html('');
870
871 // Clear existing display
872 $.each(tweet_arr, function (t, valueT) {
873 var tweet_obj = tweet_arr[t];
874 onDrillDownAtLocation(tweet_obj);
875 });
876
877 $('#drilldown_modal').modal('show');
878}
879
880/** Toggling Review and Explore Modes **/
881
882/**
883* Explore mode: Initial map creation and screen alignment
884*/
885function onOpenExploreMap () {
886 var explore_column_height = $('#explore-well').height();
887 $('#map_canvas').height(explore_column_height + "px");
888 $('#review-well').height(explore_column_height + "px");
889 $('#review-well').css('max-height', explore_column_height + "px");
890 var pad = $('#review-well').innerHeight() - $('#review-well').height();
891 var prev_window_target = $('#review-well').height() - 20 - $('#group-tweetbooks').innerHeight() - $('#group-background-query').innerHeight() - 2*pad;
892 $('#query-preview-window').height(prev_window_target +'px');
893}
894
895/**
896* Launching explore mode: clear windows/variables, show correct sidebar
897*/
898function onLaunchExploreMode() {
899 $('#review-active').removeClass('active');
900 $('#review-well').hide();
901
902 $('#explore-active').addClass('active');
903 $('#explore-well').show();
904
905 $("#clear-button").trigger("click");
906}
907
908/**
909* Launching review mode: clear windows/variables, show correct sidebar
910*/
911function onLaunchReviewMode() {
912 $('#explore-active').removeClass('active');
913 $('#explore-well').hide();
914 $('#review-active').addClass('active');
915 $('#review-well').show();
916
917 $("#clear-button").trigger("click");
918}
919
920/** Map Widget Utility Methods **/
921
922/**
923* Plots a legend onto the map, with values in progress bars
924* @param {number Array} breakpoints, an array of numbers representing natural breakpoints
925*/
926function mapControlWidgetAddLegend(breakpoints) {
927
928 // Retriever colors, lightest to darkest
929 var colors = mapWidgetGetColorPalette();
930
931 // Initial div structure
932 $("#map_canvas_legend").html('<div id="legend-holder"><div id="legend-progress-bar" class="progress"></div><span id="legend-label"></span></div>');
933
934 // Add color scale to legend
935 $('#legend-progress-bar').css("width", "200px").html('');
936
937 // Add a progress bar for each color
938 for (var color in colors) {
939
940 // Bar values
941 var upperBound = breakpoints[parseInt(color) + 1];
942
943 // Create Progress Bar
944 $('<div/>')
945 .attr("class", "bar")
946 .attr("id", "pbar" + color)
947 .css("width" , '25.0%')
948 .html("< " + upperBound)
949 .appendTo('#legend-progress-bar');
950
951 $('#pbar' + color).css({
952 "background-image" : 'none',
953 "background-color" : colors[parseInt(color)]
954 });
955
956 // Attach a message showing minimum bounds
957 $('#legend-label').html('Regions with at least ' + breakpoints[0] + ' tweets');
958 $('#legend-label').css({
959 "margin-top" : 0,
960 "color" : "black"
961 });
962 }
963
964 // Add legend to map
965 map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push(document.getElementById('legend-holder'));
966 $('#map_canvas_legend').show();
967}
968
969/**
970* Clears map elements - legend, plotted items, overlays
971*/
972function mapWidgetClearMap() {
973
974 if (selectionRect) {
975 selectionRect.setMap(null);
976 selectionRect = null;
977 }
978 for (c in map_cells) {
979 map_cells[c].setMap(null);
980 }
981 map_cells = [];
982 for (m in map_tweet_markers) {
983 map_tweet_markers[m].setMap(null);
984 }
985 map_tweet_markers = [];
986
987 // Remove legend from map
988 map.controls[google.maps.ControlPosition.LEFT_BOTTOM].clear();
989
990 // Reset map center and zoom
991 map.setCenter(new google.maps.LatLng(38.89, 77.03));
992 map.setZoom(4);
993}
994
995/**
996* Uses jenks algorithm in geostats library to find natural breaks in numeric data
997* @param {number Array} weights of points to plot
998* @returns {number Array} array of natural breakpoints, of which the top 4 subsets will be plotted
999*/
1000function mapWidgetLegendComputeNaturalBreaks(weights) {
1001 var plotDataWeights = new geostats(weights.sort());
1002 return plotDataWeights.getJenks(6).slice(2, 7);
1003}
1004
1005/**
1006* Computes values for map legend given a value and an array of jenks breakpoints
1007* @param {number} weight of point to plot on map
1008* @param {number Array} breakpoints, an array of 5 points corresponding to bounds of 4 natural ranges
1009* @returns {String} an RGB value corresponding to a subset of data
1010*/
1011function mapWidgetLegendGetHeatValue(weight, breakpoints) {
1012
1013 // Determine into which range the weight falls
1014 var weightColor = 0;
1015 if (weight >= breakpoints[3]) {
1016 weightColor = 3;
1017 } else if (weight >= breakpoints[2]) {
1018 weightColor = 2;
1019 } else if (weight >= breakpoints[1]) {
1020 weightColor = 1;
1021 }
1022
1023 // Get default map color palette
1024 var colorValues = mapWidgetGetColorPalette();
1025 return colorValues[weightColor];
1026}
1027
1028/**
1029* Returns an array containing a 4-color palette, lightest to darkest
1030* External palette source: http://www.colourlovers.com/palette/2763366/s_i_l_e_n_c_e_r
1031* @returns {Array} [colors]
1032*/
1033function mapWidgetGetColorPalette() {
1034 return [
1035 "rgb(115,189,158)",
1036 "rgb(74,142,145)",
1037 "rgb(19,93,96)",
1038 "rgb(7,51,46)"
1039 ];
1040}
1041
1042/**
1043* Computes radius for a given data point from a spatial cell
1044* @param {Object} keys => ["latSW" "lngSW" "latNE" "lngNE" "weight"]
1045* @returns {number} radius between 2 points in metres
1046*/
1047function mapWidgetComputeCircleRadius(spatialCell, breakpoints) {
1048
1049 var weight = spatialCell.weight;
1050 // Compute weight color
1051 var weightColor = 0.25;
1052 if (weight >= breakpoints[3]) {
1053 weightColor = 1.0;
1054 } else if (weight >= breakpoints[2]) {
1055 weightColor = 0.75;
1056 } else if (weight >= breakpoints[1]) {
1057 weightColor = 0.5;
1058 }
1059
1060 // Define Boundary Points
1061 var point_center = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1062 var point_left = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, spatialCell.lngSW);
1063 var point_top = new google.maps.LatLng(spatialCell.latNE, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1064
1065 // TODO not actually a weight color :)
1066 return weightColor * 1000 * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
1067}
1068
1069/** External Utility Methods **/
1070
1071/**
1072 * Calculates the distance between two latlng locations in km.
1073 * @see http://www.movable-type.co.uk/scripts/latlong.html
1074 *
1075 * @param {google.maps.LatLng} p1 The first lat lng point.
1076 * @param {google.maps.LatLng} p2 The second lat lng point.
1077 * @return {number} The distance between the two points in km.
1078 * @private
1079*/
1080function distanceBetweenPoints_(p1, p2) {
1081 if (!p1 || !p2) {
1082 return 0;
1083 }
1084
1085 var R = 6371; // Radius of the Earth in km
1086 var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
1087 var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
1088 var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
1089 Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
1090 Math.sin(dLon / 2) * Math.sin(dLon / 2);
1091 var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1092 var d = R * c;
1093 return d;
1094};