blob: 2d4225185af70af0bf58ecd8ab711f85f8c03f41 [file] [log] [blame]
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001$(function() {
2
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -07003 // Initialize connection to AsterixDB. Just one connection is needed and contains
4 // logic for connecting to each API endpoint. This object A is reused throughout the
5 // code but does not store information about any individual API call.
6 A = new AsterixDBConnection({
7
8 // We will be using the twitter dataverse, which we can configure either like this
9 // or by following our AsterixDBConnection with a dataverse call, like so:
10 // A = new AsterixDBConnection().dataverse("twitter");
11 "dataverse" : "twitter",
12
13 // Due to the setup of this demo using the Bottle server, it is necessary to change the
14 // default endpoint of API calls. The proxy server handles the call to http://localhost:19002
15 // for us, and we reconfigure this connection to connect to the proxy server.
16 "endpoint_root" : "/",
17
18 // Finally, we want to make our error function nicer so that we show errors with a call to the
19 // reportUserMessage function. Update the "error" property to do that.
20 "error" : function(data) {
21 // For an error, data will look like this:
22 // {
23 // "error-code" : [error-number, error-text]
24 // "stacktrace" : ...stack trace...
25 // "summary" : ...summary of error...
26 // }
27 // We will report this as an Asterix REST API Error, an error code, and a reason message.
28 // Note the method signature: reportUserMessage(message, isPositiveMessage, target). We will provide
29 // an error message to display, a positivity value (false in this case, errors are bad), and a
30 // target html element in which to report the message.
31 var showErrorMessage = "Asterix Error #" + data["error-code"][0] + ": " + data["error-code"][1];
32 var isPositive = false;
33 var showReportAt = "report-message";
34
35 reportUserMessage(showErrorMessage, isPositive, showReportAt);
36 }
37 });
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070038
39 // Following this is some stuff specific to the Black Cherry demo
40 // This is not necessary for working with AsterixDB
41 APIqueryTracker = {};
42 drilldown_data_map = {};
43 drilldown_data_map_vals = {};
44 asyncQueryManager = {};
45
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -070046 // Populate review mode tweetbooks
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070047 review_mode_tweetbooks = [];
48 review_mode_handles = [];
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -070049 getAllDataverseTweetbooks();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070050
51 map_cells = [];
52 map_tweet_markers = [];
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -070053 map_info_windows = {};
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070054
55 // UI Elements - Modals & perspective tabs
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -070056 $('#drilldown_modal').modal('hide');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070057 $('#explore-mode').click( onLaunchExploreMode );
58 $('#review-mode').click( onLaunchReviewMode );
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -070059 $('#about-mode').click(onLaunchAboutMode);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070060
61 // UI Elements - A button to clear current map and query data
62 $("#clear-button").button().click(function () {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -070063 mapWidgetResetMap();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070064
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -070065 $('#report-message').html('');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070066 $('#query-preview-window').html('');
67 $("#metatweetzone").html('');
68 });
69
70 // UI Elements - Query setup
71 $("#selection-button").button('toggle');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070072
73 // UI Element - Grid sliders
74 var updateSliderDisplay = function(event, ui) {
75 if (event.target.id == "grid-lat-slider") {
76 $("#gridlat").text(""+ui.value);
77 } else {
78 $("#gridlng").text(""+ui.value);
79 }
80 };
81
82 sliderOptions = {
83 max: 10,
84 min: 1.5,
85 step: .1,
86 value: 2.0,
87 slidechange: updateSliderDisplay,
88 slide: updateSliderDisplay,
89 start: updateSliderDisplay,
90 stop: updateSliderDisplay
91 };
92
93 $("#gridlat").text(""+sliderOptions.value);
94 $("#gridlng").text(""+sliderOptions.value);
95 $(".grid-slider").slider(sliderOptions);
96
97 // UI Elements - Date Pickers
98 var dateOptions = {
99 dateFormat: "yy-mm-dd",
100 defaultDate: "2012-01-02",
101 navigationAsDateFormat: true,
102 constrainInput: true
103 };
104 var start_dp = $("#start-date").datepicker(dateOptions);
105 start_dp.val(dateOptions.defaultDate);
106 dateOptions['defaultDate'] = "2012-12-31";
107 var end_dp= $("#end-date").datepicker(dateOptions);
108 end_dp.val(dateOptions.defaultDate);
109
110 // This little bit of code manages period checks of the asynchronous query manager,
111 // which holds onto handles asynchornously received. We can set the handle update
112 // frequency using seconds, and it will let us know when it is ready.
113 var intervalID = setInterval(
114 function() {
115 asynchronousQueryIntervalUpdate();
116 },
117 asynchronousQueryGetInterval()
118 );
119
120 // UI Elements - Creates map and location auto-complete
121 onOpenExploreMap();
122 var mapOptions = {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700123 center: new google.maps.LatLng(38.89, -77.03),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700124 zoom: 4,
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700125 mapTypeId: google.maps.MapTypeId.ROADMAP,
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700126 streetViewControl: false,
127 draggable : false
128 };
129 map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions);
130
131 var input = document.getElementById('location-text-box');
132 var autocomplete = new google.maps.places.Autocomplete(input);
133 autocomplete.bindTo('bounds', map);
134
135 google.maps.event.addListener(autocomplete, 'place_changed', function() {
136 var place = autocomplete.getPlace();
137 if (place.geometry.viewport) {
138 map.fitBounds(place.geometry.viewport);
139 } else {
140 map.setCenter(place.geometry.location);
141 map.setZoom(17); // Why 17? Because it looks good.
142 }
143 var address = '';
144 if (place.address_components) {
145 address = [(place.address_components[0] && place.address_components[0].short_name || ''),
146 (place.address_components[1] && place.address_components[1].short_name || ''),
147 (place.address_components[2] && place.address_components[2].short_name || '') ].join(' ');
148 }
149 });
150
151 // UI Elements - Selection Rectangle Drawing
152 shouldDraw = false;
153 var startLatLng;
154 selectionRect = null;
155 var selectionRadio = $("#selection-button");
156 var firstClick = true;
157
158 google.maps.event.addListener(map, 'mousedown', function (event) {
159 // only allow drawing if selection is selected
160 if (selectionRadio.hasClass("active")) {
161 startLatLng = event.latLng;
162 shouldDraw = true;
163 }
164 });
165
166 google.maps.event.addListener(map, 'mousemove', drawRect);
167 function drawRect (event) {
168 if (shouldDraw) {
169 if (!selectionRect) {
170 var selectionRectOpts = {
171 bounds: new google.maps.LatLngBounds(startLatLng, event.latLng),
172 map: map,
173 strokeWeight: 1,
174 strokeColor: "2b3f8c",
175 fillColor: "2b3f8c"
176 };
177 selectionRect = new google.maps.Rectangle(selectionRectOpts);
178 google.maps.event.addListener(selectionRect, 'mouseup', function () {
179 shouldDraw = false;
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700180 });
181 } else {
genia.likes.science@gmail.com4ada78c2013-09-07 13:53:48 -0700182
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700183 if (startLatLng.lng() < event.latLng.lng()) {
184 selectionRect.setBounds(new google.maps.LatLngBounds(startLatLng, event.latLng));
185 } else {
186 selectionRect.setBounds(new google.maps.LatLngBounds(event.latLng, startLatLng));
187 }
188 }
189 }
190 };
191
192 // UI Elements - Toggle location search style by location or by map selection
193 $('#selection-button').on('click', function (e) {
194 $("#location-text-box").attr("disabled", "disabled");
195 if (selectionRect) {
196 selectionRect.setMap(map);
197 }
198 });
199 $('#location-button').on('click', function (e) {
200 $("#location-text-box").removeAttr("disabled");
201 if (selectionRect) {
202 selectionRect.setMap(null);
203 }
204 });
205
206 // UI Elements - Tweetbook Management
207 $('.dropdown-menu a.holdmenu').click(function(e) {
208 e.stopPropagation();
209 });
210
211 $('#new-tweetbook-button').on('click', function (e) {
212 onCreateNewTweetBook($('#new-tweetbook-entry').val());
213
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -0700214 $('#new-tweetbook-entry').val("");
215 $('#new-tweetbook-entry').attr("placeholder", "Name a new tweetbook");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700216 });
217
218 // UI Element - Query Submission
219 $("#submit-button").button().click(function () {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700220
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700221 var kwterm = $("#keyword-textbox").val();
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700222 if (kwterm == "") {
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -0700223 reportUserMessage("Please enter a search term!", false, "report-message");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700224 } else {
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700225
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -0700226 $("#report-message").html('');
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700227 $("#submit-button").attr("disabled", true);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700228
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700229 var startdp = $("#start-date").datepicker("getDate");
230 var enddp = $("#end-date").datepicker("getDate");
231 var startdt = $.datepicker.formatDate("yy-mm-dd", startdp)+"T00:00:00Z";
232 var enddt = $.datepicker.formatDate("yy-mm-dd", enddp)+"T23:59:59Z";
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700233
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700234 var formData = {
235 "keyword": kwterm,
236 "startdt": startdt,
237 "enddt": enddt,
238 "gridlat": $("#grid-lat-slider").slider("value"),
239 "gridlng": $("#grid-lng-slider").slider("value")
240 };
241
242 // Get Map Bounds
243 var bounds;
244 if ($('#selection-button').hasClass("active") && selectionRect) {
245 bounds = selectionRect.getBounds();
246 } else {
247 bounds = map.getBounds();
248 }
249
250 var swLat = Math.abs(bounds.getSouthWest().lat());
251 var swLng = Math.abs(bounds.getSouthWest().lng());
252 var neLat = Math.abs(bounds.getNorthEast().lat());
253 var neLng = Math.abs(bounds.getNorthEast().lng());
254
255 formData["swLat"] = Math.min(swLat, neLat);
256 formData["swLng"] = Math.max(swLng, neLng);
257 formData["neLat"] = Math.max(swLat, neLat);
258 formData["neLng"] = Math.min(swLng, neLng);
259
260 var build_cherry_mode = "synchronous";
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700261
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700262 if ($('#asbox').is(":checked")) {
263 build_cherry_mode = "asynchronous";
264 $('#show-query-button').attr("disabled", false);
265 } else {
266 $('#show-query-button').attr("disabled", true);
267 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700268
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700269 var f = buildAQLQueryFromForm(formData);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700270
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700271 APIqueryTracker = {
272 "query" : "use dataverse twitter;\n" + f.val(),
273 "data" : formData
274 };
genia.likes.science@gmail.com233fe972013-09-07 13:57:46 -0700275
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700276 // TODO Make dialog work correctly.
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700277 //$('#dialog').html(APIqueryTracker["query"]);
genia.likes.science@gmail.com233fe972013-09-07 13:57:46 -0700278
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700279 if (build_cherry_mode == "synchronous") {
280 A.query(f.val(), cherryQuerySyncCallback, build_cherry_mode);
281 } else {
282 A.query(f.val(), cherryQueryAsyncCallback, build_cherry_mode);
283 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700284
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700285 // Clears selection rectangle on query execution, rather than waiting for another clear call.
286 if (selectionRect) {
287 selectionRect.setMap(null);
288 selectionRect = null;
289 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700290 }
291 });
292});
293
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700294function buildAQLQueryFromForm(parameters) {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700295
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700296 var bounds = {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700297 "ne" : { "lat" : parameters["neLat"], "lng" : -1*parameters["neLng"]},
298 "sw" : { "lat" : parameters["swLat"], "lng" : -1*parameters["swLng"]}
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700299 };
300
301 var rectangle =
302 new FunctionExpression("create-rectangle",
303 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
304 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
305
306
307 var aql = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700308 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700309 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
310 .LetClause("$region", rectangle)
311 .WhereClause().and(
312 new FunctionExpression("spatial-intersect", "$t.sender-location", "$region"),
313 new AExpression('$t.send-time > datetime("' + parameters["startdt"] + '")'),
314 new AExpression('$t.send-time < datetime("' + parameters["enddt"] + '")'),
315 new FunctionExpression("contains", "$t.message-text", "$keyword")
316 )
317 .GroupClause(
318 "$c",
319 new FunctionExpression("spatial-cell", "$t.sender-location",
320 new FunctionExpression("create-point", "24.5", "-125.5"),
321 parameters["gridlat"].toFixed(1), parameters["gridlng"].toFixed(1)),
322 "with",
323 "$t"
324 )
325 .ReturnClause({ "cell" : "$c", "count" : "count($t)" });
326
327 return aql;
328}
329
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700330/**
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700331* getAllDataverseTweetbooks
332*
333* no params
334*
335* Returns all datasets of type TweetbookEntry, populates review_mode_tweetbooks
336*/
337function getAllDataverseTweetbooks(fn_tweetbooks) {
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700338
339 // This creates a query to the Metadata for datasets of type
340 // TweetBookEntry. Note that if we throw in a WhereClause (commented out below)
341 // there is an odd error. This is being fixed and will be removed from this demo.
342 var getTweetbooksQuery = new FLWOGRExpression()
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700343 .ForClause("$ds", new AExpression("dataset Metadata.Dataset"))
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700344 //.WhereClause(new AExpression('$ds.DataTypeName = "TweetbookEntry"'))
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700345 .ReturnClause({
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700346 "DataTypeName" : "$ds.DataTypeName",
347 "DatasetName" : "$ds.DatasetName"
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700348 });
349
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700350 // Now create a function that will be called when tweetbooks succeed.
351 // In this case, we want to parse out the results object from the Asterix
352 // REST API response.
353 var tweetbooksSuccess = function(r) {
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700354 // Parse tweetbook metadata results
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700355 $.each(r.results, function(i, data) {
356 if ($.parseJSON(data)["DataTypeName"] == "TweetbookEntry") {
357 review_mode_tweetbooks.push($.parseJSON(data)["DatasetName"]);
358 }
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700359 });
360
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700361 // Now, if any tweetbooks already exist, opulate review screen.
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700362 $('#review-tweetbook-titles').html('');
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700363 $.each(review_mode_tweetbooks, function(i, tweetbook) {
364 addTweetBookDropdownItem(tweetbook);
365 });
366 };
367
368 // Now, we are ready to run a query.
369 A.meta(getTweetbooksQuery.val(), tweetbooksSuccess);
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700370
371}
372
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700373/** Asynchronous Query Management **/
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700374
375/**
376* Checks through each asynchronous query to see if they are ready yet
377*/
378function asynchronousQueryIntervalUpdate() {
379 for (var handle_key in asyncQueryManager) {
380 if (!asyncQueryManager[handle_key].hasOwnProperty("ready")) {
381 asynchronousQueryGetAPIQueryStatus( asyncQueryManager[handle_key]["handle"], handle_key );
382 }
383 }
384}
385
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700386/**
387* Returns current time interval to check for asynchronous query readiness
388* @returns {number} milliseconds between asychronous query checks
389*/
390function asynchronousQueryGetInterval() {
391 var seconds = 10;
392 return seconds * 1000;
393}
394
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700395/**
396* Retrieves status of an asynchronous query, using an opaque result handle from API
397* @param {Object} handle, an object previously returned from an async call
398* @param {number} handle_id, the integer ID parsed from the handle object
399*/
400function asynchronousQueryGetAPIQueryStatus (handle, handle_id) {
401
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700402 A.query_status(
403 {
404 "handle" : JSON.stringify(handle)
405 },
406 function (res) {
407 if (res["status"] == "SUCCESS") {
408 // We don't need to check if this one is ready again, it's not going anywhere...
409 // Unless the life cycle of handles has changed drastically
410 asyncQueryManager[handle_id]["ready"] = true;
411
412 // Indicate success.
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700413 $('#handle_' + handle_id).removeClass("btn-disabled").prop('disabled', false).addClass("btn-success");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700414 }
415 }
416 );
417}
418
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700419/**
420* On-success callback after async API query
421* @param {object} res, a result object containing an opaque result handle to Asterix
422*/
423function cherryQueryAsyncCallback(res) {
424
425 // Parse handle, handle id and query from async call result
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700426 var handle_query = APIqueryTracker["query"];
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700427 var handle = res;
428 var handle_id = res["handle"].toString().split(',')[0];
429
430 // Add to stored map of existing handles
431 asyncQueryManager[handle_id] = {
432 "handle" : handle,
433 "query" : handle_query,
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700434 "data" : APIqueryTracker["data"]
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700435 };
436
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700437 // Create a container for this async query handle
438 $('<div/>')
439 .css("margin-left", "1em")
440 .css("margin-bottom", "1em")
441 .css("display", "block")
442 .attr({
443 "class" : "btn-group",
444 "id" : "async_container_" + handle_id
445 })
446 .appendTo("#async-handle-controls");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700447
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700448 // Adds the main button for this async handle
449 var handle_action_button = '<button class="btn btn-disabled" id="handle_' + handle_id + '">Handle ' + handle_id + '</button>';
450 $('#async_container_' + handle_id).append(handle_action_button);
451 $('#handle_' + handle_id).prop('disabled', true);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700452 $('#handle_' + handle_id).on('click', function (e) {
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700453
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700454 // make sure query is ready to be run
455 if (asyncQueryManager[handle_id]["ready"]) {
456
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700457 APIqueryTracker = {
458 "query" : asyncQueryManager[handle_id]["query"],
459 "data" : asyncQueryManager[handle_id]["data"]
460 };
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700461 // TODO
462 //$('#dialog').html(APIqueryTracker["query"]);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700463
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -0700464 if (!asyncQueryManager[handle_id].hasOwnProperty("result")) {
465 // Generate new Asterix Core API Query
466 A.query_result(
467 { "handle" : JSON.stringify(asyncQueryManager[handle_id]["handle"]) },
468 function(res) {
469 asyncQueryManager[handle_id]["result"] = res;
470 cherryQuerySyncCallback(res);
471 }
472 );
473 } else {
474 cherryQuerySyncCallback(asyncQueryManager[handle_id]["result"]);
475 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700476 }
477 });
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700478
479 // Adds a removal button for this async handle
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700480 var asyncDeleteButton = addDeleteButton(
481 "trashhandle_" + handle_id,
482 "async_container_" + handle_id,
483 function (e) {
484 $('#async_container_' + handle_id).remove();
485 delete asyncQueryManager[handle_id];
486 }
487 );
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700488
489 $('#async_container_' + handle_id).append('<br/>');
490
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700491 $("#submit-button").attr("disabled", false);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700492}
493
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700494/**
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700495* returns a json object with keys: weight, latSW, lngSW, latNE, lngNE
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700496*
497* { "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 -0700498*/
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700499
500/**
501* cleanJSON
502*
503* @param json, a JSON string that is not correctly formatted.
504*
505* Quick and dirty little function to clean up an Asterix JSON quirk.
506*/
507function cleanJSON(json) {
508 return json
509 .replace("rectangle", '"rectangle"')
510 .replace("point:", '"point":')
511 .replace("point:", '"point":')
512 .replace("int64", '"int64"');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700513}
514
515/**
516* A spatial data cleaning and mapping call
517* @param {Object} res, a result object from a cherry geospatial query
518*/
519function cherryQuerySyncCallback(res) {
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700520
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700521 // Initialize coordinates and weights, to store
522 // coordinates of map cells and their weights
523 // TODO these are all included in coordinates already...
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700524 var coordinates = [];
525 var weights = [];
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700526 var al = 1;
527
528 // Parse resulting JSON objects. Here is an example record:
529 // { "cell": { rectangle: [{ point: [22.5, 64.5]}, { point: [24.5, 66.5]}]}, "count": { int64: 5 }}
530 $.each(res.results, function(i, data) {
531
532 // First, parse a JSON object from a cleaned up string.
533 var record = $.parseJSON(cleanJSON(data));
534
535 // Parse Coordinates and Weights into a record
536 var sw = record.cell.rectangle[0].point;
537 var ne = record.cell.rectangle[1].point;
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700538
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700539 var coordinate = {
540 "latSW" : sw[0],
541 "lngSW" : sw[1],
542 "latNE" : ne[0],
543 "lngNE" : ne[1],
544 "weight" : record.count.int64
545 }
546
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700547 weights.push(coordinate["weight"]);
548 coordinates.push(coordinate);
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700549 });
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700550
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700551 triggerUIUpdate(coordinates, weights);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700552}
553
554/**
555* Triggers a map update based on a set of spatial query result cells
556* @param [Array] mapPlotData, an array of coordinate and weight objects
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700557* @param [Array] plotWeights, a list of weights of the spatial cells - e.g., number of tweets
558*/
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700559function triggerUIUpdate(mapPlotData, plotWeights) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700560 /** Clear anything currently on the map **/
561 mapWidgetClearMap();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700562
563 // Compute data point spread
564 var dataBreakpoints = mapWidgetLegendComputeNaturalBreaks(plotWeights);
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -0700565
566 map_info_windows = {};
567
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700568 $.each(mapPlotData, function (m, val) {
569
570 // Only map points in data range of top 4 natural breaks
571 if (mapPlotData[m].weight > dataBreakpoints[0]) {
572
573 // Get color value of legend
574 var mapColor = mapWidgetLegendGetHeatValue(mapPlotData[m].weight, dataBreakpoints);
575 var markerRadius = mapWidgetComputeCircleRadius(mapPlotData[m], dataBreakpoints);
576 var point_opacity = 1.0;
577
578 var point_center = new google.maps.LatLng(
579 (mapPlotData[m].latSW + mapPlotData[m].latNE)/2.0,
580 (mapPlotData[m].lngSW + mapPlotData[m].lngNE)/2.0);
581
582 // Create and plot marker
583 var map_circle_options = {
584 center: point_center,
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -0700585 anchorPoint: point_center,
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700586 radius: markerRadius,
587 map: map,
588 fillOpacity: point_opacity,
589 fillColor: mapColor,
590 clickable: true
591 };
592 var map_circle = new google.maps.Circle(map_circle_options);
593 map_circle.val = mapPlotData[m];
genia.likes.science@gmail.comec46c772013-09-07 18:13:00 -0700594
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -0700595 map_info_windows[m] = new google.maps.InfoWindow({
Eugenia Gabrielova78ba1ab2013-10-18 17:38:19 -0700596 content: mapPlotData[m].weight + " tweets",
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -0700597 position: point_center
genia.likes.science@gmail.comec46c772013-09-07 18:13:00 -0700598 });
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700599
genia.likes.science@gmail.comec46c772013-09-07 18:13:00 -0700600 // Clicking on a circle drills down map to that value, hovering over it displays a count
601 // of tweets at that location.
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700602 google.maps.event.addListener(map_circle, 'click', function (event) {
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700603 $.each(map_info_windows, function(i) {
604 map_info_windows[i].close();
605 });
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700606 onMapPointDrillDown(map_circle.val);
607 });
608
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -0700609 google.maps.event.addListener(map_circle, 'mouseover', function(event) {
610 if (!map_info_windows[m].getMap()) {
611 map_info_windows[m].setPosition(map_circle.center);
612 map_info_windows[m].open(map);
613 }
genia.likes.science@gmail.comec46c772013-09-07 18:13:00 -0700614 });
615
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700616 // Add this marker to global marker cells
617 map_cells.push(map_circle);
618 }
619 });
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700620}
621
622/**
623* prepares an Asterix API query to drill down in a rectangular spatial zone
624*
625* @params {object} marker_borders [LEGACY] a set of bounds for a region from a previous api result
626*/
627function onMapPointDrillDown(marker_borders) {
628 var zoneData = APIqueryTracker["data"];
629
630 var zswBounds = new google.maps.LatLng(marker_borders.latSW, marker_borders.lngSW);
631 var zneBounds = new google.maps.LatLng(marker_borders.latNE, marker_borders.lngNE);
632
633 var zoneBounds = new google.maps.LatLngBounds(zswBounds, zneBounds);
634 zoneData["swLat"] = zoneBounds.getSouthWest().lat();
635 zoneData["swLng"] = zoneBounds.getSouthWest().lng();
636 zoneData["neLat"] = zoneBounds.getNorthEast().lat();
637 zoneData["neLng"] = zoneBounds.getNorthEast().lng();
638 var zB = {
639 "sw" : {
640 "lat" : zoneBounds.getSouthWest().lat(),
641 "lng" : zoneBounds.getSouthWest().lng()
642 },
643 "ne" : {
644 "lat" : zoneBounds.getNorthEast().lat(),
645 "lng" : zoneBounds.getNorthEast().lng()
646 }
647 };
648
649 mapWidgetClearMap();
650
651 var customBounds = new google.maps.LatLngBounds();
652 var zoomSWBounds = new google.maps.LatLng(zoneData["swLat"], zoneData["swLng"]);
653 var zoomNEBounds = new google.maps.LatLng(zoneData["neLat"], zoneData["neLng"]);
654 customBounds.extend(zoomSWBounds);
655 customBounds.extend(zoomNEBounds);
656 map.fitBounds(customBounds);
657
658 var df = getDrillDownQuery(zoneData, zB);
659
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700660 APIqueryTracker = {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700661 "query_string" : "use dataverse twitter;\n" + df.val(),
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700662 "marker_path" : "static/img/mobile2.png",
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700663 "on_clean_result" : onCleanTweetbookDrilldown,
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700664 };
665
666 A.query(df.val(), onTweetbookQuerySuccessPlot);
667}
668
669function getDrillDownQuery(parameters, bounds) {
670
671 var zoomRectangle = new FunctionExpression("create-rectangle",
672 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
673 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
674
675 var drillDown = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700676 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700677 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
678 .LetClause("$region", zoomRectangle)
679 .WhereClause().and(
680 new FunctionExpression('spatial-intersect', '$t.sender-location', '$region'),
681 new AExpression().set('$t.send-time > datetime("' + parameters["startdt"] + '")'),
682 new AExpression().set('$t.send-time < datetime("' + parameters["enddt"] + '")'),
683 new FunctionExpression('contains', '$t.message-text', '$keyword')
684 )
685 .ReturnClause({
686 "tweetId" : "$t.tweetid",
687 "tweetText" : "$t.message-text",
688 "tweetLoc" : "$t.sender-location"
689 });
690
691 return drillDown;
692}
693
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700694function onDrillDownAtLocation(tO) {
695
696 var tweetId = tO["tweetEntryId"];
697 var tweetText = tO["tweetText"];
698
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700699 // First, set tweet in drilldown modal to be this tweet's text
700 $('#modal-body-tweet').html('Tweet #' + tweetId + ": " + tweetText);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700701
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700702 // Next, empty any leftover tweetbook comments or error/success messages
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700703 $("#modal-body-add-to").val('');
704 $("#modal-body-add-note").val('');
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700705 $("#modal-body-message-holder").html("");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700706
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700707 // Next, if there is an existing tweetcomment reported, show it.
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700708 if (tO.hasOwnProperty("tweetComment")) {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700709
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700710 // Show correct panel
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700711 $("#modal-existing-note").show();
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700712 $("#modal-save-tweet-panel").hide();
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700713
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700714 // Fill in existing tweet comment
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700715 $("#modal-body-tweet-note").val(tO["tweetComment"]);
716
717 // Change Tweetbook Badge
718 $("#modal-current-tweetbook").val(APIqueryTracker["active_tweetbook"]);
719
720 // Add deletion functionality
721 $("#modal-body-trash-icon").on('click', function () {
722 // Send comment deletion to asterix
723 var deleteTweetCommentOnId = '"' + tweetId + '"';
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700724 var toDelete = new DeleteStatement(
725 "$mt",
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700726 APIqueryTracker["active_tweetbook"],
727 new AExpression("$mt.tweetid = " + deleteTweetCommentOnId.toString())
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700728 );
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700729 A.update(
730 toDelete.val()
731 );
732
733 // Hide comment from map
734 $('#drilldown_modal').modal('hide');
735
736 // Replot tweetbook
737 onPlotTweetbook(APIqueryTracker["active_tweetbook"]);
738 });
739
740 } else {
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700741 // Show correct panel
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700742 $("#modal-existing-note").hide();
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700743 $("#modal-save-tweet-panel").show();
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700744
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700745 // Now, when adding a comment on an available tweet to a tweetbook
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -0700746 $('#save-comment-tweetbook-modal').unbind('click');
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700747 $("#save-comment-tweetbook-modal").on('click', function(e) {
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700748
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700749 // Stuff to save about new comment
750 var save_metacomment_target_tweetbook = $("#modal-body-add-to").val();
751 var save_metacomment_target_comment = '"' + $("#modal-body-add-note").val() + '"';
752 var save_metacomment_target_tweet = '"' + tweetId + '"';
753
754 // Make sure content is entered, and then save this comment.
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700755 if ($("#modal-body-add-note").val() == "") {
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -0700756
757 reportUserMessage("Please enter a comment about the tweet", false, "report-message");
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700758
759 } else if ($("#modal-body-add-to").val() == "") {
760
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -0700761 reportUserMessage("Please enter a tweetbook.", false, "report-message");
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700762
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700763 } else {
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700764
765 // Check if tweetbook exists. If not, create it.
766 if (!(existsTweetbook(save_metacomment_target_tweetbook))) {
767 onCreateNewTweetBook(save_metacomment_target_tweetbook);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700768 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700769
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700770 var toInsert = new InsertStatement(
771 save_metacomment_target_tweetbook,
772 {
773 "tweetid" : save_metacomment_target_tweet.toString(),
774 "comment-text" : save_metacomment_target_comment
775 }
776 );
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -0700777
778 A.update(toInsert.val(), function () {
779 var successMessage = "Saved comment on <b>Tweet #" + tweetId +
780 "</b> in dataset <b>" + save_metacomment_target_tweetbook + "</b>.";
781 reportUserMessage(successMessage, true, "report-message");
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700782
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -0700783 $("#modal-body-add-to").val('');
784 $("#modal-body-add-note").val('');
785 $('#save-comment-tweetbook-modal').unbind('click');
786
787 // Close modal
788 $('#drilldown_modal').modal('hide');
789 });
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700790 }
791 });
792 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700793}
794
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700795/**
796* Adds a new tweetbook entry to the menu and creates a dataset of type TweetbookEntry.
797*/
798function onCreateNewTweetBook(tweetbook_title) {
799
800 var tweetbook_title = tweetbook_title.split(' ').join('_');
801
802 A.ddl(
803 "create dataset " + tweetbook_title + "(TweetbookEntry) primary key tweetid;",
804 function () {}
805 );
806
807 if (!(existsTweetbook(tweetbook_title))) {
808 review_mode_tweetbooks.push(tweetbook_title);
809 addTweetBookDropdownItem(tweetbook_title);
810 }
811}
812
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700813function onDropTweetBook(tweetbook_title) {
814
815 // AQL Call
816 A.ddl(
817 "drop dataset " + tweetbook_title + " if exists;",
818 function () {}
819 );
820
821 // Removes tweetbook from review_mode_tweetbooks
822 var remove_position = $.inArray(tweetbook_title, review_mode_tweetbooks);
823 if (remove_position >= 0) review_mode_tweetbooks.splice(remove_position, 1);
824
825 // Clear UI with review tweetbook titles
826 $('#review-tweetbook-titles').html('');
827 for (r in review_mode_tweetbooks) {
828 addTweetBookDropdownItem(review_mode_tweetbooks[r]);
829 }
830}
831
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700832function addTweetBookDropdownItem(tweetbook) {
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -0700833 // Add placeholder for this tweetbook
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700834 $('<div/>')
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700835 .attr({
836 "class" : "btn-group",
837 "id" : "rm_holder_" + tweetbook
838 }).appendTo("#review-tweetbook-titles");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700839
840 // Add plotting button for this tweetbook
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -0700841 var plot_button = '<button class="btn btn-default" id="rm_plotbook_' + tweetbook + '">' + tweetbook + '</button>';
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700842 $("#rm_holder_" + tweetbook).append(plot_button);
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -0700843 $("#rm_plotbook_" + tweetbook).width("200px");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700844 $("#rm_plotbook_" + tweetbook).on('click', function(e) {
845 onPlotTweetbook(tweetbook);
846 });
847
848 // Add trash button for this tweetbook
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700849 var onTrashTweetbookButton = addDeleteButton(
850 "rm_trashbook_" + tweetbook,
851 "rm_holder_" + tweetbook,
852 function(e) {
853 onDropTweetBook(tweetbook);
854 }
855 );
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700856}
857
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700858function onPlotTweetbook(tweetbook) {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700859
860 // Clear map for this one
861 mapWidgetResetMap();
862
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700863 var plotTweetQuery = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700864 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700865 .ForClause("$m", new AExpression("dataset " + tweetbook))
866 .WhereClause(new AExpression("$m.tweetid = $t.tweetid"))
867 .ReturnClause({
868 "tweetId" : "$m.tweetid",
869 "tweetText" : "$t.message-text",
870 "tweetLoc" : "$t.sender-location",
871 "tweetCom" : "$m.comment-text"
872 });
873
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700874 APIqueryTracker = {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700875 "query_string" : "use dataverse twitter;\n" + plotTweetQuery.val(),
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700876 "marker_path" : "static/img/mobile_green2.png",
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700877 "on_clean_result" : onCleanPlotTweetbook,
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700878 "active_tweetbook" : tweetbook
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700879 };
880
881 A.query(plotTweetQuery.val(), onTweetbookQuerySuccessPlot);
882}
883
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700884function onTweetbookQuerySuccessPlot (res) {
885
886 var records = res["results"];
887
888 var coordinates = [];
889 map_tweet_markers = [];
890 map_tweet_overlays = [];
891 drilldown_data_map = {};
892 drilldown_data_map_vals = {};
893
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700894 var micon = APIqueryTracker["marker_path"];
895 var marker_click_function = onClickTweetbookMapMarker;
896 var clean_result_function = APIqueryTracker["on_clean_result"];
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700897
898 coordinates = clean_result_function(records);
899
900 for (var dm in coordinates) {
901 var keyLat = coordinates[dm].tweetLat.toString();
902 var keyLng = coordinates[dm].tweetLng.toString();
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700903
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700904 if (!drilldown_data_map.hasOwnProperty(keyLat)) {
905 drilldown_data_map[keyLat] = {};
906 }
907 if (!drilldown_data_map[keyLat].hasOwnProperty(keyLng)) {
908 drilldown_data_map[keyLat][keyLng] = [];
909 }
910 drilldown_data_map[keyLat][keyLng].push(coordinates[dm]);
911 drilldown_data_map_vals[coordinates[dm].tweetEntryId.toString()] = coordinates[dm];
912 }
913
914 $.each(drilldown_data_map, function(drillKeyLat, valuesAtLat) {
915 $.each(drilldown_data_map[drillKeyLat], function (drillKeyLng, valueAtLng) {
916
917 // Get subset of drilldown position on map
918 var cposition = new google.maps.LatLng(parseFloat(drillKeyLat), parseFloat(drillKeyLng));
919
920 // Create a marker using the snazzy phone icon
921 var map_tweet_m = new google.maps.Marker({
922 position: cposition,
923 map: map,
924 icon: micon,
925 clickable: true,
926 });
927
928 // Open Tweet exploration window on click
929 google.maps.event.addListener(map_tweet_m, 'click', function (event) {
930 marker_click_function(drilldown_data_map[drillKeyLat][drillKeyLng]);
931 });
932
933 // Add marker to index of tweets
934 map_tweet_markers.push(map_tweet_m);
935
936 });
937 });
938}
939
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700940function existsTweetbook(tweetbook) {
941 if (parseInt($.inArray(tweetbook, review_mode_tweetbooks)) == -1) {
942 return false;
943 } else {
944 return true;
945 }
946}
947
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700948function onCleanPlotTweetbook(records) {
949 var toPlot = [];
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700950
951 // An entry looks like this:
952 // { "tweetId": "273589", "tweetText": " like verizon the network is amazing", "tweetLoc": { point: [37.78, 82.27]}, "tweetCom": "hooray comments" }
953
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700954 for (var entry in records) {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700955
956 var points = records[entry].split("point:")[1].match(/[-+]?[0-9]*\.?[0-9]+/g);
957
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700958 var tweetbook_element = {
959 "tweetEntryId" : parseInt(records[entry].split(",")[0].split(":")[1].split('"')[1]),
960 "tweetText" : records[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700961 "tweetLat" : parseFloat(points[0]),
962 "tweetLng" : parseFloat(points[1]),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700963 "tweetComment" : records[entry].split("tweetCom\": \"")[1].split("\"")[0]
964 };
965 toPlot.push(tweetbook_element);
966 }
967
968 return toPlot;
969}
970
971function onCleanTweetbookDrilldown (rec) {
972
973 var drilldown_cleaned = [];
974
975 for (var entry = 0; entry < rec.length; entry++) {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700976
977 // An entry looks like this:
978 // { "tweetId": "105491", "tweetText": " hate verizon its platform is OMG", "tweetLoc": { point: [30.55, 71.44]} }
979 var points = rec[entry].split("point:")[1].match(/[-+]?[0-9]*\.?[0-9]+/g);
980
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700981 var drill_element = {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700982 "tweetEntryId" : parseInt(rec[entry].split(",")[0].split(":")[1].replace('"', '')),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700983 "tweetText" : rec[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700984 "tweetLat" : parseFloat(points[0]),
985 "tweetLng" : parseFloat(points[1])
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700986 };
987 drilldown_cleaned.push(drill_element);
988 }
989 return drilldown_cleaned;
990}
991
992function onClickTweetbookMapMarker(tweet_arr) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700993 // Clear existing display
994 $.each(tweet_arr, function (t, valueT) {
995 var tweet_obj = tweet_arr[t];
996 onDrillDownAtLocation(tweet_obj);
997 });
998
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700999 $('#drilldown_modal').modal();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001000}
1001
1002/** Toggling Review and Explore Modes **/
1003
1004/**
1005* Explore mode: Initial map creation and screen alignment
1006*/
1007function onOpenExploreMap () {
genia.likes.science@gmail.com724476d2013-10-04 03:31:16 -07001008 var explore_column_height = $('#explore-well').height();
1009 var right_column_width = $('#right-col').width();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001010 $('#map_canvas').height(explore_column_height + "px");
genia.likes.science@gmail.com724476d2013-10-04 03:31:16 -07001011 $('#map_canvas').width(right_column_width + "px");
1012
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001013 $('#review-well').height(explore_column_height + "px");
1014 $('#review-well').css('max-height', explore_column_height + "px");
genia.likes.science@gmail.com724476d2013-10-04 03:31:16 -07001015
1016 $('#right-col').height(explore_column_height + "px");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001017}
1018
1019/**
1020* Launching explore mode: clear windows/variables, show correct sidebar
1021*/
1022function onLaunchExploreMode() {
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -07001023 $('#aboutr').hide();
1024 $('#r1').show();
1025 $('#about-active').removeClass('active');
1026
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001027 $('#review-active').removeClass('active');
1028 $('#review-well').hide();
1029
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -07001030 $('#explore-active').addClass('active');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001031 $('#explore-well').show();
1032
1033 $("#clear-button").trigger("click");
1034}
1035
1036/**
1037* Launching review mode: clear windows/variables, show correct sidebar
1038*/
1039function onLaunchReviewMode() {
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -07001040 $('#aboutr').hide();
1041 $('#r1').show();
1042 $('#about-active').removeClass('active');
1043
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001044 $('#explore-active').removeClass('active');
1045 $('#explore-well').hide();
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -07001046
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001047 $('#review-active').addClass('active');
1048 $('#review-well').show();
1049
1050 $("#clear-button").trigger("click");
1051}
1052
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -07001053/**
1054* Lauching about mode: hides all windows, shows row containing about info
1055*/
1056function onLaunchAboutMode() {
1057 $('#explore-active').removeClass('active');
1058 $('#review-active').removeClass('active');
1059 $('#about-active').addClass('active');
1060 $('#r1').hide();
1061 $('#aboutr').show();
1062}
1063
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -07001064/** Icon / Interface Utility Methods **/
1065
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001066/**
1067* Creates a delete icon button using default trash icon
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -07001068* @param {String} id, id for this element
1069* @param {String} attachTo, id string of an element to which I can attach this button.
1070* @param {Function} onClick, a function to fire when this icon is clicked
1071*/
1072function addDeleteButton(iconId, attachTo, onClick) {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -07001073
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001074 var trashIcon = '<button class="btn btn-default" id="' + iconId + '"><span class="glyphicon glyphicon-trash"></span></button>';
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -07001075 $('#' + attachTo).append(trashIcon);
1076
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001077 // When this trash button is clicked, the function is called.
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -07001078 $('#' + iconId).on('click', onClick);
1079}
1080
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001081/**
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -07001082* Creates a message and attaches it to data management area.
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001083* @param {String} message, a message to post
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -07001084* @param {Boolean} isPositiveMessage, whether or not this is a positive message.
1085* @param {String} target, the target div to attach this message.
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001086*/
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -07001087function reportUserMessage(message, isPositiveMessage, target) {
1088 // Clear out any existing messages
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001089 $('#' + target).html('');
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -07001090
1091 // Select appropriate alert-type
1092 var alertType = "alert-success";
1093 if (!isPositiveMessage) {
1094 alertType = "alert-danger";
1095 }
1096
1097 // Append the appropriate message
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001098 $('<div/>')
Eugenia Gabrielovadbd50a42013-10-19 00:30:27 -07001099 .attr("class", "alert " + alertType)
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001100 .html('<button type="button" class="close" data-dismiss="alert">&times;</button>' + message)
1101 .appendTo('#' + target);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001102}
1103
1104/**
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001105* mapWidgetResetMap
1106*
1107* [No Parameters]
1108*
1109* Clears ALL map elements - plotted items, overlays, then resets position
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001110*/
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001111function mapWidgetResetMap() {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001112
1113 if (selectionRect) {
1114 selectionRect.setMap(null);
1115 selectionRect = null;
1116 }
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001117
1118 mapWidgetClearMap();
1119
1120 // Reset map center and zoom
1121 map.setCenter(new google.maps.LatLng(38.89, -77.03));
1122 map.setZoom(4);
1123}
1124
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001125/**
1126* mapWidgetClearMap
1127*
1128* No parameters
1129*
1130* Removes data/markers
1131*/
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001132function mapWidgetClearMap() {
1133
1134 // Remove previously plotted data/markers
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001135 for (c in map_cells) {
1136 map_cells[c].setMap(null);
1137 }
1138 map_cells = [];
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -07001139
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001140 for (m in map_tweet_markers) {
1141 map_tweet_markers[m].setMap(null);
1142 }
1143 map_tweet_markers = [];
Eugenia Gabrielova12de0302013-10-18 02:25:39 -07001144
1145 $("#submit-button").attr("disabled", false);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001146}
1147
1148/**
1149* Uses jenks algorithm in geostats library to find natural breaks in numeric data
1150* @param {number Array} weights of points to plot
1151* @returns {number Array} array of natural breakpoints, of which the top 4 subsets will be plotted
1152*/
1153function mapWidgetLegendComputeNaturalBreaks(weights) {
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001154
1155 if (weights.length < 10) {
1156 return [0];
1157 }
1158
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001159 var plotDataWeights = new geostats(weights.sort());
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001160 return plotDataWeights.getJenks(6).slice(2,7);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001161}
1162
1163/**
1164* Computes values for map legend given a value and an array of jenks breakpoints
1165* @param {number} weight of point to plot on map
1166* @param {number Array} breakpoints, an array of 5 points corresponding to bounds of 4 natural ranges
1167* @returns {String} an RGB value corresponding to a subset of data
1168*/
1169function mapWidgetLegendGetHeatValue(weight, breakpoints) {
1170
1171 // Determine into which range the weight falls
1172 var weightColor = 0;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001173
1174 if (breakpoints.length == 1) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001175 weightColor = 2;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001176 } else {
1177 if (weight >= breakpoints[3]) {
1178 weightColor = 3;
1179 } else if (weight >= breakpoints[2]) {
1180 weightColor = 2;
1181 } else if (weight >= breakpoints[1]) {
1182 weightColor = 1;
1183 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001184 }
1185
1186 // Get default map color palette
1187 var colorValues = mapWidgetGetColorPalette();
1188 return colorValues[weightColor];
1189}
1190
1191/**
1192* Returns an array containing a 4-color palette, lightest to darkest
1193* External palette source: http://www.colourlovers.com/palette/2763366/s_i_l_e_n_c_e_r
1194* @returns {Array} [colors]
1195*/
1196function mapWidgetGetColorPalette() {
1197 return [
1198 "rgb(115,189,158)",
1199 "rgb(74,142,145)",
1200 "rgb(19,93,96)",
1201 "rgb(7,51,46)"
1202 ];
1203}
1204
1205/**
1206* Computes radius for a given data point from a spatial cell
1207* @param {Object} keys => ["latSW" "lngSW" "latNE" "lngNE" "weight"]
1208* @returns {number} radius between 2 points in metres
1209*/
1210function mapWidgetComputeCircleRadius(spatialCell, breakpoints) {
1211
1212 var weight = spatialCell.weight;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001213
1214 if (breakpoints.length == 1) {
1215 var weightColor = 0.25;
1216 } else {
1217 // Compute weight color
1218 var weightColor = 0.25;
1219 if (weight >= breakpoints[3]) {
1220 weightColor = 1.0;
1221 } else if (weight >= breakpoints[2]) {
1222 weightColor = 0.75;
1223 } else if (weight >= breakpoints[1]) {
1224 weightColor = 0.5;
1225 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001226 }
1227
1228 // Define Boundary Points
1229 var point_center = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1230 var point_left = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, spatialCell.lngSW);
1231 var point_top = new google.maps.LatLng(spatialCell.latNE, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1232
1233 // TODO not actually a weight color :)
Eugenia Gabrielova78ba1ab2013-10-18 17:38:19 -07001234 //return weightColor * 1000 * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
1235 return 1000 * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001236}
1237
1238/** External Utility Methods **/
1239
1240/**
1241 * Calculates the distance between two latlng locations in km.
1242 * @see http://www.movable-type.co.uk/scripts/latlong.html
1243 *
1244 * @param {google.maps.LatLng} p1 The first lat lng point.
1245 * @param {google.maps.LatLng} p2 The second lat lng point.
1246 * @return {number} The distance between the two points in km.
1247 * @private
1248*/
1249function distanceBetweenPoints_(p1, p2) {
1250 if (!p1 || !p2) {
1251 return 0;
1252 }
1253
1254 var R = 6371; // Radius of the Earth in km
1255 var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
1256 var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
1257 var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
1258 Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
1259 Math.sin(dLon / 2) * Math.sin(dLon / 2);
1260 var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1261 var d = R * c;
1262 return d;
1263};