"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} 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;