subsurface/stats/chartitem.cpp
Berthold Stoeger b07a7fe5f1 statistics: convert scatter series to use QSG
The original plan to reuse the ChartPixmapItem for the
scatteritems was dumped, because it is unclear if the
textures are shared if generated for each item.

Instead, a new ChartScatterItem was created, where all
items share the same textures (one for highlighted,
one for non-highlighted). This means that the rendering
of the scatter items is now done in the chartitem.cpp
file, which feels like a layering violation. Not good,
but the easiest for now.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2021-01-20 08:47:18 +01:00

445 lines
12 KiB
C++

// SPDX-License-Identifier: GPL-2.0
#include "chartitem.h"
#include "statscolors.h"
#include "statsview.h"
#include <cmath>
#include <QQuickWindow>
#include <QSGFlatColorMaterial>
#include <QSGImageNode>
#include <QSGRectangleNode>
#include <QSGTexture>
static int round_up(double f)
{
return static_cast<int>(ceil(f));
}
ChartItem::ChartItem(StatsView &v, ChartZValue z) :
dirty(false), dirtyPrev(nullptr), dirtyNext(nullptr),
zValue(z), view(v)
{
}
ChartItem::~ChartItem()
{
if (dirty)
view.unregisterDirtyChartItem(*this);
}
QSizeF ChartItem::sceneSize() const
{
return view.size();
}
ChartPixmapItem::ChartPixmapItem(StatsView &v, ChartZValue z) : HideableChartItem(v, z),
positionDirty(false), textureDirty(false)
{
}
ChartPixmapItem::~ChartPixmapItem()
{
painter.reset(); // Make sure to destroy painter before image that is painted on
}
void ChartPixmapItem::setTextureDirty()
{
textureDirty = true;
view.registerDirtyChartItem(*this);
}
void ChartPixmapItem::setPositionDirty()
{
positionDirty = true;
view.registerDirtyChartItem(*this);
}
void ChartPixmapItem::render()
{
if (!node) {
createNode(view.w()->createImageNode());
view.addQSGNode(node.get(), zValue);
}
if (!img) {
resize(QSizeF(1,1));
img->fill(Qt::transparent);
}
if (textureDirty) {
texture.reset(view.w()->createTextureFromImage(*img, QQuickWindow::TextureHasAlphaChannel));
node->node->setTexture(texture.get());
textureDirty = false;
}
if (positionDirty) {
node->node->setRect(rect);
positionDirty = false;
}
}
void ChartPixmapItem::resize(QSizeF size)
{
painter.reset();
img.reset(new QImage(round_up(size.width()), round_up(size.height()), QImage::Format_ARGB32));
painter.reset(new QPainter(img.get()));
painter->setRenderHint(QPainter::Antialiasing);
rect.setSize(size);
setTextureDirty();
}
void ChartPixmapItem::setPos(QPointF pos)
{
rect.moveTopLeft(pos);
setPositionDirty();
}
QRectF ChartPixmapItem::getRect() const
{
return rect;
}
static const int scatterItemDiameter = 10;
static const int scatterItemBorder = 1;
ChartScatterItem::ChartScatterItem(StatsView &v, ChartZValue z) : HideableChartItem(v, z),
positionDirty(false), textureDirty(false), highlighted(false)
{
rect.setSize(QSizeF(static_cast<double>(scatterItemDiameter), static_cast<double>(scatterItemDiameter)));
}
ChartScatterItem::~ChartScatterItem()
{
}
static std::unique_ptr<QSGTexture> createScatterTexture(StatsView &view, const QColor &color, const QColor &borderColor)
{
QImage img(scatterItemDiameter, scatterItemDiameter, QImage::Format_ARGB32);
img.fill(Qt::transparent);
QPainter painter(&img);
painter.setPen(Qt::NoPen);
painter.setRenderHint(QPainter::Antialiasing);
painter.setBrush(borderColor);
painter.drawEllipse(0, 0, scatterItemDiameter, scatterItemDiameter);
painter.setBrush(color);
painter.drawEllipse(scatterItemBorder, scatterItemBorder,
scatterItemDiameter - 2 * scatterItemBorder,
scatterItemDiameter - 2 * scatterItemBorder);
return std::unique_ptr<QSGTexture>(
view.w()->createTextureFromImage(img, QQuickWindow::TextureHasAlphaChannel)
);
}
std::unique_ptr<QSGTexture> scatterItemTexture;
std::unique_ptr<QSGTexture> scatterItemHighlightedTexture;
void ChartScatterItem::render()
{
if (!scatterItemTexture) {
scatterItemTexture = createScatterTexture(view, fillColor, borderColor);
scatterItemHighlightedTexture = createScatterTexture(view, highlightedColor, highlightedBorderColor);
}
if (!node) {
createNode(view.w()->createImageNode());
view.addQSGNode(node.get(), zValue);
textureDirty = positionDirty = true;
}
if (textureDirty) {
node->node->setTexture(highlighted ? scatterItemHighlightedTexture.get() : scatterItemTexture.get());
textureDirty = false;
}
if (positionDirty) {
node->node->setRect(rect);
positionDirty = false;
}
}
void ChartScatterItem::setPos(QPointF pos)
{
pos -= QPointF(scatterItemDiameter / 2.0, scatterItemDiameter / 2.0);
rect.moveTopLeft(pos);
positionDirty = true;
view.registerDirtyChartItem(*this);
}
static double squareDist(const QPointF &p1, const QPointF &p2)
{
QPointF diff = p1 - p2;
return QPointF::dotProduct(diff, diff);
}
bool ChartScatterItem::contains(QPointF point) const
{
return squareDist(point, rect.center()) <= (scatterItemDiameter / 2.0) * (scatterItemDiameter / 2.0);
}
void ChartScatterItem::setHighlight(bool highlightedIn)
{
if (highlighted == highlightedIn)
return;
highlighted = highlightedIn;
textureDirty = true;
view.registerDirtyChartItem(*this);
}
QRectF ChartScatterItem::getRect() const
{
return rect;
}
ChartRectItem::ChartRectItem(StatsView &v, ChartZValue z,
const QPen &pen, const QBrush &brush, double radius) : ChartPixmapItem(v, z),
pen(pen), brush(brush), radius(radius)
{
}
ChartRectItem::~ChartRectItem()
{
}
void ChartRectItem::resize(QSizeF size)
{
ChartPixmapItem::resize(size);
img->fill(Qt::transparent);
painter->setPen(pen);
painter->setBrush(brush);
QSize imgSize = img->size();
int width = pen.width();
QRect rect(width / 2, width / 2, imgSize.width() - width, imgSize.height() - width);
painter->drawRoundedRect(rect, radius, radius, Qt::AbsoluteSize);
}
ChartTextItem::ChartTextItem(StatsView &v, ChartZValue z, const QFont &f, const std::vector<QString> &text, bool center) :
ChartPixmapItem(v, z), f(f), center(center)
{
QFontMetrics fm(f);
double totalWidth = 1.0;
fontHeight = static_cast<double>(fm.height());
double totalHeight = std::max(1.0, static_cast<double>(text.size()) * fontHeight);
items.reserve(text.size());
for (const QString &s: text) {
double w = fm.size(Qt::TextSingleLine, s).width();
items.push_back({ s, w });
if (w > totalWidth)
totalWidth = w;
}
resize(QSizeF(totalWidth, totalHeight));
}
void ChartTextItem::setColor(const QColor &c)
{
img->fill(Qt::transparent);
double y = 0.0;
painter->setPen(QPen(c));
painter->setFont(f);
double totalWidth = getRect().width();
for (const auto &[s, w]: items) {
double x = center ? round((totalWidth - w) / 2.0) : 0.0;
QRectF rect(x, y, w, fontHeight);
painter->drawText(rect, s);
y += fontHeight;
}
setTextureDirty();
}
ChartLineItem::ChartLineItem(StatsView &v, ChartZValue z, QColor color, double width) : HideableChartItem(v, z),
color(color), width(width), positionDirty(false), materialDirty(false)
{
}
ChartLineItem::~ChartLineItem()
{
}
// Helper function to set points
void setPoint(QSGGeometry::Point2D &v, const QPointF &p)
{
v.set(static_cast<float>(p.x()), static_cast<float>(p.y()));
}
void ChartLineItem::render()
{
if (!node) {
geometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 2));
geometry->setDrawingMode(QSGGeometry::DrawLines);
material.reset(new QSGFlatColorMaterial);
createNode();
node->setGeometry(geometry.get());
node->setMaterial(material.get());
view.addQSGNode(node.get(), zValue);
positionDirty = materialDirty = true;
}
if (positionDirty) {
// Attention: width is a geometry property and therefore handled by position dirty!
geometry->setLineWidth(static_cast<float>(width));
auto vertices = geometry->vertexDataAsPoint2D();
setPoint(vertices[0], from);
setPoint(vertices[1], to);
node->markDirty(QSGNode::DirtyGeometry);
}
if (materialDirty) {
material->setColor(color);
node->markDirty(QSGNode::DirtyMaterial);
}
positionDirty = materialDirty = false;
}
void ChartLineItem::setLine(QPointF fromIn, QPointF toIn)
{
from = fromIn;
to = toIn;
positionDirty = true;
view.registerDirtyChartItem(*this);
}
ChartBarItem::ChartBarItem(StatsView &v, ChartZValue z, double borderWidth, bool horizontal) : HideableChartItem(v, z),
borderWidth(borderWidth), horizontal(horizontal),
positionDirty(false), colorDirty(false)
{
}
ChartBarItem::~ChartBarItem()
{
}
void ChartBarItem::render()
{
if (!node) {
createNode(view.w()->createRectangleNode());
borderGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 4));
borderGeometry->setDrawingMode(QSGGeometry::DrawLineLoop);
borderGeometry->setLineWidth(static_cast<float>(borderWidth));
borderMaterial.reset(new QSGFlatColorMaterial);
borderNode.reset(new QSGGeometryNode);
borderNode->setGeometry(borderGeometry.get());
borderNode->setMaterial(borderMaterial.get());
node->node->appendChildNode(borderNode.get());
view.addQSGNode(node.get(), zValue);
positionDirty = colorDirty = true;
}
if (colorDirty) {
node->node->setColor(color);
borderMaterial->setColor(borderColor);
node->node->markDirty(QSGNode::DirtyMaterial);
borderNode->markDirty(QSGNode::DirtyMaterial);
}
if (positionDirty) {
node->node->setRect(rect);
auto vertices = borderGeometry->vertexDataAsPoint2D();
if (horizontal) {
setPoint(vertices[0], rect.topLeft());
setPoint(vertices[1], rect.topRight());
setPoint(vertices[2], rect.bottomRight());
setPoint(vertices[3], rect.bottomLeft());
} else {
setPoint(vertices[0], rect.bottomLeft());
setPoint(vertices[1], rect.topLeft());
setPoint(vertices[2], rect.topRight());
setPoint(vertices[3], rect.bottomRight());
}
node->node->markDirty(QSGNode::DirtyGeometry);
borderNode->markDirty(QSGNode::DirtyGeometry);
}
positionDirty = colorDirty = false;
}
void ChartBarItem::setColor(QColor colorIn, QColor borderColorIn)
{
color = colorIn;
borderColor = borderColorIn;
colorDirty = true;
view.registerDirtyChartItem(*this);
}
void ChartBarItem::setRect(const QRectF &rectIn)
{
rect = rectIn;
positionDirty = true;
view.registerDirtyChartItem(*this);
}
QRectF ChartBarItem::getRect() const
{
return rect;
}
ChartBoxItem::ChartBoxItem(StatsView &v, ChartZValue z, double borderWidth) :
ChartBarItem(v, z, borderWidth, false) // Only support for vertical boxes
{
}
ChartBoxItem::~ChartBoxItem()
{
}
void ChartBoxItem::render()
{
// Remember old dirty values, since ChartBarItem::render() will clear them
bool oldPositionDirty = positionDirty;
bool oldColorDirty = colorDirty;
ChartBarItem::render(); // This will create the base node, so no need to check for that.
if (!whiskersNode) {
whiskersGeometry.reset(new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 10));
whiskersGeometry->setDrawingMode(QSGGeometry::DrawLines);
whiskersGeometry->setLineWidth(static_cast<float>(borderWidth));
whiskersMaterial.reset(new QSGFlatColorMaterial);
whiskersNode.reset(new QSGGeometryNode);
whiskersNode->setGeometry(whiskersGeometry.get());
whiskersNode->setMaterial(whiskersMaterial.get());
node->node->appendChildNode(whiskersNode.get());
// If this is the first time, make sure to update the geometry.
oldPositionDirty = oldColorDirty = true;
}
if (oldColorDirty) {
whiskersMaterial->setColor(borderColor);
whiskersNode->markDirty(QSGNode::DirtyMaterial);
}
if (oldPositionDirty) {
auto vertices = whiskersGeometry->vertexDataAsPoint2D();
double left = rect.left();
double right = rect.right();
double mid = (left + right) / 2.0;
// top bar
setPoint(vertices[0], QPointF(left, max));
setPoint(vertices[1], QPointF(right, max));
// top whisker
setPoint(vertices[2], QPointF(mid, max));
setPoint(vertices[3], QPointF(mid, rect.top()));
// bottom bar
setPoint(vertices[4], QPointF(left, min));
setPoint(vertices[5], QPointF(right, min));
// bottom whisker
setPoint(vertices[6], QPointF(mid, min));
setPoint(vertices[7], QPointF(mid, rect.bottom()));
// median indicator
setPoint(vertices[8], QPointF(left, median));
setPoint(vertices[9], QPointF(right, median));
whiskersNode->markDirty(QSGNode::DirtyGeometry);
}
}
void ChartBoxItem::setBox(const QRectF &rect, double minIn, double maxIn, double medianIn)
{
min = minIn;
max = maxIn;
median = medianIn;
setRect(rect);
}
QRectF ChartBoxItem::getRect() const
{
QRectF res = rect;
res.setTop(min);
res.setBottom(max);
return rect;
}