blob: 4c6d5aeea766c6d84b273bcdeb8f171a82df6b11 [file] [log] [blame]
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001$(function() {
2
3 // Connection to AsterixDB - Just one needed!
4 A = new AsterixDBConnection().dataverse("twitter");
5
6 // Following this is some stuff specific to the Black Cherry demo
7 // This is not necessary for working with AsterixDB
8 APIqueryTracker = {};
9 drilldown_data_map = {};
10 drilldown_data_map_vals = {};
11 asyncQueryManager = {};
12
13 review_mode_tweetbooks = [];
14 review_mode_handles = [];
15
16 map_cells = [];
17 map_tweet_markers = [];
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070018
19 // UI Elements - Modals & perspective tabs
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -070020 $('#drilldown_modal').modal('hide');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070021 $('#explore-mode').click( onLaunchExploreMode );
22 $('#review-mode').click( onLaunchReviewMode );
23
24 // UI Elements - A button to clear current map and query data
25 $("#clear-button").button().click(function () {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -070026 mapWidgetResetMap();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070027
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070028 $('#query-preview-window').html('');
29 $("#metatweetzone").html('');
30 });
31
32 // UI Elements - Query setup
33 $("#selection-button").button('toggle');
34
35 var dialog = $("#dialog").dialog({
36 width: "auto",
37 title: "AQL Query"
38 }).dialog("close");
39 $("#show-query-button")
40 .button()
41 .attr("disabled", true)
42 .click(function (event) {
43 $("#dialog").dialog("open");
44 });
45
46 // UI Element - Grid sliders
47 var updateSliderDisplay = function(event, ui) {
48 if (event.target.id == "grid-lat-slider") {
49 $("#gridlat").text(""+ui.value);
50 } else {
51 $("#gridlng").text(""+ui.value);
52 }
53 };
54
55 sliderOptions = {
56 max: 10,
57 min: 1.5,
58 step: .1,
59 value: 2.0,
60 slidechange: updateSliderDisplay,
61 slide: updateSliderDisplay,
62 start: updateSliderDisplay,
63 stop: updateSliderDisplay
64 };
65
66 $("#gridlat").text(""+sliderOptions.value);
67 $("#gridlng").text(""+sliderOptions.value);
68 $(".grid-slider").slider(sliderOptions);
69
70 // UI Elements - Date Pickers
71 var dateOptions = {
72 dateFormat: "yy-mm-dd",
73 defaultDate: "2012-01-02",
74 navigationAsDateFormat: true,
75 constrainInput: true
76 };
77 var start_dp = $("#start-date").datepicker(dateOptions);
78 start_dp.val(dateOptions.defaultDate);
79 dateOptions['defaultDate'] = "2012-12-31";
80 var end_dp= $("#end-date").datepicker(dateOptions);
81 end_dp.val(dateOptions.defaultDate);
82
83 // This little bit of code manages period checks of the asynchronous query manager,
84 // which holds onto handles asynchornously received. We can set the handle update
85 // frequency using seconds, and it will let us know when it is ready.
86 var intervalID = setInterval(
87 function() {
88 asynchronousQueryIntervalUpdate();
89 },
90 asynchronousQueryGetInterval()
91 );
92
93 // UI Elements - Creates map and location auto-complete
94 onOpenExploreMap();
95 var mapOptions = {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -070096 center: new google.maps.LatLng(38.89, -77.03),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070097 zoom: 4,
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -070098 mapTypeId: google.maps.MapTypeId.ROADMAP,
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -070099 streetViewControl: false,
100 draggable : false
101 };
102 map = new google.maps.Map(document.getElementById('map_canvas'), mapOptions);
103
104 var input = document.getElementById('location-text-box');
105 var autocomplete = new google.maps.places.Autocomplete(input);
106 autocomplete.bindTo('bounds', map);
107
108 google.maps.event.addListener(autocomplete, 'place_changed', function() {
109 var place = autocomplete.getPlace();
110 if (place.geometry.viewport) {
111 map.fitBounds(place.geometry.viewport);
112 } else {
113 map.setCenter(place.geometry.location);
114 map.setZoom(17); // Why 17? Because it looks good.
115 }
116 var address = '';
117 if (place.address_components) {
118 address = [(place.address_components[0] && place.address_components[0].short_name || ''),
119 (place.address_components[1] && place.address_components[1].short_name || ''),
120 (place.address_components[2] && place.address_components[2].short_name || '') ].join(' ');
121 }
122 });
123
124 // UI Elements - Selection Rectangle Drawing
125 shouldDraw = false;
126 var startLatLng;
127 selectionRect = null;
128 var selectionRadio = $("#selection-button");
129 var firstClick = true;
130
131 google.maps.event.addListener(map, 'mousedown', function (event) {
132 // only allow drawing if selection is selected
133 if (selectionRadio.hasClass("active")) {
134 startLatLng = event.latLng;
135 shouldDraw = true;
136 }
137 });
138
139 google.maps.event.addListener(map, 'mousemove', drawRect);
140 function drawRect (event) {
141 if (shouldDraw) {
142 if (!selectionRect) {
143 var selectionRectOpts = {
144 bounds: new google.maps.LatLngBounds(startLatLng, event.latLng),
145 map: map,
146 strokeWeight: 1,
147 strokeColor: "2b3f8c",
148 fillColor: "2b3f8c"
149 };
150 selectionRect = new google.maps.Rectangle(selectionRectOpts);
151 google.maps.event.addListener(selectionRect, 'mouseup', function () {
152 shouldDraw = false;
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700153 });
154 } else {
155 if (startLatLng.lng() < event.latLng.lng()) {
156 selectionRect.setBounds(new google.maps.LatLngBounds(startLatLng, event.latLng));
157 } else {
158 selectionRect.setBounds(new google.maps.LatLngBounds(event.latLng, startLatLng));
159 }
160 }
161 }
162 };
163
164 // UI Elements - Toggle location search style by location or by map selection
165 $('#selection-button').on('click', function (e) {
166 $("#location-text-box").attr("disabled", "disabled");
167 if (selectionRect) {
168 selectionRect.setMap(map);
169 }
170 });
171 $('#location-button').on('click', function (e) {
172 $("#location-text-box").removeAttr("disabled");
173 if (selectionRect) {
174 selectionRect.setMap(null);
175 }
176 });
177
178 // UI Elements - Tweetbook Management
179 $('.dropdown-menu a.holdmenu').click(function(e) {
180 e.stopPropagation();
181 });
182
183 $('#new-tweetbook-button').on('click', function (e) {
184 onCreateNewTweetBook($('#new-tweetbook-entry').val());
185
186 $('#new-tweetbook-entry').val($('#new-tweetbook-entry').attr('placeholder'));
187 });
188
189 // UI Element - Query Submission
190 $("#submit-button").button().click(function () {
191 // Clear current map on trigger
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700192
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700193 $("#submit-button").attr("disabled", true);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700194
195 // gather all of the data from the inputs
196 var kwterm = $("#keyword-textbox").val();
197 var startdp = $("#start-date").datepicker("getDate");
198 var enddp = $("#end-date").datepicker("getDate");
199 var startdt = $.datepicker.formatDate("yy-mm-dd", startdp)+"T00:00:00Z";
200 var enddt = $.datepicker.formatDate("yy-mm-dd", enddp)+"T23:59:59Z";
201
202 var formData = {
203 "keyword": kwterm,
204 "startdt": startdt,
205 "enddt": enddt,
206 "gridlat": $("#grid-lat-slider").slider("value"),
207 "gridlng": $("#grid-lng-slider").slider("value")
208 };
209
210 // Get Map Bounds
211 var bounds;
212 if ($('#selection-button').hasClass("active") && selectionRect) {
213 bounds = selectionRect.getBounds();
214 } else {
215 bounds = map.getBounds();
216 }
217
218 formData["swLat"] = Math.abs(bounds.getSouthWest().lat());
219 formData["swLng"] = Math.abs(bounds.getSouthWest().lng());
220 formData["neLat"] = Math.abs(bounds.getNorthEast().lat());
221 formData["neLng"] = Math.abs(bounds.getNorthEast().lng());
222
223 var build_cherry_mode = "synchronous";
224
225 if ($('#asbox').is(":checked")) {
226 build_cherry_mode = "asynchronous";
227 }
228
229 var f = buildAQLQueryFromForm(formData);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700230
231 if (build_cherry_mode == "synchronous") {
232 A.query(f.val(), cherryQuerySyncCallback, build_cherry_mode);
233 } else {
234 A.query(f.val(), cherryQueryAsyncCallback, build_cherry_mode);
235 }
236
237 APIqueryTracker = {
238 "query" : "use dataverse twitter;\n" + f.val(),
239 "data" : formData
240 };
241
242 $('#dialog').html(APIqueryTracker["query"]);
243
244 if (!$('#asbox').is(":checked")) {
245 $('#show-query-button').attr("disabled", false);
246 } else {
247 $('#show-query-button').attr("disabled", true);
248 }
249 });
250});
251
252
253function buildAQLQueryFromForm(parameters) {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700254
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700255 var bounds = {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700256 "ne" : { "lat" : parameters["neLat"], "lng" : -1*parameters["neLng"]},
257 "sw" : { "lat" : parameters["swLat"], "lng" : -1*parameters["swLng"]}
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700258 };
259
260 var rectangle =
261 new FunctionExpression("create-rectangle",
262 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
263 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
264
265
266 var aql = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700267 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700268 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
269 .LetClause("$region", rectangle)
270 .WhereClause().and(
271 new FunctionExpression("spatial-intersect", "$t.sender-location", "$region"),
272 new AExpression('$t.send-time > datetime("' + parameters["startdt"] + '")'),
273 new AExpression('$t.send-time < datetime("' + parameters["enddt"] + '")'),
274 new FunctionExpression("contains", "$t.message-text", "$keyword")
275 )
276 .GroupClause(
277 "$c",
278 new FunctionExpression("spatial-cell", "$t.sender-location",
279 new FunctionExpression("create-point", "24.5", "-125.5"),
280 parameters["gridlat"].toFixed(1), parameters["gridlng"].toFixed(1)),
281 "with",
282 "$t"
283 )
284 .ReturnClause({ "cell" : "$c", "count" : "count($t)" });
285
286 return aql;
287}
288
289/** Asynchronous Query Management **/
290
291
292/**
293* Checks through each asynchronous query to see if they are ready yet
294*/
295function asynchronousQueryIntervalUpdate() {
296 for (var handle_key in asyncQueryManager) {
297 if (!asyncQueryManager[handle_key].hasOwnProperty("ready")) {
298 asynchronousQueryGetAPIQueryStatus( asyncQueryManager[handle_key]["handle"], handle_key );
299 }
300 }
301}
302
303
304/**
305* Returns current time interval to check for asynchronous query readiness
306* @returns {number} milliseconds between asychronous query checks
307*/
308function asynchronousQueryGetInterval() {
309 var seconds = 10;
310 return seconds * 1000;
311}
312
313
314/**
315* Retrieves status of an asynchronous query, using an opaque result handle from API
316* @param {Object} handle, an object previously returned from an async call
317* @param {number} handle_id, the integer ID parsed from the handle object
318*/
319function asynchronousQueryGetAPIQueryStatus (handle, handle_id) {
320
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700321 A.query_status(
322 {
323 "handle" : JSON.stringify(handle)
324 },
325 function (res) {
326 if (res["status"] == "SUCCESS") {
327 // We don't need to check if this one is ready again, it's not going anywhere...
328 // Unless the life cycle of handles has changed drastically
329 asyncQueryManager[handle_id]["ready"] = true;
330
331 // Indicate success.
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700332 $('#handle_' + handle_id).removeClass("btn-disabled").prop('disabled', false).addClass("btn-success");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700333 }
334 }
335 );
336}
337
338
339/**
340* On-success callback after async API query
341* @param {object} res, a result object containing an opaque result handle to Asterix
342*/
343function cherryQueryAsyncCallback(res) {
344
345 // Parse handle, handle id and query from async call result
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700346 var handle_query = APIqueryTracker["query"];
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700347 var handle = res;
348 var handle_id = res["handle"].toString().split(',')[0];
349
350 // Add to stored map of existing handles
351 asyncQueryManager[handle_id] = {
352 "handle" : handle,
353 "query" : handle_query,
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700354 "data" : APIqueryTracker["data"]
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700355 };
356
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700357 // Create a container for this async query handle
358 $('<div/>')
359 .css("margin-left", "1em")
360 .css("margin-bottom", "1em")
361 .css("display", "block")
362 .attr({
363 "class" : "btn-group",
364 "id" : "async_container_" + handle_id
365 })
366 .appendTo("#async-handle-controls");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700367
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700368 // Adds the main button for this async handle
369 var handle_action_button = '<button class="btn btn-disabled" id="handle_' + handle_id + '">Handle ' + handle_id + '</button>';
370 $('#async_container_' + handle_id).append(handle_action_button);
371 $('#handle_' + handle_id).prop('disabled', true);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700372 $('#handle_' + handle_id).on('click', function (e) {
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700373
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700374 // make sure query is ready to be run
375 if (asyncQueryManager[handle_id]["ready"]) {
376
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700377 APIqueryTracker = {
378 "query" : asyncQueryManager[handle_id]["query"],
379 "data" : asyncQueryManager[handle_id]["data"]
380 };
381 $('#dialog').html(APIqueryTracker["query"]);
382
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -0700383 if (!asyncQueryManager[handle_id].hasOwnProperty("result")) {
384 // Generate new Asterix Core API Query
385 A.query_result(
386 { "handle" : JSON.stringify(asyncQueryManager[handle_id]["handle"]) },
387 function(res) {
388 asyncQueryManager[handle_id]["result"] = res;
389 cherryQuerySyncCallback(res);
390 }
391 );
392 } else {
393 cherryQuerySyncCallback(asyncQueryManager[handle_id]["result"]);
394 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700395 }
396 });
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700397
398 // Adds a removal button for this async handle
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700399 var asyncDeleteButton = addDeleteButton(
400 "trashhandle_" + handle_id,
401 "async_container_" + handle_id,
402 function (e) {
403 $('#async_container_' + handle_id).remove();
404 delete asyncQueryManager[handle_id];
405 }
406 );
407 $("#submit-button").attr("disabled", false);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700408}
409
410
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700411/**
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700412* returns a json object with keys: weight, latSW, lngSW, latNE, lngNE
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700413*
414* { "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 -0700415*/
416function getRecord(cell_count_record) {
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700417 // This is a really hacky way to pull out the digits, but it works for now.
418 var values = cell_count_record.replace("int64","").match(/[-+]?[0-9]*\.?[0-9]+/g);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700419 var record_representation = {};
420
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700421 record_representation["latSW"] = parseFloat(values[0]);
422 record_representation["lngSW"] = parseFloat(values[1]);
423 record_representation["latNE"] = parseFloat(values[2]);
424 record_representation["lngNE"] = parseFloat(values[3]);
425 record_representation["weight"] = parseInt(values[4]);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700426
427 return record_representation;
428}
429
430/**
431* A spatial data cleaning and mapping call
432* @param {Object} res, a result object from a cherry geospatial query
433*/
434function cherryQuerySyncCallback(res) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700435 records = res["results"];
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700436
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700437 if (typeof res["results"][0] == "object") {
438 records = res["results"][0];
439 }
440
441 var coordinates = [];
442 var weights = [];
443
444 for (var subrecord in records) {
445 var coordinate = getRecord(records[subrecord]);
446 weights.push(coordinate["weight"]);
447 coordinates.push(coordinate);
448 }
449
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700450 triggerUIUpdate(coordinates, weights);
451 $("#submit-button").attr("disabled", false);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700452}
453
454/**
455* Triggers a map update based on a set of spatial query result cells
456* @param [Array] mapPlotData, an array of coordinate and weight objects
457* @param [Array] params, an object containing original query parameters [LEGACY]
458* @param [Array] plotWeights, a list of weights of the spatial cells - e.g., number of tweets
459*/
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700460function triggerUIUpdate(mapPlotData, plotWeights) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700461 /** Clear anything currently on the map **/
462 mapWidgetClearMap();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700463
464 // Compute data point spread
465 var dataBreakpoints = mapWidgetLegendComputeNaturalBreaks(plotWeights);
466
467 $.each(mapPlotData, function (m, val) {
468
469 // Only map points in data range of top 4 natural breaks
470 if (mapPlotData[m].weight > dataBreakpoints[0]) {
471
472 // Get color value of legend
473 var mapColor = mapWidgetLegendGetHeatValue(mapPlotData[m].weight, dataBreakpoints);
474 var markerRadius = mapWidgetComputeCircleRadius(mapPlotData[m], dataBreakpoints);
475 var point_opacity = 1.0;
476
477 var point_center = new google.maps.LatLng(
478 (mapPlotData[m].latSW + mapPlotData[m].latNE)/2.0,
479 (mapPlotData[m].lngSW + mapPlotData[m].lngNE)/2.0);
480
481 // Create and plot marker
482 var map_circle_options = {
483 center: point_center,
484 radius: markerRadius,
485 map: map,
486 fillOpacity: point_opacity,
487 fillColor: mapColor,
488 clickable: true
489 };
490 var map_circle = new google.maps.Circle(map_circle_options);
491 map_circle.val = mapPlotData[m];
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700492
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700493 // Clicking on a circle drills down map to that value
494 google.maps.event.addListener(map_circle, 'click', function (event) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700495 onMapPointDrillDown(map_circle.val);
496 });
497
498 // Add this marker to global marker cells
499 map_cells.push(map_circle);
500 }
501 });
502
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -0700503 // Add a legend to the map
504 // TODO Remove widget for now mapControlWidgetAddLegend(dataBreakpoints);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700505}
506
507/**
508* prepares an Asterix API query to drill down in a rectangular spatial zone
509*
510* @params {object} marker_borders [LEGACY] a set of bounds for a region from a previous api result
511*/
512function onMapPointDrillDown(marker_borders) {
513 var zoneData = APIqueryTracker["data"];
514
515 var zswBounds = new google.maps.LatLng(marker_borders.latSW, marker_borders.lngSW);
516 var zneBounds = new google.maps.LatLng(marker_borders.latNE, marker_borders.lngNE);
517
518 var zoneBounds = new google.maps.LatLngBounds(zswBounds, zneBounds);
519 zoneData["swLat"] = zoneBounds.getSouthWest().lat();
520 zoneData["swLng"] = zoneBounds.getSouthWest().lng();
521 zoneData["neLat"] = zoneBounds.getNorthEast().lat();
522 zoneData["neLng"] = zoneBounds.getNorthEast().lng();
523 var zB = {
524 "sw" : {
525 "lat" : zoneBounds.getSouthWest().lat(),
526 "lng" : zoneBounds.getSouthWest().lng()
527 },
528 "ne" : {
529 "lat" : zoneBounds.getNorthEast().lat(),
530 "lng" : zoneBounds.getNorthEast().lng()
531 }
532 };
533
534 mapWidgetClearMap();
535
536 var customBounds = new google.maps.LatLngBounds();
537 var zoomSWBounds = new google.maps.LatLng(zoneData["swLat"], zoneData["swLng"]);
538 var zoomNEBounds = new google.maps.LatLng(zoneData["neLat"], zoneData["neLng"]);
539 customBounds.extend(zoomSWBounds);
540 customBounds.extend(zoomNEBounds);
541 map.fitBounds(customBounds);
542
543 var df = getDrillDownQuery(zoneData, zB);
544
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700545 APIqueryTracker = {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700546 "query_string" : "use dataverse twitter;\n" + df.val(),
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700547 "marker_path" : "static/img/mobile2.png",
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700548 "on_clean_result" : onCleanTweetbookDrilldown,
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700549 };
550
551 A.query(df.val(), onTweetbookQuerySuccessPlot);
552}
553
554function getDrillDownQuery(parameters, bounds) {
555
556 var zoomRectangle = new FunctionExpression("create-rectangle",
557 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
558 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
559
560 var drillDown = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700561 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700562 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
563 .LetClause("$region", zoomRectangle)
564 .WhereClause().and(
565 new FunctionExpression('spatial-intersect', '$t.sender-location', '$region'),
566 new AExpression().set('$t.send-time > datetime("' + parameters["startdt"] + '")'),
567 new AExpression().set('$t.send-time < datetime("' + parameters["enddt"] + '")'),
568 new FunctionExpression('contains', '$t.message-text', '$keyword')
569 )
570 .ReturnClause({
571 "tweetId" : "$t.tweetid",
572 "tweetText" : "$t.message-text",
573 "tweetLoc" : "$t.sender-location"
574 });
575
576 return drillDown;
577}
578
579
580function addTweetbookCommentDropdown(appendToDiv) {
581
582 // Creates a div to manage a radio button set of chosen tweetbooks
583 $('<div/>')
584 .attr("class","btn-group chosen-tweetbooks")
585 .attr("data-toggle", "buttons-radio")
586 .css("margin-bottom", "10px")
587 .attr("id", "metacomment-tweetbooks")
588 .appendTo(appendToDiv);
589
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700590 var highlighted = "";
591 if (APIqueryTracker.hasOwnProperty("active_tweetbook")) {
592 highlighted = APIqueryTracker["active_tweetbook"];
593 }
594
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700595 // For each existing tweetbook from review mode, adds a radio button option.
596 $('#metacomment-tweetbooks').append('<input type="hidden" id="target-tweetbook" value="" />');
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700597 for (var rmt in review_mode_tweetbooks) {
598
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700599 var tweetbook_option = '<button type="button" class="btn">' + review_mode_tweetbooks[rmt] + '</button>';
600
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700601 if (review_mode_tweetbooks[rmt] == highlighted) {
602 tweetbook_option = '<button type="button" class="btn btn-info">' + review_mode_tweetbooks[rmt] + '</button>';
603 }
604
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700605 $('#metacomment-tweetbooks').append(tweetbook_option + '<br/>');
606 }
607
608 // Creates a button + input combination to add tweet comment to new tweetbook
609 var new_tweetbook_option = '<button type="button" class="btn" id="new-tweetbook-target-m"></button>' +
610 '<input type="text" id="new-tweetbook-entry-m" placeholder="Add to new tweetbook..."><br/>';
611 $('#metacomment-tweetbooks').append(new_tweetbook_option);
612
613 $("#new-tweetbook-entry-m").keyup(function() {
614 $("#new-tweetbook-target-m").val($("#new-tweetbook-entry-m").val());
615 $("#new-tweetbook-target-m").text($("#new-tweetbook-entry-m").val());
616 });
617
618 // There is a hidden input (id = target-tweetbook) which is used to track the value
619 // of the tweetbook to which the comment on this tweet will be added.
620 $(".chosen-tweetbooks .btn").click(function() {
621 $("#target-tweetbook").val($(this).text());
622 });
623}
624
625function onDrillDownAtLocation(tO) {
626
627 var tweetId = tO["tweetEntryId"];
628 var tweetText = tO["tweetText"];
629
630 var tweetContainerId = '#drilldown_modal_body';
631 var tweetDiv = '<div id="drilltweetobj' + tweetId + '"></div>';
632
633 $(tweetContainerId).empty();
634 $(tweetContainerId).append(tweetDiv);
635 $('#drilltweetobj' + tweetId).append('<p>Tweet #' + tweetId + ": " + tweetText + '</p>');
636
637 // Add comment field
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700638 $('#drilltweetobj' + tweetId).append('<input class="textbox" type="text" id="metacomment' + tweetId + '">');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700639
640 if (tO.hasOwnProperty("tweetComment")) {
641 $("#metacomment" + tweetId).val(tO["tweetComment"]);
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700642
643 var deleteThisComment = addDeleteButton(
644 "deleteLiveComment_" + tweetId,
645 "drilltweetobj" + tweetId,
646 function () {
647
648 // TODO Maybe this should fire differnetly if another tweetbook is selected?
649
650 // Send comment deletion to asterix
651 var deleteTweetCommentOnId = '"' + tweetId + '"';
652 var toDelete = new DeleteStatement(
653 "$mt",
654 APIqueryTracker["active_tweetbook"],
655 new AExpression("$mt.tweetid = " + deleteTweetCommentOnId.toString())
656 );
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700657 A.update(
658 toDelete.val()
659 );
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700660
661 // Hide comment from map
662 $('#drilldown_modal').modal('hide');
663
664 // Replot tweetbook
665 onPlotTweetbook(APIqueryTracker["active_tweetbook"]);
666 }
667 );
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700668 }
669
670 addTweetbookCommentDropdown('#drilltweetobj' + tweetId);
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700671
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700672 $('#drilltweetobj' + tweetId).append('<br/><button type="button" class="btn" id="add-metacomment">Save Comment</button>');
673
674 $('#add-metacomment').button().click(function () {
675 var save_metacomment_target_tweetbook = $("#target-tweetbook").val();
676 var save_metacomment_target_comment = '"' + $("#metacomment" + tweetId).val() + '"';
677 var save_metacomment_target_tweet = '"' + tweetId + '"';
678
679 if (save_metacomment_target_tweetbook.length == 0) {
680 alert("Please choose a tweetbook.");
681
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700682 } else {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700683
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700684 if (!(existsTweetbook(save_metacomment_target_tweetbook))) {
685 onCreateNewTweetBook(save_metacomment_target_tweetbook);
686 }
687
688 var toDelete = new DeleteStatement(
689 "$mt",
690 save_metacomment_target_tweetbook,
691 new AExpression("$mt.tweetid = " + save_metacomment_target_tweet.toString())
692 );
693
694 A.update(toDelete.val());
695
696 var toInsert = new InsertStatement(
697 save_metacomment_target_tweetbook,
698 {
699 "tweetid" : save_metacomment_target_tweet.toString(),
700 "comment-text" : save_metacomment_target_comment
701 }
702 );
703
704 // Insert query to add metacomment to said tweetbook dataset
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700705 A.update(toInsert.val(), function () { alert("Test"); });
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700706
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700707 // TODO Some stress testing of error conditions might be good here...
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -0700708 if (APIqueryTracker.hasOwnProperty("active_tweetbook")) {
709 onPlotTweetbook(APIqueryTracker["active_tweetbook"]);
710 };
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700711 var successMessage = "Saved comment on <b>Tweet #" + tweetId +
712 "</b> in dataset <b>" + save_metacomment_target_tweetbook + "</b>.";
713 addSuccessBlock(successMessage, 'drilltweetobj' + tweetId);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700714 }
715 });
716
717 // Set width of tweetbook buttons
718 $(".chosen-tweetbooks .btn").css("width", "200px");
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700719 $(".chosen-tweetbooks .btn").css("height", "2em");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700720}
721
722
723/**
724* Adds a new tweetbook entry to the menu and creates a dataset of type TweetbookEntry.
725*/
726function onCreateNewTweetBook(tweetbook_title) {
727
728 var tweetbook_title = tweetbook_title.split(' ').join('_');
729
730 A.ddl(
731 "create dataset " + tweetbook_title + "(TweetbookEntry) primary key tweetid;",
732 function () {}
733 );
734
735 if (!(existsTweetbook(tweetbook_title))) {
736 review_mode_tweetbooks.push(tweetbook_title);
737 addTweetBookDropdownItem(tweetbook_title);
738 }
739}
740
741
742function onDropTweetBook(tweetbook_title) {
743
744 // AQL Call
745 A.ddl(
746 "drop dataset " + tweetbook_title + " if exists;",
747 function () {}
748 );
749
750 // Removes tweetbook from review_mode_tweetbooks
751 var remove_position = $.inArray(tweetbook_title, review_mode_tweetbooks);
752 if (remove_position >= 0) review_mode_tweetbooks.splice(remove_position, 1);
753
754 // Clear UI with review tweetbook titles
755 $('#review-tweetbook-titles').html('');
756 for (r in review_mode_tweetbooks) {
757 addTweetBookDropdownItem(review_mode_tweetbooks[r]);
758 }
759}
760
761
762function addTweetBookDropdownItem(tweetbook) {
763 // Add placeholder for this tweetbook
764 $('<div/>')
765 .css("padding-left", "1em")
766 .attr({
767 "class" : "btn-group",
768 "id" : "rm_holder_" + tweetbook
769 }).appendTo("#review-tweetbook-titles");
770 $("#review-tweetbook-titles").append('<br/>');
771
772 // Add plotting button for this tweetbook
773 var plot_button = '<button class="btn" id="rm_plotbook_' + tweetbook + '">' + tweetbook + '</button>';
774 $("#rm_holder_" + tweetbook).append(plot_button);
775 $("#rm_plotbook_" + tweetbook).on('click', function(e) {
776 onPlotTweetbook(tweetbook);
777 });
778
779 // Add trash button for this tweetbook
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700780 var onTrashTweetbookButton = addDeleteButton(
781 "rm_trashbook_" + tweetbook,
782 "rm_holder_" + tweetbook,
783 function(e) {
784 onDropTweetBook(tweetbook);
785 }
786 );
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700787}
788
789
790function onPlotTweetbook(tweetbook) {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700791
792 // Clear map for this one
793 mapWidgetResetMap();
794
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700795 var plotTweetQuery = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700796 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700797 .ForClause("$m", new AExpression("dataset " + tweetbook))
798 .WhereClause(new AExpression("$m.tweetid = $t.tweetid"))
799 .ReturnClause({
800 "tweetId" : "$m.tweetid",
801 "tweetText" : "$t.message-text",
802 "tweetLoc" : "$t.sender-location",
803 "tweetCom" : "$m.comment-text"
804 });
805
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700806 APIqueryTracker = {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700807 "query_string" : "use dataverse twitter;\n" + plotTweetQuery.val(),
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700808 "marker_path" : "static/img/mobile_green2.png",
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700809 "on_clean_result" : onCleanPlotTweetbook,
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700810 "active_tweetbook" : tweetbook
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700811 };
812
813 A.query(plotTweetQuery.val(), onTweetbookQuerySuccessPlot);
814}
815
816
817function onTweetbookQuerySuccessPlot (res) {
818
819 var records = res["results"];
820
821 var coordinates = [];
822 map_tweet_markers = [];
823 map_tweet_overlays = [];
824 drilldown_data_map = {};
825 drilldown_data_map_vals = {};
826
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700827 var micon = APIqueryTracker["marker_path"];
828 var marker_click_function = onClickTweetbookMapMarker;
829 var clean_result_function = APIqueryTracker["on_clean_result"];
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700830
831 coordinates = clean_result_function(records);
832
833 for (var dm in coordinates) {
834 var keyLat = coordinates[dm].tweetLat.toString();
835 var keyLng = coordinates[dm].tweetLng.toString();
836 if (!drilldown_data_map.hasOwnProperty(keyLat)) {
837 drilldown_data_map[keyLat] = {};
838 }
839 if (!drilldown_data_map[keyLat].hasOwnProperty(keyLng)) {
840 drilldown_data_map[keyLat][keyLng] = [];
841 }
842 drilldown_data_map[keyLat][keyLng].push(coordinates[dm]);
843 drilldown_data_map_vals[coordinates[dm].tweetEntryId.toString()] = coordinates[dm];
844 }
845
846 $.each(drilldown_data_map, function(drillKeyLat, valuesAtLat) {
847 $.each(drilldown_data_map[drillKeyLat], function (drillKeyLng, valueAtLng) {
848
849 // Get subset of drilldown position on map
850 var cposition = new google.maps.LatLng(parseFloat(drillKeyLat), parseFloat(drillKeyLng));
851
852 // Create a marker using the snazzy phone icon
853 var map_tweet_m = new google.maps.Marker({
854 position: cposition,
855 map: map,
856 icon: micon,
857 clickable: true,
858 });
859
860 // Open Tweet exploration window on click
861 google.maps.event.addListener(map_tweet_m, 'click', function (event) {
862 marker_click_function(drilldown_data_map[drillKeyLat][drillKeyLng]);
863 });
864
865 // Add marker to index of tweets
866 map_tweet_markers.push(map_tweet_m);
867
868 });
869 });
870}
871
872
873function existsTweetbook(tweetbook) {
874 if (parseInt($.inArray(tweetbook, review_mode_tweetbooks)) == -1) {
875 return false;
876 } else {
877 return true;
878 }
879}
880
881
882function onCleanPlotTweetbook(records) {
883 var toPlot = [];
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700884
885 // An entry looks like this:
886 // { "tweetId": "273589", "tweetText": " like verizon the network is amazing", "tweetLoc": { point: [37.78, 82.27]}, "tweetCom": "hooray comments" }
887
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700888 for (var entry in records) {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700889
890 var points = records[entry].split("point:")[1].match(/[-+]?[0-9]*\.?[0-9]+/g);
891
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700892 var tweetbook_element = {
893 "tweetEntryId" : parseInt(records[entry].split(",")[0].split(":")[1].split('"')[1]),
894 "tweetText" : records[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700895 "tweetLat" : parseFloat(points[0]),
896 "tweetLng" : parseFloat(points[1]),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700897 "tweetComment" : records[entry].split("tweetCom\": \"")[1].split("\"")[0]
898 };
899 toPlot.push(tweetbook_element);
900 }
901
902 return toPlot;
903}
904
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700905
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700906function onCleanTweetbookDrilldown (rec) {
907
908 var drilldown_cleaned = [];
909
910 for (var entry = 0; entry < rec.length; entry++) {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700911
912 // An entry looks like this:
913 // { "tweetId": "105491", "tweetText": " hate verizon its platform is OMG", "tweetLoc": { point: [30.55, 71.44]} }
914 var points = rec[entry].split("point:")[1].match(/[-+]?[0-9]*\.?[0-9]+/g);
915
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700916 var drill_element = {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700917 "tweetEntryId" : parseInt(rec[entry].split(",")[0].split(":")[1].replace('"', '')),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700918 "tweetText" : rec[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700919 "tweetLat" : parseFloat(points[0]),
920 "tweetLng" : parseFloat(points[1])
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700921 };
922 drilldown_cleaned.push(drill_element);
923 }
924 return drilldown_cleaned;
925}
926
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700927
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700928function onClickTweetbookMapMarker(tweet_arr) {
929 $('#drilldown_modal_body').html('');
930
931 // Clear existing display
932 $.each(tweet_arr, function (t, valueT) {
933 var tweet_obj = tweet_arr[t];
934 onDrillDownAtLocation(tweet_obj);
935 });
936
937 $('#drilldown_modal').modal('show');
938}
939
940/** Toggling Review and Explore Modes **/
941
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700942
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700943/**
944* Explore mode: Initial map creation and screen alignment
945*/
946function onOpenExploreMap () {
947 var explore_column_height = $('#explore-well').height();
948 $('#map_canvas').height(explore_column_height + "px");
949 $('#review-well').height(explore_column_height + "px");
950 $('#review-well').css('max-height', explore_column_height + "px");
951 var pad = $('#review-well').innerHeight() - $('#review-well').height();
952 var prev_window_target = $('#review-well').height() - 20 - $('#group-tweetbooks').innerHeight() - $('#group-background-query').innerHeight() - 2*pad;
953 $('#query-preview-window').height(prev_window_target +'px');
954}
955
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700956
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700957/**
958* Launching explore mode: clear windows/variables, show correct sidebar
959*/
960function onLaunchExploreMode() {
961 $('#review-active').removeClass('active');
962 $('#review-well').hide();
963
964 $('#explore-active').addClass('active');
965 $('#explore-well').show();
966
967 $("#clear-button").trigger("click");
968}
969
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700970
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700971/**
972* Launching review mode: clear windows/variables, show correct sidebar
973*/
974function onLaunchReviewMode() {
975 $('#explore-active').removeClass('active');
976 $('#explore-well').hide();
977 $('#review-active').addClass('active');
978 $('#review-well').show();
979
980 $("#clear-button").trigger("click");
981}
982
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700983
984/** Icon / Interface Utility Methods **/
985
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700986/**
987* Creates a delete icon button using default trash icon
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700988* @param {String} id, id for this element
989* @param {String} attachTo, id string of an element to which I can attach this button.
990* @param {Function} onClick, a function to fire when this icon is clicked
991*/
992function addDeleteButton(iconId, attachTo, onClick) {
993 // Icon structure
994 var trashIcon = '<button class="btn" id="' + iconId + '"><i class="icon-trash"></i></button>';
995
996 $('#' + attachTo).append(trashIcon);
997
998 // On Click behavior
999 $('#' + iconId).on('click', onClick);
1000}
1001
1002
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001003/**
1004* Creates a success message and attaches it to a div with provided ID.
1005* @param {String} message, a message to post
1006* @param {String} appendTarget, a target div to which to append the alert
1007*/
1008function addSuccessBlock(message, appendTarget) {
1009
1010 $('<div/>')
1011 .attr("class", "alert alert-success")
1012 .html('<button type="button" class="close" data-dismiss="alert">&times;</button>' + message)
1013 .appendTo('#' + appendTarget);
1014}
1015
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001016/** Map Widget Utility Methods **/
1017
1018/**
1019* Plots a legend onto the map, with values in progress bars
1020* @param {number Array} breakpoints, an array of numbers representing natural breakpoints
1021*/
1022function mapControlWidgetAddLegend(breakpoints) {
1023
1024 // Retriever colors, lightest to darkest
1025 var colors = mapWidgetGetColorPalette();
1026
1027 // Initial div structure
1028 $("#map_canvas_legend").html('<div id="legend-holder"><div id="legend-progress-bar" class="progress"></div><span id="legend-label"></span></div>');
1029
1030 // Add color scale to legend
1031 $('#legend-progress-bar').css("width", "200px").html('');
1032
1033 // Add a progress bar for each color
1034 for (var color in colors) {
1035
1036 // Bar values
1037 var upperBound = breakpoints[parseInt(color) + 1];
1038
1039 // Create Progress Bar
1040 $('<div/>')
1041 .attr("class", "bar")
1042 .attr("id", "pbar" + color)
1043 .css("width" , '25.0%')
1044 .html("< " + upperBound)
1045 .appendTo('#legend-progress-bar');
1046
1047 $('#pbar' + color).css({
1048 "background-image" : 'none',
1049 "background-color" : colors[parseInt(color)]
1050 });
1051
1052 // Attach a message showing minimum bounds
1053 $('#legend-label').html('Regions with at least ' + breakpoints[0] + ' tweets');
1054 $('#legend-label').css({
1055 "margin-top" : 0,
1056 "color" : "black"
1057 });
1058 }
1059
1060 // Add legend to map
1061 map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push(document.getElementById('legend-holder'));
1062 $('#map_canvas_legend').show();
1063}
1064
1065/**
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001066* Clears ALL map elements - legend, plotted items, overlays
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001067*/
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001068function mapWidgetResetMap() {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001069
1070 if (selectionRect) {
1071 selectionRect.setMap(null);
1072 selectionRect = null;
1073 }
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001074
1075 mapWidgetClearMap();
1076
1077 // Reset map center and zoom
1078 map.setCenter(new google.maps.LatLng(38.89, -77.03));
1079 map.setZoom(4);
1080}
1081
1082function mapWidgetClearMap() {
1083
1084 // Remove previously plotted data/markers
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001085 for (c in map_cells) {
1086 map_cells[c].setMap(null);
1087 }
1088 map_cells = [];
1089 for (m in map_tweet_markers) {
1090 map_tweet_markers[m].setMap(null);
1091 }
1092 map_tweet_markers = [];
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001093
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001094 // Remove legend from map
1095 map.controls[google.maps.ControlPosition.LEFT_BOTTOM].clear();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001096}
1097
1098/**
1099* Uses jenks algorithm in geostats library to find natural breaks in numeric data
1100* @param {number Array} weights of points to plot
1101* @returns {number Array} array of natural breakpoints, of which the top 4 subsets will be plotted
1102*/
1103function mapWidgetLegendComputeNaturalBreaks(weights) {
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001104
1105 if (weights.length < 10) {
1106 return [0];
1107 }
1108
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001109 var plotDataWeights = new geostats(weights.sort());
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001110 return plotDataWeights.getJenks(6).slice(2,7);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001111}
1112
1113/**
1114* Computes values for map legend given a value and an array of jenks breakpoints
1115* @param {number} weight of point to plot on map
1116* @param {number Array} breakpoints, an array of 5 points corresponding to bounds of 4 natural ranges
1117* @returns {String} an RGB value corresponding to a subset of data
1118*/
1119function mapWidgetLegendGetHeatValue(weight, breakpoints) {
1120
1121 // Determine into which range the weight falls
1122 var weightColor = 0;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001123
1124 if (breakpoints.length == 1) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001125 weightColor = 2;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001126 } else {
1127 if (weight >= breakpoints[3]) {
1128 weightColor = 3;
1129 } else if (weight >= breakpoints[2]) {
1130 weightColor = 2;
1131 } else if (weight >= breakpoints[1]) {
1132 weightColor = 1;
1133 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001134 }
1135
1136 // Get default map color palette
1137 var colorValues = mapWidgetGetColorPalette();
1138 return colorValues[weightColor];
1139}
1140
1141/**
1142* Returns an array containing a 4-color palette, lightest to darkest
1143* External palette source: http://www.colourlovers.com/palette/2763366/s_i_l_e_n_c_e_r
1144* @returns {Array} [colors]
1145*/
1146function mapWidgetGetColorPalette() {
1147 return [
1148 "rgb(115,189,158)",
1149 "rgb(74,142,145)",
1150 "rgb(19,93,96)",
1151 "rgb(7,51,46)"
1152 ];
1153}
1154
1155/**
1156* Computes radius for a given data point from a spatial cell
1157* @param {Object} keys => ["latSW" "lngSW" "latNE" "lngNE" "weight"]
1158* @returns {number} radius between 2 points in metres
1159*/
1160function mapWidgetComputeCircleRadius(spatialCell, breakpoints) {
1161
1162 var weight = spatialCell.weight;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001163
1164 if (breakpoints.length == 1) {
1165 var weightColor = 0.25;
1166 } else {
1167 // Compute weight color
1168 var weightColor = 0.25;
1169 if (weight >= breakpoints[3]) {
1170 weightColor = 1.0;
1171 } else if (weight >= breakpoints[2]) {
1172 weightColor = 0.75;
1173 } else if (weight >= breakpoints[1]) {
1174 weightColor = 0.5;
1175 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001176 }
1177
1178 // Define Boundary Points
1179 var point_center = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1180 var point_left = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, spatialCell.lngSW);
1181 var point_top = new google.maps.LatLng(spatialCell.latNE, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1182
1183 // TODO not actually a weight color :)
1184 return weightColor * 1000 * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
1185}
1186
1187/** External Utility Methods **/
1188
1189/**
1190 * Calculates the distance between two latlng locations in km.
1191 * @see http://www.movable-type.co.uk/scripts/latlong.html
1192 *
1193 * @param {google.maps.LatLng} p1 The first lat lng point.
1194 * @param {google.maps.LatLng} p2 The second lat lng point.
1195 * @return {number} The distance between the two points in km.
1196 * @private
1197*/
1198function distanceBetweenPoints_(p1, p2) {
1199 if (!p1 || !p2) {
1200 return 0;
1201 }
1202
1203 var R = 6371; // Radius of the Earth in km
1204 var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
1205 var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
1206 var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
1207 Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
1208 Math.sin(dLon / 2) * Math.sin(dLon / 2);
1209 var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1210 var d = R * c;
1211 return d;
1212};