2019-01-17 17:31:59 +00:00
|
|
|
"use strict";
|
|
|
|
|
2019-01-18 20:12:30 +00:00
|
|
|
import HeatmapOverlay from 'leaflet-heatmap';
|
2019-01-17 17:31:59 +00:00
|
|
|
|
|
|
|
import Config from './Config.mjs';
|
2019-01-19 13:35:25 +00:00
|
|
|
import GetFromUrl from './Helpers/GetFromUrl.mjs';
|
2019-01-17 17:31:59 +00:00
|
|
|
|
|
|
|
|
|
|
|
class LayerHeatmap {
|
2019-04-13 13:54:44 +00:00
|
|
|
/**
|
|
|
|
* Creates a new heatmap manager wrapper class fort he given map.
|
|
|
|
* @param {L.Map} in_map The leaflet map to attach to.
|
|
|
|
*/
|
2019-01-17 17:31:59 +00:00
|
|
|
constructor(in_map) {
|
|
|
|
this.map = in_map;
|
|
|
|
|
2019-01-18 21:25:30 +00:00
|
|
|
this.overlay_config = {
|
2019-01-18 20:12:30 +00:00
|
|
|
radius: Config.heatmap.blob_radius,
|
2019-01-19 15:09:38 +00:00
|
|
|
minOpacity: 0.5,
|
2019-01-18 20:12:30 +00:00
|
|
|
maxOpacity: 0.8,
|
|
|
|
scaleRadius: true,
|
|
|
|
useLocalExtrema: false,
|
|
|
|
|
|
|
|
latField: "latitude",
|
|
|
|
lngField: "longitude",
|
2019-01-19 14:31:46 +00:00
|
|
|
valueField: "value",
|
|
|
|
|
|
|
|
gradient: {
|
|
|
|
// 0 to 35 low (green), 36 to 53 moderate (amber) 54 to 70 high (red) and above 71 (purple)
|
|
|
|
0.233: "green",
|
|
|
|
0.593: "orange",
|
|
|
|
0.827: "red",
|
|
|
|
1: "purple"
|
|
|
|
}
|
2019-01-18 21:25:30 +00:00
|
|
|
};
|
2019-01-19 15:09:38 +00:00
|
|
|
this.layer = new L.LayerGroup();
|
2019-01-17 17:31:59 +00:00
|
|
|
this.map.addLayer(this.layer);
|
2019-01-19 15:09:38 +00:00
|
|
|
this.recreate_overlay();
|
2019-01-19 14:31:46 +00:00
|
|
|
|
|
|
|
// Custom configuration directives to apply based on the reading type displayed.
|
|
|
|
this.reading_type_configs = {
|
|
|
|
"PM25": {
|
|
|
|
// 0 to 35 low (green), 36 to 53 moderate (amber) 54 to 70 high (red) and above 71 (purple)
|
|
|
|
max: 75,
|
|
|
|
gradient: {
|
2019-01-19 15:09:38 +00:00
|
|
|
"0": "hsla(111, 76%, 42%, 0.00)",
|
|
|
|
"0.233": "hsl(111, 76%, 42%)", // green
|
|
|
|
"0.593": "orange",
|
|
|
|
"0.827": "red",
|
|
|
|
"1": "purple"
|
2019-01-19 14:31:46 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
"PM10": {
|
|
|
|
// 0 to 50 low (green) 51 to75 moderate (amber) 76 to 100 high (red) and more than 100 very high (purple)
|
|
|
|
max: 100,
|
|
|
|
gradient: {
|
2019-01-19 15:09:38 +00:00
|
|
|
"0": "hsla(111, 76%, 42%, 0)",
|
|
|
|
"0.45": "hsl(111, 76%, 42%)", // green
|
|
|
|
"0.573": "orange",
|
|
|
|
"0.8": "red",
|
|
|
|
"1": "purple"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
"humidity": {
|
|
|
|
max: 100,
|
|
|
|
gradient: {
|
|
|
|
"0": "hsla(176, 77%, 40%, 0)",
|
|
|
|
"0.5": "hsl(176, 77%, 40%)",
|
|
|
|
"1": "blue"
|
2019-01-19 14:31:46 +00:00
|
|
|
}
|
2019-01-19 15:14:59 +00:00
|
|
|
},
|
|
|
|
"temperature": {
|
|
|
|
max: 40,
|
|
|
|
gradient: {
|
|
|
|
"-0.25": "blue",
|
|
|
|
"0.25": "cyan",
|
|
|
|
"0.375": "green",
|
|
|
|
"0.5": "yellow",
|
|
|
|
"0.75": "orange",
|
|
|
|
"1": "red"
|
|
|
|
}
|
2019-01-19 14:31:46 +00:00
|
|
|
}
|
|
|
|
};
|
2019-04-13 13:54:44 +00:00
|
|
|
|
|
|
|
this.reading_cache = new Map();
|
2019-01-17 17:31:59 +00:00
|
|
|
}
|
|
|
|
|
2019-04-13 13:54:44 +00:00
|
|
|
/**
|
|
|
|
* Re-creates the heatmap overlay layer.
|
|
|
|
* Needed sometimes internally to work around an annoying bug.
|
|
|
|
*/
|
2019-01-19 15:09:38 +00:00
|
|
|
recreate_overlay() {
|
|
|
|
if(typeof this.heatmap != "undefined")
|
|
|
|
this.layer.removeLayer(this.heatmap);
|
|
|
|
this.heatmap = new HeatmapOverlay(this.overlay_config);
|
|
|
|
this.layer.addLayer(this.heatmap);
|
|
|
|
}
|
|
|
|
|
2019-04-13 13:54:44 +00:00
|
|
|
/**
|
|
|
|
* Sets the display data to the given array of data points.
|
|
|
|
* @param {object[]} readings_list The array of data points to display.
|
|
|
|
*/
|
2019-01-17 17:31:59 +00:00
|
|
|
set_data(readings_list) {
|
2019-01-18 20:12:30 +00:00
|
|
|
let data_object = {
|
2019-01-19 14:31:46 +00:00
|
|
|
max: 0,
|
2019-01-18 20:12:30 +00:00
|
|
|
data: readings_list
|
2019-01-19 14:31:46 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
if(typeof this.reading_type_configs[this.reading_type] != "undefined")
|
|
|
|
data_object.max = this.reading_type_configs[this.reading_type].max;
|
|
|
|
else
|
|
|
|
data_object.max = readings_list.reduce((prev, next) => next.value > prev ? next.value : prev, 0);
|
|
|
|
|
2019-01-19 15:09:38 +00:00
|
|
|
console.log("[map/heatmap] Displaying", this.reading_type, data_object);
|
2019-01-19 14:31:46 +00:00
|
|
|
|
2019-01-19 15:09:38 +00:00
|
|
|
this.heatmap.setData(data_object);
|
2019-01-17 17:31:59 +00:00
|
|
|
}
|
2019-01-19 13:35:25 +00:00
|
|
|
|
2019-04-13 13:54:44 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2019-01-19 13:35:25 +00:00
|
|
|
async update_data(datetime, reading_type) {
|
|
|
|
if(!(datetime instanceof Date))
|
|
|
|
throw new Exception("Error: 'datetime' must be an instance of Date.");
|
|
|
|
if(typeof reading_type != "string")
|
|
|
|
throw new Exception("Error: 'reading_type' must be a string.");
|
|
|
|
|
|
|
|
this.datetime = datetime;
|
|
|
|
this.reading_type = reading_type;
|
|
|
|
|
|
|
|
console.log("[map/heatmap] Updating values to", this.reading_type, "@", this.datetime);
|
|
|
|
|
2019-01-19 14:31:46 +00:00
|
|
|
if(typeof this.reading_type_configs[this.reading_type] != "undefined") {
|
|
|
|
this.overlay_config.gradient = this.reading_type_configs[this.reading_type].gradient;
|
2019-01-19 15:09:38 +00:00
|
|
|
console.log("[map/heatmap] Gradient is now", this.overlay_config.gradient);
|
|
|
|
this.recreate_overlay();
|
2019-01-19 14:31:46 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
delete this.overlay_config.gradient;
|
|
|
|
}
|
|
|
|
|
2019-01-19 15:09:38 +00:00
|
|
|
try {
|
2019-04-13 13:54:44 +00:00
|
|
|
this.set_data(await this.fetch_data(this.datetime, this.reading_type));
|
2019-01-19 15:09:38 +00:00
|
|
|
} catch(error) {
|
|
|
|
console.log(error);
|
|
|
|
alert(error);
|
|
|
|
}
|
2019-01-19 13:35:25 +00:00
|
|
|
}
|
|
|
|
|
2019-04-13 13:54:44 +00:00
|
|
|
/**
|
|
|
|
* 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) {
|
2019-04-14 16:39:15 +00:00
|
|
|
let cache_key = `${reading_type}|${datetime.toISOString()}`;
|
2019-04-13 13:54:44 +00:00
|
|
|
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)}`
|
|
|
|
));
|
2019-04-14 16:39:15 +00:00
|
|
|
this.prune_cache(100);
|
|
|
|
this.reading_cache.set(cache_key, { data: result, inserted: new Date() });
|
2019-04-13 13:54:44 +00:00
|
|
|
}
|
2019-04-14 16:39:15 +00:00
|
|
|
else
|
|
|
|
result = result.data;
|
2019-04-13 13:54:44 +00:00
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2019-04-14 16:39:15 +00:00
|
|
|
/**
|
|
|
|
* Prunes the reading cache, leaving at most newest_count items behind.
|
|
|
|
* The items inserted first are deleted first.
|
|
|
|
* @param {Number} newest_count The numebr of items to leave behind in the cache.
|
|
|
|
* @returns {Number} The number of items deleted from the cache.
|
|
|
|
*/
|
|
|
|
prune_cache(newest_count) {
|
|
|
|
let items = [];
|
|
|
|
for(let next_key of this.reading_cache) {
|
|
|
|
let cache_item = this.reading_cache.get(next_key);
|
|
|
|
if(typeof cache_item == "undefined") {
|
|
|
|
this.reading_cache.delete(cache_item);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
items.push({
|
|
|
|
key: next_key,
|
|
|
|
date: cache_item.inserted
|
|
|
|
});
|
|
|
|
}
|
|
|
|
items.sort((a, b) => a.date - b.date);
|
|
|
|
let deleted = 0;
|
|
|
|
for(let i = 0; i < items.length - newest_count; i++) {
|
|
|
|
this.reading_cache.delete(this.items[i].key);
|
|
|
|
deleted++;
|
|
|
|
}
|
|
|
|
return deleted;
|
|
|
|
}
|
|
|
|
|
2019-04-13 13:54:44 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
2019-01-19 13:35:25 +00:00
|
|
|
|
|
|
|
async update_reading_type(reading_type) {
|
|
|
|
await this.update_data(
|
|
|
|
this.datetime,
|
|
|
|
reading_type
|
|
|
|
);
|
|
|
|
}
|
2019-01-17 17:31:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export default LayerHeatmap;
|