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