implementing animation, color picker, and markers
diff --git a/js/app.js b/js/app.js
index 03b9fb6..f60ccf9 100644
--- a/js/app.js
+++ b/js/app.js
@@ -47,6 +47,12 @@
timeRange.domElement.style.display = 'none';
timeRange.domElement.className = 'time-range';
+ // Set the default range colors.
+ overlay.setStartColor(controls.state.style.startColor);
+ overlay.setEndColor(controls.state.style.endColor);
+ timeRange.setStartColor(controls.state.style.startColor);
+ timeRange.setEndColor(controls.state.style.endColor);
+
// Update the Overlay viewport when the map bounds change.
map.setOverlayElement(overlay.domElement);
map.onBoundsChanged(overlay.setBounds.bind(overlay));
@@ -60,16 +66,50 @@
// Listen for settings and data events from the controls.
controls.domElement.addEventListener('settings-changed', function(event) {
var setting = event.detail.folderName + '/' + event.detail.optionName;
- if (setting === 'data/multiplier') {
- overlay.setMultiplier(event.detail.value);
- } else if (setting === 'data/medianCorrection') {
- overlay.setMedianCorrection(event.detail.value);
+ var value = event.detail.value;
+ switch (setting) {
+ case 'data/multiplier':
+ overlay.setMultiplier(value);
+ break;
+ case 'data/medianCorrection':
+ overlay.setMedianCorrection(value);
+ break;
+ case 'data/showMarkers':
+ map.setMarkerVisibility(value);
+ break;
+ case 'style/startColor':
+ overlay.setStartColor(value);
+ timeRange.setStartColor(value);
+ break;
+ case 'style/endColor':
+ overlay.setEndColor(value);
+ timeRange.setEndColor(value);
+ break;
+ case 'style/lineWidth':
+ overlay.setLineWidth(value);
+ break;
+ case 'animation/enabled':
+ value ? overlay.startAnimation() : overlay.stopAnimation();
+ break;
+ case 'animation/duration':
+ overlay.setAnimationDuration(value);
+ break;
+ case 'animation/delay':
+ overlay.setAnimationDelay(value);
+ break;
+ case 'animation/showStats':
+ value ? overlay.showStats() : overlay.hideStats();
+ break;
+ default:
+ throw Error('Unrecogrized setting: ' + setting);
+ break;
}
}, false);
// Listen for data-ready events from the controls element, feed to components
// that need to know.
controls.domElement.addEventListener('data-ready', function(event) {
+ map.setData(event.detail);
overlay.setData(event.detail);
}, false);
diff --git a/js/controls.js b/js/controls.js
index 9c54a20..48bedff 100644
--- a/js/controls.js
+++ b/js/controls.js
@@ -89,7 +89,12 @@
Object.keys(folderSettings.options).forEach(function(optionName) {
var option = folderSettings.options[optionName];
folderState[optionName] = option.defaultValue;
- var ctrl = folder.add(folderState, optionName);
+ var ctrl;
+ if (option.type === 'color') {
+ ctrl = folder.addColor(folderState, optionName);
+ } else {
+ ctrl = folder.add(folderState, optionName);
+ }
if ('min' in option) {
ctrl = ctrl.min(option.min);
}
diff --git a/js/map.js b/js/map.js
index 4cbeebe..4a18cac 100644
--- a/js/map.js
+++ b/js/map.js
@@ -16,10 +16,13 @@
* limitations under the License.
*/
-// Map requires google maps.
+// Map requires Google Maps and d3.
if (typeof google === 'undefined' || typeof google.maps === 'undefined') {
throw Error('Google Maps is required but missing.');
}
+if (typeof d3 === 'undefined') {
+ throw Error('d3 is required but missing.');
+}
var geovelo;
geovelo = geovelo || {};
@@ -41,14 +44,20 @@
containerElement.appendChild(this.domElement);
}
+ // Array of Google Maps Markers. Will be created when data arrives.
+ this.markers = null;
+
+ // Whether markers should be visible.
+ this.markerVisibility = false;
+
// Callback handlers for when the bounds change.
this.boundsChangedHandlers = [];
- // Initial coordinates.
+ // Initial coordinates. Will center on data once loaded.
var initial = {
- zoom: 5,
- lon: 139.7667,
- lat: 35.6833
+ zoom: 2,
+ lon: 0,
+ lat: 0,
};
// Google Map Stylers.
@@ -82,7 +91,7 @@
// Create the Google Map
var map = this.map = new google.maps.Map(this.domElement, {
- zoom: 5,
+ zoom: initial.zoom,
center: new google.maps.LatLng(initial.lat, initial.lon),
mapTypeControl: false,
mapTypeId: google.maps.MapTypeId.TERRAIN,
@@ -109,6 +118,137 @@
//map.addListener('center_changed', emit);
map.addListener('zoom_changed', emit);
map.addListener('idle', emit);
+
+ // D3 selection which will be host to the clicked beacon's info.
+ this.infoContent = d3.select(document.createElement('div')).html(`
+ <h2 class="name"></h2>
+ <p>
+ Start: <span class="start"></span>
+ </p>
+ <p>
+ Lat, Lon: <span class="lat"></span>, <span class="lon"></span>
+ </p>
+ `);
+
+ // Info window to show data about a particular marker. Content is bound to
+ // the infoContent div.
+ this.infoWindow = new google.maps.InfoWindow({
+ content: this.infoContent[0][0]
+ });
+};
+
+/**
+ * New beacon data is available. Set up markers and zoom over there.
+ *
+ * @param {Array} beacons An array of data for the beacons.
+ */
+geovelo.Map.prototype.setData = function(beacons) {
+ // Desired map bounds based on min/max of east/west and south/north.
+ var bounds = {
+ east: -Infinity,
+ west: Infinity,
+ north: -90,
+ south: 90,
+ };
+
+ var markers = [];
+
+ // Create a marker for each beacon.
+ for (var i = 0; i < beacons.length; i++) {
+ var marker = this.createMarker(beacons[i]);
+ markers.push(marker);
+
+ var pos = marker.getPosition();
+ var lat = pos.lat();
+ var lon = pos.lng();
+
+ bounds.east = Math.max(bounds.east, lon);
+ bounds.west = Math.min(bounds.west, lon);
+ bounds.north = Math.max(bounds.north, lat);
+ bounds.south = Math.min(bounds.south, lat);
+ }
+
+ this.map.fitBounds(bounds);
+
+ this.markers = markers;
+};
+
+/**
+ * Helper function for creating a Google Maps marker from a beacon.
+ *
+ * @param {Object} beacon Data object representing a beacon.
+ */
+geovelo.Map.prototype.createMarker = function(beacon) {
+
+ // Keep track of the first, max and min lat and lon values.
+ var lat = null;
+ var lon = null;
+ var minLat = 90;
+ var maxLat = -90;
+ var minLon = Infinity;
+ var maxLon = -Infinity;
+
+ // Roll through the beacon's lat/lon pairs and take note.
+ for (var i = 0; i < beacon.lat.length; i++) {
+ var currentLat = beacon.lat[i];
+ var currentLon = beacon.lon[i];
+ if (lat === null && currentLat && currentLon) {
+ lat = currentLat;
+ lon = currentLon;
+ }
+ minLat = Math.min(minLat, currentLat);
+ minLat = Math.min(minLat, currentLat);
+ }
+
+ // If a non-missing lat/lon pair couldn't be found, that's an error.
+ if (!lat || !lon) {
+ throw Error('Beacon has no non-zero coordinates: ' + beacon.name);
+ }
+
+ var marker = new google.maps.Marker({
+ position: { lng: lon, lat: lat },
+ map: this.map,
+ title: beacon.name,
+ label: beacon.name,
+ visible: this.markerVisibility,
+ });
+
+ // Derived values from the beacon.
+ var startDate = new Date(beacon.start * 1000);
+
+ // When marker is clicked, update the Info Window content and show it.
+ marker.addListener('click', function() {
+ // This code makes heavy use of the d3 join/enter/update pattern.
+ // JOIN.
+ var content = this.infoContent.data([beacon]);
+
+ content.select('.name').text(beacon.name);
+ content.select('.start').text(startDate.toDateString());
+ content.select('.lat').text(lat.toFixed(3));
+ content.select('.lon').text(lon.toFixed(3));
+
+ this.infoWindow.open(this.map, marker);
+ }.bind(this));
+
+ return marker;
+};
+
+/**
+ * Show or hide beacon markers by setting their visibility.
+ *
+ * @param {boolean} visibility Whether to show (true) or hide (false) markers.
+ */
+geovelo.Map.prototype.setMarkerVisibility = function(visibility) {
+ this.markerVisibility = !!visibility;
+ if (!this.markers) {
+ return;
+ }
+ for (var i = 0; i < this.markers.length; i++) {
+ this.markers[i].setVisible(this.markerVisibility);
+ }
+ if (!this.markerVisibility) {
+ this.infoWindow.close();
+ }
};
/**
diff --git a/js/overlay.js b/js/overlay.js
index 1a45bd1..2337e60 100644
--- a/js/overlay.js
+++ b/js/overlay.js
@@ -34,6 +34,12 @@
*/
geovelo.Overlay = function(containerElement) {
+ // Local reference to the geovelo settings object, throw if missing.
+ var settings = geovelo.settings;
+ if (!settings) {
+ throw Error('geovelo.settings is missing!');
+ }
+
// DOM Element into which to insert content.
this.domElement = document.createElement('div');
this.domElement.style.pointerEvents = 'none';
@@ -83,12 +89,18 @@
stats.domElement.style.position = 'absolute';
stats.domElement.style.top = 0;
stats.domElement.style.left = 0;
+ stats.domElement.style.display = 'none';
this.domElement.appendChild(stats.domElement);
}
+ // Whether to show stats.
+ if (settings.animation.options.showStats.defaultValue) {
+ this.showStats();
+ }
+
// Set up the custom line shader material to use for drawing lines.
this.material = new geovelo.LineShaderMaterial({
- linewidth: 1
+ linewidth: settings.style.options.lineWidth.defaultValue
});
// Time in ms to allow processing to hold the thread before ceding to the UI.
@@ -99,6 +111,104 @@
// Time in ms to wait before invoking functions in need of debouncing.
this.debounceTimeout = 50;
+
+ // Animation duration and delay.
+ this.animationDuration = settings.animation.options.duration.defaultValue;
+ this.animationDelay = settings.animation.options.delay.defaultValue;
+
+ // Whether we're currently animating.
+ this.animating = false;
+ if (settings.animation.options.enabled.defaultValue) {
+ this.startAnimation();
+ }
+};
+
+/**
+ * Start animating.
+ */
+geovelo.Overlay.prototype.startAnimation = function() {
+ this.animating = true;
+
+ // The start timestamp for the current animation loop.
+ var start = null;
+
+ var animate = (function() {
+ // Short-circuit if animation has been turned off.
+ if (!this.animating) {
+ return;
+ }
+
+ var now = Date.now();
+
+ if (start === null) {
+ start = now;
+ }
+
+ var startTimestamp = this.material.uniforms.startTimestamp.value;
+ var endTimestamp = this.material.uniforms.endTimestamp.value;
+ var diff = endTimestamp - startTimestamp;
+
+ // If there's any time between the start and end timestamps, set the
+ // end animation clamp and render the scene.
+ if (diff) {
+ this.material.uniforms.endAnimationClamp.value =
+ startTimestamp + diff * (now - start) / this.animationDuration;
+ this.render();
+ }
+
+ // If we've finished the loop, pause for the delay, otherwise queue up the
+ // next frame.
+ if (now > start + this.animationDuration) {
+ start = null;
+ setTimeout(animate, this.animationDelay);
+ } else {
+ requestAnimationFrame(animate);
+ }
+ }).bind(this);
+
+ // Kick off animation loop.
+ requestAnimationFrame(animate);
+};
+
+/**
+ * Stop animating.
+ */
+geovelo.Overlay.prototype.stopAnimation = function() {
+ this.animating = false;
+ this.material.uniforms.endAnimationClamp.value = Infinity;
+ this.queueRender();
+};
+
+/**
+ * Set animation duration.
+ */
+geovelo.Overlay.prototype.setAnimationDuration = function(duration) {
+ this.animationDuration = duration;
+};
+
+/**
+ * Set animation delay.
+ */
+geovelo.Overlay.prototype.setAnimationDelay = function(delay) {
+ this.animationDelay = delay;
+};
+
+/**
+ * Show the FPS stats display.
+ */
+geovelo.Overlay.prototype.showStats = function() {
+ if (this.stats) {
+ this.stats.domElement.style.display = null;
+ }
+};
+
+/**
+ * Hide the FPS stats display.
+ */
+geovelo.Overlay.prototype.hideStats = function() {
+ if (this.stats) {
+ this.stats.domElement.style.display = 'none';
+ }
};
/**
@@ -175,7 +285,7 @@
/**
* New beacon data is available. Preprocess the data and reconstruct the scene.
- * @param {Array} beacons An array of data for the beacons.
+ * @param {Array} rawBeacons An array of data for the beacons.
*/
geovelo.Overlay.prototype.setData = function(rawBeacons) {
@@ -533,6 +643,38 @@
};
/**
+ * Set the color to use for the start of the range.
+ *
+ * @param {string} startColor The color to set for the start of the range.
+ */
+geovelo.Overlay.prototype.setStartColor = function(startColor) {
+ var color = d3.rgb(startColor);
+ this.material.uniforms.startColor.value
+ .set(color.r / 255, color.g / 255, color.b / 255, 1);
+ this.queueRender();
+};
+
+/**
+ * Set the color to use for the end of the range.
+ *
+ * @param {string} endColor The color to set for the end of the range.
+ */
+geovelo.Overlay.prototype.setEndColor = function(endColor) {
+ var color = d3.rgb(endColor);
+ this.material.uniforms.endColor.value
+ .set(color.r / 255, color.g / 255, color.b / 255, 1);
+ this.queueRender();
+};
+
+/**
+ * Set the line width.
+ */
+geovelo.Overlay.prototype.setLineWidth = function(lineWidth) {
+ this.material.linewidth = lineWidth;
+ this.queueRender();
+};
+
+/**
* Set the starting timestamp of the line shader material. This will map to the
* start color and start opacity.
*
diff --git a/js/settings.js b/js/settings.js
index b555a65..0343e37 100644
--- a/js/settings.js
+++ b/js/settings.js
@@ -31,6 +31,7 @@
* - description - Opitonal string describing this folder or option.
* - defaultValue - The starting value to use for this setting.
* - min, max - The smallest and largest allowed values.
+ * - type - Number (default) or color picker.
*/
geovelo.settings = {
@@ -47,7 +48,7 @@
defaultValue: 5.5,
min: 1,
max: 8,
- step: 0.01
+ step: 0.01,
},
medianCorrection: {
displayName: 'median correction',
@@ -55,9 +56,76 @@
'How much of the cumulative median movement to subtract out.',
defaultValue: 1,
min: 0,
- max: 1
- }
- }
- }
+ max: 1,
+ },
+ showMarkers: {
+ displayName: 'show markers',
+ description: 'Whether to show a Google Maps marker for each beacon.',
+ defaultValue: false,
+ },
+ },
+ },
+
+ style: {
+ displayName: 'Style',
+ description: 'Settings for the style and behavior of the visualization.',
+ open: true,
+ options: {
+ startColor: {
+ displayName: 'start color',
+ description: 'Color to use for the start of the time range.',
+ defaultValue: '#0000ff',
+ type: 'color',
+ },
+ endColor: {
+ displayName: 'end color',
+ description: 'Color to use for the end of the time range.',
+ defaultValue: '#ff0000',
+ type: 'color',
+ },
+ lineWidth: {
+ displayName: 'line width',
+ description: 'Width of line when rendering.',
+ defaultValue: 1,
+ min: 1,
+ max: 10,
+ step: 0.1,
+ },
+ },
+ },
+
+ animation: {
+ displayName: 'Animation',
+ description: 'Settings for the looping animation of lines.',
+ open: true,
+ options: {
+ enabled: {
+ displayName: 'enabled',
+ description: 'Whether animation is enabled.',
+ defaultValue: false,
+ },
+ duration: {
+ displayName: 'duration (ms)',
+ description: 'How long the animation loop takes to complete.',
+ defaultValue: 4000,
+ min: 500,
+ max: 10000,
+ step: 500,
+ },
+ delay: {
+ displayName: 'delay (ms)',
+ description: 'How long to wait before restarting a finished loop.',
+ defaultValue: 1000,
+ min: 0,
+ max: 3000,
+ step: 100,
+ },
+ showStats: {
+ displayName: 'show FPS',
+ description: 'Whether to show the FPS stats meter.',
+ defaultValue: false,
+ },
+ },
+ },
};
diff --git a/js/timerange.js b/js/timerange.js
index 363ab7f..92ebfe3 100644
--- a/js/timerange.js
+++ b/js/timerange.js
@@ -53,6 +53,12 @@
// Date object representing the position of the right (end) nub.
this.rangeEnd = null;
+ // String representing the color to fill the start nub.
+ this.startColor = null;
+
+ // String representing the color to fill the end nub.
+ this.endColor = null;
+
// D3 scale for the time range. Will be updated on draw to match parameters.
var timeScale = this.timeScale = d3.time.scale();
@@ -68,7 +74,7 @@
this.dragBehavior = d3.behavior.drag()
.origin(function (d) {
return {
- x: timeScale(d),
+ x: timeScale(d.time),
y: geovelo.TimeRange.margin.top
};
})
@@ -107,6 +113,30 @@
};
/**
+ * Set the color to use for the start of the range.
+ *
+ * @param {string} startColor The color to set for the start of the range.
+ */
+geovelo.TimeRange.prototype.setStartColor = function(startColor) {
+ this.startColor = startColor;
+ if (this.extentStart && this.extentEnd) {
+ this.drawNubs();
+ }
+};
+
+/**
+ * Set the color to use for the end of the range.
+ *
+ * @param {string} endColor The color to set for the end of the range.
+ */
+geovelo.TimeRange.prototype.setEndColor = function(endColor) {
+ this.endColor = endColor;
+ if (this.extentStart && this.extentEnd) {
+ this.drawNubs();
+ }
+};
+
+/**
* Set the extent of the time range selector. If range has not been set, this
* will initialize the range to match.
*
@@ -274,7 +304,13 @@
// Get the draggable nub elements.
var nubs = this.svg.selectAll('.nub')
- .data([this.rangeStart, this.rangeEnd]);
+ .data([{
+ time: this.rangeStart,
+ color: this.startColor,
+ }, {
+ time: this.rangeEnd,
+ color: this.endColor,
+ }]);
// Insert DOM elements for the nubs if this is the first time.
nubs.enter().append('g')
@@ -284,12 +320,13 @@
.append('path')
.attr('d', 'M -4,-16 v 8 l 4,6 l 4,-6 v -8 z');
- // Move nubs into their correct positions.
+ // Move nubs into their correct positions and set color.
var timeScale = this.timeScale;
nubs.attr('transform', function(d) {
- return 'translate(' + timeScale(d) + ',' +
+ return 'translate(' + timeScale(d.time) + ',' +
geovelo.TimeRange.margin.top + ')';
- });
+ }).select('.nub path')
+ .attr('fill', function(d) { return d.color; });
};
/**
diff --git a/js/web-mercator.js b/js/web-mercator.js
index 61376fa..5a9da99 100644
--- a/js/web-mercator.js
+++ b/js/web-mercator.js
@@ -25,7 +25,7 @@
(function() {
-// Computational contstants.
+// Computational constants.
var DEG_TO_RAD = Math.PI / 180;
var ZOOM_FACTOR = 128 / Math.PI;
var PI_OVER_FOUR = Math.PI / 4;