blob: 9c54a2071b3d71de5b6bb52f55ce6c62ca33db3a [file] [log] [blame]
/**
* @fileoverview Controls for the Geodetic Velocities visualization.
*
* @license Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var geovelo;
geovelo = geovelo || {};
/**
* Implements controls for the visualization.
*
* @param {Element} containerElement The DOM element into which to insert.
*/
geovelo.Controls = 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 the controls.
this.domElement = document.createElement('div');
if (containerElement) {
containerElement.appendChild(this.domElement);
}
// String that displays the current status of the visualization.
this.status = 'waiting for data';
// Percentage progress of the current operation, a number from 0 to 100.
this.progress = 0;
// Insert hidden file input for loading local data.
var loadFileInput = this.loadFileInput = document.createElement('input');
loadFileInput.style.display = 'none';
loadFileInput.type = 'file';
loadFileInput
.addEventListener('change', this.loadFileChange.bind(this), false);
this.domElement.appendChild(loadFileInput);
// Instance of dat.GUI for managing most visualization controls.
var gui = this.gui = new dat.GUI({
autoPlace: false,
width: 300
});
gui.add(this, 'status').listen();
gui.add(this, 'progress', 0, 100).listen();
// Collection of folders which have been added to the gui.
var folders = this.folders = {};
// Add buttons for downloading data and loading a local file.
var folder = folders.data =
gui.addFolder(settings.data.displayName || 'data');
folder.add(this, 'downloadData').name('Download data.');
folder.add(this, 'loadFile').name('Load local file.');
// Object holding visualization state based on settings, bucketed by area.
var state = this.state = {};
// Build out local state object and gui representation from settings.
Object.keys(settings).forEach(function(folderName) {
var folderSettings = settings[folderName];
var folderState = state[folderName] = {};
var folder = folders[folderName];
if (!folder) {
folder = folders[folderName] =
gui.addFolder(folderSettings.displayName || folderName);
}
if (folderSettings.open) {
folder.open();
}
// Build out a control for each of the folder's options.
Object.keys(folderSettings.options).forEach(function(optionName) {
var option = folderSettings.options[optionName];
folderState[optionName] = option.defaultValue;
var ctrl = folder.add(folderState, optionName);
if ('min' in option) {
ctrl = ctrl.min(option.min);
}
if ('max' in option) {
ctrl = ctrl.max(option.max);
}
if ('step' in option) {
ctrl = ctrl.step(option.step);
}
ctrl = ctrl.listen();
if ('displayName' in option) {
ctrl = ctrl.name(option.displayName);
}
// Fire a custom 'settings-changed' event on the gui's DOM element when
// a setting changes.
ctrl.onChange(function(value) {
var event = new CustomEvent('settings-changed', {
bubbles: true,
detail: {
folderName: folderName,
optionName: optionName,
value: value
}
});
gui.domElement.dispatchEvent(event);
});
});
});
// Finally, attach gui DOM element to the provided container element.
this.domElement.appendChild(gui.domElement);
};
/**
* Given a date, compute and set the sampleStart index.
*/
geovelo.Controls.prototype.setSampleStartDate = function(date) {
};
/**
* On the next tick, parse incoming text as JSON, then proceed to prepare the
* data as necessary.
*/
geovelo.Controls.prototype.parseText = function(text) {
this.status = 'parsing data...';
requestAnimationFrame(function() {
try {
var data = JSON.parse(text);
this.prepareData(data);
} catch (err) {
this.status = 'ERROR: Invalid JSON.';
}
}.bind(this));
};
/**
* Once raw data is ready (either downloaded or loaded locally from a file) we
* need to apply corrections and compute intermediate values (like medians).
*/
geovelo.Controls.prototype.prepareData = function(data) {
this.status = 'preparing data...';
requestAnimationFrame(function() {
geovelo.data.fillBeaconGaps(data);
this.gui.domElement.dispatchEvent(
new CustomEvent('data-ready', { bubbles: true, detail: data }));
}.bind(this));
};
/**
* Download a data file directly from the server.
*/
geovelo.Controls.prototype.downloadData = function() {
this.status = 'downloading data...';
// Update progress bar as download proceeds.
var startTime = Date.now();
var xhr = new XMLHttpRequest();
xhr.addEventListener('progress', function(event) {
var duration = Date.now() - startTime;
var rate = event.loaded / duration;
if (event.lengthComputable) {
this.progress = 100 * event.loaded / event.total;
} else {
var totalGuess = 2e8; // Assume file size is around 200MB.
var threshold = 97; // Maximum progress to show.
this.progress = Math.min(100 * event.loaded / totalGuess, threshold);
}
}.bind(this), false);
// Handle errors, or abort by the user.
var handleError = function(event) {
this.progress = 0;
this.status = 'ERROR: Download failed.';
}.bind(this);
xhr.addEventListener('abort', handleError);
xhr.addEventListener('error', handleError);
// When the file is loaded, parse it.
xhr.addEventListener('load', function(event) {
this.progress = 100;
this.parseText(xhr.responseText);
}.bind(this));
// Kick off the request.
xhr.open('GET', 'data/beacon-data.json');
xhr.send();
};
/**
* Called when the user clicks the Load button on the controls. Triggers the
* native file input's chooser dialog via a synthetic click event.
*/
geovelo.Controls.prototype.loadFile = function() {
var event = document.createEvent('MouseEvents');
event.initMouseEvent(
'click', true, true, window,
0, 0, 0, 0, 0,
false, false, false, false,
0, null
);
this.loadFileInput.value = null;
this.loadFileInput.dispatchEvent(event);
};
/**
* Called when the user has selected a file from the native file chooser for
* the load file input.
*/
geovelo.Controls.prototype.loadFileChange = function(event) {
// Get the file.
var file = event.target.files[0];
if (file) {
this.status = 'reading data...';
} else {
this.status = 'ERROR: No file chosen.';
return;
}
// Set up a file reader.
var reader = new FileReader();
reader.onload = function(event) {
this.parseText(event.target.result);
}.bind(this);
reader.onprogress = function(event) {
if (event.lengthComputable) {
this.progress = 100 * event.loaded / event.total;
}
}.bind(this);
reader.readAsText(file);
};