Initial time dimension implementation.

It's _nasty_, we might need to dispose of leaflet-timedimension 
altogether
This commit is contained in:
Starbeamrainbowlabs 2019-04-13 14:54:44 +01:00
parent 61baac288a
commit 8d61990828
Signed by: sbrl
GPG Key ID: 1BE5172E637709C2
3 changed files with 180 additions and 16 deletions

View File

@ -7,6 +7,10 @@ import GetFromUrl from './Helpers/GetFromUrl.mjs';
class LayerHeatmap {
/**
* Creates a new heatmap manager wrapper class fort he given map.
* @param {L.Map} in_map The leaflet map to attach to.
*/
constructor(in_map) {
this.map = in_map;
@ -77,8 +81,14 @@ class LayerHeatmap {
}
}
};
this.reading_cache = new Map();
}
/**
* Re-creates the heatmap overlay layer.
* Needed sometimes internally to work around an annoying bug.
*/
recreate_overlay() {
if(typeof this.heatmap != "undefined")
this.layer.removeLayer(this.heatmap);
@ -86,6 +96,10 @@ class LayerHeatmap {
this.layer.addLayer(this.heatmap);
}
/**
* Sets the display data to the given array of data points.
* @param {object[]} readings_list The array of data points to display.
*/
set_data(readings_list) {
let data_object = {
max: 0,
@ -102,6 +116,13 @@ class LayerHeatmap {
this.heatmap.setData(data_object);
}
/**
* Updates the heatmap with data for the specified datetime & reading type,
* fetching new data if necessary.
* @param {Date} datetime The datetime to display.
* @param {string} reading_type The reading type to display data for.
* @return {Promise} A promise that resolves when the operation is completed.
*/
async update_data(datetime, reading_type) {
if(!(datetime instanceof Date))
throw new Exception("Error: 'datetime' must be an instance of Date.");
@ -123,15 +144,44 @@ class LayerHeatmap {
}
try {
this.set_data(JSON.parse(await GetFromUrl(
`${Config.api_root}?action=fetch-data&datetime=${encodeURIComponent(this.datetime.toISOString())}&reading_type=${encodeURIComponent(this.reading_type)}`
)));
this.set_data(await this.fetch_data(this.datetime, this.reading_type));
} catch(error) {
console.log(error);
alert(error);
}
}
/**
* Fetches & decodes data for the given datetime and the current reading type.
* @param {Date} datetime The Date to fetch data for.
* @param {string} reading_type The reading type code to fetch data for.
* @return {Promise} The requested data array, as the return value of a promise
*/
async fetch_data(datetime, reading_type) {
let cache_key = Symbol.for(`${reading_type}|${datetime.toISOString()}`);
let result = this.reading_cache.get(cache_key);
if(typeof result == "undefined") {
result = JSON.parse(await GetFromUrl(
`${Config.api_root}?action=fetch-data&datetime=${encodeURIComponent(datetime.toISOString())}&reading_type=${encodeURIComponent(reading_type)}`
));
this.reading_cache.set(cache_key, result);
}
return result;
}
/**
* Whether the reading cache contains data for the given datetime & reading type.
* @param {Date} datetime The datetime to check.
* @param {string} reading_type The reading type code to check.
* @return {Boolean} Whether the reading cache contains data for the requested datetime & reading type.
*/
is_data_cached(datetime, reading_type) {
let cache_key = Symbol.for(`${reading_type}|${datetime.toISOString()}`);
return this.reading_cache.has(cache_key);
}
async update_reading_type(reading_type) {
await this.update_data(
@ -139,12 +189,6 @@ class LayerHeatmap {
reading_type
);
}
async update_datetime(datetime) {
await this.update_data(
datetime,
this.reading_type
);
}
}
export default LayerHeatmap;

View File

@ -0,0 +1,86 @@
"use strict";
class LayerHeatmapGlue {
constructor(time_dimension, heatmap) {
this.time_dimension = time_dimension;
this.heatmap = heatmap;
this.glue_layer_const = L.TimeDimension.Layer.extend({
isReady: this.isReady,
});
this.cache = {};
}
attachTo(map) {
this.layer_glue = new this.glue_layer_const({
timeDimension: this.time_dimension
}).addTo(map);
this.time_dimension.on("timeload", this.update.bind(this));
this.time_dimension.on("timeloading", this.onNewTime.bind(this));
// this.time_dimension.registerSyncedLayer(this.layer_glue);
}
async onNewTime(event) {
console.log("on-new-time", arguments);
// event.time here is a number - TODO: Figure out how to keep it as a date
if(typeof this.cache[event.time] == "undefined") {
await this.loadTime(new Date(event.time));
}
this.fireLoadComplete(event.time);
}
/**
* Loads data into the cache for the specified datetime.
* @param {Date} time The datetime to load data in for.
* @return {[type]} [description]
*/
async loadTime(time) {
await this.heatmap.fetch_data(
time,
this.heatmap.reading_type
);
}
/**
* Fires the loadComplete event to leaflet-timedimension know we're done loading.
* @param {Number} time The timestamp to fire the timeload event for.
*/
fireLoadComplete(time) {
this.layer_glue.fire("timeload", {
time: time
});
}
/**
* Tells leaflet-timedimension if we're ready for a given time.
* @param {object} event The event object
* @return {Boolean} Whether we're ready or not
*/
isReady(event) {
// BUG: This will go awry if the user changes the reading type on the fly
// console.log("is-ready", arguments);
return this.heatmap.is_data_cached(
new Date(event.time),
this.heatmap.reading_type
);
}
/**
* Passes an update to the heatmap manager
* @param {[type]} event [description]
* @return {[type]} [description]
*/
async update(event) {
//console.log("update", arguments);
await this.heatmap.update_data(
new Date(event.time),
this.heatmap.reading_type
);
}
}
export default LayerHeatmapGlue;

View File

@ -9,6 +9,7 @@ import '../../node_modules/leaflet-timedimension/dist/leaflet.timedimension.src.
import Config from './Config.mjs';
import LayerDeviceMarkers from './LayerDeviceMarkers.mjs';
import LayerHeatmap from './LayerHeatmap.mjs';
import LayerHeatmapGlue from './LayerHeatmapGlue.mjs';
import UI from './UI.mjs';
class MapManager {
@ -39,10 +40,6 @@ class MapManager {
this.map.attributionControl.addAttribution("Data: <a href='https://connectedhumber.org/'>Connected Humber</a>");
this.map.attributionControl.addAttribution("<a href='https://github.com/ConnectedHumber/Air-Quality-Web/'>Air Quality Web</a> by <a href='https://starbeamrainbowlabs.com/'>Starbeamrainbowlabs</a>");
// Add the time dimension
this.layer_time = L.timeDimension()
.addTo(this.map);
this.layer_time.on("timeloading", console.log);
// Add the device markers
console.info("[map] Loading device markers....");
@ -55,14 +52,51 @@ class MapManager {
// Add the heatmap
console.info("[map] Loading heatmap....");
this.setup_heatmap().then(() => {
console.info("[map] Heatmap loaded successfully.");
});
this.setup_heatmap()
.then(() => console.info("[map] Heatmap loaded successfully."))
// ...and the time dimension
.then(this.setup_time_dimension.bind(this))
.then(() => console.info("[map] Time dimension initialised."));
this.ui = new UI(Config, this);
this.ui.setup().then(() => console.log("[map] Settings initialised."));
}
setup_time_dimension() {
this.layer_time = new L.TimeDimension({
period: "PT1H", // 1 hour
timeInterval: `2019-01-01T12:00:00Z/${new Date().toISOString()}`
});
//this.layer_time.on("timeloading", console.log.bind(null, "timeloading"));
this.layer_time_player = new L.TimeDimension.Player({
transitionTime: 100,
loop: false,
startOver: true,
buffer: 10 // Default: 5
}, this.layer_time);
this.layer_time_control = new L.Control.TimeDimension({
player: this.layer_time_player,
timeDimension: this.layer_time,
position: "bottomright",
autoplay: false,
minSpeed: 1,
speedStep: 0.25,
maxSpeed: 15,
timeSliderDragUpdate: true
});
this.map.addControl(this.layer_time_control);
// Create the time dimension <---> heatmap glue object
this.layer_heatmap_glue = new LayerHeatmapGlue(
this.layer_time,
this.heatmap
);
this.layer_heatmap_glue.attachTo(this.map);
}
async setup_device_markers() {
this.device_markers = new LayerDeviceMarkers(this.map);
await this.device_markers.setup();