blob: fb83406bdabe2527957e827695140e16b737249a [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
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -070013 // Populate review mode tweetbooks
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070014 review_mode_tweetbooks = [];
15 review_mode_handles = [];
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -070016 getAllDataverseTweetbooks();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070017
18 map_cells = [];
19 map_tweet_markers = [];
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -070020 map_info_windows = {};
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070021
22 // UI Elements - Modals & perspective tabs
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -070023 $('#drilldown_modal').modal('hide');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070024 $('#explore-mode').click( onLaunchExploreMode );
25 $('#review-mode').click( onLaunchReviewMode );
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -070026 $('#about-mode').click(onLaunchAboutMode);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070027
28 // UI Elements - A button to clear current map and query data
29 $("#clear-button").button().click(function () {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -070030 mapWidgetResetMap();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070031
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -070032 $('#explore-report-message').html('');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070033 $('#query-preview-window').html('');
34 $("#metatweetzone").html('');
35 });
36
37 // UI Elements - Query setup
38 $("#selection-button").button('toggle');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070039
40 // UI Element - Grid sliders
41 var updateSliderDisplay = function(event, ui) {
42 if (event.target.id == "grid-lat-slider") {
43 $("#gridlat").text(""+ui.value);
44 } else {
45 $("#gridlng").text(""+ui.value);
46 }
47 };
48
49 sliderOptions = {
50 max: 10,
51 min: 1.5,
52 step: .1,
53 value: 2.0,
54 slidechange: updateSliderDisplay,
55 slide: updateSliderDisplay,
56 start: updateSliderDisplay,
57 stop: updateSliderDisplay
58 };
59
60 $("#gridlat").text(""+sliderOptions.value);
61 $("#gridlng").text(""+sliderOptions.value);
62 $(".grid-slider").slider(sliderOptions);
63
64 // UI Elements - Date Pickers
65 var dateOptions = {
66 dateFormat: "yy-mm-dd",
67 defaultDate: "2012-01-02",
68 navigationAsDateFormat: true,
69 constrainInput: true
70 };
71 var start_dp = $("#start-date").datepicker(dateOptions);
72 start_dp.val(dateOptions.defaultDate);
73 dateOptions['defaultDate'] = "2012-12-31";
74 var end_dp= $("#end-date").datepicker(dateOptions);
75 end_dp.val(dateOptions.defaultDate);
76
77 // This little bit of code manages period checks of the asynchronous query manager,
78 // which holds onto handles asynchornously received. We can set the handle update
79 // frequency using seconds, and it will let us know when it is ready.
80 var intervalID = setInterval(
81 function() {
82 asynchronousQueryIntervalUpdate();
83 },
84 asynchronousQueryGetInterval()
85 );
86
87 // UI Elements - Creates map and location auto-complete
88 onOpenExploreMap();
89 var mapOptions = {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -070090 center: new google.maps.LatLng(38.89, -77.03),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070091 zoom: 4,
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -070092 mapTypeId: google.maps.MapTypeId.ROADMAP,
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070093 streetViewControl: false,
94 draggable : false
95 };
96 map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions);
97
98 var input = document.getElementById('location-text-box');
99 var autocomplete = new google.maps.places.Autocomplete(input);
100 autocomplete.bindTo('bounds', map);
101
102 google.maps.event.addListener(autocomplete, 'place_changed', function() {
103 var place = autocomplete.getPlace();
104 if (place.geometry.viewport) {
105 map.fitBounds(place.geometry.viewport);
106 } else {
107 map.setCenter(place.geometry.location);
108 map.setZoom(17); // Why 17? Because it looks good.
109 }
110 var address = '';
111 if (place.address_components) {
112 address = [(place.address_components[0] && place.address_components[0].short_name || ''),
113 (place.address_components[1] && place.address_components[1].short_name || ''),
114 (place.address_components[2] && place.address_components[2].short_name || '') ].join(' ');
115 }
116 });
117
118 // UI Elements - Selection Rectangle Drawing
119 shouldDraw = false;
120 var startLatLng;
121 selectionRect = null;
122 var selectionRadio = $("#selection-button");
123 var firstClick = true;
124
125 google.maps.event.addListener(map, 'mousedown', function (event) {
126 // only allow drawing if selection is selected
127 if (selectionRadio.hasClass("active")) {
128 startLatLng = event.latLng;
129 shouldDraw = true;
130 }
131 });
132
133 google.maps.event.addListener(map, 'mousemove', drawRect);
134 function drawRect (event) {
135 if (shouldDraw) {
136 if (!selectionRect) {
137 var selectionRectOpts = {
138 bounds: new google.maps.LatLngBounds(startLatLng, event.latLng),
139 map: map,
140 strokeWeight: 1,
141 strokeColor: "2b3f8c",
142 fillColor: "2b3f8c"
143 };
144 selectionRect = new google.maps.Rectangle(selectionRectOpts);
145 google.maps.event.addListener(selectionRect, 'mouseup', function () {
146 shouldDraw = false;
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700147 });
148 } else {
genia.likes.science@gmail.com4ada78c2013-09-07 13:53:48 -0700149
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700150 if (startLatLng.lng() < event.latLng.lng()) {
151 selectionRect.setBounds(new google.maps.LatLngBounds(startLatLng, event.latLng));
152 } else {
153 selectionRect.setBounds(new google.maps.LatLngBounds(event.latLng, startLatLng));
154 }
155 }
156 }
157 };
158
159 // UI Elements - Toggle location search style by location or by map selection
160 $('#selection-button').on('click', function (e) {
161 $("#location-text-box").attr("disabled", "disabled");
162 if (selectionRect) {
163 selectionRect.setMap(map);
164 }
165 });
166 $('#location-button').on('click', function (e) {
167 $("#location-text-box").removeAttr("disabled");
168 if (selectionRect) {
169 selectionRect.setMap(null);
170 }
171 });
172
173 // UI Elements - Tweetbook Management
174 $('.dropdown-menu a.holdmenu').click(function(e) {
175 e.stopPropagation();
176 });
177
178 $('#new-tweetbook-button').on('click', function (e) {
179 onCreateNewTweetBook($('#new-tweetbook-entry').val());
180
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -0700181 $('#new-tweetbook-entry').val("");
182 $('#new-tweetbook-entry').attr("placeholder", "Name a new tweetbook");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700183 });
184
185 // UI Element - Query Submission
186 $("#submit-button").button().click(function () {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700187
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700188 var kwterm = $("#keyword-textbox").val();
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700189 if (kwterm == "") {
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700190 addFailureBlock("Please enter a search term!", "explore-report-message");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700191 } else {
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700192
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700193 $("#explore-report-message").html('');
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700194 $("#submit-button").attr("disabled", true);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700195
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700196 var startdp = $("#start-date").datepicker("getDate");
197 var enddp = $("#end-date").datepicker("getDate");
198 var startdt = $.datepicker.formatDate("yy-mm-dd", startdp)+"T00:00:00Z";
199 var enddt = $.datepicker.formatDate("yy-mm-dd", enddp)+"T23:59:59Z";
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700200
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700201 var formData = {
202 "keyword": kwterm,
203 "startdt": startdt,
204 "enddt": enddt,
205 "gridlat": $("#grid-lat-slider").slider("value"),
206 "gridlng": $("#grid-lng-slider").slider("value")
207 };
208
209 // Get Map Bounds
210 var bounds;
211 if ($('#selection-button').hasClass("active") && selectionRect) {
212 bounds = selectionRect.getBounds();
213 } else {
214 bounds = map.getBounds();
215 }
216
217 var swLat = Math.abs(bounds.getSouthWest().lat());
218 var swLng = Math.abs(bounds.getSouthWest().lng());
219 var neLat = Math.abs(bounds.getNorthEast().lat());
220 var neLng = Math.abs(bounds.getNorthEast().lng());
221
222 formData["swLat"] = Math.min(swLat, neLat);
223 formData["swLng"] = Math.max(swLng, neLng);
224 formData["neLat"] = Math.max(swLat, neLat);
225 formData["neLng"] = Math.min(swLng, neLng);
226
227 var build_cherry_mode = "synchronous";
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700228
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700229 if ($('#asbox').is(":checked")) {
230 build_cherry_mode = "asynchronous";
231 $('#show-query-button').attr("disabled", false);
232 } else {
233 $('#show-query-button').attr("disabled", true);
234 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700235
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700236 var f = buildAQLQueryFromForm(formData);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700237
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700238 APIqueryTracker = {
239 "query" : "use dataverse twitter;\n" + f.val(),
240 "data" : formData
241 };
genia.likes.science@gmail.com233fe972013-09-07 13:57:46 -0700242
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700243 // TODO Make dialog work correctly.
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700244 //$('#dialog').html(APIqueryTracker["query"]);
genia.likes.science@gmail.com233fe972013-09-07 13:57:46 -0700245
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700246 if (build_cherry_mode == "synchronous") {
247 A.query(f.val(), cherryQuerySyncCallback, build_cherry_mode);
248 } else {
249 A.query(f.val(), cherryQueryAsyncCallback, build_cherry_mode);
250 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700251
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700252 // Clears selection rectangle on query execution, rather than waiting for another clear call.
253 if (selectionRect) {
254 selectionRect.setMap(null);
255 selectionRect = null;
256 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700257 }
258 });
259});
260
261
262function buildAQLQueryFromForm(parameters) {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700263
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700264 var bounds = {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700265 "ne" : { "lat" : parameters["neLat"], "lng" : -1*parameters["neLng"]},
266 "sw" : { "lat" : parameters["swLat"], "lng" : -1*parameters["swLng"]}
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700267 };
268
269 var rectangle =
270 new FunctionExpression("create-rectangle",
271 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
272 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
273
274
275 var aql = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700276 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700277 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
278 .LetClause("$region", rectangle)
279 .WhereClause().and(
280 new FunctionExpression("spatial-intersect", "$t.sender-location", "$region"),
281 new AExpression('$t.send-time > datetime("' + parameters["startdt"] + '")'),
282 new AExpression('$t.send-time < datetime("' + parameters["enddt"] + '")'),
283 new FunctionExpression("contains", "$t.message-text", "$keyword")
284 )
285 .GroupClause(
286 "$c",
287 new FunctionExpression("spatial-cell", "$t.sender-location",
288 new FunctionExpression("create-point", "24.5", "-125.5"),
289 parameters["gridlat"].toFixed(1), parameters["gridlng"].toFixed(1)),
290 "with",
291 "$t"
292 )
293 .ReturnClause({ "cell" : "$c", "count" : "count($t)" });
294
295 return aql;
296}
297
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700298
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700299/**
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700300* getAllDataverseTweetbooks
301*
302* no params
303*
304* Returns all datasets of type TweetbookEntry, populates review_mode_tweetbooks
305*/
306function getAllDataverseTweetbooks(fn_tweetbooks) {
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700307
308 // This creates a query to the Metadata for datasets of type
309 // TweetBookEntry. Note that if we throw in a WhereClause (commented out below)
310 // there is an odd error. This is being fixed and will be removed from this demo.
311 var getTweetbooksQuery = new FLWOGRExpression()
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700312 .ForClause("$ds", new AExpression("dataset Metadata.Dataset"))
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700313 //.WhereClause(new AExpression('$ds.DataTypeName = "TweetbookEntry"'))
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700314 .ReturnClause({
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700315 "DataTypeName" : "$ds.DataTypeName",
316 "DatasetName" : "$ds.DatasetName"
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700317 });
318
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700319 // Now create a function that will be called when tweetbooks succeed.
320 // In this case, we want to parse out the results object from the Asterix
321 // REST API response.
322 var tweetbooksSuccess = function(r) {
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700323 // Parse tweetbook metadata results
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700324 $.each(r.results, function(i, data) {
325 if ($.parseJSON(data)["DataTypeName"] == "TweetbookEntry") {
326 review_mode_tweetbooks.push($.parseJSON(data)["DatasetName"]);
327 }
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700328 });
329
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700330 // Now, if any tweetbooks already exist, opulate review screen.
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700331 $('#review-tweetbook-titles').html('');
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700332 $.each(review_mode_tweetbooks, function(i, tweetbook) {
333 addTweetBookDropdownItem(tweetbook);
334 });
335 };
336
337 // Now, we are ready to run a query.
338 A.meta(getTweetbooksQuery.val(), tweetbooksSuccess);
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700339
340}
341
342
343/** Asynchronous Query Management **/
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700344
345/**
346* Checks through each asynchronous query to see if they are ready yet
347*/
348function asynchronousQueryIntervalUpdate() {
349 for (var handle_key in asyncQueryManager) {
350 if (!asyncQueryManager[handle_key].hasOwnProperty("ready")) {
351 asynchronousQueryGetAPIQueryStatus( asyncQueryManager[handle_key]["handle"], handle_key );
352 }
353 }
354}
355
356
357/**
358* Returns current time interval to check for asynchronous query readiness
359* @returns {number} milliseconds between asychronous query checks
360*/
361function asynchronousQueryGetInterval() {
362 var seconds = 10;
363 return seconds * 1000;
364}
365
366
367/**
368* Retrieves status of an asynchronous query, using an opaque result handle from API
369* @param {Object} handle, an object previously returned from an async call
370* @param {number} handle_id, the integer ID parsed from the handle object
371*/
372function asynchronousQueryGetAPIQueryStatus (handle, handle_id) {
373
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700374 A.query_status(
375 {
376 "handle" : JSON.stringify(handle)
377 },
378 function (res) {
379 if (res["status"] == "SUCCESS") {
380 // We don't need to check if this one is ready again, it's not going anywhere...
381 // Unless the life cycle of handles has changed drastically
382 asyncQueryManager[handle_id]["ready"] = true;
383
384 // Indicate success.
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700385 $('#handle_' + handle_id).removeClass("btn-disabled").prop('disabled', false).addClass("btn-success");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700386 }
387 }
388 );
389}
390
391
392/**
393* On-success callback after async API query
394* @param {object} res, a result object containing an opaque result handle to Asterix
395*/
396function cherryQueryAsyncCallback(res) {
397
398 // Parse handle, handle id and query from async call result
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700399 var handle_query = APIqueryTracker["query"];
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700400 var handle = res;
401 var handle_id = res["handle"].toString().split(',')[0];
402
403 // Add to stored map of existing handles
404 asyncQueryManager[handle_id] = {
405 "handle" : handle,
406 "query" : handle_query,
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700407 "data" : APIqueryTracker["data"]
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700408 };
409
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700410 // Create a container for this async query handle
411 $('<div/>')
412 .css("margin-left", "1em")
413 .css("margin-bottom", "1em")
414 .css("display", "block")
415 .attr({
416 "class" : "btn-group",
417 "id" : "async_container_" + handle_id
418 })
419 .appendTo("#async-handle-controls");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700420
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700421 // Adds the main button for this async handle
422 var handle_action_button = '<button class="btn btn-disabled" id="handle_' + handle_id + '">Handle ' + handle_id + '</button>';
423 $('#async_container_' + handle_id).append(handle_action_button);
424 $('#handle_' + handle_id).prop('disabled', true);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700425 $('#handle_' + handle_id).on('click', function (e) {
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700426
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700427 // make sure query is ready to be run
428 if (asyncQueryManager[handle_id]["ready"]) {
429
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700430 APIqueryTracker = {
431 "query" : asyncQueryManager[handle_id]["query"],
432 "data" : asyncQueryManager[handle_id]["data"]
433 };
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700434 // TODO
435 //$('#dialog').html(APIqueryTracker["query"]);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700436
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -0700437 if (!asyncQueryManager[handle_id].hasOwnProperty("result")) {
438 // Generate new Asterix Core API Query
439 A.query_result(
440 { "handle" : JSON.stringify(asyncQueryManager[handle_id]["handle"]) },
441 function(res) {
442 asyncQueryManager[handle_id]["result"] = res;
443 cherryQuerySyncCallback(res);
444 }
445 );
446 } else {
447 cherryQuerySyncCallback(asyncQueryManager[handle_id]["result"]);
448 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700449 }
450 });
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700451
452 // Adds a removal button for this async handle
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700453 var asyncDeleteButton = addDeleteButton(
454 "trashhandle_" + handle_id,
455 "async_container_" + handle_id,
456 function (e) {
457 $('#async_container_' + handle_id).remove();
458 delete asyncQueryManager[handle_id];
459 }
460 );
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700461
462 $('#async_container_' + handle_id).append('<br/>');
463
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700464 $("#submit-button").attr("disabled", false);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700465}
466
467
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700468/**
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700469* returns a json object with keys: weight, latSW, lngSW, latNE, lngNE
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700470*
471* { "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 -0700472*/
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700473
474/**
475* cleanJSON
476*
477* @param json, a JSON string that is not correctly formatted.
478*
479* Quick and dirty little function to clean up an Asterix JSON quirk.
480*/
481function cleanJSON(json) {
482 return json
483 .replace("rectangle", '"rectangle"')
484 .replace("point:", '"point":')
485 .replace("point:", '"point":')
486 .replace("int64", '"int64"');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700487}
488
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700489
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700490/**
491* A spatial data cleaning and mapping call
492* @param {Object} res, a result object from a cherry geospatial query
493*/
494function cherryQuerySyncCallback(res) {
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700495
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700496 // Initialize coordinates and weights, to store
497 // coordinates of map cells and their weights
498 // TODO these are all included in coordinates already...
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700499 var coordinates = [];
500 var weights = [];
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700501 var al = 1;
502
503 // Parse resulting JSON objects. Here is an example record:
504 // { "cell": { rectangle: [{ point: [22.5, 64.5]}, { point: [24.5, 66.5]}]}, "count": { int64: 5 }}
505 $.each(res.results, function(i, data) {
506
507 // First, parse a JSON object from a cleaned up string.
508 var record = $.parseJSON(cleanJSON(data));
509
510 // Parse Coordinates and Weights into a record
511 var sw = record.cell.rectangle[0].point;
512 var ne = record.cell.rectangle[1].point;
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700513
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700514 var coordinate = {
515 "latSW" : sw[0],
516 "lngSW" : sw[1],
517 "latNE" : ne[0],
518 "lngNE" : ne[1],
519 "weight" : record.count.int64
520 }
521
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700522 weights.push(coordinate["weight"]);
523 coordinates.push(coordinate);
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700524 });
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700525
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700526 triggerUIUpdate(coordinates, weights);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700527}
528
529/**
530* Triggers a map update based on a set of spatial query result cells
531* @param [Array] mapPlotData, an array of coordinate and weight objects
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700532* @param [Array] plotWeights, a list of weights of the spatial cells - e.g., number of tweets
533*/
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700534function triggerUIUpdate(mapPlotData, plotWeights) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700535 /** Clear anything currently on the map **/
536 mapWidgetClearMap();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700537
538 // Compute data point spread
539 var dataBreakpoints = mapWidgetLegendComputeNaturalBreaks(plotWeights);
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -0700540
541 map_info_windows = {};
542
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700543 $.each(mapPlotData, function (m, val) {
544
545 // Only map points in data range of top 4 natural breaks
546 if (mapPlotData[m].weight > dataBreakpoints[0]) {
547
548 // Get color value of legend
549 var mapColor = mapWidgetLegendGetHeatValue(mapPlotData[m].weight, dataBreakpoints);
550 var markerRadius = mapWidgetComputeCircleRadius(mapPlotData[m], dataBreakpoints);
551 var point_opacity = 1.0;
552
553 var point_center = new google.maps.LatLng(
554 (mapPlotData[m].latSW + mapPlotData[m].latNE)/2.0,
555 (mapPlotData[m].lngSW + mapPlotData[m].lngNE)/2.0);
556
557 // Create and plot marker
558 var map_circle_options = {
559 center: point_center,
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -0700560 anchorPoint: point_center,
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700561 radius: markerRadius,
562 map: map,
563 fillOpacity: point_opacity,
564 fillColor: mapColor,
565 clickable: true
566 };
567 var map_circle = new google.maps.Circle(map_circle_options);
568 map_circle.val = mapPlotData[m];
genia.likes.science@gmail.comec46c772013-09-07 18:13:00 -0700569
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -0700570 map_info_windows[m] = new google.maps.InfoWindow({
Eugenia Gabrielova78ba1ab2013-10-18 17:38:19 -0700571 content: mapPlotData[m].weight + " tweets",
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -0700572 position: point_center
genia.likes.science@gmail.comec46c772013-09-07 18:13:00 -0700573 });
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700574
genia.likes.science@gmail.comec46c772013-09-07 18:13:00 -0700575 // Clicking on a circle drills down map to that value, hovering over it displays a count
576 // of tweets at that location.
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700577 google.maps.event.addListener(map_circle, 'click', function (event) {
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700578 $.each(map_info_windows, function(i) {
579 map_info_windows[i].close();
580 });
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700581 onMapPointDrillDown(map_circle.val);
582 });
583
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -0700584 google.maps.event.addListener(map_circle, 'mouseover', function(event) {
585 if (!map_info_windows[m].getMap()) {
586 map_info_windows[m].setPosition(map_circle.center);
587 map_info_windows[m].open(map);
588 }
genia.likes.science@gmail.comec46c772013-09-07 18:13:00 -0700589 });
590
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700591 // Add this marker to global marker cells
592 map_cells.push(map_circle);
593 }
594 });
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700595}
596
597/**
598* prepares an Asterix API query to drill down in a rectangular spatial zone
599*
600* @params {object} marker_borders [LEGACY] a set of bounds for a region from a previous api result
601*/
602function onMapPointDrillDown(marker_borders) {
603 var zoneData = APIqueryTracker["data"];
604
605 var zswBounds = new google.maps.LatLng(marker_borders.latSW, marker_borders.lngSW);
606 var zneBounds = new google.maps.LatLng(marker_borders.latNE, marker_borders.lngNE);
607
608 var zoneBounds = new google.maps.LatLngBounds(zswBounds, zneBounds);
609 zoneData["swLat"] = zoneBounds.getSouthWest().lat();
610 zoneData["swLng"] = zoneBounds.getSouthWest().lng();
611 zoneData["neLat"] = zoneBounds.getNorthEast().lat();
612 zoneData["neLng"] = zoneBounds.getNorthEast().lng();
613 var zB = {
614 "sw" : {
615 "lat" : zoneBounds.getSouthWest().lat(),
616 "lng" : zoneBounds.getSouthWest().lng()
617 },
618 "ne" : {
619 "lat" : zoneBounds.getNorthEast().lat(),
620 "lng" : zoneBounds.getNorthEast().lng()
621 }
622 };
623
624 mapWidgetClearMap();
625
626 var customBounds = new google.maps.LatLngBounds();
627 var zoomSWBounds = new google.maps.LatLng(zoneData["swLat"], zoneData["swLng"]);
628 var zoomNEBounds = new google.maps.LatLng(zoneData["neLat"], zoneData["neLng"]);
629 customBounds.extend(zoomSWBounds);
630 customBounds.extend(zoomNEBounds);
631 map.fitBounds(customBounds);
632
633 var df = getDrillDownQuery(zoneData, zB);
634
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700635 APIqueryTracker = {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700636 "query_string" : "use dataverse twitter;\n" + df.val(),
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700637 "marker_path" : "static/img/mobile2.png",
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700638 "on_clean_result" : onCleanTweetbookDrilldown,
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700639 };
640
641 A.query(df.val(), onTweetbookQuerySuccessPlot);
642}
643
644function getDrillDownQuery(parameters, bounds) {
645
646 var zoomRectangle = new FunctionExpression("create-rectangle",
647 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
648 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
649
650 var drillDown = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700651 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700652 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
653 .LetClause("$region", zoomRectangle)
654 .WhereClause().and(
655 new FunctionExpression('spatial-intersect', '$t.sender-location', '$region'),
656 new AExpression().set('$t.send-time > datetime("' + parameters["startdt"] + '")'),
657 new AExpression().set('$t.send-time < datetime("' + parameters["enddt"] + '")'),
658 new FunctionExpression('contains', '$t.message-text', '$keyword')
659 )
660 .ReturnClause({
661 "tweetId" : "$t.tweetid",
662 "tweetText" : "$t.message-text",
663 "tweetLoc" : "$t.sender-location"
664 });
665
666 return drillDown;
667}
668
669
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700670function onDrillDownAtLocation(tO) {
671
672 var tweetId = tO["tweetEntryId"];
673 var tweetText = tO["tweetText"];
674
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700675 // First, set tweet in drilldown modal to be this tweet's text
676 $('#modal-body-tweet').html('Tweet #' + tweetId + ": " + tweetText);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700677
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700678 // Next, empty any leftover tweetbook comments or error/success messages
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700679 $("#modal-body-add-to").val('');
680 $("#modal-body-add-note").val('');
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700681 $("#modal-body-message-holder").html("");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700682
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700683 // Next, if there is an existing tweetcomment reported, show it.
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700684 if (tO.hasOwnProperty("tweetComment")) {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700685
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700686 // Show correct panel
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700687 $("#modal-existing-note").show();
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700688 $("#modal-save-tweet-panel").hide();
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700689
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700690 // Fill in existing tweet comment
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700691 $("#modal-body-tweet-note").val(tO["tweetComment"]);
692
693 // Change Tweetbook Badge
694 $("#modal-current-tweetbook").val(APIqueryTracker["active_tweetbook"]);
695
696 // Add deletion functionality
697 $("#modal-body-trash-icon").on('click', function () {
698 // Send comment deletion to asterix
699 var deleteTweetCommentOnId = '"' + tweetId + '"';
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700700 var toDelete = new DeleteStatement(
701 "$mt",
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700702 APIqueryTracker["active_tweetbook"],
703 new AExpression("$mt.tweetid = " + deleteTweetCommentOnId.toString())
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700704 );
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700705 A.update(
706 toDelete.val()
707 );
708
709 // Hide comment from map
710 $('#drilldown_modal').modal('hide');
711
712 // Replot tweetbook
713 onPlotTweetbook(APIqueryTracker["active_tweetbook"]);
714 });
715
716 } else {
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700717 // Show correct panel
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700718 $("#modal-existing-note").hide();
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700719 $("#modal-save-tweet-panel").show();
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700720
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700721 // Now, when adding a comment on an available tweet to a tweetbook
722 $("#save-comment-tweetbook-modal").on('click', function(e) {
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700723
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700724 // Stuff to save about new comment
725 var save_metacomment_target_tweetbook = $("#modal-body-add-to").val();
726 var save_metacomment_target_comment = '"' + $("#modal-body-add-note").val() + '"';
727 var save_metacomment_target_tweet = '"' + tweetId + '"';
728
729 // Make sure content is entered, and then save this comment.
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700730 if ($("#modal-body-add-note").val() == "") {
731
732 addFailureBlock("Please enter a comment.", "modal-body-message-holder");
733
734 } else if ($("#modal-body-add-to").val() == "") {
735
736 addFailureBlock("Please enter a tweetbook.", "modal-body-message-holder");
737
genia.likes.science@gmail.com0f67d9e2013-10-04 17:08:50 -0700738 } else {
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700739
740 // Check if tweetbook exists. If not, create it.
741 if (!(existsTweetbook(save_metacomment_target_tweetbook))) {
742 onCreateNewTweetBook(save_metacomment_target_tweetbook);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700743 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700744
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700745 var toInsert = new InsertStatement(
746 save_metacomment_target_tweetbook,
747 {
748 "tweetid" : save_metacomment_target_tweet.toString(),
749 "comment-text" : save_metacomment_target_comment
750 }
751 );
752 A.update(toInsert.val(), function () {});
753
754 var successMessage = "Saved comment on <b>Tweet #" + tweetId +
755 "</b> in dataset <b>" + save_metacomment_target_tweetbook + "</b>.";
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700756 addSuccessBlock(successMessage, "modal-body-message-holder");
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700757
758 $("#modal-body-add-to").val('');
759 $("#modal-body-add-note").val('');
Eugenia Gabrielova12de0302013-10-18 02:25:39 -0700760 $("#modal-body-message-holder").html('');
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -0700761 }
762 });
763 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700764}
765
766
767/**
768* Adds a new tweetbook entry to the menu and creates a dataset of type TweetbookEntry.
769*/
770function onCreateNewTweetBook(tweetbook_title) {
771
772 var tweetbook_title = tweetbook_title.split(' ').join('_');
773
774 A.ddl(
775 "create dataset " + tweetbook_title + "(TweetbookEntry) primary key tweetid;",
776 function () {}
777 );
778
779 if (!(existsTweetbook(tweetbook_title))) {
780 review_mode_tweetbooks.push(tweetbook_title);
781 addTweetBookDropdownItem(tweetbook_title);
782 }
783}
784
785
786function onDropTweetBook(tweetbook_title) {
787
788 // AQL Call
789 A.ddl(
790 "drop dataset " + tweetbook_title + " if exists;",
791 function () {}
792 );
793
794 // Removes tweetbook from review_mode_tweetbooks
795 var remove_position = $.inArray(tweetbook_title, review_mode_tweetbooks);
796 if (remove_position >= 0) review_mode_tweetbooks.splice(remove_position, 1);
797
798 // Clear UI with review tweetbook titles
799 $('#review-tweetbook-titles').html('');
800 for (r in review_mode_tweetbooks) {
801 addTweetBookDropdownItem(review_mode_tweetbooks[r]);
802 }
803}
804
805
806function addTweetBookDropdownItem(tweetbook) {
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -0700807 // Add placeholder for this tweetbook
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700808 $('<div/>')
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700809 .attr({
810 "class" : "btn-group",
811 "id" : "rm_holder_" + tweetbook
812 }).appendTo("#review-tweetbook-titles");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700813
814 // Add plotting button for this tweetbook
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -0700815 var plot_button = '<button class="btn btn-default" id="rm_plotbook_' + tweetbook + '">' + tweetbook + '</button>';
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700816 $("#rm_holder_" + tweetbook).append(plot_button);
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -0700817 $("#rm_plotbook_" + tweetbook).width("200px");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700818 $("#rm_plotbook_" + tweetbook).on('click', function(e) {
819 onPlotTweetbook(tweetbook);
820 });
821
822 // Add trash button for this tweetbook
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700823 var onTrashTweetbookButton = addDeleteButton(
824 "rm_trashbook_" + tweetbook,
825 "rm_holder_" + tweetbook,
826 function(e) {
827 onDropTweetBook(tweetbook);
828 }
829 );
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700830}
831
832
833function onPlotTweetbook(tweetbook) {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700834
835 // Clear map for this one
836 mapWidgetResetMap();
837
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700838 var plotTweetQuery = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700839 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700840 .ForClause("$m", new AExpression("dataset " + tweetbook))
841 .WhereClause(new AExpression("$m.tweetid = $t.tweetid"))
842 .ReturnClause({
843 "tweetId" : "$m.tweetid",
844 "tweetText" : "$t.message-text",
845 "tweetLoc" : "$t.sender-location",
846 "tweetCom" : "$m.comment-text"
847 });
848
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700849 APIqueryTracker = {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700850 "query_string" : "use dataverse twitter;\n" + plotTweetQuery.val(),
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700851 "marker_path" : "static/img/mobile_green2.png",
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700852 "on_clean_result" : onCleanPlotTweetbook,
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700853 "active_tweetbook" : tweetbook
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700854 };
855
856 A.query(plotTweetQuery.val(), onTweetbookQuerySuccessPlot);
857}
858
859
860function onTweetbookQuerySuccessPlot (res) {
861
862 var records = res["results"];
863
864 var coordinates = [];
865 map_tweet_markers = [];
866 map_tweet_overlays = [];
867 drilldown_data_map = {};
868 drilldown_data_map_vals = {};
869
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700870 var micon = APIqueryTracker["marker_path"];
871 var marker_click_function = onClickTweetbookMapMarker;
872 var clean_result_function = APIqueryTracker["on_clean_result"];
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700873
874 coordinates = clean_result_function(records);
875
876 for (var dm in coordinates) {
877 var keyLat = coordinates[dm].tweetLat.toString();
878 var keyLng = coordinates[dm].tweetLng.toString();
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700879
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700880 if (!drilldown_data_map.hasOwnProperty(keyLat)) {
881 drilldown_data_map[keyLat] = {};
882 }
883 if (!drilldown_data_map[keyLat].hasOwnProperty(keyLng)) {
884 drilldown_data_map[keyLat][keyLng] = [];
885 }
886 drilldown_data_map[keyLat][keyLng].push(coordinates[dm]);
887 drilldown_data_map_vals[coordinates[dm].tweetEntryId.toString()] = coordinates[dm];
888 }
889
890 $.each(drilldown_data_map, function(drillKeyLat, valuesAtLat) {
891 $.each(drilldown_data_map[drillKeyLat], function (drillKeyLng, valueAtLng) {
892
893 // Get subset of drilldown position on map
894 var cposition = new google.maps.LatLng(parseFloat(drillKeyLat), parseFloat(drillKeyLng));
895
896 // Create a marker using the snazzy phone icon
897 var map_tweet_m = new google.maps.Marker({
898 position: cposition,
899 map: map,
900 icon: micon,
901 clickable: true,
902 });
903
904 // Open Tweet exploration window on click
905 google.maps.event.addListener(map_tweet_m, 'click', function (event) {
906 marker_click_function(drilldown_data_map[drillKeyLat][drillKeyLng]);
907 });
908
909 // Add marker to index of tweets
910 map_tweet_markers.push(map_tweet_m);
911
912 });
913 });
914}
915
916
917function existsTweetbook(tweetbook) {
918 if (parseInt($.inArray(tweetbook, review_mode_tweetbooks)) == -1) {
919 return false;
920 } else {
921 return true;
922 }
923}
924
925
926function onCleanPlotTweetbook(records) {
927 var toPlot = [];
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700928
929 // An entry looks like this:
930 // { "tweetId": "273589", "tweetText": " like verizon the network is amazing", "tweetLoc": { point: [37.78, 82.27]}, "tweetCom": "hooray comments" }
931
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700932 for (var entry in records) {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700933
934 var points = records[entry].split("point:")[1].match(/[-+]?[0-9]*\.?[0-9]+/g);
935
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700936 var tweetbook_element = {
937 "tweetEntryId" : parseInt(records[entry].split(",")[0].split(":")[1].split('"')[1]),
938 "tweetText" : records[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700939 "tweetLat" : parseFloat(points[0]),
940 "tweetLng" : parseFloat(points[1]),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700941 "tweetComment" : records[entry].split("tweetCom\": \"")[1].split("\"")[0]
942 };
943 toPlot.push(tweetbook_element);
944 }
945
946 return toPlot;
947}
948
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700949
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700950function onCleanTweetbookDrilldown (rec) {
951
952 var drilldown_cleaned = [];
953
954 for (var entry = 0; entry < rec.length; entry++) {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700955
956 // An entry looks like this:
957 // { "tweetId": "105491", "tweetText": " hate verizon its platform is OMG", "tweetLoc": { point: [30.55, 71.44]} }
958 var points = rec[entry].split("point:")[1].match(/[-+]?[0-9]*\.?[0-9]+/g);
959
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700960 var drill_element = {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700961 "tweetEntryId" : parseInt(rec[entry].split(",")[0].split(":")[1].replace('"', '')),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700962 "tweetText" : rec[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700963 "tweetLat" : parseFloat(points[0]),
964 "tweetLng" : parseFloat(points[1])
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700965 };
966 drilldown_cleaned.push(drill_element);
967 }
968 return drilldown_cleaned;
969}
970
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700971
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700972function onClickTweetbookMapMarker(tweet_arr) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700973 // Clear existing display
974 $.each(tweet_arr, function (t, valueT) {
975 var tweet_obj = tweet_arr[t];
976 onDrillDownAtLocation(tweet_obj);
977 });
978
genia.likes.science@gmail.com1400a752013-10-04 04:59:12 -0700979 $('#drilldown_modal').modal();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700980}
981
982/** Toggling Review and Explore Modes **/
983
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700984
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700985/**
986* Explore mode: Initial map creation and screen alignment
987*/
988function onOpenExploreMap () {
genia.likes.science@gmail.com724476d2013-10-04 03:31:16 -0700989 var explore_column_height = $('#explore-well').height();
990 var right_column_width = $('#right-col').width();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700991 $('#map_canvas').height(explore_column_height + "px");
genia.likes.science@gmail.com724476d2013-10-04 03:31:16 -0700992 $('#map_canvas').width(right_column_width + "px");
993
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700994 $('#review-well').height(explore_column_height + "px");
995 $('#review-well').css('max-height', explore_column_height + "px");
genia.likes.science@gmail.com724476d2013-10-04 03:31:16 -0700996
997 $('#right-col').height(explore_column_height + "px");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700998}
999
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001000
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001001/**
1002* Launching explore mode: clear windows/variables, show correct sidebar
1003*/
1004function onLaunchExploreMode() {
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -07001005 $('#aboutr').hide();
1006 $('#r1').show();
1007 $('#about-active').removeClass('active');
1008
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001009 $('#review-active').removeClass('active');
1010 $('#review-well').hide();
1011
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -07001012
1013 $('#explore-active').addClass('active');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001014 $('#explore-well').show();
1015
1016 $("#clear-button").trigger("click");
1017}
1018
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001019
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001020/**
1021* Launching review mode: clear windows/variables, show correct sidebar
1022*/
1023function onLaunchReviewMode() {
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -07001024 $('#aboutr').hide();
1025 $('#r1').show();
1026 $('#about-active').removeClass('active');
1027
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001028 $('#explore-active').removeClass('active');
1029 $('#explore-well').hide();
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -07001030
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001031 $('#review-active').addClass('active');
1032 $('#review-well').show();
1033
1034 $("#clear-button").trigger("click");
1035}
1036
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -07001037
genia.likes.science@gmail.com2de4c182013-10-04 11:39:11 -07001038/**
1039* Lauching about mode: hides all windows, shows row containing about info
1040*/
1041function onLaunchAboutMode() {
1042 $('#explore-active').removeClass('active');
1043 $('#review-active').removeClass('active');
1044 $('#about-active').addClass('active');
1045 $('#r1').hide();
1046 $('#aboutr').show();
1047}
1048
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -07001049/** Icon / Interface Utility Methods **/
1050
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001051/**
1052* Creates a delete icon button using default trash icon
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -07001053* @param {String} id, id for this element
1054* @param {String} attachTo, id string of an element to which I can attach this button.
1055* @param {Function} onClick, a function to fire when this icon is clicked
1056*/
1057function addDeleteButton(iconId, attachTo, onClick) {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -07001058
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001059 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 -07001060 $('#' + attachTo).append(trashIcon);
1061
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001062 // When this trash button is clicked, the function is called.
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -07001063 $('#' + iconId).on('click', onClick);
1064}
1065
1066
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001067/**
1068* Creates a success message and attaches it to a div with provided ID.
1069* @param {String} message, a message to post
1070* @param {String} appendTarget, a target div to which to append the alert
1071*/
1072function addSuccessBlock(message, appendTarget) {
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001073 $('#' + appendTarget).html('');
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001074 $('<div/>')
1075 .attr("class", "alert alert-success")
1076 .html('<button type="button" class="close" data-dismiss="alert">&times;</button>' + message)
1077 .appendTo('#' + appendTarget);
1078}
1079
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001080/**
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001081* Creates a failure mesage and attaches it to a div with provided id.
1082* @param {String} message, a message to post
1083* @param {String} target, a target div to append the message
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001084*/
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001085function addFailureBlock(message, target) {
1086 $('#' + target).html('');
1087 $('<div/>')
1088 .attr("class", "alert alert-danger")
1089 .html('<button type="button" class="close" data-dismiss="alert">&times;</button>' + message)
1090 .appendTo('#' + target);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001091}
1092
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001093
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001094/**
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001095* mapWidgetResetMap
1096*
1097* [No Parameters]
1098*
1099* Clears ALL map elements - plotted items, overlays, then resets position
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001100*/
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001101function mapWidgetResetMap() {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001102
1103 if (selectionRect) {
1104 selectionRect.setMap(null);
1105 selectionRect = null;
1106 }
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001107
1108 mapWidgetClearMap();
1109
1110 // Reset map center and zoom
1111 map.setCenter(new google.maps.LatLng(38.89, -77.03));
1112 map.setZoom(4);
1113}
1114
Eugenia Gabrielovad07556a2013-10-11 03:45:06 -07001115/**
1116* mapWidgetClearMap
1117*
1118* No parameters
1119*
1120* Removes data/markers
1121*/
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001122function mapWidgetClearMap() {
1123
1124 // Remove previously plotted data/markers
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001125 for (c in map_cells) {
1126 map_cells[c].setMap(null);
1127 }
1128 map_cells = [];
genia.likes.science@gmail.com65e04182013-10-04 04:31:41 -07001129
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001130 for (m in map_tweet_markers) {
1131 map_tweet_markers[m].setMap(null);
1132 }
1133 map_tweet_markers = [];
Eugenia Gabrielova12de0302013-10-18 02:25:39 -07001134
1135 $("#submit-button").attr("disabled", false);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001136}
1137
1138/**
1139* Uses jenks algorithm in geostats library to find natural breaks in numeric data
1140* @param {number Array} weights of points to plot
1141* @returns {number Array} array of natural breakpoints, of which the top 4 subsets will be plotted
1142*/
1143function mapWidgetLegendComputeNaturalBreaks(weights) {
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001144
1145 if (weights.length < 10) {
1146 return [0];
1147 }
1148
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001149 var plotDataWeights = new geostats(weights.sort());
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001150 return plotDataWeights.getJenks(6).slice(2,7);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001151}
1152
1153/**
1154* Computes values for map legend given a value and an array of jenks breakpoints
1155* @param {number} weight of point to plot on map
1156* @param {number Array} breakpoints, an array of 5 points corresponding to bounds of 4 natural ranges
1157* @returns {String} an RGB value corresponding to a subset of data
1158*/
1159function mapWidgetLegendGetHeatValue(weight, breakpoints) {
1160
1161 // Determine into which range the weight falls
1162 var weightColor = 0;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001163
1164 if (breakpoints.length == 1) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001165 weightColor = 2;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001166 } else {
1167 if (weight >= breakpoints[3]) {
1168 weightColor = 3;
1169 } else if (weight >= breakpoints[2]) {
1170 weightColor = 2;
1171 } else if (weight >= breakpoints[1]) {
1172 weightColor = 1;
1173 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001174 }
1175
1176 // Get default map color palette
1177 var colorValues = mapWidgetGetColorPalette();
1178 return colorValues[weightColor];
1179}
1180
1181/**
1182* Returns an array containing a 4-color palette, lightest to darkest
1183* External palette source: http://www.colourlovers.com/palette/2763366/s_i_l_e_n_c_e_r
1184* @returns {Array} [colors]
1185*/
1186function mapWidgetGetColorPalette() {
1187 return [
1188 "rgb(115,189,158)",
1189 "rgb(74,142,145)",
1190 "rgb(19,93,96)",
1191 "rgb(7,51,46)"
1192 ];
1193}
1194
1195/**
1196* Computes radius for a given data point from a spatial cell
1197* @param {Object} keys => ["latSW" "lngSW" "latNE" "lngNE" "weight"]
1198* @returns {number} radius between 2 points in metres
1199*/
1200function mapWidgetComputeCircleRadius(spatialCell, breakpoints) {
1201
1202 var weight = spatialCell.weight;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001203
1204 if (breakpoints.length == 1) {
1205 var weightColor = 0.25;
1206 } else {
1207 // Compute weight color
1208 var weightColor = 0.25;
1209 if (weight >= breakpoints[3]) {
1210 weightColor = 1.0;
1211 } else if (weight >= breakpoints[2]) {
1212 weightColor = 0.75;
1213 } else if (weight >= breakpoints[1]) {
1214 weightColor = 0.5;
1215 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001216 }
1217
1218 // Define Boundary Points
1219 var point_center = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1220 var point_left = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, spatialCell.lngSW);
1221 var point_top = new google.maps.LatLng(spatialCell.latNE, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1222
1223 // TODO not actually a weight color :)
Eugenia Gabrielova78ba1ab2013-10-18 17:38:19 -07001224 //return weightColor * 1000 * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
1225 return 1000 * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001226}
1227
1228/** External Utility Methods **/
1229
1230/**
1231 * Calculates the distance between two latlng locations in km.
1232 * @see http://www.movable-type.co.uk/scripts/latlong.html
1233 *
1234 * @param {google.maps.LatLng} p1 The first lat lng point.
1235 * @param {google.maps.LatLng} p2 The second lat lng point.
1236 * @return {number} The distance between the two points in km.
1237 * @private
1238*/
1239function distanceBetweenPoints_(p1, p2) {
1240 if (!p1 || !p2) {
1241 return 0;
1242 }
1243
1244 var R = 6371; // Radius of the Earth in km
1245 var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
1246 var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
1247 var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
1248 Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
1249 Math.sin(dLon / 2) * Math.sin(dLon / 2);
1250 var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1251 var d = R * c;
1252 return d;
1253};