Merge "Run yapf on Spline UI"
diff --git a/aos/events/logging/BUILD b/aos/events/logging/BUILD
index b00d2ad..9f6234a 100644
--- a/aos/events/logging/BUILD
+++ b/aos/events/logging/BUILD
@@ -497,3 +497,13 @@
gen_reflections = 1,
target_compatible_with = ["@platforms//os:linux"],
)
+
+cc_binary(
+ name = "timestamp_plot",
+ srcs = ["timestamp_plot.cc"],
+ deps = [
+ "//aos:init",
+ "//frc971/analysis:in_process_plotter",
+ "@com_google_absl//absl/strings",
+ ],
+)
diff --git a/aos/events/logging/boot_timestamp.h b/aos/events/logging/boot_timestamp.h
index d0fde73..e818fb9 100644
--- a/aos/events/logging/boot_timestamp.h
+++ b/aos/events/logging/boot_timestamp.h
@@ -4,6 +4,7 @@
#include <iostream>
#include "aos/time/time.h"
+#include "glog/logging.h"
namespace aos::logger {
@@ -24,6 +25,18 @@
return {boot, duration - d};
}
+ BootDuration operator-(BootDuration d) const {
+ CHECK_EQ(d.boot, boot);
+ return {boot, duration - d.duration};
+ }
+
+ BootDuration operator+(BootDuration d) const {
+ CHECK_EQ(d.boot, boot);
+ return {boot, duration + d.duration};
+ }
+
+ BootDuration operator/(int x) const { return {boot, duration / x}; }
+
bool operator==(const BootDuration &m2) const {
return boot == m2.boot && duration == m2.duration;
}
diff --git a/aos/events/logging/timestamp_plot.cc b/aos/events/logging/timestamp_plot.cc
new file mode 100644
index 0000000..63c3de4
--- /dev/null
+++ b/aos/events/logging/timestamp_plot.cc
@@ -0,0 +1,220 @@
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_split.h"
+#include "aos/init.h"
+#include "aos/util/file.h"
+#include "frc971/analysis/in_process_plotter.h"
+
+using frc971::analysis::Plotter;
+
+// Simple C++ application to read the CSV files and use the in process plotter
+// to plot them. This smokes the pants off gnuplot in terms of interactivity.
+
+namespace aos {
+
+std::pair<std::vector<double>, std::vector<double>> ReadSamples(
+ std::string_view node1, std::string_view node2, bool flip) {
+ std::vector<double> samplefile12_t;
+ std::vector<double> samplefile12_o;
+
+ const std::string file = aos::util::ReadFileToStringOrDie(absl::StrCat(
+ "/tmp/timestamp_noncausal_", node1, "_", node2, "_samples.csv"));
+ bool first = true;
+ std::vector<std::string_view> lines = absl::StrSplit(file, '\n');
+ samplefile12_t.reserve(lines.size());
+ for (const std::string_view n : lines) {
+ if (first) {
+ first = false;
+ continue;
+ }
+ if (n == "") {
+ continue;
+ }
+
+ std::vector<std::string_view> l = absl::StrSplit(n, ", ");
+ CHECK_EQ(l.size(), 4u);
+ double t;
+ double o;
+ CHECK(absl::SimpleAtod(l[0], &t));
+ CHECK(absl::SimpleAtod(l[1], &o));
+ samplefile12_t.emplace_back(t);
+ samplefile12_o.emplace_back(flip ? -o : o);
+ }
+ return std::make_pair(samplefile12_t, samplefile12_o);
+}
+
+std::pair<std::vector<double>, std::vector<double>> ReadLines(
+ std::string_view node1, std::string_view node2, bool flip) {
+ std::vector<double> samplefile12_t;
+ std::vector<double> samplefile12_o;
+
+ const std::string file = aos::util::ReadFileToStringOrDie(
+ absl::StrCat("/tmp/timestamp_noncausal_", node1, "_", node2, ".csv"));
+ bool first = true;
+ std::vector<std::string_view> lines = absl::StrSplit(file, '\n');
+ samplefile12_t.reserve(lines.size());
+ for (const std::string_view n : lines) {
+ if (first) {
+ first = false;
+ continue;
+ }
+ if (n == "") {
+ continue;
+ }
+
+ std::vector<std::string_view> l = absl::StrSplit(n, ", ");
+ CHECK_EQ(l.size(), 3u);
+ double t;
+ double o;
+ CHECK(absl::SimpleAtod(l[0], &t));
+ CHECK(absl::SimpleAtod(l[2], &o));
+ samplefile12_t.emplace_back(t);
+ samplefile12_o.emplace_back(flip ? -o : o);
+ }
+ return std::make_pair(samplefile12_t, samplefile12_o);
+}
+
+std::pair<std::vector<double>, std::vector<double>> ReadOffset(
+ std::string_view node1, std::string_view node2) {
+ int node1_index = -1;
+ int node2_index = -1;
+
+ {
+ const std::string start_time_file = aos::util::ReadFileToStringOrDie(
+ "/tmp/timestamp_noncausal_starttime.csv");
+ std::vector<std::string_view> nodes = absl::StrSplit(start_time_file, '\n');
+
+ int index = 0;
+ for (const std::string_view n : nodes) {
+ if (n == "") {
+ continue;
+ }
+
+ std::vector<std::string_view> l = absl::StrSplit(n, ", ");
+ CHECK_EQ(l.size(), 2u) << "'" << n << "'";
+ if (l[0] == node1) {
+ node1_index = index;
+ }
+ if (l[0] == node2) {
+ node2_index = index;
+ }
+ ++index;
+ }
+ }
+ CHECK_NE(node1_index, -1) << ": Unknown node " << node1;
+ CHECK_NE(node2_index, -1) << ": Unknown node " << node2;
+ std::vector<double> offsetfile_t;
+ std::vector<double> offsetfile_o;
+
+ const std::string file =
+ aos::util::ReadFileToStringOrDie("/tmp/timestamp_noncausal_offsets.csv");
+ bool first = true;
+ std::vector<std::string_view> lines = absl::StrSplit(file, '\n');
+ offsetfile_t.reserve(lines.size());
+ for (const std::string_view n : lines) {
+ if (first) {
+ first = false;
+ continue;
+ }
+ if (n == "") {
+ continue;
+ }
+
+ std::vector<std::string_view> l = absl::StrSplit(n, ", ");
+ CHECK_LT(static_cast<size_t>(node1_index + 1), l.size());
+ CHECK_LT(static_cast<size_t>(node2_index + 1), l.size());
+ double t;
+ double o1;
+ double o2;
+ CHECK(absl::SimpleAtod(l[0], &t));
+ CHECK(absl::SimpleAtod(l[1 + node1_index], &o1));
+ CHECK(absl::SimpleAtod(l[1 + node2_index], &o2));
+ offsetfile_t.emplace_back(t);
+ offsetfile_o.emplace_back(o2 - o1);
+ }
+ return std::make_pair(offsetfile_t, offsetfile_o);
+}
+
+void AddNodes(Plotter *plotter, std::string_view node1,
+ std::string_view node2) {
+ const std::pair<std::vector<double>, std::vector<double>> samplefile12 =
+ ReadSamples(node1, node2, false);
+ const std::pair<std::vector<double>, std::vector<double>> samplefile21 =
+ ReadSamples(node2, node1, true);
+
+ const std::pair<std::vector<double>, std::vector<double>> noncausalfile12 =
+ ReadLines(node1, node2, false);
+ const std::pair<std::vector<double>, std::vector<double>> noncausalfile21 =
+ ReadLines(node2, node1, true);
+
+ const std::pair<std::vector<double>, std::vector<double>> offsetfile =
+ ReadOffset(node1, node2);
+
+ CHECK_EQ(samplefile12.first.size(), samplefile12.second.size());
+ CHECK_EQ(samplefile21.first.size(), samplefile21.second.size());
+ CHECK_EQ(noncausalfile12.first.size(), noncausalfile12.second.size());
+ CHECK_EQ(noncausalfile21.first.size(), noncausalfile21.second.size());
+
+ LOG(INFO) << samplefile12.first.size() + samplefile21.first.size() +
+ noncausalfile12.first.size() + noncausalfile21.first.size()
+ << " points";
+ plotter->AddLine(
+ samplefile12.first, samplefile12.second,
+ Plotter::LineOptions{.label = absl::StrCat("sample ", node1, " ", node2),
+ .line_style = "*",
+ .color = "purple"});
+ plotter->AddLine(
+ samplefile21.first, samplefile21.second,
+ Plotter::LineOptions{.label = absl::StrCat("sample ", node2, " ", node1),
+ .line_style = "*",
+ .color = "green"});
+
+ plotter->AddLine(
+ noncausalfile12.first, noncausalfile12.second,
+ Plotter::LineOptions{.label = absl::StrCat("nc ", node1, " ", node2),
+ .line_style = "-",
+ .color = "blue"});
+ plotter->AddLine(
+ noncausalfile21.first, noncausalfile21.second,
+ Plotter::LineOptions{.label = absl::StrCat("nc ", node2, " ", node1),
+ .line_style = "-",
+ .color = "orange"});
+
+ plotter->AddLine(offsetfile.first, offsetfile.second,
+ Plotter::LineOptions{
+ .label = absl::StrCat("filter ", node2, " ", node1),
+ // TODO(austin): roboRIO compiler wants all the fields
+ // filled out, but other compilers don't... Sigh.
+ .line_style = "*-",
+ .color = "yellow"});
+}
+
+int Main(int argc, const char *const *argv) {
+ CHECK_EQ(argc, 3);
+
+ LOG(INFO) << argv[1];
+ LOG(INFO) << argv[2];
+
+ // TODO(austin): Find all node pairs and plot them...
+
+ const std::string_view node1 = argv[1];
+ const std::string_view node2 = argv[2];
+
+ Plotter plotter;
+ plotter.AddFigure("Time");
+
+ AddNodes(&plotter, node1, node2);
+
+ plotter.Publish();
+
+ plotter.Spin();
+
+ return 0;
+}
+
+} // namespace aos
+
+int main(int argc, char **argv) {
+ aos::InitGoogle(&argc, &argv);
+
+ aos::Main(argc, argv);
+}
diff --git a/aos/network/timestamp_filter.cc b/aos/network/timestamp_filter.cc
index c1cf612..4ec9e0a 100644
--- a/aos/network/timestamp_filter.cc
+++ b/aos/network/timestamp_filter.cc
@@ -769,37 +769,7 @@
chrono::nanoseconds NoncausalTimestampFilter::ExtrapolateOffset(
std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> p0,
monotonic_clock::time_point ta) {
- const chrono::nanoseconds dt = ta - std::get<0>(p0);
- if (dt <= std::chrono::nanoseconds(0)) {
- // Extrapolate backwards, using the (positive) MaxVelocity slope
- // We've been asked to extrapolate the offset to a time before our first
- // sample point. To be conservative, we'll return an extrapolated
- // offset that is less than (less tight an estimate of the network delay)
- // than our sample offset, bound by the max slew velocity we allow
- // p0
- // /
- // /
- // ta
- // Since dt < 0, we shift by dt * slope to get that value
- return std::get<1>(p0) +
- chrono::nanoseconds(static_cast<int64_t>(
- (absl::int128(dt.count() - MaxVelocityRatio::den / 2) *
- absl::int128(MaxVelocityRatio::num)) /
- absl::int128(MaxVelocityRatio::den)));
- } else {
- // Extrapolate forwards, using the (negative) MaxVelocity slope
- // Same concept, except going foward past our last (most recent) sample:
- // pN
- // |
- // |
- // ta
- // Since dt > 0, we shift by - dt * slope to get that value
- return std::get<1>(p0) -
- chrono::nanoseconds(static_cast<int64_t>(
- (absl::int128(dt.count() + MaxVelocityRatio::den / 2) *
- absl::int128(MaxVelocityRatio::num)) /
- absl::int128(MaxVelocityRatio::den)));
- }
+ return ExtrapolateOffset(p0, ta, 0.0).first;
}
chrono::nanoseconds NoncausalTimestampFilter::InterpolateOffset(
@@ -831,24 +801,12 @@
chrono::nanoseconds NoncausalTimestampFilter::InterpolateOffset(
std::tuple<monotonic_clock::time_point, chrono::nanoseconds> p0,
- std::tuple<monotonic_clock::time_point, chrono::nanoseconds> /*p1*/,
- monotonic_clock::time_point /*ta_base*/, double /*ta*/) {
- // For the double variant, we want to split the result up into a large integer
- // portion, and the rest. We want to do this without introducing numerical
- // precision problems.
- //
- // One way would be to carefully compute the integer portion, and then compute
- // the double portion in such a way that the two are guaranteed to add up
- // correctly.
- //
- // The simpler way is to simply just use the offset from p0 as the integer
- // portion, and make the rest be the double portion. It will get us most of
- // the way there for a lot less work, and we can revisit if this breaks down.
- //
- // oa = p0.o + (ta - p0.t) * (p1.o - p0.o) / (p1.t - p0.t)
- // ^^^^
- // TODO(austin): Use 128 bit math and the remainder to be more accurate here.
- return std::get<1>(p0);
+ std::tuple<monotonic_clock::time_point, chrono::nanoseconds> p1,
+ monotonic_clock::time_point ta_base, double ta) {
+ DCHECK_GE(ta, 0.0);
+ DCHECK_LT(ta, 1.0);
+
+ return InterpolateOffset(p0, p1, ta_base);
}
double NoncausalTimestampFilter::InterpolateOffsetRemainder(
@@ -857,42 +815,111 @@
monotonic_clock::time_point ta_base, double ta) {
const chrono::nanoseconds time_in = ta_base - std::get<0>(p0);
const chrono::nanoseconds dt = std::get<0>(p1) - std::get<0>(p0);
+ const chrono::nanoseconds doffset = std::get<1>(p1) - std::get<1>(p0);
- // The remainder then is the rest of the equation.
- //
- // oa = p0.o + (ta - p0.t) * (p1.o - p0.o) / (p1.t - p0.t)
- // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- // TODO(austin): Use 128 bit math and the remainder to be more accurate here.
- return static_cast<double>(ta + time_in.count()) /
- static_cast<double>(dt.count()) *
- (std::get<1>(p1) - std::get<1>(p0)).count();
+ // Compute the remainder of the division in InterpolateOffset above, and then
+ // use double math to compute it accurately.
+ absl::int128 numerator =
+ absl::int128(time_in.count()) * absl::int128(doffset.count());
+ numerator += numerator > 0 ? absl::int128(dt.count() / 2)
+ : -absl::int128(dt.count() / 2);
+ return static_cast<double>(numerator % absl::int128(dt.count())) /
+ dt.count() +
+ (numerator > 0 ? -0.5 : 0.5) +
+ ta * static_cast<double>(doffset.count()) /
+ static_cast<double>(dt.count());
}
-chrono::nanoseconds NoncausalTimestampFilter::ExtrapolateOffset(
- std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> p0,
- monotonic_clock::time_point /*ta_base*/, double /*ta*/) {
- // TODO(austin): 128 bit math again? ...
- // For this version, use the base offset from p0 as the base for the offset
- return std::get<1>(p0);
+chrono::nanoseconds NoncausalTimestampFilter::BoundOffset(
+ std::tuple<monotonic_clock::time_point, chrono::nanoseconds> p0,
+ std::tuple<monotonic_clock::time_point, chrono::nanoseconds> p1,
+ monotonic_clock::time_point ta) {
+ // We are trying to solve for worst case offset given the two known points.
+ // This is on the two worst case lines from the two points, and we switch
+ // lines at the interstection. This is equivilent to the lowest of the two
+ // lines.
+ return std::max(NoncausalTimestampFilter::ExtrapolateOffset(p0, ta),
+ NoncausalTimestampFilter::ExtrapolateOffset(p1, ta));
}
-double NoncausalTimestampFilter::ExtrapolateOffsetRemainder(
+std::pair<chrono::nanoseconds, double> NoncausalTimestampFilter::BoundOffset(
+ std::tuple<monotonic_clock::time_point, chrono::nanoseconds> p0,
+ std::tuple<monotonic_clock::time_point, chrono::nanoseconds> p1,
+ monotonic_clock::time_point ta_base, double ta) {
+ DCHECK_GE(ta, 0.0);
+ DCHECK_LT(ta, 1.0);
+
+ const std::pair<chrono::nanoseconds, double> o0 =
+ NoncausalTimestampFilter::ExtrapolateOffset(p0, ta_base, ta);
+ const std::pair<chrono::nanoseconds, double> o1 =
+ NoncausalTimestampFilter::ExtrapolateOffset(p1, ta_base, ta);
+
+ // Want to calculate max(o0 + o0r, o1 + o1r) without precision problems.
+ if (static_cast<double>((o0.first - o1.first).count()) >
+ o1.second - o0.second) {
+ // Ok, o0 is now > o1. We want the max, so return o0.
+ return o0;
+ } else {
+ return o1;
+ }
+}
+
+std::pair<chrono::nanoseconds, double>
+NoncausalTimestampFilter::ExtrapolateOffset(
std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> p0,
monotonic_clock::time_point ta_base, double ta) {
- // Compute the remainder portion of this offset
- // oa = p0.o +/- ((ta + ta_base) - p0.t)) * kMaxVelocity()
- // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- // But compute (ta + ta_base - p0.t) as (ta + (ta_base - p0.t))
- // to handle numerical precision
- const chrono::nanoseconds time_in = ta_base - std::get<0>(p0);
- const double dt = static_cast<double>(ta + time_in.count());
- if (dt < 0.0) {
- // Extrapolate backwards with max (positive) slope (which means
- // the returned offset should be negative)
- return dt * kMaxVelocity();
+ DCHECK_GE(ta, 0.0);
+ DCHECK_LT(ta, 1.0);
+ // Since the point (p0) is an integer, we now can guarantee that ta won't put
+ // us on a different side of p0. This is because ta is between 0 and 1, and
+ // always positive. Compute the integer and double portions and return them.
+ const chrono::nanoseconds dt = ta_base - std::get<0>(p0);
+
+ if (dt < std::chrono::nanoseconds(0)) {
+ // Extrapolate backwards, using the (positive) MaxVelocity slope
+ // We've been asked to extrapolate the offset to a time before our first
+ // sample point. To be conservative, we'll return an extrapolated
+ // offset that is less than (less tight an estimate of the network delay)
+ // than our sample offset, bound by the max slew velocity we allow
+ // p0
+ // /
+ // /
+ // ta
+ // Since dt < 0, we shift by dt * slope to get that value
+ //
+ // Take the remainder of the math in ExtrapolateOffset above and compute it
+ // with floating point math. Our tests are good enough to confirm that this
+ // works as designed.
+ const absl::int128 numerator =
+ (absl::int128(dt.count() - MaxVelocityRatio::den / 2) *
+ absl::int128(MaxVelocityRatio::num));
+ return std::make_pair(
+ std::get<1>(p0) + chrono::nanoseconds(static_cast<int64_t>(
+ numerator / absl::int128(MaxVelocityRatio::den))),
+ static_cast<double>(numerator % absl::int128(MaxVelocityRatio::den)) /
+ static_cast<double>(MaxVelocityRatio::den) +
+ 0.5 + ta * kMaxVelocity());
} else {
- // Extrapolate forwards with max (negative) slope
- return -dt * kMaxVelocity();
+ // Extrapolate forwards, using the (negative) MaxVelocity slope
+ // Same concept, except going foward past our last (most recent) sample:
+ // pN
+ // |
+ // |
+ // ta
+ // Since dt > 0, we shift by - dt * slope to get that value
+ //
+ // Take the remainder of the math in ExtrapolateOffset above and compute it
+ // with floating point math. Our tests are good enough to confirm that this
+ // works as designed.
+ const absl::int128 numerator =
+ absl::int128(dt.count() + MaxVelocityRatio::den / 2) *
+ absl::int128(MaxVelocityRatio::num);
+ return std::make_pair(
+ std::get<1>(p0) - chrono::nanoseconds(static_cast<int64_t>(
+ numerator / absl::int128(MaxVelocityRatio::den))),
+ -static_cast<double>(numerator % absl::int128(MaxVelocityRatio::den)) /
+ static_cast<double>(MaxVelocityRatio::den) +
+ 0.5 - ta * kMaxVelocity());
}
}
@@ -931,12 +958,9 @@
std::pair<Pointer,
std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds>>
reference_timestamp = GetReferenceTimestamp(ta_base, ta);
- return std::make_pair(
- reference_timestamp.first,
- std::make_pair(NoncausalTimestampFilter::ExtrapolateOffset(
- reference_timestamp.second, ta_base, ta),
- NoncausalTimestampFilter::ExtrapolateOffsetRemainder(
- reference_timestamp.second, ta_base, ta)));
+ return std::make_pair(reference_timestamp.first,
+ NoncausalTimestampFilter::ExtrapolateOffset(
+ reference_timestamp.second, ta_base, ta));
}
std::pair<
@@ -1061,25 +1085,22 @@
auto reference_timestamp = GetReferenceTimestamp(ta_base, ta);
// Special case size = 1 or ta before first timestamp, so we extrapolate
- const chrono::nanoseconds offset_base =
+ const std::pair<chrono::nanoseconds, double> offset =
NoncausalTimestampFilter::ExtrapolateOffset(reference_timestamp.second,
ta_base, ta);
- const double offset_remainder =
- NoncausalTimestampFilter::ExtrapolateOffsetRemainder(
- reference_timestamp.second, ta_base, ta);
// We want to do offset + ta > tb, but we need to do it with minimal
// numerical precision problems.
// See below for why this is a >=
- if (static_cast<double>((offset_base + ta_base - tb_base).count()) >=
- tb - ta - offset_remainder) {
+ if (static_cast<double>((offset.first + ta_base - tb_base).count()) >=
+ tb - ta - offset.second) {
LOG(ERROR) << node_names_ << " "
- << TimeString(ta_base, ta, offset_base, offset_remainder)
+ << TimeString(ta_base, ta, offset.first, offset.second)
<< " > solution time "
<< tb_base + chrono::nanoseconds(
static_cast<int64_t>(std::round(tb)))
<< ", " << tb - std::round(tb) << " foo";
- LOG(INFO) << "Remainder " << offset_remainder;
+ LOG(INFO) << "Remainder " << offset.second;
return false;
}
return true;
diff --git a/aos/network/timestamp_filter.h b/aos/network/timestamp_filter.h
index fd430ce..a056fce 100644
--- a/aos/network/timestamp_filter.h
+++ b/aos/network/timestamp_filter.h
@@ -16,9 +16,6 @@
namespace aos {
namespace message_bridge {
-// TODO<jim>: Should do something to help with precision, like make it an
-// integer and divide by the value (e.g., / 1000)
-
// Max velocity to clamp the filter to in seconds/second.
typedef std::ratio<1, 1000> MaxVelocityRatio;
inline constexpr double kMaxVelocity() {
@@ -577,6 +574,11 @@
std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> p1,
monotonic_clock::time_point ta);
+ static std::chrono::nanoseconds BoundOffset(
+ std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> p0,
+ std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> p1,
+ monotonic_clock::time_point ta);
+
static std::chrono::nanoseconds ExtrapolateOffset(
std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> p0,
monotonic_clock::time_point ta);
@@ -591,11 +593,12 @@
std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> /*p1*/,
monotonic_clock::time_point ta_base, double ta);
- static std::chrono::nanoseconds ExtrapolateOffset(
+ static std::pair<std::chrono::nanoseconds, double> BoundOffset(
std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> p0,
- monotonic_clock::time_point /*ta_base*/, double /*ta*/);
+ std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> p1,
+ monotonic_clock::time_point ta_base, double ta);
- static double ExtrapolateOffsetRemainder(
+ static std::pair<std::chrono::nanoseconds, double> ExtrapolateOffset(
std::tuple<monotonic_clock::time_point, std::chrono::nanoseconds> p0,
monotonic_clock::time_point ta_base, double ta);
diff --git a/aos/network/timestamp_filter_test.cc b/aos/network/timestamp_filter_test.cc
index 5932e3d..1669e62 100644
--- a/aos/network/timestamp_filter_test.cc
+++ b/aos/network/timestamp_filter_test.cc
@@ -36,6 +36,28 @@
}
};
+void NormalizeTimestamps(monotonic_clock::time_point *ta_base, double *ta) {
+ double ta_orig = *ta;
+ chrono::nanoseconds ta_digits(static_cast<int64_t>(std::floor(*ta)));
+ *ta_base += ta_digits;
+ *ta -= static_cast<double>(ta_digits.count());
+
+ // Sign, numerical precision wins again.
+ // *ta_base=1000.300249970sec, *ta=-1.35525e-20
+ // We then promptly round this to
+ // *ta_base=1000.300249969sec, *ta=1
+ // The 1.0 then breaks the LT assumption below, so we kersplat.
+ //
+ // Detect this case directly and move the 1.0 back into ta_base.
+ if (*ta == 1.0) {
+ *ta = 0.0;
+ *ta_base += chrono::nanoseconds(1);
+ }
+
+ CHECK_GE(*ta, 0.0) << ta_digits.count() << "ns " << ta_orig;
+ CHECK_LT(*ta, 1.0);
+}
+
// Tests that adding samples tracks more negative offsets down quickly, and
// slowly comes back up.
TEST(TimestampFilterTest, Sample) {
@@ -775,11 +797,9 @@
const monotonic_clock::time_point t1 = e + chrono::nanoseconds(10000);
const chrono::nanoseconds o1 = chrono::nanoseconds(100);
- const double o1d = static_cast<double>(o1.count());
const monotonic_clock::time_point t2 = t1 + chrono::nanoseconds(1000);
const chrono::nanoseconds o2 = chrono::nanoseconds(150);
- const double o2d = static_cast<double>(o2.count());
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
std::make_tuple(t1, o1), std::make_tuple(t2, o2), t1),
@@ -796,10 +816,10 @@
o2);
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
std::make_tuple(t1, o1), std::make_tuple(t2, o2), t2, 0.0),
- o1);
+ o2);
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffsetRemainder(
std::make_tuple(t1, o1), std::make_tuple(t2, o2), t2, 0.0),
- o2d - o1d);
+ 0.0);
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
std::make_tuple(t1, o1), std::make_tuple(t2, o2),
@@ -808,56 +828,112 @@
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
std::make_tuple(t1, o1), std::make_tuple(t2, o2),
t1 + chrono::nanoseconds(500), 0.0),
- o1);
+ o1 + chrono::nanoseconds(25));
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffsetRemainder(
std::make_tuple(t1, o1), std::make_tuple(t2, o2),
t1 + chrono::nanoseconds(500), 0.0),
- 25.);
+ 0.0);
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
std::make_tuple(t1, o1), std::make_tuple(t2, o2),
t1 + chrono::nanoseconds(-200)),
chrono::nanoseconds(90));
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
- std::make_tuple(t1, o1), std::make_tuple(t2, o2), t1, -200.),
- o1);
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2),
+ t1 - chrono::nanoseconds(200), 0.0),
+ o1 - chrono::nanoseconds(10));
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffsetRemainder(
- std::make_tuple(t1, o1), std::make_tuple(t2, o2), t1, -200.),
- -10.);
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2),
+ t1 - chrono::nanoseconds(200), 0.0),
+ 0.0);
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
std::make_tuple(t1, o1), std::make_tuple(t2, o2),
t1 + chrono::nanoseconds(200)),
chrono::nanoseconds(110));
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
- std::make_tuple(t1, o1), std::make_tuple(t2, o2), t1, 200.),
- o1);
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2),
+ t1 + chrono::nanoseconds(200), 0.0),
+ o1 + chrono::nanoseconds(10));
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffsetRemainder(
- std::make_tuple(t1, o1), std::make_tuple(t2, o2), t1, 200.),
- 10.);
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2),
+ t1 + chrono::nanoseconds(200), 0.0),
+ 0.0);
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
std::make_tuple(t1, o1), std::make_tuple(t2, o2),
t1 + chrono::nanoseconds(800)),
chrono::nanoseconds(140));
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
- std::make_tuple(t1, o1), std::make_tuple(t2, o2), t1, 800.),
- o1);
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2),
+ t1 + chrono::nanoseconds(800), 0.0),
+ o1 + chrono::nanoseconds(40));
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffsetRemainder(
- std::make_tuple(t1, o1), std::make_tuple(t2, o2), t1, 800.),
- 40.);
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2),
+ t1 + chrono::nanoseconds(800), 0.0),
+ 0.0);
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
std::make_tuple(t1, o1), std::make_tuple(t2, o2),
t1 + chrono::nanoseconds(1200)),
chrono::nanoseconds(160));
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffset(
- std::make_tuple(t1, o1), std::make_tuple(t2, o2), t1, 1200.),
- o1);
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2),
+ t1 + chrono::nanoseconds(1200), 0.0),
+ o1 + chrono::nanoseconds(60));
EXPECT_EQ(NoncausalTimestampFilter::InterpolateOffsetRemainder(
- std::make_tuple(t1, o1), std::make_tuple(t2, o2), t1, 1200.),
- 60.);
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2),
+ t1 + chrono::nanoseconds(1200), 0.0),
+ 0.0);
+ for (int i = -MaxVelocityRatio::den * MaxVelocityRatio::num * 6;
+ i <
+ MaxVelocityRatio::den * MaxVelocityRatio::num * 6 + (t2 - t1).count();
+ ++i) {
+ monotonic_clock::time_point ta_base = t1;
+ const double ta_orig = static_cast<double>(i) / 3.0;
+ double ta = ta_orig;
+
+ NormalizeTimestamps(&ta_base, &ta);
+ CHECK_GE(ta, 0.0);
+ CHECK_LT(ta, 1.0);
+
+ const chrono::nanoseconds expected_offset =
+ NoncausalTimestampFilter::InterpolateOffset(
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2), ta_base);
+
+ EXPECT_EQ(expected_offset, NoncausalTimestampFilter::InterpolateOffset(
+ std::make_tuple(t1, o1),
+ std::make_tuple(t2, o2), ta_base, ta));
+
+ const double expected_double_offset =
+ static_cast<double>(o1.count()) +
+ static_cast<double>(ta_orig) / static_cast<double>((t2 - t1).count()) *
+ (o2 - o1).count();
+
+ EXPECT_NEAR(
+ static_cast<double>(
+ NoncausalTimestampFilter::InterpolateOffset(
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2), ta_base, ta)
+ .count()) +
+ NoncausalTimestampFilter::InterpolateOffsetRemainder(
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2), ta_base, ta),
+ expected_double_offset, 1e-9)
+ << ": i " << i << " t " << ta_base << " " << ta << " t1 " << t1
+ << " o1 " << o1.count() << "ns t2 " << t2 << " o2 " << o2.count()
+ << "ns Non-rounded: " << expected_offset.count() << "ns";
+ }
+}
+
+// Tests that all variants of ExtrapolateOffset do reasonable things.
+TEST_F(NoncausalTimestampFilterTest, ExtrapolateOffset) {
+ const monotonic_clock::time_point e = monotonic_clock::epoch();
+
+ const monotonic_clock::time_point t1 = e + chrono::nanoseconds(10000);
+ const chrono::nanoseconds o1 = chrono::nanoseconds(100);
+
+ const monotonic_clock::time_point t2 = t1 + chrono::nanoseconds(1000);
+ const chrono::nanoseconds o2 = chrono::nanoseconds(150);
// Test extrapolation functions before t1 and after t2
EXPECT_EQ(
NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t1, o1), t1),
@@ -889,24 +965,14 @@
// Test base + double version
EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t1, o1),
e, 0.),
- o1);
- EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffsetRemainder(
- std::make_tuple(t1, o1), e, 0.),
- -(t1 - e).count() * kMaxVelocity());
-
+ std::make_pair(chrono::nanoseconds(90), 0.0));
EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t1, o1),
t1, 0.),
- o1);
- EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffsetRemainder(
- std::make_tuple(t1, o1), t1, 0.),
- 0.);
+ std::make_pair(o1, 0.0));
EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t1, o1),
- t1, -1000.),
- o1);
- EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffsetRemainder(
- std::make_tuple(t1, o1), t1, -1000.),
- -1000. * kMaxVelocity());
+ t1, 0.5),
+ std::make_pair(o1, -0.5 * kMaxVelocity()));
EXPECT_EQ(
NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t2, o2), t2),
@@ -914,10 +980,7 @@
EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t2, o2),
t2, 0.0),
- o2);
- EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffsetRemainder(
- std::make_tuple(t2, o2), t2, 0.0),
- 0.0);
+ std::make_pair(o2, 0.0));
// Test points past our last sample
EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffset(
@@ -925,12 +988,117 @@
chrono::nanoseconds(
static_cast<int64_t>(o2.count() - 10000. * kMaxVelocity())));
- EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t2, o2),
- t2, 100.0),
+ EXPECT_EQ(
+ NoncausalTimestampFilter::ExtrapolateOffset(
+ std::make_tuple(t2, o2), t2 + chrono::nanoseconds(10000), 0.5),
+ std::make_pair(o2 - chrono::nanoseconds(10), -0.5 * kMaxVelocity()));
+
+ // Now, test that offset + remainder functions add up to the right answer for
+ // a lot of cases. This is enough to catch all the various rounding cases.
+ for (int i = -MaxVelocityRatio::den * MaxVelocityRatio::num * 6;
+ i < MaxVelocityRatio::den * MaxVelocityRatio::num * 4; ++i) {
+ monotonic_clock::time_point ta_base = t1;
+ const double ta_orig = static_cast<double>(i) / 3.0;
+ double ta = ta_orig;
+
+ NormalizeTimestamps(&ta_base, &ta);
+ CHECK_GE(ta, 0.0);
+ CHECK_LT(ta, 1.0);
+
+ const chrono::nanoseconds expected_offset =
+ NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t1, o1),
+ ta_base);
+
+
+ std::pair<chrono::nanoseconds, double> offset =
+ NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t1, o1),
+ ta_base, ta);
+
+ EXPECT_EQ(expected_offset, offset.first);
+ EXPECT_NEAR(
+ static_cast<double>(offset.first.count()) + offset.second,
+ static_cast<double>(o1.count()) - std::abs(ta_orig) * kMaxVelocity(),
+ 1e-9)
+ << ": i " << i << " t " << ta_base << " " << ta
+ << " Non-rounded: " << expected_offset.count() << "ns";
+ }
+}
+
+// Tests that all variants of BoundOffset do reasonable things.
+TEST_F(NoncausalTimestampFilterTest, BoundOffset) {
+ const monotonic_clock::time_point e = monotonic_clock::epoch();
+
+ const monotonic_clock::time_point t1 = e + chrono::nanoseconds(10000);
+ const chrono::nanoseconds o1 = chrono::nanoseconds(100);
+
+ const monotonic_clock::time_point t2 = t1 + chrono::nanoseconds(100000);
+ const chrono::nanoseconds o2 = chrono::nanoseconds(150);
+
+ EXPECT_EQ(NoncausalTimestampFilter::BoundOffset(std::make_tuple(t1, o1),
+ std::make_tuple(t2, o2), t1),
+ o1);
+ EXPECT_EQ(NoncausalTimestampFilter::BoundOffset(
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2), t1, 0.0),
+ std::pair(o1, 0.0));
+
+ EXPECT_EQ(NoncausalTimestampFilter::BoundOffset(std::make_tuple(t1, o1),
+ std::make_tuple(t2, o2), t2),
o2);
- EXPECT_EQ(NoncausalTimestampFilter::ExtrapolateOffsetRemainder(
- std::make_tuple(t2, o2), t2, 100.0),
- -100.0 * kMaxVelocity());
+ EXPECT_EQ(NoncausalTimestampFilter::BoundOffset(
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2), t2, 0.0),
+ std::pair(o2, 0.0));
+
+ // Iterate from before t1 to after t2 and confirm that the solution is right.
+ // We must always be >= than interpolation, and must also be equal to the max
+ // of extrapolating both. Since the numbers are small enough (by
+ // construction!), the double calculation will be close enough that we can
+ // trust it.
+
+ for (int i = -MaxVelocityRatio::den * MaxVelocityRatio::num * 6;
+ i <
+ MaxVelocityRatio::den * MaxVelocityRatio::num * 6 + (t2 - t1).count();
+ ++i) {
+ monotonic_clock::time_point ta_base = t1;
+ const double ta_orig = static_cast<double>(i) / 3.0;
+ double ta = ta_orig;
+
+ NormalizeTimestamps(&ta_base, &ta);
+ CHECK_GE(ta, 0.0);
+ CHECK_LT(ta, 1.0);
+
+ const chrono::nanoseconds expected_offset_1 =
+ NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t1, o1),
+ ta_base);
+ const chrono::nanoseconds expected_offset_2 =
+ NoncausalTimestampFilter::ExtrapolateOffset(std::make_tuple(t2, o2),
+ ta_base);
+
+ // Each of the extrapolation functions have their max at the points. They
+ // slope up before and down after. So, we want the max.
+ //
+ //
+ // p0 p1 |
+ // / \/ \ |
+ // / \ |
+
+ const std::pair<chrono::nanoseconds, double> offset =
+ NoncausalTimestampFilter::BoundOffset(
+ std::make_tuple(t1, o1), std::make_tuple(t2, o2), ta_base, ta);
+
+ EXPECT_EQ(std::max(expected_offset_1, expected_offset_2), offset.first);
+
+ const double expected_double_offset = std::max(
+ static_cast<double>(o1.count()) - std::abs(ta_orig) * kMaxVelocity(),
+ static_cast<double>(o2.count()) -
+ std::abs(ta_orig - (t2 - t1).count()) * kMaxVelocity());
+
+ EXPECT_NEAR(static_cast<double>(offset.first.count()) + offset.second,
+ expected_double_offset, 1e-9)
+ << ": i " << i << " t " << ta_base << " " << ta << " t1 " << t1
+ << " o1 " << o1.count() << "ns t2 " << t2 << " o2 " << o2.count()
+ << "ns Non-rounded: "
+ << std::max(expected_offset_1, expected_offset_2).count() << "ns";
+ }
}
// Tests that FindTimestamps finds timestamps in a sequence.
@@ -1114,17 +1282,17 @@
// Confirm the problem statement is reasonable... We've had enough trouble
// here in the past.
- EXPECT_TRUE(
- filter_a.ValidateSolution(&filter_b, Pointer(), t1_a, t1_a + o1_a + chrono::nanoseconds(1)));
- EXPECT_TRUE(
- filter_a.ValidateSolution(&filter_b, Pointer(), t2_a, t2_a + o2_a + chrono::nanoseconds(1)));
+ EXPECT_TRUE(filter_a.ValidateSolution(&filter_b, Pointer(), t1_a,
+ t1_a + o1_a + chrono::nanoseconds(1)));
+ EXPECT_TRUE(filter_a.ValidateSolution(&filter_b, Pointer(), t2_a,
+ t2_a + o2_a + chrono::nanoseconds(1)));
- EXPECT_TRUE(
- filter_b.ValidateSolution(&filter_a, Pointer(), t1_b, t1_b + o1_b + chrono::nanoseconds(1)));
- EXPECT_TRUE(
- filter_b.ValidateSolution(&filter_a, Pointer(), t2_b, t2_b + o2_b + chrono::nanoseconds(1)));
- EXPECT_TRUE(
- filter_b.ValidateSolution(&filter_a, Pointer(), t3_b, t3_b + o3_b + chrono::nanoseconds(1)));
+ EXPECT_TRUE(filter_b.ValidateSolution(&filter_a, Pointer(), t1_b,
+ t1_b + o1_b + chrono::nanoseconds(1)));
+ EXPECT_TRUE(filter_b.ValidateSolution(&filter_a, Pointer(), t2_b,
+ t2_b + o2_b + chrono::nanoseconds(1)));
+ EXPECT_TRUE(filter_b.ValidateSolution(&filter_a, Pointer(), t3_b,
+ t3_b + o3_b + chrono::nanoseconds(1)));
// Before the start
result = filter_a.FindTimestamps(&filter_b, Pointer(),
@@ -1186,15 +1354,12 @@
// doesn't modify the timestamps.
const BootTimestamp t1 = e + chrono::nanoseconds(1000);
const BootDuration o1{0, chrono::nanoseconds(100)};
- const double o1d = static_cast<double>(o1.duration.count());
const BootTimestamp t2 = e + chrono::microseconds(2000);
const BootDuration o2{0, chrono::nanoseconds(150)};
- const double o2d = static_cast<double>(o2.duration.count());
const BootTimestamp t3 = e + chrono::microseconds(3000);
const BootDuration o3{0, chrono::nanoseconds(50)};
- const double o3d = static_cast<double>(o3.duration.count());
const BootTimestamp t4 = e + chrono::microseconds(4000);
@@ -1211,14 +1376,18 @@
const double offset_pre = -(t1.time - e.time).count() * kMaxVelocity();
EXPECT_EQ(filter.Offset(nullptr, Pointer(), e, 0),
o1 + chrono::nanoseconds(static_cast<int64_t>(offset_pre)));
- EXPECT_EQ(filter.Offset(nullptr, Pointer(), e, 0.0, 0),
- std::make_pair(o1, offset_pre));
+ EXPECT_EQ(
+ filter.Offset(nullptr, Pointer(), e, 0.0, 0),
+ std::make_pair(o1 + chrono::nanoseconds(static_cast<int64_t>(offset_pre)),
+ 0.0));
double offset_post = -(t2.time - t1.time).count() * kMaxVelocity();
EXPECT_EQ(filter.Offset(nullptr, Pointer(), t2, 0),
o1 + chrono::nanoseconds(static_cast<int64_t>(offset_post)));
- EXPECT_EQ(filter.Offset(nullptr, Pointer(), t2, 0.0, 0),
- std::make_pair(o1, offset_post));
+ EXPECT_EQ(
+ filter.Offset(nullptr, Pointer(), t2, 0.0, 0),
+ std::make_pair(
+ o1 + chrono::nanoseconds(static_cast<int64_t>(offset_post)), 0.0));
filter.Sample(t2, o2);
filter.Sample(t3, o3);
@@ -1234,7 +1403,7 @@
filter.Offset(nullptr, Pointer(),
e + (t2.time_since_epoch() + t1.time_since_epoch()) / 2,
0.0, 0),
- std::make_pair(o1, (o2d - o1d) / 2.));
+ std::make_pair(o1 + (o2 - o1) / 2, 0.0));
EXPECT_EQ(filter.Offset(nullptr, Pointer(), t2, 0.0, 0),
std::make_pair(o2, 0.0));
@@ -1243,7 +1412,7 @@
filter.Offset(nullptr, Pointer(),
e + (t2.time_since_epoch() + t3.time_since_epoch()) / 2,
0.0, 0),
- std::make_pair(o2, (o2d + o3d) / 2. - o2d));
+ std::make_pair((o2 + o3) / 2, 0.0));
EXPECT_EQ(filter.Offset(nullptr, Pointer(), t3, 0.0, 0),
std::make_pair(o3, 0.0));
@@ -1251,14 +1420,18 @@
// Check that we still get same answer for times before our sample data...
EXPECT_EQ(filter.Offset(nullptr, Pointer(), e, 0),
o1 + chrono::nanoseconds(static_cast<int64_t>(offset_pre)));
- EXPECT_EQ(filter.Offset(nullptr, Pointer(), e, 0.0, 0),
- std::make_pair(o1, offset_pre));
+ EXPECT_EQ(
+ filter.Offset(nullptr, Pointer(), e, 0.0, 0),
+ std::make_pair(o1 + chrono::nanoseconds(static_cast<int64_t>(offset_pre)),
+ 0.0));
// ... and after
offset_post = -(t4.time - t3.time).count() * kMaxVelocity();
EXPECT_EQ(filter.Offset(nullptr, Pointer(), t4, 0),
(o3 + chrono::nanoseconds(static_cast<int64_t>(offset_post))));
- EXPECT_EQ(filter.Offset(nullptr, Pointer(), t4, 0.0, 0),
- std::make_pair(o3, offset_post));
+ EXPECT_EQ(
+ filter.Offset(nullptr, Pointer(), t4, 0.0, 0),
+ std::make_pair(
+ o3 + chrono::nanoseconds(static_cast<int64_t>(offset_post)), 0.0));
}
// Tests that adding duplicates gets correctly deduplicated.
diff --git a/aos/network/www/BUILD b/aos/network/www/BUILD
index 442e8be..03f41a5 100644
--- a/aos/network/www/BUILD
+++ b/aos/network/www/BUILD
@@ -2,6 +2,8 @@
load("//tools/build_rules:js.bzl", "rollup_bundle")
load("//aos:config.bzl", "aos_config")
+exports_files(["styles.css"])
+
filegroup(
name = "files",
srcs = glob([
diff --git a/aos/network/www/plotter.ts b/aos/network/www/plotter.ts
index 8d462f3..89b360c 100644
--- a/aos/network/www/plotter.ts
+++ b/aos/network/www/plotter.ts
@@ -105,6 +105,7 @@
private colorLocation: WebGLUniformLocation | null;
private pointSizeLocation: WebGLUniformLocation | null;
private _label: string|null = null;
+ private _hidden: boolean = false;
constructor(
private readonly ctx: WebGLRenderingContext,
private readonly program: WebGLProgram,
@@ -190,6 +191,15 @@
}
}
+ hidden(): boolean {
+ return this._hidden;
+ }
+
+ setHidden(hidden: boolean) {
+ this._hasUpdate = true;
+ this._hidden = hidden;
+ }
+
getPoints(): Point[] {
return this.points;
}
@@ -220,6 +230,10 @@
return;
}
+ if (this._hidden) {
+ return;
+ }
+
this.ctx.bindBuffer(this.ctx.ARRAY_BUFFER, this.buffer);
// Note: if this is generating errors associated with the buffer size,
// confirm that this.points really is a Float32Array.
@@ -297,92 +311,120 @@
export class Legend {
// Location, in pixels, of the legend in the text canvas.
private location: number[] = [0, 0];
- constructor(private ctx: CanvasRenderingContext2D, private lines: Line[]) {
- this.location = [80, 30];
+ constructor(
+ private plot: Plot, private lines: Line[],
+ private legend: HTMLDivElement) {
+ this.setPosition([80, 30]);
}
setPosition(location: number[]): void {
this.location = location;
+ this.legend.style.left = location[0] + 'px';
+ this.legend.style.top = location[1] + 'px';
}
draw(): void {
- this.ctx.save();
+ // First, figure out if anything has changed. The legend is created and
+ // then titles are changed afterwords, so we have to do this lazily.
+ let needsUpdate = false;
+ {
+ let child = 0;
+ for (let line of this.lines) {
+ if (line.label() === null) {
+ continue;
+ }
- this.ctx.translate(this.location[0], this.location[1]);
+ if (child >= this.legend.children.length) {
+ needsUpdate = true;
+ break;
+ }
- // Space between rows of the legend.
- const step = 20;
-
- let maxWidth = 0;
-
- // In the legend, we render both a small line of the appropriate color as
- // well as the text label--start/endPoint are the relative locations of the
- // endpoints of the miniature line within the row, and textStart is where
- // we begin rendering the text within the row.
- const startPoint = [0, 0];
- const endPoint = [10, -10];
- const textStart = endPoint[0] + 5;
-
- // Calculate how wide the legend needs to be to fit all the text.
- this.ctx.textAlign = 'left';
- let numLabels = 0;
- for (let line of this.lines) {
- if (line.label() === null) {
- continue;
+ // Make sure both have text in the right spot. Don't be too picky since
+ // nothing should really be changing here, and it's handy to let the
+ // user edit the HTML for testing.
+ if (this.legend.children[child].lastChild.textContent.length == 0 &&
+ line.label().length != 0) {
+ needsUpdate = true;
+ break;
+ }
+ child += 1;
}
- ++numLabels;
- const width =
- textStart + this.ctx.measureText(line.label()).actualBoundingBoxRight;
- maxWidth = Math.max(width, maxWidth);
- }
- if (numLabels === 0) {
- this.ctx.restore();
+ // If we got through everything, we should be pointed past the last child.
+ // If not, more children exists than lines.
+ if (child != this.legend.children.length) {
+ needsUpdate = true;
+ }
+ }
+ if (!needsUpdate) {
return;
}
- // Total height of the body of the legend.
- const height = step * numLabels;
+ // Nuke the old legend.
+ while (this.legend.firstChild) {
+ this.legend.removeChild(this.legend.firstChild);
+ }
- // Set the legend background to be white and opaque.
- this.ctx.fillStyle = 'rgba(255, 255, 255, 1.0)';
- const backgroundBuffer = 5;
- this.ctx.fillRect(
- -backgroundBuffer, 0, maxWidth + 2.0 * backgroundBuffer,
- height + backgroundBuffer);
-
- // Go through each line and render the little lines and text for each Line.
+ // Now, build up a new legend.
for (let line of this.lines) {
if (line.label() === null) {
continue;
}
- this.ctx.translate(0, step);
+
+ // The legend is a div containing both a canvas for the style/color, and a
+ // div for the text. Make those, color in the canvas, and add it to the
+ // page.
+ let l = document.createElement('div');
+ l.classList.add('aos_legend_line');
+ let text = document.createElement('div');
+ text.textContent = line.label();
+
+ l.appendChild(text);
+ this.legend.appendChild(l);
+
+ let c = document.createElement('canvas');
+ c.width = text.offsetHeight;
+ c.height = text.offsetHeight;
+
+ const linestyleContext = c.getContext("2d");
+ linestyleContext.clearRect(0, 0, c.width, c.height);
+
const color = line.color();
- this.ctx.strokeStyle = `rgb(${255.0 * color[0]}, ${255.0 * color[1]}, ${255.0 * color[2]})`;
- this.ctx.fillStyle = this.ctx.strokeStyle;
- if (line.drawLine()) {
- this.ctx.beginPath();
- this.ctx.moveTo(startPoint[0], startPoint[1]);
- this.ctx.lineTo(endPoint[0], endPoint[1]);
- this.ctx.closePath();
- this.ctx.stroke();
- }
+ linestyleContext.strokeStyle = `rgb(${255.0 * color[0]}, ${
+ 255.0 * color[1]}, ${255.0 * color[2]})`;
+ linestyleContext.fillStyle = linestyleContext.strokeStyle;
+
const pointSize = line.pointSize();
- if (pointSize > 0) {
- this.ctx.fillRect(
- startPoint[0] - pointSize / 2.0, startPoint[1] - pointSize / 2.0,
- pointSize, pointSize);
- this.ctx.fillRect(
- endPoint[0] - pointSize / 2.0, endPoint[1] - pointSize / 2.0,
- pointSize, pointSize);
+ const kDistanceIn = pointSize / 2.0;
+
+ if (line.drawLine()) {
+ linestyleContext.beginPath();
+ linestyleContext.moveTo(0, 0);
+ linestyleContext.lineTo(c.height, c.width);
+ linestyleContext.closePath();
+ linestyleContext.stroke();
}
- this.ctx.fillStyle = 'black';
- this.ctx.textAlign = 'left';
- this.ctx.fillText(line.label(), textStart, 0);
- }
+ if (pointSize > 0) {
+ linestyleContext.fillRect(0, 0, pointSize, pointSize);
+ linestyleContext.fillRect(
+ c.height - 1 - pointSize, c.width - 1 - pointSize, pointSize,
+ pointSize);
+ }
- this.ctx.restore();
+ c.addEventListener('click', (e) => {
+ if (!line.hidden()) {
+ l.classList.add('aos_legend_line_hidden');
+ } else {
+ l.classList.remove('aos_legend_line_hidden');
+ }
+
+ line.setHidden(!line.hidden());
+ this.plot.draw();
+ });
+
+ l.prepend(c);
+ }
}
}
@@ -440,7 +482,7 @@
return divideVec(subtractVec(canvasPos, this.zoom.offset), this.zoom.scale);
}
- // Tehse return the max/min rendered points, in plot-space (this is helpful
+ // These return the max/min rendered points, in plot-space (this is helpful
// for drawing axis labels).
maxVisiblePoint(): number[] {
return this.canvasToPlotCoordinates([1.0, 1.0]);
@@ -850,6 +892,7 @@
export class Plot {
private canvas = document.createElement('canvas');
private textCanvas = document.createElement('canvas');
+ private legendDiv = document.createElement('div');
private lineDrawerContext: WebGLRenderingContext;
private drawer: LineDrawer;
private static keysPressed:
@@ -873,24 +916,20 @@
constructor(wrapperDiv: HTMLDivElement) {
wrapperDiv.appendChild(this.canvas);
wrapperDiv.appendChild(this.textCanvas);
+ this.legendDiv.classList.add('aos_legend');
+ wrapperDiv.appendChild(this.legendDiv);
this.lastTimeMs = (new Date()).getTime();
this.canvas.style.paddingLeft = this.axisLabelBuffer.left.toString() + "px";
this.canvas.style.paddingRight = this.axisLabelBuffer.right.toString() + "px";
this.canvas.style.paddingTop = this.axisLabelBuffer.top.toString() + "px";
this.canvas.style.paddingBottom = this.axisLabelBuffer.bottom.toString() + "px";
- this.canvas.style.width = "100%";
- this.canvas.style.height = "100%";
- this.canvas.style.boxSizing = "border-box";
+ this.canvas.classList.add('aos_plot');
- this.canvas.style.position = 'absolute';
this.lineDrawerContext = this.canvas.getContext('webgl');
this.drawer = new LineDrawer(this.lineDrawerContext);
- this.textCanvas.style.position = 'absolute';
- this.textCanvas.style.width = "100%";
- this.textCanvas.style.height = "100%";
- this.textCanvas.style.pointerEvents = 'none';
+ this.textCanvas.classList.add('aos_plot_text');
this.canvas.addEventListener('dblclick', (e) => {
this.handleDoubleClick(e);
@@ -922,7 +961,7 @@
const textCtx = this.textCanvas.getContext("2d");
this.axisLabels =
new AxisLabels(textCtx, this.drawer, this.axisLabelBuffer);
- this.legend = new Legend(textCtx, this.drawer.getLines());
+ this.legend = new Legend(this, this.drawer.getLines(), this.legendDiv);
this.zoomRectangle = this.getDrawer().addLine(false);
this.zoomRectangle.setColor(Colors.WHITE);
diff --git a/aos/network/www/styles.css b/aos/network/www/styles.css
index 23ceb21..547ec94 100644
--- a/aos/network/www/styles.css
+++ b/aos/network/www/styles.css
@@ -3,3 +3,59 @@
border-bottom: 1px solid;
font-size: 24px;
}
+
+.aos_plot {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ box-sizing: border-box;
+}
+
+.aos_plot_text {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+}
+
+.aos_legend {
+ position: absolute;
+ z-index: 1;
+ pointer-events: none;
+}
+
+.aos_legend_line {
+ background: white;
+ padding: 2px;
+ border-radius: 2px;
+ margin-top: 3px;
+ margin-bottom: 3px;
+ font-size: 12;
+}
+
+.aos_legend_line>div {
+ display: inline-block;
+ vertical-align: middle;
+ margin-left: 5px;
+}
+.aos_legend_line>canvas {
+ vertical-align: middle;
+ pointer-events: all;
+}
+
+.aos_legend_line_hidden {
+ filter: contrast(0.75);
+}
+
+.aos_cpp_plot {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ align-items: flex-start;
+}
+
+.aos_cpp_plot>div {
+ flex: 1;
+ width: 100%;
+}
diff --git a/frc971/analysis/BUILD b/frc971/analysis/BUILD
index 2d71410..abb950b 100644
--- a/frc971/analysis/BUILD
+++ b/frc971/analysis/BUILD
@@ -70,11 +70,21 @@
],
)
+genrule(
+ name = "copy_css",
+ srcs = [
+ "//aos/network/www:styles.css",
+ ],
+ outs = ["styles.css"],
+ cmd = "cp $< $@",
+)
+
filegroup(
name = "plotter_files",
srcs = [
"index.html",
"plot_index_bundle.min.js",
+ "styles.css",
],
)
diff --git a/frc971/analysis/cpp_plot/BUILD b/frc971/analysis/cpp_plot/BUILD
index 156594c..d6e74c3 100644
--- a/frc971/analysis/cpp_plot/BUILD
+++ b/frc971/analysis/cpp_plot/BUILD
@@ -23,10 +23,21 @@
],
)
+genrule(
+ name = "copy_css",
+ srcs = [
+ "//aos/network/www:styles.css",
+ ],
+ outs = ["styles.css"],
+ cmd = "cp $< $@",
+)
+
filegroup(
name = "cpp_plot_files",
srcs = [
+ "cpp_plot_bundle.js",
"cpp_plot_bundle.min.js",
"index.html",
+ "styles.css",
],
)
diff --git a/frc971/analysis/cpp_plot/cpp_plot.ts b/frc971/analysis/cpp_plot/cpp_plot.ts
index ffbd485..a704e89 100644
--- a/frc971/analysis/cpp_plot/cpp_plot.ts
+++ b/frc971/analysis/cpp_plot/cpp_plot.ts
@@ -4,7 +4,7 @@
import {plotData} from 'org_frc971/frc971/analysis/plot_data_utils';
const rootDiv = document.createElement('div');
-rootDiv.style.width = '100%';
+rootDiv.classList.add('aos_cpp_plot');
document.body.appendChild(rootDiv);
const conn = new Connection();
diff --git a/frc971/analysis/cpp_plot/index.html b/frc971/analysis/cpp_plot/index.html
index 776c103..fbb5199 100644
--- a/frc971/analysis/cpp_plot/index.html
+++ b/frc971/analysis/cpp_plot/index.html
@@ -1,6 +1,7 @@
<html>
<head>
<script src="cpp_plot_bundle.min.js" defer></script>
+ <link rel="stylesheet" href="styles.css">
</head>
<body>
</body>
diff --git a/frc971/analysis/in_process_plotter.cc b/frc971/analysis/in_process_plotter.cc
index 585c933..5f08e5d 100644
--- a/frc971/analysis/in_process_plotter.cc
+++ b/frc971/analysis/in_process_plotter.cc
@@ -19,15 +19,26 @@
builder_(plot_sender_.MakeBuilder()) {
web_proxy_.SetDataPath(kDataPath);
event_loop_->SkipTimingReport();
- color_wheel_.push_back(Color(1, 0, 0));
- color_wheel_.push_back(Color(0, 1, 0));
- color_wheel_.push_back(Color(0, 0, 1));
- color_wheel_.push_back(Color(1, 1, 0));
- color_wheel_.push_back(Color(0, 1, 1));
- color_wheel_.push_back(Color(1, 0, 1));
- color_wheel_.push_back(Color(1, 0.6, 0));
- color_wheel_.push_back(Color(0.6, 0.3, 0));
- color_wheel_.push_back(Color(1, 1, 1));
+
+ color_wheel_.emplace_back(ColorWheelColor{.name = "red", .color = {1, 0, 0}});
+ color_wheel_.emplace_back(
+ ColorWheelColor{.name = "green", .color = {0, 1, 0}});
+ color_wheel_.emplace_back(
+ ColorWheelColor{.name = "purple", .color = {0.54, 0.3, 0.75}});
+ color_wheel_.emplace_back(
+ ColorWheelColor{.name = "blue", .color = {0, 0, 1}});
+ color_wheel_.emplace_back(
+ ColorWheelColor{.name = "yellow", .color = {1, 1, 0}});
+ color_wheel_.emplace_back(
+ ColorWheelColor{.name = "teal", .color = {0, 1, 1}});
+ color_wheel_.emplace_back(
+ ColorWheelColor{.name = "pink", .color = {1, 0, 1}});
+ color_wheel_.emplace_back(
+ ColorWheelColor{.name = "orange", .color = {1, 0.6, 0}});
+ color_wheel_.emplace_back(
+ ColorWheelColor{.name = "brown", .color = {0.6, 0.3, 0}});
+ color_wheel_.emplace_back(
+ ColorWheelColor{.name = "white", .color = {1, 1, 1}});
}
void Plotter::Spin() { event_loop_factory_.Run(); }
@@ -63,15 +74,14 @@
}
void Plotter::AddLine(const std::vector<double> &x,
- const std::vector<double> &y, std::string_view label,
- std::string_view line_style) {
+ const std::vector<double> &y, LineOptions options) {
CHECK_EQ(x.size(), y.size());
CHECK(!position_.IsNull())
<< "You must call AddFigure() before calling AddLine().";
flatbuffers::Offset<flatbuffers::String> label_offset;
- if (!label.empty()) {
- label_offset = builder_.fbb()->CreateString(label);
+ if (!options.label.empty()) {
+ label_offset = builder_.fbb()->CreateString(options.label);
}
std::vector<Point> points;
@@ -81,16 +91,28 @@
const flatbuffers::Offset<flatbuffers::Vector<const Point *>> points_offset =
builder_.fbb()->CreateVectorOfStructs(points);
- const Color *color = &color_wheel_.at(color_wheel_position_);
- color_wheel_position_ = (color_wheel_position_ + 1) % color_wheel_.size();
+ const Color *color;
+ if (options.color.empty()) {
+ color = &color_wheel_.at(color_wheel_position_).color;
+ color_wheel_position_ = (color_wheel_position_ + 1) % color_wheel_.size();
+ } else {
+ auto it = std::find_if(
+ color_wheel_.begin(), color_wheel_.end(),
+ [options_color = options.color](const ColorWheelColor &color) {
+ return color.name == options_color;
+ });
+ CHECK(it != color_wheel_.end()) << ": Failed to find " << options.color;
+ color = &(it->color);
+ }
LineStyle::Builder style_builder = builder_.MakeBuilder<LineStyle>();
- if (line_style.find('*') != line_style.npos) {
+ if (options.line_style.find('*') != options.line_style.npos) {
style_builder.add_point_size(3.0);
} else {
style_builder.add_point_size(0.0);
}
- style_builder.add_draw_line(line_style.find('-') != line_style.npos);
+ style_builder.add_draw_line(options.line_style.find('-') !=
+ options.line_style.npos);
const flatbuffers::Offset<LineStyle> style_offset = style_builder.Finish();
auto line_builder = builder_.MakeBuilder<Line>();
@@ -132,8 +154,7 @@
plot_builder.add_title(title_);
plot_builder.add_figures(figures_offset);
- CHECK_EQ(builder_.Send(plot_builder.Finish()),
- aos::RawSender::Error::kOk);
+ CHECK_EQ(builder_.Send(plot_builder.Finish()), aos::RawSender::Error::kOk);
builder_ = plot_sender_.MakeBuilder();
diff --git a/frc971/analysis/in_process_plotter.h b/frc971/analysis/in_process_plotter.h
index 8d4c84c..b78a8fb 100644
--- a/frc971/analysis/in_process_plotter.h
+++ b/frc971/analysis/in_process_plotter.h
@@ -42,14 +42,30 @@
void Title(std::string_view title);
void AddFigure(std::string_view title = "", double width = 0,
double height = 0);
+ struct LineOptions {
+ std::string_view label = "";
+ std::string_view line_style = "*-";
+ std::string_view color = "";
+ };
+
void AddLine(const std::vector<double> &x, const std::vector<double> &y,
- std::string_view label = "", std::string_view line_style = "*-");
+ std::string_view label) {
+ AddLine(x, y, LineOptions{.label = label});
+ }
+ void AddLine(const std::vector<double> &x, const std::vector<double> &y,
+ std::string_view label, std::string_view line_style) {
+ AddLine(x, y, LineOptions{.label = label, .line_style = line_style});
+ }
+ void AddLine(const std::vector<double> &x, const std::vector<double> &y,
+ LineOptions options);
+
void ShareXAxis(bool share) { share_x_axis_ = share; }
void XLabel(std::string_view label);
void YLabel(std::string_view label);
void Publish();
void Spin();
+
private:
void MaybeFinishFigure();
@@ -70,8 +86,13 @@
std::vector<flatbuffers::Offset<Figure>> figures_;
std::vector<flatbuffers::Offset<Line>> lines_;
+ struct ColorWheelColor {
+ std::string name;
+ Color color;
+ };
+
size_t color_wheel_position_ = 0;
- std::vector<Color> color_wheel_;
+ std::vector<ColorWheelColor> color_wheel_;
};
} // namespace analysis
diff --git a/frc971/analysis/index.html b/frc971/analysis/index.html
index edd2483..22b42c7 100644
--- a/frc971/analysis/index.html
+++ b/frc971/analysis/index.html
@@ -1,6 +1,7 @@
<html>
<head>
<script src="plot_index_bundle.min.js" defer></script>
+ <link rel="stylesheet" href="styles.css">
</head>
<body>
</body>
diff --git a/frc971/analysis/plot_data_utils.ts b/frc971/analysis/plot_data_utils.ts
index f8debe7..1362a09 100644
--- a/frc971/analysis/plot_data_utils.ts
+++ b/frc971/analysis/plot_data_utils.ts
@@ -48,10 +48,7 @@
figureDiv.style.width = figure.position().width().toString() + 'px';
}
if (figure.position().height() == 0) {
- // TODO(austin): I don't know the css for 100%, excluding other
- // stuff in the div... Just go with a little less for now, it's
- // good enough and quite helpful.
- figureDiv.style.height = '97%';
+ figureDiv.style.height = '100%';
} else {
figureDiv.style.height =
figure.position().height().toString() + 'px';
diff --git a/frc971/analysis/plotter_config.json b/frc971/analysis/plotter_config.json
index 49266ee..fef4a50 100644
--- a/frc971/analysis/plotter_config.json
+++ b/frc971/analysis/plotter_config.json
@@ -3,7 +3,7 @@
{
"name": "/analysis",
"type": "frc971.analysis.Plot",
- "max_size": 10000000
+ "max_size": 1000000000
}
],
"imports": [