Client code for robot position webpage.

Change-Id: I935dfe7242cd4a8c5b9b3bc8d6e137d29786e0f1
diff --git a/y2019/vision/server/BUILD b/y2019/vision/server/BUILD
index d0a08a1..180b41e 100644
--- a/y2019/vision/server/BUILD
+++ b/y2019/vision/server/BUILD
@@ -10,12 +10,6 @@
     ],
 )
 
-rollup_bundle(
-    name = "bundle",
-    entry_point = "y2019/vision/server/demo",
-    deps = [":demo"],
-)
-
 gen_embedded(
     name = "gen_embedded",
     srcs = glob(
@@ -26,10 +20,9 @@
 
 aos_downloader_dir(
     name = "www_files",
-    srcs = glob([
-        "www/**/*",
-    ]) + [
-        ":bundle",
+    srcs = [
+        "//y2019/vision/server/www:visualizer_bundle",
+        "//y2019/vision/server/www:files",
     ],
     dir = "www",
     visibility = ["//visibility:public"],
@@ -40,10 +33,9 @@
     srcs = [
         "server.cc",
     ],
-    data = glob([
-        "www/**/*",
-    ]) + [
-        ":bundle",
+    data = [
+        "//y2019/vision/server/www:visualizer_bundle",
+        "//y2019/vision/server/www:files",
     ],
     visibility = ["//visibility:public"],
     deps = [
diff --git a/y2019/vision/server/server.cc b/y2019/vision/server/server.cc
index 80e5fb4..1ed6ac9 100644
--- a/y2019/vision/server/server.cc
+++ b/y2019/vision/server/server.cc
@@ -155,10 +155,10 @@
       std::ostringstream stream;
       stream << "{\n";
 
-      stream << "'robot': {";
-      stream << "'x': " << drivetrain_status->x << ",";
-      stream << "'y': " << drivetrain_status->y << ",";
-      stream << "'theta': " << drivetrain_status->theta;
+      stream << "\"robot\": {";
+      stream << "\"x\": " << drivetrain_status->x << ",";
+      stream << "\"y\": " << drivetrain_status->y << ",";
+      stream << "\"theta\": " << drivetrain_status->theta;
       stream << "}\n";
 
       stream << "}";
diff --git a/y2019/vision/server/www/BUILD b/y2019/vision/server/www/BUILD
new file mode 100644
index 0000000..937b495
--- /dev/null
+++ b/y2019/vision/server/www/BUILD
@@ -0,0 +1,26 @@
+load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
+load("@build_bazel_rules_nodejs//:defs.bzl", "rollup_bundle")
+
+package(default_visibility = ["//visibility:public"])
+
+filegroup(
+    name = "files",
+    srcs = glob([
+        "**/*",
+    ]),
+)
+
+ts_library(
+    name = "visualizer",
+    srcs = glob([
+        "*.ts",
+    ]),
+)
+
+rollup_bundle(
+    name = "visualizer_bundle",
+    entry_point = "y2019/vision/server/www/main",
+    deps = [
+        ":visualizer",
+    ],
+)
diff --git a/y2019/vision/server/www/constants.ts b/y2019/vision/server/www/constants.ts
new file mode 100644
index 0000000..6180df1
--- /dev/null
+++ b/y2019/vision/server/www/constants.ts
@@ -0,0 +1,5 @@
+// Conversion constants to meters
+export const IN_TO_M = 0.0254;
+export const FT_TO_M = 0.3048;
+// Width of the field in meters
+export const FIELD_WIDTH = 27 * FT_TO_M;
diff --git a/y2019/vision/server/www/field.ts b/y2019/vision/server/www/field.ts
new file mode 100644
index 0000000..5b43c36
--- /dev/null
+++ b/y2019/vision/server/www/field.ts
@@ -0,0 +1,120 @@
+import {IN_TO_M, FT_TO_M} from './constants';
+
+const CENTER_FIELD_X = 27 * FT_TO_M + 1.125 * IN_TO_M;
+
+const FAR_CARGO_X = CENTER_FIELD_X - 20.875 * IN_TO_M;
+const MID_CARGO_X = FAR_CARGO_X - 21.75 * IN_TO_M;
+const NEAR_CARGO_X = MID_CARGO_X - 21.75 * IN_TO_M;
+const SIDE_CARGO_Y = (24 + 3 + 0.875) * IN_TO_M;
+const SIDE_CARGO_THETA = -Math.PI / 2;
+
+const FACE_CARGO_X = CENTER_FIELD_X - (7 * 12 + 11.75 + 9) * IN_TO_M;
+const FACE_CARGO_Y = (10.875) * IN_TO_M;
+const FACE_CARGO_THETA = 0;
+
+const ROCKET_X = CENTER_FIELD_X - 8 * FT_TO_M;
+const ROCKET_Y = (26 * 12 + 10.5) / 2.0 * IN_TO_M;
+
+const ROCKET_PORT_X = ROCKET_X;
+const ROCKET_PORT_Y = ROCKET_Y - 0.7;
+const ROCKET_PORT_THETA = Math.PI / 2;
+
+const ROCKET_HATCH_X_OFFSET = 14.634 * IN_TO_M;
+const ROCKET_HATCH_Y = ROCKET_PORT_Y + 9.326 * IN_TO_M;
+const ROCKET_NEAR_X = ROCKET_X - ROCKET_HATCH_X_OFFSET;
+const ROCKET_FAR_X = ROCKET_X + ROCKET_HATCH_X_OFFSET;
+const ROCKET_NEAR_THETA = -28.5 * 180 / Math.PI;
+const ROCKET_FAR_THETA = Math.PI - ROCKET_NEAR_THETA;
+
+const HP_Y = ((26 * 12 + 10.5) / 2 - 25.9) * IN_TO_M;
+const HP_THETA = Math.PI;
+
+export function drawField(ctx : CanvasRenderingContext2D) : void {
+  drawTargets(ctx);
+}
+
+function drawHab(ctx : CanvasRenderingContext2D) : void {
+  ctx.fillstyle = 'rgb(100,100,100)';
+  const habLeft = 5 * FT_TO_M;
+  const habWidth = 5 * FT_TO_M;
+  const habTop = -5 * FT_TO_M;
+  const habHeight = 10 * FT_TO_M;
+  ctx.fillRect(habLeft,habTop,habWidth,habHeight);
+}
+
+function drawTargets(ctx : CanvasRenderingContext2D) : void {
+  drawHalfCargo(ctx);
+  drawRocket(ctx);
+  drawHP(ctx);
+  ctx.save();
+
+  ctx.scale(1, -1);
+  drawHalfCargo(ctx);
+  drawRocket(ctx);
+  drawHP(ctx);
+
+  ctx.restore();
+}
+
+function drawHP(ctx : CanvasRenderingContext2D) : void {
+  ctx.save();
+  ctx.translate(0, HP_Y)
+  ctx.rotate(HP_THETA);
+  drawTarget(ctx);
+  ctx.restore();
+}
+
+function drawRocket(ctx : CanvasRenderingContext2D) : void {
+  ctx.save();
+  ctx.translate(ROCKET_PORT_X, ROCKET_PORT_Y)
+  ctx.rotate(ROCKET_PORT_THETA);
+  drawTarget(ctx);
+  ctx.restore();
+
+  ctx.save();
+  ctx.translate(ROCKET_NEAR_X, ROCKET_HATCH_Y)
+  ctx.rotate(ROCKET_NEAR_THETA);
+  drawTarget(ctx);
+  ctx.restore();
+
+  ctx.save();
+  ctx.translate(ROCKET_FAR_X, ROCKET_HATCH_Y)
+  ctx.rotate(ROCKET_FAR_THETA);
+  drawTarget(ctx);
+  ctx.restore();
+}
+
+function drawHalfCargo(ctx : CanvasRenderingContext2D) : void {
+  ctx.save();
+  ctx.translate(FAR_CARGO_X, SIDE_CARGO_Y)
+  ctx.rotate(SIDE_CARGO_THETA);
+  drawTarget(ctx);
+  ctx.restore();
+
+  ctx.save();
+  ctx.translate(MID_CARGO_X, SIDE_CARGO_Y)
+  ctx.rotate(SIDE_CARGO_THETA);
+  drawTarget(ctx);
+  ctx.restore();
+
+  ctx.save();
+  ctx.translate(NEAR_CARGO_X, SIDE_CARGO_Y)
+  ctx.rotate(SIDE_CARGO_THETA);
+  drawTarget(ctx);
+  ctx.restore();
+
+  ctx.save();
+  ctx.translate(FACE_CARGO_X, FACE_CARGO_Y)
+  ctx.rotate(FACE_CARGO_THETA);
+  drawTarget(ctx);
+  ctx.restore();
+}
+
+function drawTarget(ctx : CanvasRenderingContext2D) : void {
+  ctx.moveTo(0, -0.15);
+  ctx.lineTo(0, 0.15);
+  ctx.moveTo(0, 0);
+  ctx.lineTo(-0.15, 0);
+  ctx.closePath();
+  ctx.stroke();
+}
diff --git a/y2019/vision/server/www/index.html b/y2019/vision/server/www/index.html
index 345a407..4e6e316 100644
--- a/y2019/vision/server/www/index.html
+++ b/y2019/vision/server/www/index.html
@@ -1,8 +1,10 @@
 <!doctype html>
 <html>
-	<head>
-		<title>Vision Debug Server</title>
-	</head>
-	<body>
-	</body>
+  <head>
+    <title>Vision Debug Server</title>
+  </head>
+  <body style="overflow:hidden">
+    <canvas id="field" style="border: 1px solid"></canvas>
+  </body>
+  <script src="visualizer_bundle.min.js"></script>
 </html>
diff --git a/y2019/vision/server/www/main.ts b/y2019/vision/server/www/main.ts
new file mode 100644
index 0000000..0806e16
--- /dev/null
+++ b/y2019/vision/server/www/main.ts
@@ -0,0 +1,62 @@
+import {FT_TO_M, FIELD_WIDTH} from './constants';
+import {drawField} from './field';
+import {drawRobot} from './robot';
+
+const FIELD_WIDTH = 27 * FT_TO_M;
+
+function main(): void {
+  const vis = new Visualiser();
+}
+
+class Visualiser {
+  private x = 3;
+  private y = 0;
+  private theta = 0;
+
+  constructor() {
+    const canvas = <HTMLCanvasElement>document.getElementById('field');
+    const ctx = canvas.getContext('2d');
+
+    const server = location.host;
+    const socket = new WebSocket(`ws://${server}/ws`);
+    const reader = new FileReader();
+    reader.addEventListener('loadend', (e) => {
+      const text = e.srcElement.result;
+      const j = JSON.parse(text);
+      this.x = j.robot.x;
+      this.y = j.robot.y;
+      this.theta = j.robot.theta;
+    });
+    socket.addEventListener('message', (event) => {
+      reader.readAsText(event.data);
+    });
+    window.requestAnimationFrame(() => this.draw(ctx));
+  }
+
+  reset(ctx : CanvasRenderingContext2D) : void {
+    ctx.setTransform(1,0,0,1,0,0);
+    const size = Math.min(window.innerHeight, window.innerWidth) * 0.98;
+    ctx.canvas.height = size;
+    ctx.canvas.width = size;
+    ctx.clearRect(0,0,size,size);
+
+    ctx.translate(size/2, size);
+    ctx.rotate(-Math.PI / 2);
+    ctx.scale(1, -1);
+    const M_TO_PX = size / FIELD_WIDTH
+    ctx.scale(M_TO_PX, M_TO_PX);
+    ctx.lineWidth = 1 / M_TO_PX;
+
+    ctx.beginPath();
+  }
+
+  draw(ctx : CanvasRenderingContext2D) : void {
+    this.reset(ctx);
+
+    drawField(ctx);
+    drawRobot(ctx, this.x, this.y, this.theta);
+    window.requestAnimationFrame(() => this.draw(ctx));
+  }
+}
+
+main();
diff --git a/y2019/vision/server/www/robot.ts b/y2019/vision/server/www/robot.ts
new file mode 100644
index 0000000..bc1f47f
--- /dev/null
+++ b/y2019/vision/server/www/robot.ts
@@ -0,0 +1,21 @@
+import {IN_TO_M, FT_TO_M} from './constants';
+
+const ROBOT_WIDTH = 25 * IN_TO_M;
+const ROBOT_LENGTH = 31 * IN_TO_M;
+
+export function drawRobot(ctx : CanvasRenderingContext2D, x : number, y : number, theta : number) : void {
+  ctx.save();
+  ctx.translate(x, y);
+  ctx.rotate(theta);
+
+  ctx.fillStyle = 'blue';
+  ctx.fillRect(-ROBOT_LENGTH / 2, -ROBOT_WIDTH / 2, ROBOT_LENGTH, ROBOT_WIDTH);
+
+  ctx.moveTo(ROBOT_LENGTH / 2, -ROBOT_WIDTH/2);
+  ctx.lineTo(ROBOT_LENGTH / 2 + 0.1, 0);
+  ctx.lineTo(ROBOT_LENGTH / 2, ROBOT_WIDTH/2);
+  ctx.closePath();
+  ctx.stroke();
+
+  ctx.restore();
+}