esp32displaytest/src/graphs.cpp
2022-03-12 12:33:52 +01:00

781 lines
20 KiB
C++

//#include "Commander.h"
#include "fptrig.h"
#include <Arduino.h>
#include <WiFi.h>
#include <Adafruit_SSD1351.h>
#include <SPI.h>
#include <vector>
#include "screen-settings.h"
#include "colors.h"
#include "FixedPoint.h"
typedef FixedPoint<1000000> Decimal;
constexpr Decimal operator "" _d(long double v) {
return v;
}
constexpr Decimal operator "" _d(unsigned long long int v) {
return v;
}
static_assert(1_d == 1.0_d, "1 == 1.0");
static_assert(1_d == 1_d, "1 == 1");
static_assert(1_d + 1_d == 2_d, "1 + 1 == 2");
static_assert(2_d * 3_d == 6_d, "2 * 3 == 6");
static_assert(1.25_d * 2.3_d == 2.875_d, "1.25 * 2.3 == 2.875");
static_assert(.9_d / 3_d == .3_d, ".9 / 3 == .3");
SPIClass hspi(HSPI);
Adafruit_SSD1351 screen(SCREEN_WIDTH, SCREEN_HEIGHT, &hspi, CS_PIN, DC_PIN, RST_PIN);
//Commander commander;
struct Point {
Decimal x;
Decimal y;
};
enum InterpolationMethod {
NEAREST_NEIGHBOR,
LINEAR,
COSINE,
CUBIC_HERMITE,
MONOTONIC_CUBIC_HERMITE,
};
enum Visualisation {
LINE_DRAW, // todo: this is just for debugging
BAR,
LINE,
AREA,
AREA_WITH_LINE,
};
struct CubicCurve {
Decimal a;
Decimal b;
Decimal c;
Decimal d;
Decimal GetValueAt(Decimal x) const {
return a * x * x * x
+ b * x * x
+ c * x
+ d;
}
Decimal GetSlopeAt(Decimal x) const {
return 3_d * a * x * x
+ 2_d * b * x
+ c;
}
bool const IsEmpty() const {
return a == 0 && b == 0 && c == 0 && d == 0;
}
static constexpr CubicCurve make(Decimal x1, Decimal y1, Decimal m1, Decimal x2, Decimal y2, Decimal m2) {
Decimal a = (m1 + m2 - 2 * (y2 - y1) / (x2 - x1)) / ((x1 - x2) * (x1 - x2));
Decimal b = (m2 - m1) / (2 * (x2 - x1)) - 3 / 2 * (x1 + x2) * a;
Decimal c = m1 - 3 * x1 * x1 * a - 2 * x1 * b;
Decimal d = y1 - x1 * x1 * x1 * a - x1 * x1 * b - x1 * c;
return CubicCurve{a, b, c, d};
}
static constexpr CubicCurve makeLeftEnd(Decimal x1, Decimal y1, Decimal x2, Decimal y2, Decimal m2) {
// set second derivative to 0 at first point
// todo: actually implement this
/*
* f(x) = ax^3 + bx^2 + cx + d
* f'(x) = 3ax^2 + 2bx + c
* f''(x) = 6ax + 2b
*
y1 = a*x1^3 + b*x1^2 + c*x1 + d
y2 = a*x2^3 + b*x2^2 + c*x2 + d
m2 = 3*a*x2^2 + 2*b*x2 + c
0 = 6*a*x1 + 2*b
*
*
*
*
*
*/
return make(x1, y1, 0, x2, y2, m2);
}
static constexpr CubicCurve makeRightEnd(Decimal x1, Decimal y1, Decimal m1, Decimal x2, Decimal y2) {
// set second derivative to 0 at second point
// todo: actually implement this
return make(x1, y1, m1, x2, y2, 0);
}
};
struct Series {
RGB color;
String label;
Visualisation visualisation;
InterpolationMethod interpolation; // does not apply to "bar" visualisation
// point x and y values are scaled with 1000
std::vector<Point> points;
mutable CubicCurve cached_cubic_curve;
mutable Decimal cached_cubic_curve_from;
mutable Decimal cached_cubic_curve_to;
std::tuple<Decimal, bool> Interpolate(Decimal at) const {
if (points.size() < 2) {
return std::make_tuple(0, false);
}
if (at < points.front().x || at > points.back().x) {
return std::make_tuple(0, false);
}
switch (interpolation) {
case NEAREST_NEIGHBOR: return InterpolateNearestNeighbour(at);
case LINEAR: return InterpolateLinear(at);
case COSINE: return InterpolateCosine(at);
case CUBIC_HERMITE: return InterpolateCubicHermite(at);
case MONOTONIC_CUBIC_HERMITE: return InterpolateMonotonicCubicHermite(at);
}
return std::make_tuple(0, false);
}
std::tuple<Decimal, bool> InterpolateNearestNeighbour(Decimal at) const {
assert(points.size() >= 2);
if (at == points.front().x) {
return std::make_tuple(points.front().y, true);
}
for (auto current = points.begin() + 1; current < points.end(); current++) {
if (at == current->x) {
return std::make_tuple(current->y, true);
}
auto prev = current - 1;
if (at > prev->x && at < current->x) {
if (at - prev->x < current->x - at) {
return std::make_tuple(prev->y, true);
} else {
return std::make_tuple(current->y, true);
}
}
}
assert(false);
}
std::tuple<Decimal, bool> InterpolateLinear(Decimal at) const {
assert(points.size() >= 2);
if (at == points.front().x) {
return std::make_tuple(points.front().y, true);
}
for (auto current = points.begin() + 1; current < points.end(); current++) {
if (at == current->x) {
return std::make_tuple(current->y, true);
}
auto prev = current - 1;
if (at > prev->x && at < current->x) {
Decimal dist = current->x - prev->x;
Decimal dist_val = at - prev->x;
Decimal val = (current->y * dist_val + prev->y * (dist - dist_val)) / dist;
return std::make_tuple(val, true);
}
}
assert(false);
}
std::tuple<Decimal, bool> InterpolateCosine(Decimal at) const {
assert(points.size() >= 2);
if (at == points.front().x) {
return std::make_tuple(points.front().y, true);
}
for (auto current = points.begin() + 1; current < points.end(); current++) {
if (at == current->x) {
return std::make_tuple(current->y, true);
}
auto prev = current - 1;
if (at > prev->x && at < current->x) {
Decimal dist = current->x - prev->x;
Decimal dist_val = at - prev->x;
Decimal mu =
0; // todo: rework this to work with the FixedPoint class (dist - fpcos((1 << 14) * dist_val / dist) * dist / 8192 / 2);
if (mu > dist) {
Serial.print("mu = ");
Serial.println(mu.toString().c_str());
Serial.print("dist = ");
Serial.println(dist.toString().c_str());
for (;;);
}
assert(mu <= dist);
Decimal val = (prev->y * (dist - mu) + current->y * mu) / dist;
return std::make_tuple(val, true);
}
}
assert(false);
}
CubicCurve &GetCubicCurve(Decimal at, bool monotonic) const {
if (!cached_cubic_curve.IsEmpty()) {
if (at >= cached_cubic_curve_from && at <= cached_cubic_curve_to) {
return cached_cubic_curve;
}
}
for (auto p2 = points.begin() + 1; p2 < points.end(); p2++) {
auto p1 = p2 - 1;
if (at >= p1->x && at <= p2->x) {
cached_cubic_curve_from = p1->x;
cached_cubic_curve_to = p2->x;
Decimal m_after_p1 = (p2->y - p1->y) / (p2->x - p1->x);
Decimal m_before_p2 = (p2->y - p1->y) / (p2->x - p1->x);
Decimal m_before_p1, m_after_p2, m1, m2;
auto p0 = p1 - 1;
auto p3 = p2 + 1;
bool is_beginning = p0 < points.begin();
bool is_end = p3 >= points.end();
if (!is_beginning) {
m_before_p1 = (p1->y - p0->y) / (p1->x - p0->x);
if (monotonic &&
(p0->y == p1->y
|| p1->y == p2->y
|| (p0->y < p1->y && p2->y < p1->y)
|| (p0->y > p1->y && p2->y > p1->y)
)) {
m1 = 0;
} else {
m1 = (m_before_p1 + m_after_p1) / 2; // todo: use a mean that is weighted towards 0?
}
}
if (!is_end) {
m_after_p2 = (p3->y - p2->y) / (p3->x - p2->x);
if (monotonic &&
(p1->y == p2->y
|| p2->y == p3->y
|| (p1->y < p2->y && p3->y < p2->y)
|| (p1->y > p2->y && p3->y > p2->y)
)) {
m1 = 0;
} else {
m2 = (m_before_p2 + m_after_p2) / 2; // todo: use a mean that is weighted towards 0?
}
}
if (is_beginning) {
cached_cubic_curve = CubicCurve::makeLeftEnd(p1->x, p1->y, p2->x, p2->y, m2);
} else if (is_end) {
cached_cubic_curve = CubicCurve::makeRightEnd(p1->x, p1->y, m1, p2->x, p2->y);
} else {
cached_cubic_curve = CubicCurve::make(p1->x, p1->y, m1, p2->x, p2->y, m2);
}
return cached_cubic_curve;
}
}
assert(false);
}
std::tuple<Decimal, bool> InterpolateCubicHermite(Decimal at) const {
assert(points.size() >= 2);
if (points.size() == 2) {
return InterpolateLinear(at);
}
if (at < points.front().x || at > points.back().x) {
return std::make_tuple(0, false);
}
CubicCurve curve = GetCubicCurve(at, false);
return std::make_tuple(curve.GetValueAt(at), true);
}
std::tuple<Decimal, bool> InterpolateMonotonicCubicHermite(Decimal at) const {
assert(points.size() >= 2);
if (points.size() == 2) {
return InterpolateLinear(at);
}
if (at < points.front().x || at > points.back().x) {
return std::make_tuple(0, false);
}
CubicCurve curve = GetCubicCurve(at, true);
return std::make_tuple(curve.GetValueAt(at), true);
}
};
#define PADDING_BETWEEN_NUMBERS_AND_GRAPH 5
#define PADDING_BETWEEN_NUMBERS 5
[[gnu::pure]] Decimal find_step(Decimal step) {
int exp = 0;
while (step < 1) {
step *= 10;
exp--;
}
while (step >= 10) {
step /= 10;
exp++;
}
if (step > 5) {
step = 10;
} else if (step > 2.5) {
step = 5;
} else if (step > 2) {
step = 2.5;
} else if (step > 1) {
step = 2;
} else {
step = 1;
}
return step.exp10(exp);
}
struct Chart {
RGB background_color;
RGB foreground_color;
bool show_axes;
bool show_labels;
bool show_numbers;
bool auto_range_ordinate;
bool auto_range_abscissa;
Decimal ordinate_from;
Decimal ordinate_to;
Decimal abscissa_from;
Decimal abscissa_to;
std::vector<Series> series;
std::tuple<Decimal, Decimal> GetAbscissaRange() const {
Decimal from = abscissa_from;
Decimal to = abscissa_to;
if (auto_range_abscissa) {
from = INT32_MAX;
to = INT32_MIN;
for (const auto &s : series) {
for (const auto &p : s.points) {
if (p.x < from) {
from = p.x;
}
if (p.x > to) {
to = p.x;
}
}
}
}
return std::make_tuple(from, to);
}
std::tuple<Decimal, Decimal> GetOrdinateRange() const {
Decimal from = ordinate_from;
Decimal to = ordinate_to;
if (auto_range_ordinate) {
from = INT32_MAX;
to = INT32_MIN;
for (const auto &s : series) {
for (const auto &p : s.points) {
if (p.y < from) {
from = p.y;
}
if (p.y > to) {
to = p.y;
}
}
}
}
return std::make_tuple(from, to);
}
void Draw(Adafruit_GFX &target,
int16_t rect_x, int16_t rect_y, uint16_t rect_w, uint16_t rect_h) {
target.fillRect(rect_x, rect_y, rect_w, rect_h, background_color.to_565());
target.setTextSize(1);
int16_t canvas_x = rect_x;
int16_t canvas_y = rect_y;
uint16_t canvas_width = rect_w;
uint16_t canvas_height = rect_h;
Decimal ord_min, ord_max, abs_min, abs_max;
std::tie(ord_min, ord_max) = GetOrdinateRange();
std::tie(abs_min, abs_max) = GetAbscissaRange();
if (show_labels) {
// todo: draw labels, change canvas size accordingly
}
if (show_numbers) {
DrawNumbers(target, canvas_x, canvas_y, canvas_width, canvas_height);
}
DrawGraph(target, canvas_x, canvas_y, canvas_width, canvas_height);
if (show_axes) {
int16_t ord_pos = val_to_screen(0, canvas_x, canvas_width, abs_min, abs_max, false);
target.drawLine(ord_pos, canvas_y, ord_pos, canvas_y + canvas_height, foreground_color.to_565());
int16_t abs_pos = val_to_screen(0, canvas_y, canvas_height, ord_min, ord_max, true);
target.drawLine(canvas_x, abs_pos, canvas_x + canvas_width, abs_pos, foreground_color.to_565());
}
}
void DrawGraph(Adafruit_GFX &target,
int16_t &canvas_x,
int16_t &canvas_y,
uint16_t &canvas_width,
uint16_t &canvas_height) {
Decimal ord_min, ord_max, abs_min, abs_max;
std::tie(ord_min, ord_max) = GetOrdinateRange();
std::tie(abs_min, abs_max) = GetAbscissaRange();
std::vector<Series *> normal_series;
for (auto &s : series) {
if (s.visualisation == Visualisation::LINE_DRAW || s.visualisation == Visualisation::BAR) {
continue;
}
normal_series.push_back(&s);
}
enum PixelType {
NONE,
LINE,
AREA,
};
std::vector<std::vector<std::tuple<PixelType, const Series *>>> pixels(canvas_height);
assert(pixels.size() == canvas_height);
std::vector<std::tuple<int16_t, const Series *>> values;
if (!normal_series.empty()) {
for (uint16_t x = 0; x < canvas_width; x++) {
Decimal abs = (abs_min * (canvas_width - 1 - x) + abs_max * x) / (canvas_width - 1);
values.clear();
for (auto &p : pixels) {
p.clear();
}
for (auto s : normal_series) {
Decimal val;
bool success;
std::tie(val, success) = s->Interpolate(abs);
if (!success) continue;
int16_t y = static_cast<int16_t>(((val - ord_min) * (canvas_height - 1) / (ord_max - ord_min)).toInt());
values.emplace_back(y, s);
}
for (const auto &item : values) {
// todo: combine this loop with the previous
int16_t y;
const Series *s;
std::tie(y, s) = item;
assert(y >= 0 && y < canvas_height);
if (s->visualisation == Visualisation::LINE || s->visualisation == Visualisation::AREA_WITH_LINE) {
pixels[y].emplace_back(PixelType::LINE, s);
// todo: this leaves gaps between line segments
}
if (s->visualisation == Visualisation::AREA || s->visualisation == Visualisation::AREA_WITH_LINE) {
for (size_t i = 0; i <= y; ++i) {
pixels[i].emplace_back(PixelType::AREA, s);
}
}
}
std::vector<RGB> line_pixels;
std::vector<RGB> area_pixels;
const Series *s;
PixelType t;
for (int16_t y = 0; y < canvas_height; ++y) {
line_pixels.clear();
area_pixels.clear();
auto pixel = pixels[y];
for (const auto &item : pixel) {
std::tie(t, s) = item;
if (t == PixelType::LINE) {
line_pixels.push_back(s->color);
} else if (t == PixelType::AREA) {
area_pixels.push_back(s->color);
}
}
int16_t screen_x = canvas_x + x;
int16_t screen_y = canvas_y + canvas_height - y;
if (!line_pixels.empty()) {
// draw line pixel
RGB color = RGB::blend(line_pixels);
target.drawPixel(screen_x, screen_y, color.to_565());
} else if (!area_pixels.empty()) {
// draw area pixel
RGB color = RGB::blend(area_pixels);
uint16_t c2 = color.alpha_blend_over_to565(background_color, 100); // TODO: make configurable
target.drawPixel(screen_x, screen_y, c2);
} else {
// draw background pixel
target.drawPixel(screen_x, screen_y, background_color.to_565());
}
}
}
}
for (const auto &s : series) {
if (s.visualisation == Visualisation::LINE_DRAW) {
const Point *last = nullptr;
for (const auto &p : s.points) {
if (last != nullptr) {
int16_t tox = val_to_screen(p.x, canvas_x, canvas_width, abs_min, abs_max, false);
int16_t toy = val_to_screen(p.y, canvas_y, canvas_height, ord_min, ord_max, true);
int16_t fromx = val_to_screen(last->x, canvas_x, canvas_width, abs_min, abs_max, false);
int16_t fromy = val_to_screen(last->y, canvas_y, canvas_height, ord_min, ord_max, true);
target.drawLine(fromx, fromy, tox, toy, s.color.to_565());
}
last = &p;
}
} else if (s.visualisation == Visualisation::BAR) {
// todo: draw bar series
}
}
/*for (const auto &s : series) {
if (s.visualisation != Visualisation::BAR) {
for (const auto &p : s.points) {
int16_t x = val_to_screen(p.x, canvas_x, canvas_width, abs_min, abs_max, false);
int16_t y = val_to_screen(p.y, canvas_y, canvas_height, ord_min, ord_max, true);
target.drawCircle(x, y, 1, s.color.to_565());
}
}
}*/
}
void DrawNumbers(Adafruit_GFX &target,
int16_t &canvas_x,
int16_t &canvas_y,
uint16_t &canvas_width,
uint16_t &canvas_height) const {
Decimal ord_min, ord_max, abs_min, abs_max;
std::tie(ord_min, ord_max) = GetOrdinateRange();
std::tie(abs_min, abs_max) = GetAbscissaRange();
int16_t dummy_x, dummy_y;
uint16_t text_width, text_height;
target.getTextBounds("0", 0, 0, &dummy_x, &dummy_y, &text_width, &text_height);
uint16_t text_y_offset = (text_height + 1) / 2; // rounded-up half height of text
canvas_height -= text_height + PADDING_BETWEEN_NUMBERS_AND_GRAPH;
canvas_height -= 2 * text_y_offset;
canvas_y += text_y_offset;
Decimal ord_max_num = ord_max;
Decimal ord_min_num = ord_min;
uint8_t max_num_ord_numbers = canvas_height / (text_height + PADDING_BETWEEN_NUMBERS);
Decimal ord_num_step = (ord_max_num - ord_min_num) / max_num_ord_numbers;
ord_num_step = find_step(ord_num_step);
if (ord_max_num > 0) {
ord_max_num = (ord_max_num / ord_num_step) * ord_num_step;
} else {
ord_max_num = ((ord_max_num - ord_num_step) / ord_num_step) * ord_num_step;
}
if (ord_min_num > 0) {
ord_min_num = ((ord_min_num + ord_num_step) / ord_num_step) * ord_num_step;
} else {
ord_min_num = (ord_min_num / ord_num_step) * ord_num_step;
}
String str;
// value text string width
std::vector<std::tuple<Decimal, String, uint16_t>> ord_steps;
uint16_t max_width = 0;
for (Decimal step = ord_min_num; step <= ord_max_num; step += ord_num_step) {
str =
step.toString(2, ord_min_num.abs() < 1000 && ord_max_num.abs() < 1000); // todo: use digits of ord_num_step
target.getTextBounds(str, 0, 0, &dummy_x, &dummy_y, &text_width, &text_height);
ord_steps.emplace_back(step, str, text_width);
max_width = std::max(max_width, text_width);
}
canvas_x += max_width + PADDING_BETWEEN_NUMBERS_AND_GRAPH;
canvas_width -= max_width + PADDING_BETWEEN_NUMBERS_AND_GRAPH;
target.setTextColor(foreground_color.to_565());
for (const auto &step : ord_steps) {
Decimal val;
String text;
uint16_t width;
std::tie(val, text, width) = step;
int16_t posx = canvas_x - width - PADDING_BETWEEN_NUMBERS_AND_GRAPH;
int16_t posy = val_to_screen(val, canvas_y, canvas_height, ord_min, ord_max, true);
target.setCursor(posx, posy - text_y_offset);
target.print(text);
target.drawLine(canvas_x - 1, posy, canvas_x + 1, posy, foreground_color.to_565());
}
Decimal abs_max_num = abs_max;
Decimal abs_min_num = abs_min;
}
[[gnu::pure]] static int16_t val_to_screen(Decimal val,
int16_t canvas_start,
uint16_t canvas_size,
Decimal val_min,
Decimal val_max,
bool reverse) {
int16_t canvas_val = static_cast<int16_t>(((val - val_min) * canvas_size / (val_max - val_min)).toInt());
if (reverse) {
return canvas_start + canvas_size - canvas_val;
} else {
return canvas_start + canvas_val;
}
}
};
RGB color_white = "FFFFFF"_rgb;
RGB color_black = "000000"_rgb;
std::vector<Point> v1 = {{0_d, 10_d}, {5_d, 20_d}, {10_d, 16_d}, {15_d, 50_d},
{20_d, 0_d}, {25_d, 40_d}};
std::vector<Point>
v2 = {{0_d, 5_d}, {3_d, 10_d}, {7_d, 20_d}, {11_d, 6_d}, {15_d, 22_d}, {22_d, 36_d}, {25_d, 50_d}};
std::vector<Point>
v3 = {{-10_d, 100000_d}, {-5_d, 2000_d}, {0_d, 100000_d}, {5_d, 2000_d}, {10_d, -16000_d}, {15_d, 50000_d},
{20_d, 1000_d}, {25_d, 40000_d}};
std::vector<Point> v4 =
{{0, 12374_d}, {3_d, 43563_d}, {7_d, 6512_d}, {11_d, 14434_d}, {15_d, 43143_d}, {22_d, 298_d}, {25_d, 93834_d}};
Series s1{
"00FFFF"_rgb,
"CYAN",
Visualisation::AREA_WITH_LINE,
InterpolationMethod::MONOTONIC_CUBIC_HERMITE,
v1
};
Series s2{
"FF0000"_rgb,
"RED",
Visualisation::AREA_WITH_LINE,
InterpolationMethod::MONOTONIC_CUBIC_HERMITE,
v2
};
std::vector<Series> series{s1, s2};
Chart chart1 = {color_black, color_white,
true, true, true,
true, true, 0, 0, 0, 0,
series};
Series s3{
"00FFFF"_rgb,
"CYAN",
Visualisation::AREA_WITH_LINE,
InterpolationMethod::CUBIC_HERMITE,
v1
};
Series s4{
"FF0000"_rgb,
"RED",
Visualisation::AREA_WITH_LINE,
InterpolationMethod::CUBIC_HERMITE,
v2
};
std::vector<Series> series2{s3, s4};
Chart chart2 = {color_white, color_black,
true, true, true,
true, false, 0, 0, 0_d, 5_d,
series2};
void setup() {
Serial.begin(9600);
screen.begin(32000000);
// commander.EnableSerial();
// commander.SetPrompt("graph> ");
// commander.Begin();
screen.fillScreen("555555"_rgb565);
chart1.Draw(screen, 5, 5, screen.width() - 10, screen.height() / 2 - 10);
chart2.Draw(screen, 5, screen.height() / 2 + 5, screen.width() - 10, screen.height() / 2 - 10);
}
Decimal chart2_offset = 0_d;
Decimal chart2_step = 0.2;
bool chart2_direction = true;
void loop() {
if (chart2_direction) {
chart2_offset += chart2_step;
if (chart2_offset > 20) {
chart2_offset = 20;
chart2_direction = false;
}
} else {
chart2_offset -= chart2_step;
if (chart2_offset < 0) {
chart2_offset = 0;
chart2_direction = true;
}
}
chart2.abscissa_from = chart2_offset;
chart2.abscissa_to = chart2_offset + 5;
chart2.Draw(screen, 5, screen.height() / 2 + 5, screen.width() - 10, screen.height() / 2 - 10);
delay(100);
}