Merge changes Id9336d68,I85d0735c

* changes:
  Make timestamp_to_csv work with multiple boots
  Estimate the distributed clock with boots accounted for
diff --git a/aos/BUILD b/aos/BUILD
index 212c462..63046e7 100644
--- a/aos/BUILD
+++ b/aos/BUILD
@@ -614,3 +614,21 @@
         "//aos/testing:googletest",
     ],
 )
+
+cc_binary(
+    name = "aos_graph_channels",
+    srcs = [
+        "aos_graph_channels.cc",
+    ],
+    target_compatible_with = ["@platforms//os:linux"],
+    deps = [
+        "//aos:configuration",
+        "//aos:init",
+        "//aos:json_to_flatbuffer",
+        "//aos/events:simulated_event_loop",
+        "//aos/events/logging:log_reader",
+        "//aos/time",
+        "@com_github_gflags_gflags//:gflags",
+        "@com_github_google_glog//:glog",
+    ],
+)
diff --git a/aos/aos_graph_channels.cc b/aos/aos_graph_channels.cc
new file mode 100644
index 0000000..e96494a
--- /dev/null
+++ b/aos/aos_graph_channels.cc
@@ -0,0 +1,223 @@
+#include <iomanip>
+#include <iostream>
+
+#include "absl/strings/str_format.h"
+#include "absl/strings/str_split.h"
+#include "aos/events/logging/log_reader.h"
+#include "aos/events/simulated_event_loop.h"
+#include "aos/init.h"
+#include "aos/json_to_flatbuffer.h"
+#include "aos/time/time.h"
+#include "gflags/gflags.h"
+
+DEFINE_string(skip, "", "Applications to skip, seperated by ;");
+
+struct ChannelState {
+  const aos::Channel *channel = nullptr;
+  double frequency_sum = 0.0;
+  size_t frequency_count = 0;
+};
+
+// List of channels for an application.
+struct Application {
+  std::vector<ChannelState> watchers;
+  std::vector<ChannelState> fetchers;
+  std::vector<ChannelState> senders;
+};
+
+// List of all applications connected to a channel.
+struct ChannelConnections {
+  std::vector<std::pair<std::string, double>> senders;
+  std::vector<std::string> watchers;
+  std::vector<std::string> fetchers;
+};
+
+int main(int argc, char **argv) {
+  gflags::SetUsageMessage(
+      "Usage: \n"
+      "  aos_graph_channels [args] logfile1 logfile2 ...\n"
+      "\n"
+      "The output is in dot format.  Typical usage will be to pipe the results "
+      "to dot\n"
+      "\n"
+      "  aos_graph_channels ./log/ | dot -Tx11");
+
+  aos::InitGoogle(&argc, &argv);
+
+  if (argc < 2) {
+    LOG(FATAL) << "Expected at least 1 logfile as an argument.";
+  }
+
+  const std::vector<std::string> skip_list = absl::StrSplit(FLAGS_skip, ";");
+  aos::logger::LogReader reader(
+      aos::logger::SortParts(aos::logger::FindLogs(argc, argv)));
+
+  aos::SimulatedEventLoopFactory factory(reader.configuration());
+  reader.Register(&factory);
+
+  // Now: hook everything up to grab all the timing reports and extract them
+  // into the Application data structure.
+  std::map<std::string, Application> applications;
+
+  std::vector<std::unique_ptr<aos::EventLoop>> loops;
+  for (const aos::Node *node :
+       aos::configuration::GetNodes(factory.configuration())) {
+    std::unique_ptr<aos::EventLoop> event_loop =
+        factory.MakeEventLoop("timing_reports", node);
+    event_loop->SkipTimingReport();
+    event_loop->SkipAosLog();
+
+    event_loop->MakeWatcher("/aos", [&](const aos::timing::Report
+                                            &timing_report) {
+      if (std::find(skip_list.begin(), skip_list.end(),
+                    timing_report.name()->str()) != skip_list.end()) {
+        return;
+      }
+      // Make an application if one doesn't exist.
+      auto it = applications.find(timing_report.name()->str());
+      if (it == applications.end()) {
+        it = applications.emplace(timing_report.name()->str(), Application())
+                 .first;
+      }
+
+      // Add watcher state.
+      if (timing_report.has_watchers()) {
+        for (const aos::timing::Watcher *watcher : *timing_report.watchers()) {
+          const aos::Channel *channel =
+              factory.configuration()->channels()->Get(
+                  watcher->channel_index());
+          auto watcher_it = std::find_if(
+              it->second.watchers.begin(), it->second.watchers.end(),
+              [&](const ChannelState &c) { return c.channel == channel; });
+          if (watcher_it == it->second.watchers.end()) {
+            it->second.watchers.push_back(ChannelState{.channel = channel,
+                                                       .frequency_sum = 0.0,
+                                                       .frequency_count = 0});
+            watcher_it = it->second.watchers.end() - 1;
+          }
+          watcher_it->frequency_sum += watcher->count();
+          ++watcher_it->frequency_count;
+        }
+      }
+
+      // Add sender state.
+      if (timing_report.has_senders()) {
+        for (const aos::timing::Sender *sender : *timing_report.senders()) {
+          const aos::Channel *channel =
+              factory.configuration()->channels()->Get(sender->channel_index());
+          auto sender_it = std::find_if(
+              it->second.senders.begin(), it->second.senders.end(),
+              [&](const ChannelState &c) { return c.channel == channel; });
+          if (sender_it == it->second.senders.end()) {
+            it->second.senders.push_back(ChannelState{.channel = channel,
+                                                      .frequency_sum = 0.0,
+                                                      .frequency_count = 0});
+            sender_it = it->second.senders.end() - 1;
+          }
+          sender_it->frequency_sum += sender->count();
+          ++sender_it->frequency_count;
+        }
+      }
+
+      // Add fetcher state.
+      if (timing_report.has_fetchers()) {
+        for (const aos::timing::Fetcher *fetcher : *timing_report.fetchers()) {
+          const aos::Channel *channel =
+              factory.configuration()->channels()->Get(
+                  fetcher->channel_index());
+          auto fetcher_it = std::find_if(
+              it->second.fetchers.begin(), it->second.fetchers.end(),
+              [&](const ChannelState &c) { return c.channel == channel; });
+          if (fetcher_it == it->second.fetchers.end()) {
+            it->second.fetchers.push_back(ChannelState{.channel = channel,
+                                                       .frequency_sum = 0.0,
+                                                       .frequency_count = 0});
+            fetcher_it = it->second.fetchers.end() - 1;
+          }
+          fetcher_it->frequency_sum += fetcher->count();
+          ++fetcher_it->frequency_count;
+        }
+      }
+    });
+    loops.emplace_back(std::move(event_loop));
+  }
+
+  factory.Run();
+
+  reader.Deregister();
+
+  // Now, we need to flip this graph on it's head to deduplicate and draw the
+  // correct graph.  Build it all up as a list of applications per channel.
+  std::map<const aos::Channel *, ChannelConnections> connections;
+  for (const std::pair<const std::string, Application> &app : applications) {
+    for (const ChannelState &state : app.second.senders) {
+      auto it = connections.find(state.channel);
+      if (it == connections.end()) {
+        it = connections.emplace(state.channel, ChannelConnections()).first;
+      }
+
+      it->second.senders.emplace_back(std::make_pair(
+          app.first, state.frequency_count == 0
+                         ? 0.0
+                         : state.frequency_sum / state.frequency_count));
+    }
+    for (const ChannelState &state : app.second.watchers) {
+      auto it = connections.find(state.channel);
+      if (it == connections.end()) {
+        it = connections.emplace(state.channel, ChannelConnections()).first;
+      }
+
+      it->second.watchers.emplace_back(app.first);
+    }
+    for (const ChannelState &state : app.second.fetchers) {
+      auto it = connections.find(state.channel);
+      if (it == connections.end()) {
+        it = connections.emplace(state.channel, ChannelConnections()).first;
+      }
+
+      it->second.fetchers.emplace_back(app.first);
+    }
+  }
+
+  const std::vector<std::string> color_list = {
+      "red", "blue", "orange", "green", "violet", "gold3", "magenta"};
+
+  // Now generate graphvis compatible output.
+  std::stringstream graph_out;
+  graph_out << "digraph g {" << std::endl;
+  for (const std::pair<const aos::Channel *, ChannelConnections> &c :
+       connections) {
+    const std::string channel = absl::StrCat(
+        c.first->name()->string_view(), "\n", c.first->type()->string_view());
+    for (const std::pair<std::string, double> &sender : c.second.senders) {
+      graph_out << "\t\"" << sender.first << "\" -> \"" << channel
+                << "\" [label=\"" << sender.second << "\" color=\""
+                << color_list[0]
+                << "\" weight=" << static_cast<int>(sender.second) << "];"
+                << std::endl;
+    }
+    for (const std::string &watcher : c.second.watchers) {
+      graph_out << "\t\"" << channel << "\" -> \"" << watcher << "\" [color=\""
+                << color_list[1] << "\"];" << std::endl;
+    }
+    for (const std::string &watcher : c.second.fetchers) {
+      graph_out << "\t\"" << channel << "\" -> \"" << watcher << "\" [color=\""
+                << color_list[2] << "\"];" << std::endl;
+    }
+  }
+
+  size_t index = 0;
+  for (const std::pair<const std::string, Application> &app : applications) {
+    graph_out << "\t\"" << app.first << "\" [color=\"" << color_list[index]
+              << "\" shape=box style=filled];" << std::endl;
+    ++index;
+    if (index >= color_list.size()) {
+      index = 0;
+    }
+  }
+  graph_out << "}" << std::endl;
+
+  std::cout << graph_out.str();
+
+  return 0;
+}
diff --git a/aos/events/logging/log_cat.cc b/aos/events/logging/log_cat.cc
index 9741f70..5079606 100644
--- a/aos/events/logging/log_cat.cc
+++ b/aos/events/logging/log_cat.cc
@@ -156,6 +156,11 @@
           aos::logger::Sha256(raw_header_reader->raw_log_file_header().span()));
       full_header = raw_header_reader->log_file_header();
     }
+
+    if (!FLAGS_print) {
+      return 0;
+    }
+
     std::cout << aos::FlatbufferToJson(full_header,
                                        {.multi_line = FLAGS_pretty,
                                         .max_vector_size = static_cast<size_t>(
diff --git a/frc971/control_loops/python/constants.py b/frc971/control_loops/python/constants.py
index 10e6786..b515626 100644
--- a/frc971/control_loops/python/constants.py
+++ b/frc971/control_loops/python/constants.py
@@ -118,7 +118,7 @@
         field_id="autonav_bounce"),
 }
 
-FIELD = FIELDS["2021 Galactic Search BRed"]
+FIELD = FIELDS["2020 Field"]
 
 
 def get_json_folder(field):
@@ -127,14 +127,5 @@
     else:
         return "frc971/control_loops/python/spline_jsons"
 
-
-def pxToM(p):
-    return p * FIELD.width / SCREEN_SIZE
-
-
-def mToPx(m):
-    return (m * SCREEN_SIZE / FIELD.width)
-
-
 def inToM(i):
     return (i * 0.0254)
diff --git a/frc971/control_loops/python/path_edit.py b/frc971/control_loops/python/path_edit.py
index 50400b9..d743e0d 100755
--- a/frc971/control_loops/python/path_edit.py
+++ b/frc971/control_loops/python/path_edit.py
@@ -13,7 +13,9 @@
 from libspline import Spline
 import enum
 import json
-from constants import *
+from constants import FIELD
+from constants import get_json_folder
+from constants import ROBOT_SIDE_TO_BALL_CENTER, ROBOT_SIDE_TO_HATCH_PANEL, HATCH_PANEL_WIDTH, BALL_RADIUS
 from drawing_constants import set_color, draw_px_cross, draw_px_x, display_text, draw_control_points
 from points import Points
 import time
@@ -30,7 +32,9 @@
 
     def __init__(self):
         super(FieldWidget, self).__init__()
-        self.set_size_request(mToPx(FIELD.width), mToPx(FIELD.length))
+        self.set_field(FIELD)
+        self.set_size_request(
+            self.mToPx(self.field.width), self.mToPx(self.field.length))
 
         self.points = Points()
         self.graph = Graph()
@@ -52,25 +56,60 @@
         self.held_x = 0
         self.spline_edit = -1
 
+        self.transform = cairo.Matrix()
+
+        self.set_events(Gdk.EventMask.BUTTON_PRESS_MASK
+                        | Gdk.EventMask.BUTTON_PRESS_MASK
+                        | Gdk.EventMask.BUTTON_RELEASE_MASK
+                        | Gdk.EventMask.POINTER_MOTION_MASK
+                        | Gdk.EventMask.SCROLL_MASK)
+
+    def set_field(self, field):
+        self.field = field
         try:
             self.field_png = cairo.ImageSurface.create_from_png(
-                "frc971/control_loops/python/field_images/" + FIELD.field_id +
-                ".png")
+                "frc971/control_loops/python/field_images/" +
+                self.field.field_id + ".png")
         except cairo.Error:
             self.field_png = None
+        self.queue_draw()
+
+    # returns the transform from widget space to field space
+    @property
+    def input_transform(self):
+        xx, yx, xy, yy, x0, y0 = self.transform
+        matrix = cairo.Matrix(xx, yx, xy, yy, x0, y0)
+        # the transform for input needs to be the opposite of the transform for drawing
+        matrix.invert()
+        return matrix
+
+    # returns the scale from pixels in field space to meters in field space
+    def pxToM_scale(self):
+        available_space = self.get_allocation()
+        return np.maximum(self.field.width / available_space.width,
+                          self.field.length / available_space.height)
+
+    def pxToM(self, p):
+        return p * self.pxToM_scale()
+
+    def mToPx(self, m):
+        return m / self.pxToM_scale()
 
     def draw_robot_at_point(self, cr, i, p, spline):
-        p1 = [mToPx(spline.Point(i)[0]), mToPx(spline.Point(i)[1])]
-        p2 = [mToPx(spline.Point(i + p)[0]), mToPx(spline.Point(i + p)[1])]
+        p1 = [self.mToPx(spline.Point(i)[0]), self.mToPx(spline.Point(i)[1])]
+        p2 = [
+            self.mToPx(spline.Point(i + p)[0]),
+            self.mToPx(spline.Point(i + p)[1])
+        ]
 
         #Calculate Robot
         distance = np.sqrt((p2[1] - p1[1])**2 + (p2[0] - p1[0])**2)
         x_difference_o = p2[0] - p1[0]
         y_difference_o = p2[1] - p1[1]
-        x_difference = x_difference_o * mToPx(
-            FIELD.robot.length / 2) / distance
-        y_difference = y_difference_o * mToPx(
-            FIELD.robot.length / 2) / distance
+        x_difference = x_difference_o * self.mToPx(
+            self.field.robot.length / 2) / distance
+        y_difference = y_difference_o * self.mToPx(
+            self.field.robot.length / 2) / distance
 
         front_middle = []
         front_middle.append(p1[0] + x_difference)
@@ -83,8 +122,10 @@
         slope = [-(1 / x_difference_o) / (1 / y_difference_o)]
         angle = np.arctan(slope)
 
-        x_difference = np.sin(angle[0]) * mToPx(FIELD.robot.width / 2)
-        y_difference = np.cos(angle[0]) * mToPx(FIELD.robot.width / 2)
+        x_difference = np.sin(angle[0]) * self.mToPx(
+            self.field.robot.width / 2)
+        y_difference = np.cos(angle[0]) * self.mToPx(
+            self.field.robot.width / 2)
 
         front_1 = []
         front_1.append(front_middle[0] - x_difference)
@@ -102,28 +143,28 @@
         back_2.append(back_middle[0] + x_difference)
         back_2.append(back_middle[1] + y_difference)
 
-        x_difference = x_difference_o * mToPx(
-            FIELD.robot.length / 2 + ROBOT_SIDE_TO_BALL_CENTER) / distance
-        y_difference = y_difference_o * mToPx(
-            FIELD.robot.length / 2 + ROBOT_SIDE_TO_BALL_CENTER) / distance
+        x_difference = x_difference_o * self.mToPx(
+            self.field.robot.length / 2 + ROBOT_SIDE_TO_BALL_CENTER) / distance
+        y_difference = y_difference_o * self.mToPx(
+            self.field.robot.length / 2 + ROBOT_SIDE_TO_BALL_CENTER) / distance
 
         #Calculate Ball
         ball_center = []
         ball_center.append(p1[0] + x_difference)
         ball_center.append(p1[1] + y_difference)
 
-        x_difference = x_difference_o * mToPx(
-            FIELD.robot.length / 2 + ROBOT_SIDE_TO_HATCH_PANEL) / distance
-        y_difference = y_difference_o * mToPx(
-            FIELD.robot.length / 2 + ROBOT_SIDE_TO_HATCH_PANEL) / distance
+        x_difference = x_difference_o * self.mToPx(
+            self.field.robot.length / 2 + ROBOT_SIDE_TO_HATCH_PANEL) / distance
+        y_difference = y_difference_o * self.mToPx(
+            self.field.robot.length / 2 + ROBOT_SIDE_TO_HATCH_PANEL) / distance
 
         #Calculate Panel
         panel_center = []
         panel_center.append(p1[0] + x_difference)
         panel_center.append(p1[1] + y_difference)
 
-        x_difference = np.sin(angle[0]) * mToPx(HATCH_PANEL_WIDTH / 2)
-        y_difference = np.cos(angle[0]) * mToPx(HATCH_PANEL_WIDTH / 2)
+        x_difference = np.sin(angle[0]) * self.mToPx(HATCH_PANEL_WIDTH / 2)
+        y_difference = np.cos(angle[0]) * self.mToPx(HATCH_PANEL_WIDTH / 2)
 
         panel_1 = []
         panel_1.append(panel_center[0] + x_difference)
@@ -146,7 +187,7 @@
         set_color(cr, palette["ORANGE"], 0.5)
         cr.move_to(back_middle[0], back_middle[1])
         cr.line_to(ball_center[0], ball_center[1])
-        cr.arc(ball_center[0], ball_center[1], mToPx(BALL_RADIUS), 0,
+        cr.arc(ball_center[0], ball_center[1], self.mToPx(BALL_RADIUS), 0,
                2 * np.pi)
         cr.stroke()
 
@@ -159,21 +200,23 @@
         cr.set_source_rgba(0, 0, 0, 1)
 
     def do_draw(self, cr):  # main
-
-        start_time = time.perf_counter()
+        cr.set_matrix(self.transform.multiply(cr.get_matrix()))
 
         cr.save()
+
         set_color(cr, palette["BLACK"])
 
-        cr.rectangle(0, 0, mToPx(FIELD.width), mToPx(FIELD.length))
+        cr.set_line_width(1.0)
+        cr.rectangle(0, 0, self.mToPx(self.field.width),
+                     self.mToPx(self.field.length))
         cr.set_line_join(cairo.LINE_JOIN_ROUND)
         cr.stroke()
 
         if self.field_png:
             cr.save()
             cr.scale(
-                mToPx(FIELD.width) / self.field_png.get_width(),
-                mToPx(FIELD.length) / self.field_png.get_height(),
+                self.mToPx(self.field.width) / self.field_png.get_width(),
+                self.mToPx(self.field.length) / self.field_png.get_height(),
             )
             cr.set_source_surface(self.field_png)
             cr.paint()
@@ -181,10 +224,11 @@
 
         # update everything
 
+        cr.set_line_width(2.0)
         if self.mode == Mode.kPlacing or self.mode == Mode.kViewing:
             set_color(cr, palette["BLACK"])
             for i, point in enumerate(self.points.getPoints()):
-                draw_px_x(cr, mToPx(point[0]), mToPx(point[1]), 10)
+                draw_px_x(cr, self.mToPx(point[0]), self.mToPx(point[1]), 10)
             set_color(cr, palette["WHITE"])
         elif self.mode == Mode.kEditing:
             set_color(cr, palette["BLACK"])
@@ -193,7 +237,7 @@
                 for i, points in enumerate(self.points.getSplines()):
 
                     points = [
-                        np.array([mToPx(x), mToPx(y)])
+                        np.array([self.mToPx(x), self.mToPx(y)])
                         for (x, y) in points
                     ]
                     draw_control_points(cr, points)
@@ -216,7 +260,6 @@
 
                     cr.stroke()
                     cr.set_line_width(2.0)
-            self.points.update_lib_spline()
             set_color(cr, palette["WHITE"])
 
         cr.paint_with_alpha(0.2)
@@ -224,49 +267,25 @@
         draw_px_cross(cr, self.mousex, self.mousey, 10)
         cr.restore()
 
-        print("spent {:.2f} ms drawing the field widget".format(1000 * (time.perf_counter() - start_time)))
-
     def draw_splines(self, cr):
-        for i, points in enumerate(self.points.getSplines()):
-            array = np.zeros(shape=(6, 2), dtype=float)
-            for j, point in enumerate(points):
-                array[j, 0] = point[0]
-                array[j, 1] = point[1]
-            spline = Spline(np.ascontiguousarray(np.transpose(array)))
+        for i, spline in enumerate(self.points.getLibsplines()):
             for k in np.linspace(0.01, 1, 100):
                 cr.move_to(
-                    mToPx(spline.Point(k - 0.01)[0]),
-                    mToPx(spline.Point(k - 0.01)[1]))
+                    self.mToPx(spline.Point(k - 0.01)[0]),
+                    self.mToPx(spline.Point(k - 0.01)[1]))
                 cr.line_to(
-                    mToPx(spline.Point(k)[0]), mToPx(spline.Point(k)[1]))
+                    self.mToPx(spline.Point(k)[0]),
+                    self.mToPx(spline.Point(k)[1]))
                 cr.stroke()
             if i == 0:
                 self.draw_robot_at_point(cr, 0.00, 0.01, spline)
             self.draw_robot_at_point(cr, 1, 0.01, spline)
 
-    def mouse_move(self, event):
-        old_x = self.mousex
-        old_y = self.mousey
-        self.mousex, self.mousey = event.x, event.y
-        dif_x = self.mousex - old_x
-        dif_y = self.mousey - old_y
-        difs = np.array([pxToM(dif_x), pxToM(dif_y)])
-
-        if self.mode == Mode.kEditing and self.spline_edit != -1:
-            self.points.updates_for_mouse_move(self.index_of_edit,
-                                               self.spline_edit,
-                                               pxToM(self.mousex),
-                                               pxToM(self.mousey), difs)
-
-            self.points.update_lib_spline()
-            self.graph.schedule_recalculate(self.points)
-        self.queue_draw()
-
     def export_json(self, file_name):
         self.path_to_export = os.path.join(
             self.module_path,  # position of the python
             "../../..",  # root of the repository
-            get_json_folder(FIELD),  # path from the root
+            get_json_folder(self.field),  # path from the root
             file_name  # selected file
         )
 
@@ -280,7 +299,7 @@
         self.path_to_export = os.path.join(
             self.module_path,  # position of the python
             "../../..",  # root of the repository
-            get_json_folder(FIELD),  # path from the root
+            get_json_folder(self.field),  # path from the root
             file_name  # selected file
         )
 
@@ -310,8 +329,10 @@
 
         print("SPLINES LOADED")
         self.mode = Mode.kEditing
+        self.queue_draw()
+        self.graph.schedule_recalculate(self.points)
 
-    def key_press(self, event):
+    def do_key_press_event(self, event):
         keyval = Gdk.keyval_to_lower(event.keyval)
 
         # TODO: This should be a button
@@ -328,13 +349,33 @@
                 self.points.getSplines()[len(self.points.getSplines()) - 1][3])
             self.queue_draw()
 
-    def button_press(self, event):
-        self.mousex, self.mousey = event.x, event.y
+    def do_button_release_event(self, event):
+        self.mousex, self.mousey = self.input_transform.transform_point(
+            event.x, event.y)
+        if self.mode == Mode.kEditing:
+            if self.index_of_edit > -1 and self.held_x != self.mousex:
+
+                self.points.setSplines(self.spline_edit, self.index_of_edit,
+                                       self.pxToM(self.mousex),
+                                       self.pxToM(self.mousey))
+
+                self.points.splineExtrapolate(self.spline_edit)
+
+                self.points.update_lib_spline()
+                self.graph.schedule_recalculate(self.points)
+
+                self.index_of_edit = -1
+                self.spline_edit = -1
+
+    def do_button_press_event(self, event):
+        self.mousex, self.mousey = self.input_transform.transform_point(
+            event.x, event.y)
 
         if self.mode == Mode.kPlacing:
             if self.points.add_point(
-                    pxToM(self.mousex), pxToM(self.mousey)):
+                    self.pxToM(self.mousex), self.pxToM(self.mousey)):
                 self.mode = Mode.kEditing
+                self.graph.schedule_recalculate(self.points)
         elif self.mode == Mode.kEditing:
             # Now after index_of_edit is not -1, the point is selected, so
             # user can click for new point
@@ -342,7 +383,7 @@
                 # Get clicked point
                 # Find nearest
                 # Move nearest to clicked
-                cur_p = [pxToM(self.mousex), pxToM(self.mousey)]
+                cur_p = [self.pxToM(self.mousex), self.pxToM(self.mousey)]
                 # Get the distance between each for x and y
                 # Save the index of the point closest
                 nearest = 1  # Max distance away a the selected point can be in meters
@@ -362,19 +403,60 @@
                             self.held_x = self.mousex
         self.queue_draw()
 
-    def button_release(self, event):
-        self.mousex, self.mousey = event.x, event.y
-        if self.mode == Mode.kEditing:
-            if self.index_of_edit > -1 and self.held_x != self.mousex:
+    def do_motion_notify_event(self, event):
+        old_x = self.mousex
+        old_y = self.mousey
+        self.mousex, self.mousey = self.input_transform.transform_point(
+            event.x, event.y)
+        dif_x = self.mousex - old_x
+        dif_y = self.mousey - old_y
+        difs = np.array([self.pxToM(dif_x), self.pxToM(dif_y)])
 
-                self.points.setSplines(self.spline_edit, self.index_of_edit,
-                                       pxToM(self.mousex),
-                                       pxToM(self.mousey))
+        if self.mode == Mode.kEditing and self.spline_edit != -1:
+            self.points.updates_for_mouse_move(self.index_of_edit,
+                                               self.spline_edit,
+                                               self.pxToM(self.mousex),
+                                               self.pxToM(self.mousey), difs)
 
-                self.points.splineExtrapolate(self.spline_edit)
+            self.points.update_lib_spline()
+            self.graph.schedule_recalculate(self.points)
+        self.queue_draw()
 
-                self.points.update_lib_spline()
-                self.graph.schedule_recalculate(self.points)
+    def do_scroll_event(self, event):
+        self.mousex, self.mousey = self.input_transform.transform_point(
+            event.x, event.y)
 
-                self.index_of_edit = -1
-                self.spline_edit = -1
+        step_size = 20  # px
+
+        if event.direction == Gdk.ScrollDirection.UP:
+            # zoom out
+            scale_by = step_size
+        elif event.direction == Gdk.ScrollDirection.DOWN:
+            # zoom in
+            scale_by = -step_size
+        else:
+            return
+
+        apparent_width, apparent_height = self.transform.transform_distance(
+            self.mToPx(FIELD.width), self.mToPx(FIELD.length))
+        scale = (apparent_width + scale_by) / apparent_width
+
+        # scale from point in field coordinates
+        point = self.mousex, self.mousey
+
+        # move the origin to point
+        self.transform.translate(point[0], point[1])
+
+        # scale from new origin
+        self.transform.scale(scale, scale)
+
+        # move back
+        self.transform.translate(-point[0], -point[1])
+
+        # snap to the edge when near 1x scaling
+        if 0.99 < self.transform.xx < 1.01 and -50 < self.transform.x0 < 50:
+            self.transform.x0 = 0
+            self.transform.y0 = 0
+            print("snap")
+
+        self.queue_draw()
diff --git a/frc971/control_loops/python/spline_graph.py b/frc971/control_loops/python/spline_graph.py
index f56b0d3..258dcfa 100755
--- a/frc971/control_loops/python/spline_graph.py
+++ b/frc971/control_loops/python/spline_graph.py
@@ -20,9 +20,6 @@
 
         self.connect(event, handler)
 
-    def configure(self, event):
-        self.field.window_shape = (event.width, event.height)
-
     def output_json_clicked(self, button):
         self.field.export_json(self.file_name_box.get_text())
 
@@ -37,10 +34,12 @@
     def long_changed(self, button):
         value = self.long_input.get_value()
         self.field.points.setConstraint("LONGITUDINAL_ACCELERATION", value)
+        self.field.graph.schedule_recalculate(self.field.points)
 
     def lat_changed(self, button):
         value = self.lat_input.get_value()
         self.field.points.setConstraint("LATERAL_ACCELERATION", value)
+        self.field.graph.schedule_recalculate(self.field.points)
 
     def vel_changed(self, button):
         value = self.vel_input.get_value()
@@ -48,12 +47,13 @@
     def vol_changed(self, button):
         value = self.vol_input.get_value()
         self.field.points.setConstraint("VOLTAGE", value)
+        self.field.graph.schedule_recalculate(self.field.points)
 
     def input_combobox_choice(self, combo):
         text = combo.get_active_text()
         if text is not None:
             print("Combo Clicked on: " + text)
-            #set_field(text)
+            self.field.set_field(FIELDS[text])
 
     def __init__(self):
         Gtk.Window.__init__(self)
@@ -64,22 +64,10 @@
         container.set_vexpand(True)
         self.add(container)
 
-        self.eventBox = Gtk.EventBox()
-        self.eventBox.set_events(Gdk.EventMask.BUTTON_PRESS_MASK
-                                 | Gdk.EventMask.BUTTON_PRESS_MASK
-                                 | Gdk.EventMask.BUTTON_RELEASE_MASK
-                                 | Gdk.EventMask.POINTER_MOTION_MASK
-                                 | Gdk.EventMask.SCROLL_MASK
-                                 | Gdk.EventMask.KEY_PRESS_MASK)
-
         self.field = FieldWidget()
 
         self.method_connect("delete-event", basic_window.quit_main_loop)
-        self.method_connect("key-release-event", self.field.key_press)
-        self.method_connect("button-press-event", self.field.button_press)
-        self.method_connect("button-release-event", self.field.button_release)
-        self.method_connect("configure-event", self.configure)
-        self.method_connect("motion_notify_event", self.field.mouse_move)
+        self.method_connect("key-release-event", self.field.do_key_press_event)
 
         self.file_name_box = Gtk.Entry()
         self.file_name_box.set_size_request(200, 40)
@@ -155,7 +143,8 @@
         for game in FIELDS.keys():
             self.game_combo.append_text(game)
 
-        self.game_combo.set_active(0)
+        if FIELD in FIELDS.values():
+            self.game_combo.set_active(list(FIELDS.values()).index(FIELD))
         self.game_combo.set_size_request(100, 40)
 
         limitControls = Gtk.FlowBox()
@@ -185,8 +174,7 @@
         container.attach(self.label, 4, 0, 1, 1)
         container.attach(self.game_combo, 5, 0, 1, 1)
 
-        self.eventBox.add(self.field)
-        container.attach(self.eventBox, 1, 1, 4, 4)
+        container.attach(self.field, 1, 1, 4, 4)
 
         container.attach(self.field.graph, 0, 10, 10, 1)