"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([ Config.colour_scale.min, Config.colour_scale.max ]).domain( this.index.properties.rssi_min, this.index.properties.rssi_max ); // 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: 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 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(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|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) { // Generate the GeoJSON feature for this cell of the map this.geojson.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: 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;