2019-07-23 12:49:45 +00:00
"use strict" ;
import L from 'leaflet' ;
2019-07-23 14:45:29 +00:00
import chroma from 'chroma-js' ;
2019-07-23 12:49:45 +00:00
import GetFromUrl from './Helpers/GetFromUrl.mjs' ;
import Config from './ClientConfig.mjs' ;
2019-07-25 15:44:26 +00:00
2019-07-23 12:49:45 +00:00
class LayerAI {
2019-07-25 15:44:26 +00:00
/ * *
* 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
* /
2019-07-23 12:49:45 +00:00
get gateway _bounds ( ) {
let result = {
east : Infinity ,
west : - Infinity ,
north : Infinity ,
south : - Infinity
} ;
2019-07-24 17:08:53 +00:00
for ( let gateway of this . index . index ) {
2019-07-23 12:49:45 +00:00
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 ;
}
2019-07-25 15:44:26 +00:00
/ * *
* Initialises a new Leaflet AI layer instance .
* @ param { [ type ] } map [ description ]
* /
2019-07-23 12:49:45 +00:00
constructor ( map ) {
this . map = map ;
2019-07-25 17:56:59 +00:00
this . worker = new Worker ( "worker.js" ) ; // We could pass { type: "module" } as the 2nd argument here to allow Rollup code splitting to function as normal, but it's not supported by browsers yet
2019-07-25 15:44:26 +00:00
this . map _bounds = null ;
2019-07-23 12:49:45 +00:00
}
2019-07-25 15:44:26 +00:00
/ * *
* 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" ,
2019-07-25 17:56:59 +00:00
setup _info : {
bounds : this . map _bounds ,
index : this . index ,
Config
}
2019-07-25 15:44:26 +00:00
} ) ;
} )
}
/ * *
* 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 .
* /
2019-07-23 12:49:45 +00:00
async setup ( ) {
2019-07-25 15:44:26 +00:00
// Download the index file that tells us where the gateways and their
// trained models are located
2019-07-23 12:49:45 +00:00
this . index = JSON . parse (
await GetFromUrl ( Config . ai _index _file )
) ;
2019-07-23 14:58:11 +00:00
console . log ( this . index ) ;
2019-07-23 12:49:45 +00:00
2019-07-25 15:44:26 +00:00
// Figure out the bounds of the map we're going to generate
this . map _bounds = this . gateway _bounds ;
2019-07-25 17:56:59 +00:00
this . map _bounds . north += Config . border . lat ;
this . map _bounds . south -= Config . border . lat ;
2019-07-25 15:44:26 +00:00
2019-07-25 17:56:59 +00:00
this . map _bounds . east += Config . border . lng ;
this . map _bounds . west -= Config . border . lng ;
2019-07-25 15:44:26 +00:00
// Setup the web worker
await this . worker _setup ( ) ;
2019-07-23 14:45:29 +00:00
2019-07-25 15:44:26 +00:00
// Generate the Leaflet layer
this . layer = await this . generate _layer ( ) ;
2019-07-23 14:45:29 +00:00
this . layer . addTo ( this . map ) ;
2019-07-25 13:02:16 +00:00
console . log ( "[Layer/AI] Complete" ) ;
2019-07-23 14:45:29 +00:00
}
2019-07-25 15:44:26 +00:00
/ * *
* 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 ( ) {
2019-07-25 13:02:16 +00:00
console . log ( "[Layer/AI] Rendering map" ) ;
2019-07-25 17:56:59 +00:00
let map = await this . render _map ( ) ;
2019-07-25 13:02:16 +00:00
console . log ( "[Layer/AI] Passing to Leaflet" ) ;
2019-07-24 17:08:53 +00:00
return L . geoJSON ( map , {
2019-07-23 14:45:29 +00:00
style : ( feature ) => { return {
2019-07-25 12:33:11 +00:00
stroke : true ,
color : feature . properties . colour ,
weight : 1 ,
2019-07-23 14:45:29 +00:00
fillColor : feature . properties . colour ,
fillOpacity : 0.4
} }
} ) ;
2019-07-23 12:49:45 +00:00
}
2019-07-25 15:44:26 +00:00
/ * *
* 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 ( ) {
2019-07-23 14:45:29 +00:00
let coverage = [ ] ,
2019-07-24 17:08:53 +00:00
colour _scale = chroma . scale ( [
2019-07-23 14:45:29 +00:00
Config . colour _scale . min ,
Config . colour _scale . max
2019-07-24 17:08:53 +00:00
] ) . domain (
2019-07-23 14:45:29 +00:00
this . index . properties . rssi _min ,
this . index . properties . rssi _max
) ;
2019-07-25 15:44:26 +00:00
let stats = {
rssi _min : Infinity ,
rssi _max : - Infinity
} ;
for ( let lat = this . map _bounds . south ; lat < this . map _bounds . north ; lat += Config . step . lat ) {
2019-07-25 17:56:59 +00:00
let next _result = await this . worker _predict _row ( lat ) ;
2019-07-25 15:44:26 +00:00
let lng = this . map _bounds . west ;
2019-07-25 17:56:59 +00:00
// Keep up with the statistics
if ( next _result . stats . rssi _max > stats . rssi _max )
stats . rssi _max = next _result . stats . rssi _max ;
if ( next _result . stats . rssi _min < stats . rssi _min )
stats . rssi _min = next _result . stats . rssi _min ;
2019-07-25 15:44:26 +00:00
2019-07-25 17:56:59 +00:00
for ( let value of next _result . result ) {
2019-07-25 15:44:26 +00:00
// Generate the GeoJSON feature for this cell of the map
2019-07-23 14:45:29 +00:00
coverage . push ( {
type : "Feature" ,
geometry : {
type : "Polygon" ,
coordinates : [
2019-07-25 12:33:11 +00:00
[ // Outer shape
2019-07-25 13:02:16 +00:00
[ lng , lat ] ,
[ lng , lat + Config . step . lat ] ,
[ lng + Config . step . lng , lat + Config . step . lat ] ,
[ lng + Config . step . lng , lat ]
2019-07-25 12:33:11 +00:00
]
// If there were any holes in the shape, we'd put them here
2019-07-23 14:45:29 +00:00
]
} ,
properties : {
2019-07-25 15:44:26 +00:00
colour : colour _scale ( value ) . toString ( )
2019-07-23 14:45:29 +00:00
}
2019-07-25 15:44:26 +00:00
} ) ;
lng += Config . step . lng ;
2019-07-23 14:45:29 +00:00
}
}
2019-07-23 12:49:45 +00:00
2019-07-25 17:56:59 +00:00
console . log ( "Stats:" , stats ) ;
2019-07-25 15:44:26 +00:00
2019-07-23 14:45:29 +00:00
return coverage ;
2019-07-23 12:49:45 +00:00
}
}
export default LayerAI ;