215 lines
6.3 KiB
JavaScript
215 lines
6.3 KiB
JavaScript
"use strict";
|
|
|
|
import path from 'path';
|
|
|
|
import L from 'leaflet';
|
|
import chroma from 'chroma-js';
|
|
|
|
import WorkerWrapper from './WorkerWrapper.mjs';
|
|
import GetFromUrl from './Helpers/GetFromUrl.mjs';
|
|
import Config from './ClientConfig.mjs';
|
|
import promise_unwind from './Helpers/PromiseUnwind.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) {
|
|
// The Leaflet map instance to attach to
|
|
this.map = map;
|
|
// The array of web workers that do the computation
|
|
this.workers = [];
|
|
// The array the GeoJSON is stored in as it's generated
|
|
this.geojson = [];
|
|
|
|
this.stats = { rssi_min: Infinity, rssi_max: -Infinity };
|
|
|
|
// The bounding box of the map we're generating
|
|
this.map_bounds = null;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 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);
|
|
|
|
for(let gateway of this.index.index) {
|
|
gateway.frozen_net = JSON.parse(await GetFromUrl(`${window.location.href}/${path.dirname(Config.ai_index_file)}/${gateway.filename}`))
|
|
}
|
|
|
|
// Figure out the bounds of the map we're going to generate
|
|
this.map_bounds = this.gateway_bounds;
|
|
this.map_bounds.north += Config.border.lat;
|
|
this.map_bounds.south -= Config.border.lat;
|
|
|
|
this.map_bounds.east += Config.border.lng;
|
|
this.map_bounds.west -= Config.border.lng;
|
|
|
|
// Setup the colour scale
|
|
this.colour_scale = chroma.scale(
|
|
Object.values(Config.colour_scale),
|
|
).domain(
|
|
Object.keys(Config.colour_scale).map(item => parseInt(item, 10))
|
|
);
|
|
|
|
// Setup the web worker army
|
|
for(let i = 0; i < (navigator.hardwareConcurrency || 4); i++) {
|
|
console.info(`Starting worker ${i}`);
|
|
let next_worker = new WorkerWrapper();
|
|
|
|
await next_worker.setup(this.map_bounds, this.index);
|
|
this.workers.push(next_worker);
|
|
}
|
|
|
|
// 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 = await 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: 1
|
|
} }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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 work_generator = (function*() {
|
|
for(let lat = this.map_bounds.south; lat < this.map_bounds.north; lat += Config.step.lat) {
|
|
yield lat;
|
|
}
|
|
}).bind(this)();
|
|
|
|
let unwinders = [];
|
|
for(let i = 0; i < this.workers.length; i++) {
|
|
let next_latitude = work_generator.next().value;
|
|
let next_promise = this.workers[i].predict_row(next_latitude)
|
|
.then(this.process_result_row.bind(this, next_latitude, i, work_generator));
|
|
|
|
unwinders.push(promise_unwind(next_promise));
|
|
}
|
|
|
|
// Wait for all the workers to finish
|
|
await Promise.all(unwinders);
|
|
|
|
this.workers.length = 0; // Empty the array of workers
|
|
|
|
console.log("Statistics:", this.stats);
|
|
|
|
return this.geojson;
|
|
}
|
|
|
|
/**
|
|
* Processes a single row of results from a web worker.
|
|
* @param {number} lat The latitude value that was processed by the web worker.
|
|
* @param {number} worker_index The index of the worker that returned the results.
|
|
* @param {Generator} work_generator The work generator to use to queue the next work item.
|
|
* @param {Object} next_result The result object returned by the web worker.
|
|
* @return {Promise<Object>|null} Returns a Promise that resolves to the next work item that need processing, or null if allt he work hass been completed.
|
|
*/
|
|
process_result_row(lat, worker_index, work_generator, next_result) {
|
|
let lng = this.map_bounds.west;
|
|
|
|
// Keep up with the statistics
|
|
if(next_result.stats.rssi_max > this.stats.rssi_max)
|
|
this.stats.rssi_max = next_result.stats.rssi_max;
|
|
if(next_result.stats.rssi_min < this.stats.rssi_min)
|
|
this.stats.rssi_min = next_result.stats.rssi_min;
|
|
|
|
|
|
for(let value of next_result.result) {
|
|
let c_lat = lat - Config.step.lat,
|
|
c_lng = lng - Config.step.lng;
|
|
|
|
// Generate the GeoJSON feature for this cell of the map
|
|
this.geojson.push({
|
|
type: "Feature",
|
|
geometry: {
|
|
type: "Polygon",
|
|
coordinates: [
|
|
[ // Outer shape
|
|
[c_lng, c_lat],
|
|
[c_lng, c_lat + Config.step.lat],
|
|
[c_lng + Config.step.lng, c_lat + Config.step.lat],
|
|
[c_lng + Config.step.lng, c_lat]
|
|
]
|
|
// If there were any holes in the shape, we'd put them here
|
|
]
|
|
},
|
|
properties: {
|
|
colour: this.colour_scale(value).toString()
|
|
}
|
|
});
|
|
|
|
lng += Config.step.lng;
|
|
}
|
|
|
|
let next_work_item = work_generator.next();
|
|
if(!next_work_item.done) {
|
|
return this.workers[worker_index].predict_row(next_work_item.value)
|
|
.then(this.process_result_row.bind(this, next_work_item.value, worker_index, work_generator));
|
|
}
|
|
else {
|
|
console.log(`Ending worker ${worker_index}`);
|
|
// Shut down the worker - we're done!
|
|
this.workers[worker_index].end();
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export default LayerAI;
|