blob: 70f2c4e45abcc64a94e116a51904d3a95542bb1d [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
383 // Generate new Asterix Core API Query
384 A.query_result(
385 { "handle" : JSON.stringify(asyncQueryManager[handle_id]["handle"]) },
386 cherryQuerySyncCallback
387 );
388 }
389 });
genia.likes.science@gmail.com4259a542013-08-18 20:06:58 -0700390
391 // Adds a removal button for this async handle
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700392 var asyncDeleteButton = addDeleteButton(
393 "trashhandle_" + handle_id,
394 "async_container_" + handle_id,
395 function (e) {
396 $('#async_container_' + handle_id).remove();
397 delete asyncQueryManager[handle_id];
398 }
399 );
400 $("#submit-button").attr("disabled", false);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700401}
402
403
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700404/**
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700405* returns a json object with keys: weight, latSW, lngSW, latNE, lngNE
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700406*
407* { "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 -0700408*/
409function getRecord(cell_count_record) {
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700410 // This is a really hacky way to pull out the digits, but it works for now.
411 var values = cell_count_record.replace("int64","").match(/[-+]?[0-9]*\.?[0-9]+/g);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700412 var record_representation = {};
413
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700414 record_representation["latSW"] = parseFloat(values[0]);
415 record_representation["lngSW"] = parseFloat(values[1]);
416 record_representation["latNE"] = parseFloat(values[2]);
417 record_representation["lngNE"] = parseFloat(values[3]);
418 record_representation["weight"] = parseInt(values[4]);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700419
420 return record_representation;
421}
422
423/**
424* A spatial data cleaning and mapping call
425* @param {Object} res, a result object from a cherry geospatial query
426*/
427function cherryQuerySyncCallback(res) {
428
429 records = res["results"];
genia.likes.science@gmail.comd42b4022013-08-09 05:05:23 -0700430
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700431 if (typeof res["results"][0] == "object") {
432 records = res["results"][0];
433 }
434
435 var coordinates = [];
436 var weights = [];
437
438 for (var subrecord in records) {
439 var coordinate = getRecord(records[subrecord]);
440 weights.push(coordinate["weight"]);
441 coordinates.push(coordinate);
442 }
443
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700444 triggerUIUpdate(coordinates, weights);
445 $("#submit-button").attr("disabled", false);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700446}
447
448/**
449* Triggers a map update based on a set of spatial query result cells
450* @param [Array] mapPlotData, an array of coordinate and weight objects
451* @param [Array] params, an object containing original query parameters [LEGACY]
452* @param [Array] plotWeights, a list of weights of the spatial cells - e.g., number of tweets
453*/
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700454function triggerUIUpdate(mapPlotData, plotWeights) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700455 /** Clear anything currently on the map **/
456 mapWidgetClearMap();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700457
458 // Compute data point spread
459 var dataBreakpoints = mapWidgetLegendComputeNaturalBreaks(plotWeights);
460
461 $.each(mapPlotData, function (m, val) {
462
463 // Only map points in data range of top 4 natural breaks
464 if (mapPlotData[m].weight > dataBreakpoints[0]) {
465
466 // Get color value of legend
467 var mapColor = mapWidgetLegendGetHeatValue(mapPlotData[m].weight, dataBreakpoints);
468 var markerRadius = mapWidgetComputeCircleRadius(mapPlotData[m], dataBreakpoints);
469 var point_opacity = 1.0;
470
471 var point_center = new google.maps.LatLng(
472 (mapPlotData[m].latSW + mapPlotData[m].latNE)/2.0,
473 (mapPlotData[m].lngSW + mapPlotData[m].lngNE)/2.0);
474
475 // Create and plot marker
476 var map_circle_options = {
477 center: point_center,
478 radius: markerRadius,
479 map: map,
480 fillOpacity: point_opacity,
481 fillColor: mapColor,
482 clickable: true
483 };
484 var map_circle = new google.maps.Circle(map_circle_options);
485 map_circle.val = mapPlotData[m];
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700486
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700487 // Clicking on a circle drills down map to that value
488 google.maps.event.addListener(map_circle, 'click', function (event) {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700489 onMapPointDrillDown(map_circle.val);
490 });
491
492 // Add this marker to global marker cells
493 map_cells.push(map_circle);
494 }
495 });
496
497 // Add a legend to the map
498 mapControlWidgetAddLegend(dataBreakpoints);
499}
500
501/**
502* prepares an Asterix API query to drill down in a rectangular spatial zone
503*
504* @params {object} marker_borders [LEGACY] a set of bounds for a region from a previous api result
505*/
506function onMapPointDrillDown(marker_borders) {
507 var zoneData = APIqueryTracker["data"];
508
509 var zswBounds = new google.maps.LatLng(marker_borders.latSW, marker_borders.lngSW);
510 var zneBounds = new google.maps.LatLng(marker_borders.latNE, marker_borders.lngNE);
511
512 var zoneBounds = new google.maps.LatLngBounds(zswBounds, zneBounds);
513 zoneData["swLat"] = zoneBounds.getSouthWest().lat();
514 zoneData["swLng"] = zoneBounds.getSouthWest().lng();
515 zoneData["neLat"] = zoneBounds.getNorthEast().lat();
516 zoneData["neLng"] = zoneBounds.getNorthEast().lng();
517 var zB = {
518 "sw" : {
519 "lat" : zoneBounds.getSouthWest().lat(),
520 "lng" : zoneBounds.getSouthWest().lng()
521 },
522 "ne" : {
523 "lat" : zoneBounds.getNorthEast().lat(),
524 "lng" : zoneBounds.getNorthEast().lng()
525 }
526 };
527
528 mapWidgetClearMap();
529
530 var customBounds = new google.maps.LatLngBounds();
531 var zoomSWBounds = new google.maps.LatLng(zoneData["swLat"], zoneData["swLng"]);
532 var zoomNEBounds = new google.maps.LatLng(zoneData["neLat"], zoneData["neLng"]);
533 customBounds.extend(zoomSWBounds);
534 customBounds.extend(zoomNEBounds);
535 map.fitBounds(customBounds);
536
537 var df = getDrillDownQuery(zoneData, zB);
538
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700539 APIqueryTracker = {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700540 "query_string" : "use dataverse twitter;\n" + df.val(),
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700541 "marker_path" : "static/img/mobile2.png",
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700542 "on_clean_result" : onCleanTweetbookDrilldown,
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700543 };
544
545 A.query(df.val(), onTweetbookQuerySuccessPlot);
546}
547
548function getDrillDownQuery(parameters, bounds) {
549
550 var zoomRectangle = new FunctionExpression("create-rectangle",
551 new FunctionExpression("create-point", bounds["sw"]["lat"], bounds["sw"]["lng"]),
552 new FunctionExpression("create-point", bounds["ne"]["lat"], bounds["ne"]["lng"]));
553
554 var drillDown = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700555 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700556 .LetClause("$keyword", new AExpression('"' + parameters["keyword"] + '"'))
557 .LetClause("$region", zoomRectangle)
558 .WhereClause().and(
559 new FunctionExpression('spatial-intersect', '$t.sender-location', '$region'),
560 new AExpression().set('$t.send-time > datetime("' + parameters["startdt"] + '")'),
561 new AExpression().set('$t.send-time < datetime("' + parameters["enddt"] + '")'),
562 new FunctionExpression('contains', '$t.message-text', '$keyword')
563 )
564 .ReturnClause({
565 "tweetId" : "$t.tweetid",
566 "tweetText" : "$t.message-text",
567 "tweetLoc" : "$t.sender-location"
568 });
569
570 return drillDown;
571}
572
573
574function addTweetbookCommentDropdown(appendToDiv) {
575
576 // Creates a div to manage a radio button set of chosen tweetbooks
577 $('<div/>')
578 .attr("class","btn-group chosen-tweetbooks")
579 .attr("data-toggle", "buttons-radio")
580 .css("margin-bottom", "10px")
581 .attr("id", "metacomment-tweetbooks")
582 .appendTo(appendToDiv);
583
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700584 var highlighted = "";
585 if (APIqueryTracker.hasOwnProperty("active_tweetbook")) {
586 highlighted = APIqueryTracker["active_tweetbook"];
587 }
588
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700589 // For each existing tweetbook from review mode, adds a radio button option.
590 $('#metacomment-tweetbooks').append('<input type="hidden" id="target-tweetbook" value="" />');
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700591 for (var rmt in review_mode_tweetbooks) {
592
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700593 var tweetbook_option = '<button type="button" class="btn">' + review_mode_tweetbooks[rmt] + '</button>';
594
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700595 if (review_mode_tweetbooks[rmt] == highlighted) {
596 tweetbook_option = '<button type="button" class="btn btn-info">' + review_mode_tweetbooks[rmt] + '</button>';
597 }
598
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700599 $('#metacomment-tweetbooks').append(tweetbook_option + '<br/>');
600 }
601
602 // Creates a button + input combination to add tweet comment to new tweetbook
603 var new_tweetbook_option = '<button type="button" class="btn" id="new-tweetbook-target-m"></button>' +
604 '<input type="text" id="new-tweetbook-entry-m" placeholder="Add to new tweetbook..."><br/>';
605 $('#metacomment-tweetbooks').append(new_tweetbook_option);
606
607 $("#new-tweetbook-entry-m").keyup(function() {
608 $("#new-tweetbook-target-m").val($("#new-tweetbook-entry-m").val());
609 $("#new-tweetbook-target-m").text($("#new-tweetbook-entry-m").val());
610 });
611
612 // There is a hidden input (id = target-tweetbook) which is used to track the value
613 // of the tweetbook to which the comment on this tweet will be added.
614 $(".chosen-tweetbooks .btn").click(function() {
615 $("#target-tweetbook").val($(this).text());
616 });
617}
618
619function onDrillDownAtLocation(tO) {
620
621 var tweetId = tO["tweetEntryId"];
622 var tweetText = tO["tweetText"];
623
624 var tweetContainerId = '#drilldown_modal_body';
625 var tweetDiv = '<div id="drilltweetobj' + tweetId + '"></div>';
626
627 $(tweetContainerId).empty();
628 $(tweetContainerId).append(tweetDiv);
629 $('#drilltweetobj' + tweetId).append('<p>Tweet #' + tweetId + ": " + tweetText + '</p>');
630
631 // Add comment field
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700632 $('#drilltweetobj' + tweetId).append('<input class="textbox" type="text" id="metacomment' + tweetId + '">');
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700633
634 if (tO.hasOwnProperty("tweetComment")) {
635 $("#metacomment" + tweetId).val(tO["tweetComment"]);
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700636
637 var deleteThisComment = addDeleteButton(
638 "deleteLiveComment_" + tweetId,
639 "drilltweetobj" + tweetId,
640 function () {
641
642 // TODO Maybe this should fire differnetly if another tweetbook is selected?
643
644 // Send comment deletion to asterix
645 var deleteTweetCommentOnId = '"' + tweetId + '"';
646 var toDelete = new DeleteStatement(
647 "$mt",
648 APIqueryTracker["active_tweetbook"],
649 new AExpression("$mt.tweetid = " + deleteTweetCommentOnId.toString())
650 );
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700651 A.update(
652 toDelete.val()
653 );
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700654
655 // Hide comment from map
656 $('#drilldown_modal').modal('hide');
657
658 // Replot tweetbook
659 onPlotTweetbook(APIqueryTracker["active_tweetbook"]);
660 }
661 );
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700662 }
663
664 addTweetbookCommentDropdown('#drilltweetobj' + tweetId);
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700665
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700666 $('#drilltweetobj' + tweetId).append('<br/><button type="button" class="btn" id="add-metacomment">Save Comment</button>');
667
668 $('#add-metacomment').button().click(function () {
669 var save_metacomment_target_tweetbook = $("#target-tweetbook").val();
670 var save_metacomment_target_comment = '"' + $("#metacomment" + tweetId).val() + '"';
671 var save_metacomment_target_tweet = '"' + tweetId + '"';
672
673 if (save_metacomment_target_tweetbook.length == 0) {
674 alert("Please choose a tweetbook.");
675
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700676 } else {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700677
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700678 if (!(existsTweetbook(save_metacomment_target_tweetbook))) {
679 onCreateNewTweetBook(save_metacomment_target_tweetbook);
680 }
681
682 var toDelete = new DeleteStatement(
683 "$mt",
684 save_metacomment_target_tweetbook,
685 new AExpression("$mt.tweetid = " + save_metacomment_target_tweet.toString())
686 );
687
688 A.update(toDelete.val());
689
690 var toInsert = new InsertStatement(
691 save_metacomment_target_tweetbook,
692 {
693 "tweetid" : save_metacomment_target_tweet.toString(),
694 "comment-text" : save_metacomment_target_comment
695 }
696 );
697
698 // Insert query to add metacomment to said tweetbook dataset
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700699 A.update(toInsert.val(), function () { alert("Test"); });
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700700
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700701 // TODO Some stress testing of error conditions might be good here...
702 onPlotTweetbook(APIqueryTracker["active_tweetbook"]);
703 var successMessage = "Saved comment on <b>Tweet #" + tweetId +
704 "</b> in dataset <b>" + save_metacomment_target_tweetbook + "</b>.";
705 addSuccessBlock(successMessage, 'drilltweetobj' + tweetId);
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700706 }
707 });
708
709 // Set width of tweetbook buttons
710 $(".chosen-tweetbooks .btn").css("width", "200px");
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700711 $(".chosen-tweetbooks .btn").css("height", "2em");
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700712}
713
714
715/**
716* Adds a new tweetbook entry to the menu and creates a dataset of type TweetbookEntry.
717*/
718function onCreateNewTweetBook(tweetbook_title) {
719
720 var tweetbook_title = tweetbook_title.split(' ').join('_');
721
722 A.ddl(
723 "create dataset " + tweetbook_title + "(TweetbookEntry) primary key tweetid;",
724 function () {}
725 );
726
727 if (!(existsTweetbook(tweetbook_title))) {
728 review_mode_tweetbooks.push(tweetbook_title);
729 addTweetBookDropdownItem(tweetbook_title);
730 }
731}
732
733
734function onDropTweetBook(tweetbook_title) {
735
736 // AQL Call
737 A.ddl(
738 "drop dataset " + tweetbook_title + " if exists;",
739 function () {}
740 );
741
742 // Removes tweetbook from review_mode_tweetbooks
743 var remove_position = $.inArray(tweetbook_title, review_mode_tweetbooks);
744 if (remove_position >= 0) review_mode_tweetbooks.splice(remove_position, 1);
745
746 // Clear UI with review tweetbook titles
747 $('#review-tweetbook-titles').html('');
748 for (r in review_mode_tweetbooks) {
749 addTweetBookDropdownItem(review_mode_tweetbooks[r]);
750 }
751}
752
753
754function addTweetBookDropdownItem(tweetbook) {
755 // Add placeholder for this tweetbook
756 $('<div/>')
757 .css("padding-left", "1em")
758 .attr({
759 "class" : "btn-group",
760 "id" : "rm_holder_" + tweetbook
761 }).appendTo("#review-tweetbook-titles");
762 $("#review-tweetbook-titles").append('<br/>');
763
764 // Add plotting button for this tweetbook
765 var plot_button = '<button class="btn" id="rm_plotbook_' + tweetbook + '">' + tweetbook + '</button>';
766 $("#rm_holder_" + tweetbook).append(plot_button);
767 $("#rm_plotbook_" + tweetbook).on('click', function(e) {
768 onPlotTweetbook(tweetbook);
769 });
770
771 // Add trash button for this tweetbook
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700772 var onTrashTweetbookButton = addDeleteButton(
773 "rm_trashbook_" + tweetbook,
774 "rm_holder_" + tweetbook,
775 function(e) {
776 onDropTweetBook(tweetbook);
777 }
778 );
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700779}
780
781
782function onPlotTweetbook(tweetbook) {
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700783
784 // Clear map for this one
785 mapWidgetResetMap();
786
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700787 var plotTweetQuery = new FLWOGRExpression()
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -0700788 .ForClause("$t", new AExpression("dataset TweetMessagesShifted"))
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700789 .ForClause("$m", new AExpression("dataset " + tweetbook))
790 .WhereClause(new AExpression("$m.tweetid = $t.tweetid"))
791 .ReturnClause({
792 "tweetId" : "$m.tweetid",
793 "tweetText" : "$t.message-text",
794 "tweetLoc" : "$t.sender-location",
795 "tweetCom" : "$m.comment-text"
796 });
797
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700798 APIqueryTracker = {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700799 "query_string" : "use dataverse twitter;\n" + plotTweetQuery.val(),
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700800 "marker_path" : "static/img/mobile_green2.png",
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700801 "on_clean_result" : onCleanPlotTweetbook,
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700802 "active_tweetbook" : tweetbook
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700803 };
804
805 A.query(plotTweetQuery.val(), onTweetbookQuerySuccessPlot);
806}
807
808
809function onTweetbookQuerySuccessPlot (res) {
810
811 var records = res["results"];
812
813 var coordinates = [];
814 map_tweet_markers = [];
815 map_tweet_overlays = [];
816 drilldown_data_map = {};
817 drilldown_data_map_vals = {};
818
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700819 var micon = APIqueryTracker["marker_path"];
820 var marker_click_function = onClickTweetbookMapMarker;
821 var clean_result_function = APIqueryTracker["on_clean_result"];
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700822
823 coordinates = clean_result_function(records);
824
825 for (var dm in coordinates) {
826 var keyLat = coordinates[dm].tweetLat.toString();
827 var keyLng = coordinates[dm].tweetLng.toString();
828 if (!drilldown_data_map.hasOwnProperty(keyLat)) {
829 drilldown_data_map[keyLat] = {};
830 }
831 if (!drilldown_data_map[keyLat].hasOwnProperty(keyLng)) {
832 drilldown_data_map[keyLat][keyLng] = [];
833 }
834 drilldown_data_map[keyLat][keyLng].push(coordinates[dm]);
835 drilldown_data_map_vals[coordinates[dm].tweetEntryId.toString()] = coordinates[dm];
836 }
837
838 $.each(drilldown_data_map, function(drillKeyLat, valuesAtLat) {
839 $.each(drilldown_data_map[drillKeyLat], function (drillKeyLng, valueAtLng) {
840
841 // Get subset of drilldown position on map
842 var cposition = new google.maps.LatLng(parseFloat(drillKeyLat), parseFloat(drillKeyLng));
843
844 // Create a marker using the snazzy phone icon
845 var map_tweet_m = new google.maps.Marker({
846 position: cposition,
847 map: map,
848 icon: micon,
849 clickable: true,
850 });
851
852 // Open Tweet exploration window on click
853 google.maps.event.addListener(map_tweet_m, 'click', function (event) {
854 marker_click_function(drilldown_data_map[drillKeyLat][drillKeyLng]);
855 });
856
857 // Add marker to index of tweets
858 map_tweet_markers.push(map_tweet_m);
859
860 });
861 });
862}
863
864
865function existsTweetbook(tweetbook) {
866 if (parseInt($.inArray(tweetbook, review_mode_tweetbooks)) == -1) {
867 return false;
868 } else {
869 return true;
870 }
871}
872
873
874function onCleanPlotTweetbook(records) {
875 var toPlot = [];
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700876
877 // An entry looks like this:
878 // { "tweetId": "273589", "tweetText": " like verizon the network is amazing", "tweetLoc": { point: [37.78, 82.27]}, "tweetCom": "hooray comments" }
879
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700880 for (var entry in records) {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700881
882 var points = records[entry].split("point:")[1].match(/[-+]?[0-9]*\.?[0-9]+/g);
883
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700884 var tweetbook_element = {
885 "tweetEntryId" : parseInt(records[entry].split(",")[0].split(":")[1].split('"')[1]),
886 "tweetText" : records[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700887 "tweetLat" : parseFloat(points[0]),
888 "tweetLng" : parseFloat(points[1]),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700889 "tweetComment" : records[entry].split("tweetCom\": \"")[1].split("\"")[0]
890 };
891 toPlot.push(tweetbook_element);
892 }
893
894 return toPlot;
895}
896
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700897
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700898function onCleanTweetbookDrilldown (rec) {
899
900 var drilldown_cleaned = [];
901
902 for (var entry = 0; entry < rec.length; entry++) {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700903
904 // An entry looks like this:
905 // { "tweetId": "105491", "tweetText": " hate verizon its platform is OMG", "tweetLoc": { point: [30.55, 71.44]} }
906 var points = rec[entry].split("point:")[1].match(/[-+]?[0-9]*\.?[0-9]+/g);
907
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700908 var drill_element = {
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700909 "tweetEntryId" : parseInt(rec[entry].split(",")[0].split(":")[1].replace('"', '')),
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700910 "tweetText" : rec[entry].split("tweetText\": \"")[1].split("\", \"tweetLoc\":")[0],
genia.likes.science@gmail.com4833b412013-08-09 06:20:58 -0700911 "tweetLat" : parseFloat(points[0]),
912 "tweetLng" : parseFloat(points[1])
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700913 };
914 drilldown_cleaned.push(drill_element);
915 }
916 return drilldown_cleaned;
917}
918
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700919
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700920function onClickTweetbookMapMarker(tweet_arr) {
921 $('#drilldown_modal_body').html('');
922
923 // Clear existing display
924 $.each(tweet_arr, function (t, valueT) {
925 var tweet_obj = tweet_arr[t];
926 onDrillDownAtLocation(tweet_obj);
927 });
928
929 $('#drilldown_modal').modal('show');
930}
931
932/** Toggling Review and Explore Modes **/
933
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700934
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700935/**
936* Explore mode: Initial map creation and screen alignment
937*/
938function onOpenExploreMap () {
939 var explore_column_height = $('#explore-well').height();
940 $('#map_canvas').height(explore_column_height + "px");
941 $('#review-well').height(explore_column_height + "px");
942 $('#review-well').css('max-height', explore_column_height + "px");
943 var pad = $('#review-well').innerHeight() - $('#review-well').height();
944 var prev_window_target = $('#review-well').height() - 20 - $('#group-tweetbooks').innerHeight() - $('#group-background-query').innerHeight() - 2*pad;
945 $('#query-preview-window').height(prev_window_target +'px');
946}
947
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700948
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -0700949/**
950* Launching explore mode: clear windows/variables, show correct sidebar
951*/
952function onLaunchExploreMode() {
953 $('#review-active').removeClass('active');
954 $('#review-well').hide();
955
956 $('#explore-active').addClass('active');
957 $('#explore-well').show();
958
959 $("#clear-button").trigger("click");
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 review mode: clear windows/variables, show correct sidebar
965*/
966function onLaunchReviewMode() {
967 $('#explore-active').removeClass('active');
968 $('#explore-well').hide();
969 $('#review-active').addClass('active');
970 $('#review-well').show();
971
972 $("#clear-button").trigger("click");
973}
974
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700975
976/** Icon / Interface Utility Methods **/
977
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700978/**
979* Creates a delete icon button using default trash icon
genia.likes.science@gmail.com8125bd92013-08-21 18:04:27 -0700980* @param {String} id, id for this element
981* @param {String} attachTo, id string of an element to which I can attach this button.
982* @param {Function} onClick, a function to fire when this icon is clicked
983*/
984function addDeleteButton(iconId, attachTo, onClick) {
985 // Icon structure
986 var trashIcon = '<button class="btn" id="' + iconId + '"><i class="icon-trash"></i></button>';
987
988 $('#' + attachTo).append(trashIcon);
989
990 // On Click behavior
991 $('#' + iconId).on('click', onClick);
992}
993
994
genia.likes.science@gmail.comdd669072013-08-21 22:53:34 -0700995/**
996* Creates a success message and attaches it to a div with provided ID.
997* @param {String} message, a message to post
998* @param {String} appendTarget, a target div to which to append the alert
999*/
1000function addSuccessBlock(message, appendTarget) {
1001
1002 $('<div/>')
1003 .attr("class", "alert alert-success")
1004 .html('<button type="button" class="close" data-dismiss="alert">&times;</button>' + message)
1005 .appendTo('#' + appendTarget);
1006}
1007
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001008/** Map Widget Utility Methods **/
1009
1010/**
1011* Plots a legend onto the map, with values in progress bars
1012* @param {number Array} breakpoints, an array of numbers representing natural breakpoints
1013*/
1014function mapControlWidgetAddLegend(breakpoints) {
1015
1016 // Retriever colors, lightest to darkest
1017 var colors = mapWidgetGetColorPalette();
1018
1019 // Initial div structure
1020 $("#map_canvas_legend").html('<div id="legend-holder"><div id="legend-progress-bar" class="progress"></div><span id="legend-label"></span></div>');
1021
1022 // Add color scale to legend
1023 $('#legend-progress-bar').css("width", "200px").html('');
1024
1025 // Add a progress bar for each color
1026 for (var color in colors) {
1027
1028 // Bar values
1029 var upperBound = breakpoints[parseInt(color) + 1];
1030
1031 // Create Progress Bar
1032 $('<div/>')
1033 .attr("class", "bar")
1034 .attr("id", "pbar" + color)
1035 .css("width" , '25.0%')
1036 .html("< " + upperBound)
1037 .appendTo('#legend-progress-bar');
1038
1039 $('#pbar' + color).css({
1040 "background-image" : 'none',
1041 "background-color" : colors[parseInt(color)]
1042 });
1043
1044 // Attach a message showing minimum bounds
1045 $('#legend-label').html('Regions with at least ' + breakpoints[0] + ' tweets');
1046 $('#legend-label').css({
1047 "margin-top" : 0,
1048 "color" : "black"
1049 });
1050 }
1051
1052 // Add legend to map
1053 map.controls[google.maps.ControlPosition.LEFT_BOTTOM].push(document.getElementById('legend-holder'));
1054 $('#map_canvas_legend').show();
1055}
1056
1057/**
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001058* Clears ALL map elements - legend, plotted items, overlays
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001059*/
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001060function mapWidgetResetMap() {
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001061
1062 if (selectionRect) {
1063 selectionRect.setMap(null);
1064 selectionRect = null;
1065 }
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001066
1067 mapWidgetClearMap();
1068
1069 // Reset map center and zoom
1070 map.setCenter(new google.maps.LatLng(38.89, -77.03));
1071 map.setZoom(4);
1072}
1073
1074function mapWidgetClearMap() {
1075
1076 // Remove previously plotted data/markers
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001077 for (c in map_cells) {
1078 map_cells[c].setMap(null);
1079 }
1080 map_cells = [];
1081 for (m in map_tweet_markers) {
1082 map_tweet_markers[m].setMap(null);
1083 }
1084 map_tweet_markers = [];
genia.likes.science@gmail.com1b30f3d2013-08-17 23:53:37 -07001085
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001086 // Remove legend from map
1087 map.controls[google.maps.ControlPosition.LEFT_BOTTOM].clear();
genia.likes.science@gmail.com6d6aa8e2013-07-23 01:23:21 -07001088}
1089
1090/**
1091* Uses jenks algorithm in geostats library to find natural breaks in numeric data
1092* @param {number Array} weights of points to plot
1093* @returns {number Array} array of natural breakpoints, of which the top 4 subsets will be plotted
1094*/
1095function mapWidgetLegendComputeNaturalBreaks(weights) {
1096 var plotDataWeights = new geostats(weights.sort());
1097 return plotDataWeights.getJenks(6).slice(2, 7);
1098}
1099
1100/**
1101* Computes values for map legend given a value and an array of jenks breakpoints
1102* @param {number} weight of point to plot on map
1103* @param {number Array} breakpoints, an array of 5 points corresponding to bounds of 4 natural ranges
1104* @returns {String} an RGB value corresponding to a subset of data
1105*/
1106function mapWidgetLegendGetHeatValue(weight, breakpoints) {
1107
1108 // Determine into which range the weight falls
1109 var weightColor = 0;
1110 if (weight >= breakpoints[3]) {
1111 weightColor = 3;
1112 } else if (weight >= breakpoints[2]) {
1113 weightColor = 2;
1114 } else if (weight >= breakpoints[1]) {
1115 weightColor = 1;
1116 }
1117
1118 // Get default map color palette
1119 var colorValues = mapWidgetGetColorPalette();
1120 return colorValues[weightColor];
1121}
1122
1123/**
1124* Returns an array containing a 4-color palette, lightest to darkest
1125* External palette source: http://www.colourlovers.com/palette/2763366/s_i_l_e_n_c_e_r
1126* @returns {Array} [colors]
1127*/
1128function mapWidgetGetColorPalette() {
1129 return [
1130 "rgb(115,189,158)",
1131 "rgb(74,142,145)",
1132 "rgb(19,93,96)",
1133 "rgb(7,51,46)"
1134 ];
1135}
1136
1137/**
1138* Computes radius for a given data point from a spatial cell
1139* @param {Object} keys => ["latSW" "lngSW" "latNE" "lngNE" "weight"]
1140* @returns {number} radius between 2 points in metres
1141*/
1142function mapWidgetComputeCircleRadius(spatialCell, breakpoints) {
1143
1144 var weight = spatialCell.weight;
1145 // Compute weight color
1146 var weightColor = 0.25;
1147 if (weight >= breakpoints[3]) {
1148 weightColor = 1.0;
1149 } else if (weight >= breakpoints[2]) {
1150 weightColor = 0.75;
1151 } else if (weight >= breakpoints[1]) {
1152 weightColor = 0.5;
1153 }
1154
1155 // Define Boundary Points
1156 var point_center = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1157 var point_left = new google.maps.LatLng((spatialCell.latSW + spatialCell.latNE)/2.0, spatialCell.lngSW);
1158 var point_top = new google.maps.LatLng(spatialCell.latNE, (spatialCell.lngSW + spatialCell.lngNE)/2.0);
1159
1160 // TODO not actually a weight color :)
1161 return weightColor * 1000 * Math.min(distanceBetweenPoints_(point_center, point_left), distanceBetweenPoints_(point_center, point_top));
1162}
1163
1164/** External Utility Methods **/
1165
1166/**
1167 * Calculates the distance between two latlng locations in km.
1168 * @see http://www.movable-type.co.uk/scripts/latlong.html
1169 *
1170 * @param {google.maps.LatLng} p1 The first lat lng point.
1171 * @param {google.maps.LatLng} p2 The second lat lng point.
1172 * @return {number} The distance between the two points in km.
1173 * @private
1174*/
1175function distanceBetweenPoints_(p1, p2) {
1176 if (!p1 || !p2) {
1177 return 0;
1178 }
1179
1180 var R = 6371; // Radius of the Earth in km
1181 var dLat = (p2.lat() - p1.lat()) * Math.PI / 180;
1182 var dLon = (p2.lng() - p1.lng()) * Math.PI / 180;
1183 var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
1184 Math.cos(p1.lat() * Math.PI / 180) * Math.cos(p2.lat() * Math.PI / 180) *
1185 Math.sin(dLon / 2) * Math.sin(dLon / 2);
1186 var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
1187 var d = R * c;
1188 return d;
1189};