From 61edaf6fa38f90913509852e004d8ea77d2e15e0 Mon Sep 17 00:00:00 2001
From: Starbeamrainbowlabs <sbrl@starbeamrainbowlabs.com>
Date: Tue, 23 Jul 2019 15:45:29 +0100
Subject: [PATCH] Write AI layer, but it's untested.

---
 client_src/js/ClientConfig.mjs      |  7 ++-
 client_src/js/LayerAI.mjs           | 83 ++++++++++++++++++++++++-----
 {server/Helpers => common}/Math.mjs |  0
 package-lock.json                   | 23 +++++---
 package.json                        |  2 +
 rollup.config.js                    |  3 ++
 server/train-ai/AITrainer.mjs       |  8 ++-
 server/train-ai/DatasetFetcher.mjs  |  2 +-
 8 files changed, 106 insertions(+), 22 deletions(-)
 rename {server/Helpers => common}/Math.mjs (100%)

diff --git a/client_src/js/ClientConfig.mjs b/client_src/js/ClientConfig.mjs
index b938362..88fe0b6 100644
--- a/client_src/js/ClientConfig.mjs
+++ b/client_src/js/ClientConfig.mjs
@@ -12,5 +12,10 @@ export default {
 	border: 0.1,
 	
 	// The resolution of the coverage map
-	step: 0.005
+	step: 0.005,
+	
+	colour_scale: {
+		min: "#ffffff",
+		max: "#2cb42c"
+	}
 };
diff --git a/client_src/js/LayerAI.mjs b/client_src/js/LayerAI.mjs
index 7a0c8cb..5938900 100644
--- a/client_src/js/LayerAI.mjs
+++ b/client_src/js/LayerAI.mjs
@@ -3,10 +3,12 @@
 import path from 'path';
 
 import L from 'leaflet';
-import tf from '@tensorflow/tfjs';
+import * as tf from '@tensorflow/tfjs';
+import chroma from 'chroma-js';
 
 import GetFromUrl from './Helpers/GetFromUrl.mjs';
 import Config from './ClientConfig.mjs';
+import { normalise } from '../../common/Math.mjs';
 
 class LayerAI {
 	get gateway_bounds() {
@@ -37,21 +39,28 @@ class LayerAI {
 		);
 		console.log(index);
 		
-		for(let gateway of this.index) {
-			let gateway_data = {
-				// TODO: Swap this out for the real thing - probably a GeoJSON layer or something
-				// This is just a placeholder
-				layer: L.layerGroup([
-					
-				]),
-				
-				ai: await tf.loadModel(`${window.location.href}/${path.dirname(Config.ai_index_file)}/${gateway.id}`)
-			}
-			this.gateways.set(gateway.id, gateway_data);
+		for(let gateway of this.index.index) {
+			this.gateways.set(
+				gateway.id,
+				await tf.LayersModel.loadModel(`${window.location.href}/${path.dirname(Config.ai_index_file)}/${gateway.id}`)
+			);
 		}
+		
+		this.layer = this.generate_layer();
+		this.layer.addTo(this.map);
 	}
 	
-	async render_map() {
+	generate_layer() {
+		return L.geoJSON(this.render_map(), {
+			style: (feature) => { return {
+				fillColor: feature.properties.colour,
+				fillOpacity: 0.4
+			} }
+		});
+	}
+	
+	render_map() {
+		// FUTURE: Do this in a web worker?
 		let map_bounds = this.gateway_bounds;
 		map_bounds.north += Config.border;
 		map_bounds.south -= Config.border;
@@ -59,7 +68,55 @@ class LayerAI {
 		map_bounds.east += Config.border;
 		map_bounds.west -= Config.border;
 		
+		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
+			);
 		
+		for(let lat = map_bounds.west; lat < map_bounds.east; lat += Config.step) {
+			for(let lng = map_bounds.south; lng < map_bounds.north; lng += Config.step) {
+				let max_predicted_rssi = -Infinity;
+				
+				for(let [, ai] of this.gateways) {
+					max_predicted_rssi = Math.max(
+						max_predicted_rssi,
+						ai.predict(
+							tf.tensor1d([ lat, lng ])
+						)
+					);
+				}
+				
+				max_predicted_rssi = normalise(max_predicted_rssi,
+					{ min: 0, max: 1 },
+					{
+						min: this.index.properties.rssi_min,
+						max: this.index.properties.rssi_max
+					}
+				);
+				
+				coverage.push({
+					type: "Feature",
+					geometry: {
+						type: "Polygon",
+						coordinates: [
+							[lat, lng],
+							[lat + Config.step, lng],
+							[lat + Config.step, lng + Config.step],
+							[lat, lng + Config.step]
+						]
+					},
+					properties: {
+						colour: colour_scale(max_predicted_rssi)
+					}
+				})
+			}
+		}
+		
+		return coverage;
 	}
 }
 
diff --git a/server/Helpers/Math.mjs b/common/Math.mjs
similarity index 100%
rename from server/Helpers/Math.mjs
rename to common/Math.mjs
diff --git a/package-lock.json b/package-lock.json
index 1b74713..7302bbd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -93,6 +93,12 @@
         "@types/integer": "*"
       }
     },
+    "@types/chroma-js": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-1.4.1.tgz",
+      "integrity": "sha512-i9hUiO3bwgmzZUDwBuR65WqsBQ/nwN+H2fKX0bykXCdd8cFQEuIj8vI7FXjyb2f5z5h+pv76I/uakikKSgaqTA==",
+      "dev": true
+    },
     "@types/estree": {
       "version": "0.0.39",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
@@ -492,9 +498,9 @@
       }
     },
     "caniuse-lite": {
-      "version": "1.0.30000984",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000984.tgz",
-      "integrity": "sha512-n5tKOjMaZ1fksIpQbjERuqCyfgec/m9pferkFQbLmWtqLUdmt12hNhjSwsmPdqeiG2NkITOQhr1VYIwWSAceiA==",
+      "version": "1.0.30000985",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000985.tgz",
+      "integrity": "sha512-1ngiwkgqAYPG0JSSUp3PUDGPKKY59EK7NrGGX+VOxaKCNzRbNc7uXMny+c3VJfZxtoK3wSImTvG9T9sXiTw2+w==",
       "dev": true
     },
     "chalk": {
@@ -512,6 +518,11 @@
       "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz",
       "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g=="
     },
+    "chroma-js": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.0.4.tgz",
+      "integrity": "sha512-gk71qOrSdBTLbsd0DIUO3QjZL8tTvMwpG1EoXYScy7rI4rcO4EyYH6zGuvCgUDumKumqg0pt6Ua+vWnMJsTYhw=="
+    },
     "class-utils": {
       "version": "0.3.6",
       "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@@ -1041,9 +1052,9 @@
       }
     },
     "electron-to-chromium": {
-      "version": "1.3.194",
-      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.194.tgz",
-      "integrity": "sha512-w0LHR2YD9Ex1o+Sz4IN2hYzCB8vaFtMNW+yJcBf6SZlVqgFahkne/4rGVJdk4fPF98Gch9snY7PiabOh+vqHNg==",
+      "version": "1.3.199",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.199.tgz",
+      "integrity": "sha512-gachlDdHSK47s0N2e58GH9HMC6Z4ip0SfmYUa5iEbE50AKaOUXysaJnXMfKj0xB245jWbYcyFSH+th3rqsF8hA==",
       "dev": true
     },
     "emojis-list": {
diff --git a/package.json b/package.json
index 14dd5fa..da4fc97 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
 		"async-mqtt": "^2.3.0",
 		"awilix": "^4.2.2",
 		"better-sqlite3": "^5.4.0",
+		"chroma-js": "^2.0.4",
 		"debug": "^4.1.1",
 		"leaflet": "^1.5.1",
 		"path": "^0.12.7",
@@ -36,6 +37,7 @@
 	},
 	"devDependencies": {
 		"@types/better-sqlite3": "^5.4.0",
+		"@types/chroma-js": "^1.4.1",
 		"@types/leaflet": "^1.4.6",
 		"rollup": "^1.17.0",
 		"rollup-plugin-commonjs": "^10.0.1",
diff --git a/rollup.config.js b/rollup.config.js
index ecbfc22..eea969f 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -34,6 +34,9 @@ let plugins = [
 
 		// not all files you want to resolve are .js files
 		extensions: ['.mjs', '.js', '.jsx', '.json'], // Default: [ '.mjs', '.js', '.json', '.node' ]
+		
+		// Use the polyfills we've installed as dependencies instead of the Node.js natives, since those aren't available in the browser
+		preferBuiltins: false
 	}),
 	
 	// json({
diff --git a/server/train-ai/AITrainer.mjs b/server/train-ai/AITrainer.mjs
index adb661f..3cc8418 100644
--- a/server/train-ai/AITrainer.mjs
+++ b/server/train-ai/AITrainer.mjs
@@ -65,7 +65,13 @@ class AITrainer {
 				this.settings.ai.output_directory,
 				"index.json"
 			),
-			JSON.stringify(index)
+			JSON.stringify({
+				properties: {
+					rssi_min: this.settings.ai.rssi_min,
+					rssi_max:  this.settings.ai.rssi_max
+				},
+				index
+			})
 		);
 	}
 	
diff --git a/server/train-ai/DatasetFetcher.mjs b/server/train-ai/DatasetFetcher.mjs
index e5b0d87..136b890 100644
--- a/server/train-ai/DatasetFetcher.mjs
+++ b/server/train-ai/DatasetFetcher.mjs
@@ -1,6 +1,6 @@
 "use strict";
 
-import { normalise, clamp } from '../Helpers/Math.mjs';
+import { normalise, clamp } from '../../common/Math.mjs';
 
 class DatasetFetcher {
 	constructor({ settings, RSSIRepo }) {
