Add basic plotter for WebGL

There're still a bunch of TODOs. Will address those when we actually
have a need for this.

Change-Id: I137390d384967ffb618e0ecda37503e1e0df8301
diff --git a/frc971/analysis/plotting/webgl2_plotter.cc b/frc971/analysis/plotting/webgl2_plotter.cc
new file mode 100644
index 0000000..b10a1de
--- /dev/null
+++ b/frc971/analysis/plotting/webgl2_plotter.cc
@@ -0,0 +1,277 @@
+#include "frc971/analysis/plotting/webgl2_plotter.h"
+
+#include <assert.h>
+#include <stdlib.h>
+
+#include <iostream>
+
+#include <emscripten/emscripten.h>
+#include <emscripten/html5.h>
+
+namespace frc971 {
+namespace plotting {
+
+namespace {
+// Shader and program construction taken from examples at
+// https://github.com/emscripten-core/emscripten/blob/incoming/tests/webgl2_draw_packed_triangle.c
+GLuint compile_shader(GLenum shaderType, const char *src) {
+  GLuint shader = glCreateShader(shaderType);
+  glShaderSource(shader, 1, &src, nullptr);
+  glCompileShader(shader);
+
+  GLint isCompiled = 0;
+  glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);
+  if (!isCompiled) {
+    GLint maxLength = 0;
+    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);
+    char *buf = (char *)malloc(maxLength + 1);
+    glGetShaderInfoLog(shader, maxLength, &maxLength, buf);
+    printf("%s\n", buf);
+    free(buf);
+    return 0;
+  }
+
+  return shader;
+}
+
+GLuint create_program(GLuint vertexShader, GLuint fragmentShader,
+                      GLuint attribute_location) {
+  GLuint program = glCreateProgram();
+  glAttachShader(program, vertexShader);
+  glAttachShader(program, fragmentShader);
+  glBindAttribLocation(program, attribute_location, "apos");
+  glLinkProgram(program);
+  return program;
+}
+
+GLuint CreateLineProgram(GLuint attribute_location) {
+  // Create a shader program which will take in:
+  // -A series of points to plot
+  // -Scale/offset parameters for choosing how to zoom/pan
+  // -Point size/color information
+  //
+  // The vertex shader then takes in the series of points (apos) and
+  // transforms the points by the scale/offset to determine their on-screen
+  // position. We aren't doing any funny with 3D or perspective, so we leave
+  // the z and w components untouched.
+  //
+  // We set the color of the line in the fragment shader.
+  const char vertex_shader[] =
+    "#version 100\n"
+    "attribute vec2 apos;"
+    "uniform vec2 scale;"
+    "uniform vec2 offset;"
+    "uniform float point_size;"
+    "void main() {"
+      "gl_Position.xy = apos.xy * scale.xy + offset.xy;"
+      "gl_Position.z = 0.0;"
+      "gl_Position.w = 1.0;"
+      "gl_PointSize = point_size;"
+    "}";
+  GLuint vs = compile_shader(GL_VERTEX_SHADER, vertex_shader);
+
+  const char fragment_shader[] =
+    "#version 100\n"
+    "precision lowp float;"
+    "uniform vec4 color;"
+    "void main() {"
+      "gl_FragColor = color;"
+    "}";
+  GLuint fs = compile_shader(GL_FRAGMENT_SHADER, fragment_shader);
+  return create_program(vs, fs, attribute_location);
+}
+
+const Eigen::Vector2d Vector2dInfinity() {
+  return Eigen::Vector2d::Ones() * std::numeric_limits<double>::infinity();
+}
+
+}  // namespace
+
+class WebglLine : public Line {
+ public:
+  WebglLine(GLuint program, size_t buffer_size = 1000000)
+      : color_uniform_location_(glGetUniformLocation(program, "color")),
+        point_size_uniform_location_(
+            glGetUniformLocation(program, "point_size")),
+        line_size_(0) {}
+  virtual ~WebglLine() {}
+  void SetPoints(const std::vector<Eigen::Vector2d> &pts) override {
+    updated_ = true;
+    max_values_ = -Vector2dInfinity();
+    min_values_ = Vector2dInfinity();
+    line_size_ = 0;
+    buffer_.clear();
+    for (const auto &pt : pts) {
+      buffer_.push_back(pt.x());
+      buffer_.push_back(pt.y());
+      max_values_ = max_values_.cwiseMax(pt);
+      min_values_ = min_values_.cwiseMin(pt);
+      ++line_size_;
+    }
+  }
+  void Draw() override {
+    updated_ = false;
+    if (buffer_.empty()) {
+      return;
+    }
+    // TODO(james): Flushing and rewriting the buffer on every line draw seems
+    // like it should be inefficient, but in practice it seems to actually be
+    // fine for the amounts of data that we are dealing with (i.e., doing a few
+    // tens of MB of copies at the most is not actually that expensive).
+    glBufferData(GL_ARRAY_BUFFER, buffer_.size() * sizeof(float),
+                 buffer_.data(), GL_STATIC_DRAW);
+    glUniform4f(color_uniform_location_, color_.r, color_.g, color_.b, 1.0);
+    glUniform1f(point_size_uniform_location_, point_size_);
+    if (point_size_ != 0) {
+      glDrawArrays(GL_POINTS, 0, line_size_);
+    }
+    if (line_width_ != 0) {
+      glDrawArrays(GL_LINE_STRIP, 0, line_size_);
+    }
+    assert(GL_NO_ERROR == glGetError() && "glDrawArray failed");
+  }
+  void SetColor(const Color &color) override {
+    updated_ = true;
+    color_ = color;
+  }
+
+  Eigen::Vector2d MaxValues() const override { return max_values_; }
+  Eigen::Vector2d MinValues() const override { return min_values_; }
+
+  void SetLineWidth(const float width) override {
+    updated_ = true;
+    line_width_ = width;
+  }
+  void SetPointSize(const float point_size) override {
+    updated_ = true;
+    point_size_ = point_size;
+  }
+
+  bool HasUpdate() override { return updated_; }
+
+ private:
+  const GLuint color_uniform_location_;
+  const GLuint point_size_uniform_location_;
+  std::vector<float> buffer_;
+  size_t line_size_;
+  Color color_;
+  Eigen::Vector2d max_values_ = -Vector2dInfinity();
+  Eigen::Vector2d min_values_ = Vector2dInfinity();
+  float line_width_ = 1.0;
+  float point_size_ = 3.0;
+  bool updated_ = true;
+};
+
+WebglCanvasPlotter::WebglCanvasPlotter(const std::string &canvas_id,
+                                       GLuint attribute_location)  {
+  EmscriptenWebGLContextAttributes attr;
+  emscripten_webgl_init_context_attributes(&attr);
+  assert(attr.antialias && "Antialiasing should be enabled by default.");
+  attr.majorVersion = 2;
+  EMSCRIPTEN_WEBGL_CONTEXT_HANDLE ctx = emscripten_webgl_create_context("#canvas", &attr);
+  assert(ctx && "Failed to create WebGL2 context");
+  emscripten_webgl_make_context_current(ctx);
+
+  program_ = CreateLineProgram(attribute_location);
+  scale_uniform_location_ = glGetUniformLocation(program_, "scale");
+  offset_uniform_location_ = glGetUniformLocation(program_, "offset");
+
+  glGenBuffers(1, &gl_buffer_);
+  glUseProgram(program_);
+  glBindBuffer(GL_ARRAY_BUFFER, gl_buffer_);
+  glVertexAttribPointer(attribute_location, 2, GL_FLOAT, GL_FALSE, 8, 0);
+  glEnableVertexAttribArray(attribute_location);
+
+  zoom_rectangle_ = std::make_unique<WebglLine>(program_);
+  zoom_rectangle_->SetColor({.r = 1.0, .g = 1.0, .b = 1.0});
+  zoom_rectangle_->SetLineWidth(2.0);
+  zoom_rectangle_->SetPointSize(0.0);
+}
+
+Line *WebglCanvasPlotter::AddLine() {
+  lines_.push_back(std::make_unique<WebglLine>(program_));
+  return lines_.back().get();
+}
+void WebglCanvasPlotter::Undo() {
+  if (old_scales_.empty() || old_offsets_.empty()) {
+    return;
+  }
+  scale_ = old_scales_.back();
+  old_scales_.pop_back();
+  offset_ = old_offsets_.back();
+  old_offsets_.pop_back();
+}
+
+void WebglCanvasPlotter::RecordState() {
+  old_scales_.push_back(scale_);
+  old_offsets_ .push_back(offset_);
+}
+
+void WebglCanvasPlotter::SetScale(const Eigen::Vector2d &scale) {
+  scale_ = scale;
+}
+
+Eigen::Vector2d WebglCanvasPlotter::GetScale() const {
+  return scale_;
+}
+
+void WebglCanvasPlotter::SetOffset(const Eigen::Vector2d &offset) {
+  offset_ = offset;
+}
+
+Eigen::Vector2d WebglCanvasPlotter::GetOffset() const {
+  return offset_;
+}
+
+void WebglCanvasPlotter::Redraw() {
+  const bool scaling_update = last_scale_ != scale_ || last_offset_ != offset_;
+  bool data_update = zoom_rectangle_->HasUpdate();
+  for (const auto &line : lines_) {
+    data_update = line->HasUpdate() || data_update;
+  }
+  if (!scaling_update && !data_update) {
+    return;
+  }
+  glUseProgram(program_);
+  glBindBuffer(GL_ARRAY_BUFFER, gl_buffer_);
+  glUniform2f(scale_uniform_location_, scale_.x(), scale_.y());
+  glUniform2f(offset_uniform_location_, offset_.x(), offset_.y());
+  for (const auto &line : lines_) {
+    line->Draw();
+  }
+  zoom_rectangle_->Draw();
+  last_scale_ = scale_;
+  last_offset_ = offset_;
+}
+
+Eigen::Vector2d WebglCanvasPlotter::MaxValues() const {
+  Eigen::Vector2d max = -Vector2dInfinity();
+  for (const auto &line : lines_) {
+    max = max.cwiseMax(line->MaxValues());
+  }
+  return max;
+}
+Eigen::Vector2d WebglCanvasPlotter::MinValues() const {
+  Eigen::Vector2d min = Vector2dInfinity();
+  for (const auto &line : lines_) {
+    min = min.cwiseMin(line->MinValues());
+  }
+  return min;
+}
+
+void WebglCanvasPlotter::ClearZoomRectangle() {
+  zoom_rectangle_->SetPoints({});
+}
+
+
+void WebglCanvasPlotter::SetZoomRectangle(const Eigen::Vector2d &corner1,
+                                          const Eigen::Vector2d &corner2) {
+  zoom_rectangle_->SetPoints({corner1,
+                              {corner1.x(), corner2.y()},
+                              corner2,
+                              {corner2.x(), corner1.y()},
+                              corner1});
+}
+
+}  // namespace plotting
+}  // namespace frc971