blob: ad92e79b08379e3c7fe606fc92d6cbe901e22dd8 [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 {
genia.likes.science@gmail.com4ada78c2013-09-07 13:53:48 -0700155
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700156 if (startLatLng.lng() < event.latLng.lng()) {
157 selectionRect.setBounds(new google.maps.LatLngBounds(startLatLng, event.latLng));
158 } else {
159 selectionRect.setBounds(new google.maps.LatLngBounds(event.latLng, startLatLng));
160 }
161 }
162 }
163 };
164
165 // UI Elements - Toggle location search style by location or by map selection
166 $('#selection-button').on('click', function (e) {
167 $("#location-text-box").attr("disabled", "disabled");
168 if (selectionRect) {
169 selectionRect.setMap(map);
170 }
171 });
172 $('#location-button').on('click', function (e) {
173 $("#location-text-box").removeAttr("disabled");
174 if (selectionRect) {
175 selectionRect.setMap(null);
176 }
177 });
178
179 // UI Elements - Tweetbook Management
180 $('.dropdown-menu a.holdmenu').click(function(e) {
181 e.stopPropagation();
182 });
183
184 $('#new-tweetbook-button').on('click', function (e) {
185 onCreateNewTweetBook($('#new-tweetbook-entry').val());
186
187 $('#new-tweetbook-entry').val($('#new-tweetbook-entry').attr('placeholder'));
188 });
189
190 // UI Element - Query Submission
191 $("#submit-button").button().click(function () {
192 // Clear current map on trigger
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700193
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700194 $("#submit-button").attr("disabled", true);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700195
196 // gather all of the data from the inputs
197 var kwterm = $("#keyword-textbox").val();
198 var startdp = $("#start-date").datepicker("getDate");
199 var enddp = $("#end-date").datepicker("getDate");
200 var startdt = $.datepicker.formatDate("yy-mm-dd", startdp)+"T00:00:00Z";
201 var enddt = $.datepicker.formatDate("yy-mm-dd", enddp)+"T23:59:59Z";
202
203 var formData = {
204 "keyword": kwterm,
205 "startdt": startdt,
206 "enddt": enddt,
207 "gridlat": $("#grid-lat-slider").slider("value"),
208 "gridlng": $("#grid-lng-slider").slider("value")
209 };
210
211 // Get Map Bounds
212 var bounds;
213 if ($('#selection-button').hasClass("active") && selectionRect) {
214 bounds = selectionRect.getBounds();
215 } else {
216 bounds = map.getBounds();
217 }
218
genia.likes.science@gmail.com4ada78c2013-09-07 13:53:48 -0700219 var swLat = Math.abs(bounds.getSouthWest().lat());
220 var swLng = Math.abs(bounds.getSouthWest().lng());
221 var neLat = Math.abs(bounds.getNorthEast().lat());
222 var neLng = Math.abs(bounds.getNorthEast().lng());
223
224 formData["swLat"] = Math.min(swLat, neLat);
225 formData["swLng"] = Math.max(swLng, neLng);
226 formData["neLat"] = Math.max(swLat, neLat);
227 formData["neLng"] = Math.min(swLng, neLng);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700228
229 var build_cherry_mode = "synchronous";
230
231 if ($('#asbox').is(":checked")) {
232 build_cherry_mode = "asynchronous";
233 }
234
235 var f = buildAQLQueryFromForm(formData);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700236
237 if (build_cherry_mode == "synchronous") {
238 A.query(f.val(), cherryQuerySyncCallback, build_cherry_mode);
239 } else {
240 A.query(f.val(), cherryQueryAsyncCallback, build_cherry_mode);
241 }
242
243 APIqueryTracker = {
244 "query" : "use dataverse twitter;\n" + f.val(),
245 "data" : formData
246 };
247
248 $('#dialog').html(APIqueryTracker["query"]);
249
250 if (!$('#asbox').is(":checked")) {
251 $('#show-query-button').attr("disabled", false);
252 } else {
253 $('#show-query-button').attr("disabled", true);
254 }
255 });
256});
257
258
259function buildAQLQueryFromForm(parameters) {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700260
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700261 var bounds = {
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700262 "ne" : { "lat" : parameters["neLat"], "lng" : -1*parameters["neLng"]},
263 "sw" : { "lat" : parameters["swLat"], "lng" : -1*parameters["swLng"]}
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700264 };
265
266 var rectangle =
267 new FunctionExpression("create-rectangle",
268 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
269 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
270
271
272 var aql = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700273 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700274 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
275 .LetClause("$region", rectangle)
276 .WhereClause().and(
277 new FunctionExpression("spatial-intersect", "$t.sender-location", "$region"),
278 new AExpression('$t.send-time > datetime("' + parameters["startdt"] + '")'),
279 new AExpression('$t.send-time < datetime("' + parameters["enddt"] + '")'),
280 new FunctionExpression("contains", "$t.message-text", "$keyword")
281 )
282 .GroupClause(
283 "$c",
284 new FunctionExpression("spatial-cell", "$t.sender-location",
285 new FunctionExpression("create-point", "24.5", "-125.5"),
286 parameters["gridlat"].toFixed(1), parameters["gridlng"].toFixed(1)),
287 "with",
288 "$t"
289 )
290 .ReturnClause({ "cell" : "$c", "count" : "count($t)" });
291
292 return aql;
293}
294
295/** Asynchronous Query Management **/
296
297
298/**
299* Checks through each asynchronous query to see if they are ready yet
300*/
301function asynchronousQueryIntervalUpdate() {
302 for (var handle_key in asyncQueryManager) {
303 if (!asyncQueryManager[handle_key].hasOwnProperty("ready")) {
304 asynchronousQueryGetAPIQueryStatus( asyncQueryManager[handle_key]["handle"], handle_key );
305 }
306 }
307}
308
309
310/**
311* Returns current time interval to check for asynchronous query readiness
312* @returns {number} milliseconds between asychronous query checks
313*/
314function asynchronousQueryGetInterval() {
315 var seconds = 10;
316 return seconds * 1000;
317}
318
319
320/**
321* Retrieves status of an asynchronous query, using an opaque result handle from API
322* @param {Object} handle, an object previously returned from an async call
323* @param {number} handle_id, the integer ID parsed from the handle object
324*/
325function asynchronousQueryGetAPIQueryStatus (handle, handle_id) {
326
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700327 A.query_status(
328 {
329 "handle" : JSON.stringify(handle)
330 },
331 function (res) {
332 if (res["status"] == "SUCCESS") {
333 // We don't need to check if this one is ready again, it's not going anywhere...
334 // Unless the life cycle of handles has changed drastically
335 asyncQueryManager[handle_id]["ready"] = true;
336
337 // Indicate success.
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700338 $('#handle_' + handle_id).removeClass("btn-disabled").prop('disabled', false).addClass("btn-success");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700339 }
340 }
341 );
342}
343
344
345/**
346* On-success callback after async API query
347* @param {object} res, a result object containing an opaque result handle to Asterix
348*/
349function cherryQueryAsyncCallback(res) {
350
351 // Parse handle, handle id and query from async call result
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700352 var handle_query = APIqueryTracker["query"];
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700353 var handle = res;
354 var handle_id = res["handle"].toString().split(',')[0];
355
356 // Add to stored map of existing handles
357 asyncQueryManager[handle_id] = {
358 "handle" : handle,
359 "query" : handle_query,
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700360 "data" : APIqueryTracker["data"]
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700361 };
362
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700363 // Create a container for this async query handle
364 $('<div/>')
365 .css("margin-left", "1em")
366 .css("margin-bottom", "1em")
367 .css("display", "block")
368 .attr({
369 "class" : "btn-group",
370 "id" : "async_container_" + handle_id
371 })
372 .appendTo("#async-handle-controls");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700373
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700374 // Adds the main button for this async handle
375 var handle_action_button = '<button class="btn btn-disabled" id="handle_' + handle_id + '">Handle ' + handle_id + '</button>';
376 $('#async_container_' + handle_id).append(handle_action_button);
377 $('#handle_' + handle_id).prop('disabled', true);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700378 $('#handle_' + handle_id).on('click', function (e) {
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700379
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700380 // make sure query is ready to be run
381 if (asyncQueryManager[handle_id]["ready"]) {
382
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700383 APIqueryTracker = {
384 "query" : asyncQueryManager[handle_id]["query"],
385 "data" : asyncQueryManager[handle_id]["data"]
386 };
387 $('#dialog').html(APIqueryTracker["query"]);
388
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -0700389 if (!asyncQueryManager[handle_id].hasOwnProperty("result")) {
390 // Generate new Asterix Core API Query
391 A.query_result(
392 { "handle" : JSON.stringify(asyncQueryManager[handle_id]["handle"]) },
393 function(res) {
394 asyncQueryManager[handle_id]["result"] = res;
395 cherryQuerySyncCallback(res);
396 }
397 );
398 } else {
399 cherryQuerySyncCallback(asyncQueryManager[handle_id]["result"]);
400 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700401 }
402 });
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700403
404 // Adds a removal button for this async handle
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700405 var asyncDeleteButton = addDeleteButton(
406 "trashhandle_" + handle_id,
407 "async_container_" + handle_id,
408 function (e) {
409 $('#async_container_' + handle_id).remove();
410 delete asyncQueryManager[handle_id];
411 }
412 );
413 $("#submit-button").attr("disabled", false);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700414}
415
416
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700417/**
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700418* returns a json object with keys: weight, latSW, lngSW, latNE, lngNE
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700419*
420* { "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 -0700421*/
422function getRecord(cell_count_record) {
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700423 // This is a really hacky way to pull out the digits, but it works for now.
424 var values = cell_count_record.replace("int64","").match(/[-+]?[0-9]*\.?[0-9]+/g);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700425 var record_representation = {};
426
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700427 record_representation["latSW"] = parseFloat(values[0]);
428 record_representation["lngSW"] = parseFloat(values[1]);
429 record_representation["latNE"] = parseFloat(values[2]);
430 record_representation["lngNE"] = parseFloat(values[3]);
431 record_representation["weight"] = parseInt(values[4]);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700432
433 return record_representation;
434}
435
436/**
437* A spatial data cleaning and mapping call
438* @param {Object} res, a result object from a cherry geospatial query
439*/
440function cherryQuerySyncCallback(res) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700441 records = res["results"];
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700442
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700443 if (typeof res["results"][0] == "object") {
444 records = res["results"][0];
445 }
446
447 var coordinates = [];
448 var weights = [];
449
450 for (var subrecord in records) {
451 var coordinate = getRecord(records[subrecord]);
452 weights.push(coordinate["weight"]);
453 coordinates.push(coordinate);
454 }
455
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700456 triggerUIUpdate(coordinates, weights);
457 $("#submit-button").attr("disabled", false);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700458}
459
460/**
461* Triggers a map update based on a set of spatial query result cells
462* @param [Array] mapPlotData, an array of coordinate and weight objects
463* @param [Array] params, an object containing original query parameters [LEGACY]
464* @param [Array] plotWeights, a list of weights of the spatial cells - e.g., number of tweets
465*/
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700466function triggerUIUpdate(mapPlotData, plotWeights) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700467 /** Clear anything currently on the map **/
468 mapWidgetClearMap();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700469
470 // Compute data point spread
471 var dataBreakpoints = mapWidgetLegendComputeNaturalBreaks(plotWeights);
472
473 $.each(mapPlotData, function (m, val) {
474
475 // Only map points in data range of top 4 natural breaks
476 if (mapPlotData[m].weight > dataBreakpoints[0]) {
477
478 // Get color value of legend
479 var mapColor = mapWidgetLegendGetHeatValue(mapPlotData[m].weight, dataBreakpoints);
480 var markerRadius = mapWidgetComputeCircleRadius(mapPlotData[m], dataBreakpoints);
481 var point_opacity = 1.0;
482
483 var point_center = new google.maps.LatLng(
484 (mapPlotData[m].latSW + mapPlotData[m].latNE)/2.0,
485 (mapPlotData[m].lngSW + mapPlotData[m].lngNE)/2.0);
486
487 // Create and plot marker
488 var map_circle_options = {
489 center: point_center,
490 radius: markerRadius,
491 map: map,
492 fillOpacity: point_opacity,
493 fillColor: mapColor,
494 clickable: true
495 };
496 var map_circle = new google.maps.Circle(map_circle_options);
497 map_circle.val = mapPlotData[m];
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700498
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700499 // Clicking on a circle drills down map to that value
500 google.maps.event.addListener(map_circle, 'click', function (event) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700501 onMapPointDrillDown(map_circle.val);
502 });
503
504 // Add this marker to global marker cells
505 map_cells.push(map_circle);
506 }
507 });
508
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -0700509 // Add a legend to the map
510 // TODO Remove widget for now mapControlWidgetAddLegend(dataBreakpoints);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700511}
512
513/**
514* prepares an Asterix API query to drill down in a rectangular spatial zone
515*
516* @params {object} marker_borders [LEGACY] a set of bounds for a region from a previous api result
517*/
518function onMapPointDrillDown(marker_borders) {
519 var zoneData = APIqueryTracker["data"];
520
521 var zswBounds = new google.maps.LatLng(marker_borders.latSW, marker_borders.lngSW);
522 var zneBounds = new google.maps.LatLng(marker_borders.latNE, marker_borders.lngNE);
523
524 var zoneBounds = new google.maps.LatLngBounds(zswBounds, zneBounds);
525 zoneData["swLat"] = zoneBounds.getSouthWest().lat();
526 zoneData["swLng"] = zoneBounds.getSouthWest().lng();
527 zoneData["neLat"] = zoneBounds.getNorthEast().lat();
528 zoneData["neLng"] = zoneBounds.getNorthEast().lng();
529 var zB = {
530 "sw" : {
531 "lat" : zoneBounds.getSouthWest().lat(),
532 "lng" : zoneBounds.getSouthWest().lng()
533 },
534 "ne" : {
535 "lat" : zoneBounds.getNorthEast().lat(),
536 "lng" : zoneBounds.getNorthEast().lng()
537 }
538 };
539
540 mapWidgetClearMap();
541
542 var customBounds = new google.maps.LatLngBounds();
543 var zoomSWBounds = new google.maps.LatLng(zoneData["swLat"], zoneData["swLng"]);
544 var zoomNEBounds = new google.maps.LatLng(zoneData["neLat"], zoneData["neLng"]);
545 customBounds.extend(zoomSWBounds);
546 customBounds.extend(zoomNEBounds);
547 map.fitBounds(customBounds);
548
549 var df = getDrillDownQuery(zoneData, zB);
550
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700551 APIqueryTracker = {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700552 "query_string" : "use dataverse twitter;\n" + df.val(),
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700553 "marker_path" : "static/img/mobile2.png",
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700554 "on_clean_result" : onCleanTweetbookDrilldown,
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700555 };
556
557 A.query(df.val(), onTweetbookQuerySuccessPlot);
558}
559
560function getDrillDownQuery(parameters, bounds) {
561
562 var zoomRectangle = new FunctionExpression("create-rectangle",
563 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
564 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
565
566 var drillDown = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700567 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700568 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
569 .LetClause("$region", zoomRectangle)
570 .WhereClause().and(
571 new FunctionExpression('spatial-intersect', '$t.sender-location', '$region'),
572 new AExpression().set('$t.send-time > datetime("' + parameters["startdt"] + '")'),
573 new AExpression().set('$t.send-time < datetime("' + parameters["enddt"] + '")'),
574 new FunctionExpression('contains', '$t.message-text', '$keyword')
575 )
576 .ReturnClause({
577 "tweetId" : "$t.tweetid",
578 "tweetText" : "$t.message-text",
579 "tweetLoc" : "$t.sender-location"
580 });
581
582 return drillDown;
583}
584
585
586function addTweetbookCommentDropdown(appendToDiv) {
587
588 // Creates a div to manage a radio button set of chosen tweetbooks
589 $('<div/>')
590 .attr("class","btn-group chosen-tweetbooks")
591 .attr("data-toggle", "buttons-radio")
592 .css("margin-bottom", "10px")
593 .attr("id", "metacomment-tweetbooks")
594 .appendTo(appendToDiv);
595
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700596 var highlighted = "";
597 if (APIqueryTracker.hasOwnProperty("active_tweetbook")) {
598 highlighted = APIqueryTracker["active_tweetbook"];
599 }
600
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700601 // For each existing tweetbook from review mode, adds a radio button option.
602 $('#metacomment-tweetbooks').append('<input type="hidden" id="target-tweetbook" value="" />');
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700603 for (var rmt in review_mode_tweetbooks) {
604
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700605 var tweetbook_option = '<button type="button" class="btn">' + review_mode_tweetbooks[rmt] + '</button>';
606
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700607 if (review_mode_tweetbooks[rmt] == highlighted) {
608 tweetbook_option = '<button type="button" class="btn btn-info">' + review_mode_tweetbooks[rmt] + '</button>';
609 }
610
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700611 $('#metacomment-tweetbooks').append(tweetbook_option + '<br/>');
612 }
613
614 // Creates a button + input combination to add tweet comment to new tweetbook
615 var new_tweetbook_option = '<button type="button" class="btn" id="new-tweetbook-target-m"></button>' +
616 '<input type="text" id="new-tweetbook-entry-m" placeholder="Add to new tweetbook..."><br/>';
617 $('#metacomment-tweetbooks').append(new_tweetbook_option);
618
619 $("#new-tweetbook-entry-m").keyup(function() {
620 $("#new-tweetbook-target-m").val($("#new-tweetbook-entry-m").val());
621 $("#new-tweetbook-target-m").text($("#new-tweetbook-entry-m").val());
622 });
623
624 // There is a hidden input (id = target-tweetbook) which is used to track the value
625 // of the tweetbook to which the comment on this tweet will be added.
626 $(".chosen-tweetbooks .btn").click(function() {
627 $("#target-tweetbook").val($(this).text());
628 });
629}
630
631function onDrillDownAtLocation(tO) {
632
633 var tweetId = tO["tweetEntryId"];
634 var tweetText = tO["tweetText"];
635
636 var tweetContainerId = '#drilldown_modal_body';
637 var tweetDiv = '<div id="drilltweetobj' + tweetId + '"></div>';
638
639 $(tweetContainerId).empty();
640 $(tweetContainerId).append(tweetDiv);
641 $('#drilltweetobj' + tweetId).append('<p>Tweet #' + tweetId + ": " + tweetText + '</p>');
642
643 // Add comment field
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700644 $('#drilltweetobj' + tweetId).append('<input class="textbox" type="text" id="metacomment' + tweetId + '">');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700645
646 if (tO.hasOwnProperty("tweetComment")) {
647 $("#metacomment" + tweetId).val(tO["tweetComment"]);
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700648
649 var deleteThisComment = addDeleteButton(
650 "deleteLiveComment_" + tweetId,
651 "drilltweetobj" + tweetId,
652 function () {
653
654 // TODO Maybe this should fire differnetly if another tweetbook is selected?
655
656 // Send comment deletion to asterix
657 var deleteTweetCommentOnId = '"' + tweetId + '"';
658 var toDelete = new DeleteStatement(
659 "$mt",
660 APIqueryTracker["active_tweetbook"],
661 new AExpression("$mt.tweetid = " + deleteTweetCommentOnId.toString())
662 );
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700663 A.update(
664 toDelete.val()
665 );
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700666
667 // Hide comment from map
668 $('#drilldown_modal').modal('hide');
669
670 // Replot tweetbook
671 onPlotTweetbook(APIqueryTracker["active_tweetbook"]);
672 }
673 );
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700674 }
675
676 addTweetbookCommentDropdown('#drilltweetobj' + tweetId);
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700677
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700678 $('#drilltweetobj' + tweetId).append('<br/><button type="button" class="btn" id="add-metacomment">Save Comment</button>');
679
680 $('#add-metacomment').button().click(function () {
681 var save_metacomment_target_tweetbook = $("#target-tweetbook").val();
682 var save_metacomment_target_comment = '"' + $("#metacomment" + tweetId).val() + '"';
683 var save_metacomment_target_tweet = '"' + tweetId + '"';
684
685 if (save_metacomment_target_tweetbook.length == 0) {
686 alert("Please choose a tweetbook.");
687
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700688 } else {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700689
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700690 if (!(existsTweetbook(save_metacomment_target_tweetbook))) {
691 onCreateNewTweetBook(save_metacomment_target_tweetbook);
692 }
693
694 var toDelete = new DeleteStatement(
695 "$mt",
696 save_metacomment_target_tweetbook,
697 new AExpression("$mt.tweetid = " + save_metacomment_target_tweet.toString())
698 );
699
700 A.update(toDelete.val());
701
702 var toInsert = new InsertStatement(
703 save_metacomment_target_tweetbook,
704 {
705 "tweetid" : save_metacomment_target_tweet.toString(),
706 "comment-text" : save_metacomment_target_comment
707 }
708 );
709
710 // Insert query to add metacomment to said tweetbook dataset
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700711 A.update(toInsert.val(), function () { alert("Test"); });
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700712
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700713 // TODO Some stress testing of error conditions might be good here...
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -0700714 if (APIqueryTracker.hasOwnProperty("active_tweetbook")) {
715 onPlotTweetbook(APIqueryTracker["active_tweetbook"]);
716 };
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700717 var successMessage = "Saved comment on <b>Tweet #" + tweetId +
718 "</b> in dataset <b>" + save_metacomment_target_tweetbook + "</b>.";
719 addSuccessBlock(successMessage, 'drilltweetobj' + tweetId);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700720 }
721 });
722
723 // Set width of tweetbook buttons
724 $(".chosen-tweetbooks .btn").css("width", "200px");
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700725 $(".chosen-tweetbooks .btn").css("height", "2em");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700726}
727
728
729/**
730* Adds a new tweetbook entry to the menu and creates a dataset of type TweetbookEntry.
731*/
732function onCreateNewTweetBook(tweetbook_title) {
733
734 var tweetbook_title = tweetbook_title.split(' ').join('_');
735
736 A.ddl(
737 "create dataset " + tweetbook_title + "(TweetbookEntry) primary key tweetid;",
738 function () {}
739 );
740
741 if (!(existsTweetbook(tweetbook_title))) {
742 review_mode_tweetbooks.push(tweetbook_title);
743 addTweetBookDropdownItem(tweetbook_title);
744 }
745}
746
747
748function onDropTweetBook(tweetbook_title) {
749
750 // AQL Call
751 A.ddl(
752 "drop dataset " + tweetbook_title + " if exists;",
753 function () {}
754 );
755
756 // Removes tweetbook from review_mode_tweetbooks
757 var remove_position = $.inArray(tweetbook_title, review_mode_tweetbooks);
758 if (remove_position >= 0) review_mode_tweetbooks.splice(remove_position, 1);
759
760 // Clear UI with review tweetbook titles
761 $('#review-tweetbook-titles').html('');
762 for (r in review_mode_tweetbooks) {
763 addTweetBookDropdownItem(review_mode_tweetbooks[r]);
764 }
765}
766
767
768function addTweetBookDropdownItem(tweetbook) {
769 // Add placeholder for this tweetbook
770 $('<div/>')
771 .css("padding-left", "1em")
772 .attr({
773 "class" : "btn-group",
774 "id" : "rm_holder_" + tweetbook
775 }).appendTo("#review-tweetbook-titles");
776 $("#review-tweetbook-titles").append('<br/>');
777
778 // Add plotting button for this tweetbook
779 var plot_button = '<button class="btn" id="rm_plotbook_' + tweetbook + '">' + tweetbook + '</button>';
780 $("#rm_holder_" + tweetbook).append(plot_button);
781 $("#rm_plotbook_" + tweetbook).on('click', function(e) {
782 onPlotTweetbook(tweetbook);
783 });
784
785 // Add trash button for this tweetbook
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700786 var onTrashTweetbookButton = addDeleteButton(
787 "rm_trashbook_" + tweetbook,
788 "rm_holder_" + tweetbook,
789 function(e) {
790 onDropTweetBook(tweetbook);
791 }
792 );
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700793}
794
795
796function onPlotTweetbook(tweetbook) {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700797
798 // Clear map for this one
799 mapWidgetResetMap();
800
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700801 var plotTweetQuery = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700802 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700803 .ForClause("$m", new AExpression("dataset " + tweetbook))
804 .WhereClause(new AExpression("$m.tweetid = $t.tweetid"))
805 .ReturnClause({
806 "tweetId" : "$m.tweetid",
807 "tweetText" : "$t.message-text",
808 "tweetLoc" : "$t.sender-location",
809 "tweetCom" : "$m.comment-text"
810 });
811
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700812 APIqueryTracker = {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700813 "query_string" : "use dataverse twitter;\n" + plotTweetQuery.val(),
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700814 "marker_path" : "static/img/mobile_green2.png",
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700815 "on_clean_result" : onCleanPlotTweetbook,
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700816 "active_tweetbook" : tweetbook
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700817 };
818
819 A.query(plotTweetQuery.val(), onTweetbookQuerySuccessPlot);
820}
821
822
823function onTweetbookQuerySuccessPlot (res) {
824
825 var records = res["results"];
826
827 var coordinates = [];
828 map_tweet_markers = [];
829 map_tweet_overlays = [];
830 drilldown_data_map = {};
831 drilldown_data_map_vals = {};
832
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700833 var micon = APIqueryTracker["marker_path"];
834 var marker_click_function = onClickTweetbookMapMarker;
835 var clean_result_function = APIqueryTracker["on_clean_result"];
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700836
837 coordinates = clean_result_function(records);
838
839 for (var dm in coordinates) {
840 var keyLat = coordinates[dm].tweetLat.toString();
841 var keyLng = coordinates[dm].tweetLng.toString();
842 if (!drilldown_data_map.hasOwnProperty(keyLat)) {
843 drilldown_data_map[keyLat] = {};
844 }
845 if (!drilldown_data_map[keyLat].hasOwnProperty(keyLng)) {
846 drilldown_data_map[keyLat][keyLng] = [];
847 }
848 drilldown_data_map[keyLat][keyLng].push(coordinates[dm]);
849 drilldown_data_map_vals[coordinates[dm].tweetEntryId.toString()] = coordinates[dm];
850 }
851
852 $.each(drilldown_data_map, function(drillKeyLat, valuesAtLat) {
853 $.each(drilldown_data_map[drillKeyLat], function (drillKeyLng, valueAtLng) {
854
855 // Get subset of drilldown position on map
856 var cposition = new google.maps.LatLng(parseFloat(drillKeyLat), parseFloat(drillKeyLng));
857
858 // Create a marker using the snazzy phone icon
859 var map_tweet_m = new google.maps.Marker({
860 position: cposition,
861 map: map,
862 icon: micon,
863 clickable: true,
864 });
865
866 // Open Tweet exploration window on click
867 google.maps.event.addListener(map_tweet_m, 'click', function (event) {
868 marker_click_function(drilldown_data_map[drillKeyLat][drillKeyLng]);
869 });
870
871 // Add marker to index of tweets
872 map_tweet_markers.push(map_tweet_m);
873
874 });
875 });
876}
877
878
879function existsTweetbook(tweetbook) {
880 if (parseInt($.inArray(tweetbook, review_mode_tweetbooks)) == -1) {
881 return false;
882 } else {
883 return true;
884 }
885}
886
887
888function onCleanPlotTweetbook(records) {
889 var toPlot = [];
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700890
891 // An entry looks like this:
892 // { "tweetId": "273589", "tweetText": " like verizon the network is amazing", "tweetLoc": { point: [37.78, 82.27]}, "tweetCom": "hooray comments" }
893
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700894 for (var entry in records) {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700895
896 var points = records[entry].split("point:")[1].match(/[-+]?[0-9]*\.?[0-9]+/g);
897
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700898 var tweetbook_element = {
899 "tweetEntryId" : parseInt(records[entry].split(",")[0].split(":")[1].split('"')[1]),
900 "tweetText" : records[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700901 "tweetLat" : parseFloat(points[0]),
902 "tweetLng" : parseFloat(points[1]),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700903 "tweetComment" : records[entry].split("tweetCom\": \"")[1].split("\"")[0]
904 };
905 toPlot.push(tweetbook_element);
906 }
907
908 return toPlot;
909}
910
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700911
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700912function onCleanTweetbookDrilldown (rec) {
913
914 var drilldown_cleaned = [];
915
916 for (var entry = 0; entry < rec.length; entry++) {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700917
918 // An entry looks like this:
919 // { "tweetId": "105491", "tweetText": " hate verizon its platform is OMG", "tweetLoc": { point: [30.55, 71.44]} }
920 var points = rec[entry].split("point:")[1].match(/[-+]?[0-9]*\.?[0-9]+/g);
921
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700922 var drill_element = {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700923 "tweetEntryId" : parseInt(rec[entry].split(",")[0].split(":")[1].replace('"', '')),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700924 "tweetText" : rec[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700925 "tweetLat" : parseFloat(points[0]),
926 "tweetLng" : parseFloat(points[1])
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700927 };
928 drilldown_cleaned.push(drill_element);
929 }
930 return drilldown_cleaned;
931}
932
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700933
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700934function onClickTweetbookMapMarker(tweet_arr) {
935 $('#drilldown_modal_body').html('');
936
937 // Clear existing display
938 $.each(tweet_arr, function (t, valueT) {
939 var tweet_obj = tweet_arr[t];
940 onDrillDownAtLocation(tweet_obj);
941 });
942
943 $('#drilldown_modal').modal('show');
944}
945
946/** Toggling Review and Explore Modes **/
947
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700948
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700949/**
950* Explore mode: Initial map creation and screen alignment
951*/
952function onOpenExploreMap () {
953 var explore_column_height = $('#explore-well').height();
954 $('#map_canvas').height(explore_column_height + "px");
955 $('#review-well').height(explore_column_height + "px");
956 $('#review-well').css('max-height', explore_column_height + "px");
957 var pad = $('#review-well').innerHeight() - $('#review-well').height();
958 var prev_window_target = $('#review-well').height() - 20 - $('#group-tweetbooks').innerHeight() - $('#group-background-query').innerHeight() - 2*pad;
959 $('#query-preview-window').height(prev_window_target +'px');
960}
961
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700962
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700963/**
964* Launching explore mode: clear windows/variables, show correct sidebar
965*/
966function onLaunchExploreMode() {
967 $('#review-active').removeClass('active');
968 $('#review-well').hide();
969
970 $('#explore-active').addClass('active');
971 $('#explore-well').show();
972
973 $("#clear-button").trigger("click");
974}
975
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700976
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700977/**
978* Launching review mode: clear windows/variables, show correct sidebar
979*/
980function onLaunchReviewMode() {
981 $('#explore-active').removeClass('active');
982 $('#explore-well').hide();
983 $('#review-active').addClass('active');
984 $('#review-well').show();
985
986 $("#clear-button").trigger("click");
987}
988
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700989
990/** Icon / Interface Utility Methods **/
991
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700992/**
993* Creates a delete icon button using default trash icon
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700994* @param {String} id, id for this element
995* @param {String} attachTo, id string of an element to which I can attach this button.
996* @param {Function} onClick, a function to fire when this icon is clicked
997*/
998function addDeleteButton(iconId, attachTo, onClick) {
999 // Icon structure
1000 var trashIcon = '<button class="btn" id="' + iconId + '"><i class="icon-trash"></i></button>';
1001
1002 $('#' + attachTo).append(trashIcon);
1003
1004 // On Click behavior
1005 $('#' + iconId).on('click', onClick);
1006}
1007
1008
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -07001009/**
1010* Creates a success message and attaches it to a div with provided ID.
1011* @param {String} message, a message to post
1012* @param {String} appendTarget, a target div to which to append the alert
1013*/
1014function addSuccessBlock(message, appendTarget) {
1015
1016 $('<div/>')
1017 .attr("class", "alert alert-success")
1018 .html('<button type="button" class="close" data-dismiss="alert">&times;</button>' + message)
1019 .appendTo('#' + appendTarget);
1020}
1021
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001022/** Map Widget Utility Methods **/
1023
1024/**
1025* Plots a legend onto the map, with values in progress bars
1026* @param {number Array} breakpoints, an array of numbers representing natural breakpoints
1027*/
1028function mapControlWidgetAddLegend(breakpoints) {
1029
1030 // Retriever colors, lightest to darkest
1031 var colors = mapWidgetGetColorPalette();
1032
1033 // Initial div structure
1034 $("#map_canvas_legend").html('<div id="legend-holder"><div id="legend-progress-bar" class="progress"></div><span id="legend-label"></span></div>');
1035
1036 // Add color scale to legend
1037 $('#legend-progress-bar').css("width", "200px").html('');
1038
1039 // Add a progress bar for each color
1040 for (var color in colors) {
1041
1042 // Bar values
1043 var upperBound = breakpoints[parseInt(color) + 1];
1044
1045 // Create Progress Bar
1046 $('<div/>')
1047 .attr("class", "bar")
1048 .attr("id", "pbar" + color)
1049 .css("width" , '25.0%')
1050 .html("< " + upperBound)
1051 .appendTo('#legend-progress-bar');
1052
1053 $('#pbar' + color).css({
1054 "background-image" : 'none',
1055 "background-color" : colors[parseInt(color)]
1056 });
1057
1058 // Attach a message showing minimum bounds
1059 $('#legend-label').html('Regions with at least ' + breakpoints[0] + ' tweets');
1060 $('#legend-label').css({
1061 "margin-top" : 0,
1062 "color" : "black"
1063 });
1064 }
1065
1066 // Add legend to map
1067 map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push(document.getElementById('legend-holder'));
1068 $('#map_canvas_legend').show();
1069}
1070
1071/**
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001072* Clears ALL map elements - legend, plotted items, overlays
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001073*/
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001074function mapWidgetResetMap() {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001075
1076 if (selectionRect) {
1077 selectionRect.setMap(null);
1078 selectionRect = null;
1079 }
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001080
1081 mapWidgetClearMap();
1082
1083 // Reset map center and zoom
1084 map.setCenter(new google.maps.LatLng(38.89, -77.03));
1085 map.setZoom(4);
1086}
1087
1088function mapWidgetClearMap() {
1089
1090 // Remove previously plotted data/markers
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001091 for (c in map_cells) {
1092 map_cells[c].setMap(null);
1093 }
1094 map_cells = [];
1095 for (m in map_tweet_markers) {
1096 map_tweet_markers[m].setMap(null);
1097 }
1098 map_tweet_markers = [];
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001099
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001100 // Remove legend from map
1101 map.controls[google.maps.ControlPosition.LEFT_BOTTOM].clear();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001102}
1103
1104/**
1105* Uses jenks algorithm in geostats library to find natural breaks in numeric data
1106* @param {number Array} weights of points to plot
1107* @returns {number Array} array of natural breakpoints, of which the top 4 subsets will be plotted
1108*/
1109function mapWidgetLegendComputeNaturalBreaks(weights) {
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001110
1111 if (weights.length < 10) {
1112 return [0];
1113 }
1114
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001115 var plotDataWeights = new geostats(weights.sort());
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001116 return plotDataWeights.getJenks(6).slice(2,7);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001117}
1118
1119/**
1120* Computes values for map legend given a value and an array of jenks breakpoints
1121* @param {number} weight of point to plot on map
1122* @param {number Array} breakpoints, an array of 5 points corresponding to bounds of 4 natural ranges
1123* @returns {String} an RGB value corresponding to a subset of data
1124*/
1125function mapWidgetLegendGetHeatValue(weight, breakpoints) {
1126
1127 // Determine into which range the weight falls
1128 var weightColor = 0;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001129
1130 if (breakpoints.length == 1) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001131 weightColor = 2;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001132 } else {
1133 if (weight >= breakpoints[3]) {
1134 weightColor = 3;
1135 } else if (weight >= breakpoints[2]) {
1136 weightColor = 2;
1137 } else if (weight >= breakpoints[1]) {
1138 weightColor = 1;
1139 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001140 }
1141
1142 // Get default map color palette
1143 var colorValues = mapWidgetGetColorPalette();
1144 return colorValues[weightColor];
1145}
1146
1147/**
1148* Returns an array containing a 4-color palette, lightest to darkest
1149* External palette source: http://www.colourlovers.com/palette/2763366/s_i_l_e_n_c_e_r
1150* @returns {Array} [colors]
1151*/
1152function mapWidgetGetColorPalette() {
1153 return [
1154 "rgb(115,189,158)",
1155 "rgb(74,142,145)",
1156 "rgb(19,93,96)",
1157 "rgb(7,51,46)"
1158 ];
1159}
1160
1161/**
1162* Computes radius for a given data point from a spatial cell
1163* @param {Object} keys => ["latSW" "lngSW" "latNE" "lngNE" "weight"]
1164* @returns {number} radius between 2 points in metres
1165*/
1166function mapWidgetComputeCircleRadius(spatialCell, breakpoints) {
1167
1168 var weight = spatialCell.weight;
genia.likes.science@gmail.com84711b82013-08-22 03:47:41 -07001169
1170 if (breakpoints.length == 1) {
1171 var weightColor = 0.25;
1172 } else {
1173 // Compute weight color
1174 var weightColor = 0.25;
1175 if (weight >= breakpoints[3]) {
1176 weightColor = 1.0;
1177 } else if (weight >= breakpoints[2]) {
1178 weightColor = 0.75;
1179 } else if (weight >= breakpoints[1]) {
1180 weightColor = 0.5;
1181 }
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001182 }
1183
1184 // Define Boundary Points
1185 var point_center = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1186 var point_left = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, spatialCell.lngSW);
1187 var point_top = new google.maps.LatLng(spatialCell.latNE, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1188
1189 // TODO not actually a weight color :)
1190 return weightColor * 1000 * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
1191}
1192
1193/** External Utility Methods **/
1194
1195/**
1196 * Calculates the distance between two latlng locations in km.
1197 * @see http://www.movable-type.co.uk/scripts/latlong.html
1198 *
1199 * @param {google.maps.LatLng} p1 The first lat lng point.
1200 * @param {google.maps.LatLng} p2 The second lat lng point.
1201 * @return {number} The distance between the two points in km.
1202 * @private
1203*/
1204function distanceBetweenPoints_(p1, p2) {
1205 if (!p1 || !p2) {
1206 return 0;
1207 }
1208
1209 var R = 6371; // Radius of the Earth in km
1210 var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
1211 var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
1212 var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
1213 Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
1214 Math.sin(dLon / 2) * Math.sin(dLon / 2);
1215 var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1216 var d = R * c;
1217 return d;
1218};