LoRaWAN-Signal-Mapping/client_src/js/LayerAI.mjs

214 lines
5.5 KiB
JavaScript

"use strict";
import L from 'leaflet';
import chroma from 'chroma-js';
import GetFromUrl from './Helpers/GetFromUrl.mjs';
import Config from './ClientConfig.mjs';
import AIWorker from './Worker/AI.worker.mjs';
class LayerAI {
/**
* Computes a bounding box that exactly encompasses all the gateways in
* the index.
* @return {{north:number,south:number,east:number,west:number}} The computed bounding box
*/
get gateway_bounds() {
let result = {
east: Infinity,
west: -Infinity,
north: Infinity,
south: -Infinity
};
for(let gateway of this.index.index) {
result.east = Math.min(gateway.longitude, result.east);
result.west = Math.max(gateway.longitude, result.west);
result.north = Math.min(gateway.latitude, result.north);
result.south = Math.max(gateway.latitude, result.south);
}
return result;
}
/**
* Initialises a new Leaflet AI layer instance.
* @param {[type]} map [description]
*/
constructor(map) {
this.map = map;
this.worker = new AIWorker();
this.map_bounds = null;
}
/**
* Sets up the Web Worker that does the TensorFlow prediction.
* Using a web worker avoids hanging the main thread.
* @return {Promise} A Promise that resolves when setup is complete.
*/
worker_setup() {
// Arrow functions inherit the parent scope, including the "this"
// special variable.
return new Promise((resolve, reject) => {
// Attach the listener first
this.worker.addEventListener("message", (event) => {
if(event.data.event !== "setup-complete") {
reject(`Error: AIWorker responded with event ${event.data.event}, but 'setup-complete' was expected.`, event.data);
return;
}
resolve();
}, { once: true });
// Ask the web worker to set itself up
this.worker.postMessage({
event: "setup",
bounds: this.gateway_bounds,
index: this.index
});
})
}
/**
* Uses the Web Worker to predict a row of signal strength values.
* @param {number} latitude The latitude for which predictions should be made.
* @return {Promise<number[]>} A Promise returning the array of predictions calculated by the web worker.
*/
worker_predict_row(latitude) {
return new Promise((resolve, reject) => {
// Attach the event listener....
this.worker.addEventListener("message", (event) => {
if(event.data.event !== "result") {
reject(`Error: AIWorker responded with event ${event.data.event}, but 'result' was expected.`, event.data);
return;
}
resolve(event.data);
}, { once: true });
// ....and send the request
this.worker.postMessage({
event: "predict-row",
latitude
});
});
}
/**
* Sets up the Leaflet AI visualisation layer.
* @return {Promise} A promise that resolves when setup is complete.
*/
async setup() {
// Download the index file that tells us where the gateways and their
// trained models are located
this.index = JSON.parse(
await GetFromUrl(Config.ai_index_file)
);
console.log(this.index);
// Figure out the bounds of the map we're going to generate
this.map_bounds = this.gateway_bounds;
map_bounds.north += Config.border.lat;
map_bounds.south -= Config.border.lat;
map_bounds.east += Config.border.lng;
map_bounds.west -= Config.border.lng;
// Setup the web worker
await this.worker_setup();
// Generate the Leaflet layer
this.layer = await this.generate_layer();
this.layer.addTo(this.map);
console.log("[Layer/AI] Complete");
}
/**
* Generates and returns the Leaflet layer containing the AI-predicted
* values.
* @return {Promise} A Promise that resolves to the generated Leaflet layer.
*/
async generate_layer() {
console.log("[Layer/AI] Rendering map");
let map = this.render_map();
console.log("[Layer/AI] Passing to Leaflet");
return L.geoJSON(map, {
style: (feature) => { return {
stroke: true,
color: feature.properties.colour,
weight: 1,
fillColor: feature.properties.colour,
fillOpacity: 0.4
} }
});
}
/**
* Uses a Web Worker and pre-trained AIs to generate a GeoJSON map of
* signal strength for a bounding box around the known gateways.
* @return {Promise} A Promise that resolves to a GeoJSON array representing the map.
*/
async render_map() {
let coverage = [],
colour_scale = chroma.scale([
Config.colour_scale.min,
Config.colour_scale.max
]).domain(
this.index.properties.rssi_min,
this.index.properties.rssi_max
);
let stats = {
rssi_min: Infinity,
rssi_max: -Infinity
};
for(let lat = this.map_bounds.south; lat < this.map_bounds.north; lat += Config.step.lat) {
let next_row = await worker_predict_row(lat);
let lng = this.map_bounds.west;
for(let value of next_row) {
// Keep up with the statistics
if(value > stats.rssi_max)
stats.rssi_max = value;
if(value < stats.rssi_min)
stats.rssi_min = value;
// Generate the GeoJSON feature for this cell of the map
coverage.push({
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [
[ // Outer shape
[lng, lat],
[lng, lat + Config.step.lat],
[lng + Config.step.lng, lat + Config.step.lat],
[lng + Config.step.lng, lat]
]
// If there were any holes in the shape, we'd put them here
]
},
properties: {
colour: colour_scale(value).toString()
}
});
lng += Config.step.lng;
}
}
console.log(stats);
return coverage;
}
}
export default LayerAI;