mirror of
https://github.com/subsurface/subsurface.git
synced 2025-02-19 22:16:15 +00:00
Instead of a host of virtual functions, let the base class (DiveCartesianAxis) do the formatting of the axis labels. To do so, it needs to know how to convert the internal representation (e.g. mm) into the displayed value (e.g. feet). Moreover, this transformation has to be adapted when changing the locale-setting, therefore do it for every plot() call. The transformation itself cannot be a simple linear translation, because we have non-absolute display units, namely °C and °F. Thankfully affine transformations are enough though. Only one custom formatter remains: the time axis. It might be a good idea to remove the virtual function and do this via a flag. This is all done not so much for code simplification, but because for a general layout of the axis labels, the axis has to understand the values of the labels and not only handle them as opaque texts. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
434 lines
12 KiB
C++
434 lines
12 KiB
C++
// SPDX-License-Identifier: GPL-2.0
|
|
#include "profile-widget/divecartesianaxis.h"
|
|
#include "profile-widget/divetextitem.h"
|
|
#include "core/qthelper.h"
|
|
#include "core/subsurface-string.h"
|
|
#include "qt-models/diveplotdatamodel.h"
|
|
#include "profile-widget/animationfunctions.h"
|
|
#include "profile-widget/divelineitem.h"
|
|
#include "profile-widget/profilescene.h"
|
|
|
|
static const double labelSpaceHorizontal = 2.0; // space between label and ticks
|
|
static const double labelSpaceVertical = 2.0; // space between label and ticks
|
|
|
|
void DiveCartesianAxis::setBounds(double minimum, double maximum)
|
|
{
|
|
changed = !IS_FP_SAME(max, maximum) || !IS_FP_SAME(min, minimum);
|
|
min = minimum;
|
|
max = maximum;
|
|
}
|
|
|
|
DiveCartesianAxis::DiveCartesianAxis(Position position, int integralDigits, int fractionalDigits, color_index_t gridColor, double dpr,
|
|
double labelScale, bool printMode, bool isGrayscale, ProfileScene &scene) :
|
|
printMode(printMode),
|
|
position(position),
|
|
fractionalDigits(fractionalDigits),
|
|
gridColor(gridColor),
|
|
scene(scene),
|
|
orientation(LeftToRight),
|
|
min(0),
|
|
max(0),
|
|
interval(1),
|
|
textVisibility(true),
|
|
lineVisibility(true),
|
|
labelScale(labelScale),
|
|
changed(true),
|
|
dpr(dpr),
|
|
transform({1.0, 0.0})
|
|
{
|
|
QPen pen;
|
|
pen.setColor(getColor(TIME_GRID, isGrayscale));
|
|
/* cosmetic width() == 0 for lines in printMode
|
|
* having setCosmetic(true) and width() > 0 does not work when
|
|
* printing on OSX and Linux */
|
|
pen.setWidth(DiveCartesianAxis::printMode ? 0 : 2);
|
|
pen.setCosmetic(true);
|
|
setPen(pen);
|
|
|
|
pen.setBrush(getColor(gridColor, isGrayscale));
|
|
gridPen = pen;
|
|
|
|
/* Create the longest expected label, e.g. 999.99. */
|
|
QString label;
|
|
label.reserve(integralDigits + fractionalDigits + 1);
|
|
for (int i = 0; i < integralDigits; ++i)
|
|
label.append('9');
|
|
if (fractionalDigits > 0) {
|
|
label.append('.');
|
|
for (int i = 0; i < fractionalDigits; ++i)
|
|
label.append('9');
|
|
}
|
|
|
|
/* Use the label to estimate size of the labels.
|
|
* Round up, because non-integers tend to give abysmal rendering.
|
|
*/
|
|
QFont fnt = DiveTextItem::getFont(dpr, labelScale);
|
|
double outlineSpace = DiveTextItem::outlineSpace(dpr);
|
|
QFontMetrics fm(fnt);
|
|
labelWidth = ceil(fm.size(Qt::TextSingleLine, label).width() + outlineSpace);
|
|
labelHeight = ceil(fm.height() + outlineSpace);
|
|
}
|
|
|
|
DiveCartesianAxis::~DiveCartesianAxis()
|
|
{
|
|
}
|
|
|
|
void DiveCartesianAxis::setOrientation(Orientation o)
|
|
{
|
|
orientation = o;
|
|
changed = true;
|
|
}
|
|
|
|
void DiveCartesianAxis::setTransform(double a, double b)
|
|
{
|
|
transform.a = a;
|
|
transform.b = b;
|
|
changed = true;
|
|
}
|
|
|
|
QColor DiveCartesianAxis::colorForValue(double) const
|
|
{
|
|
return QColor(Qt::black);
|
|
}
|
|
|
|
void DiveCartesianAxis::setTextVisible(bool arg1)
|
|
{
|
|
if (textVisibility == arg1) {
|
|
return;
|
|
}
|
|
textVisibility = arg1;
|
|
Q_FOREACH (DiveTextItem *item, labels) {
|
|
item->setVisible(textVisibility);
|
|
}
|
|
}
|
|
|
|
void DiveCartesianAxis::setLinesVisible(bool arg1)
|
|
{
|
|
if (lineVisibility == arg1) {
|
|
return;
|
|
}
|
|
lineVisibility = arg1;
|
|
Q_FOREACH (DiveLineItem *item, lines) {
|
|
item->setVisible(lineVisibility);
|
|
}
|
|
}
|
|
|
|
template <typename T>
|
|
void emptyList(QList<T *> &list, int steps, int speed)
|
|
{
|
|
while (list.size() > steps) {
|
|
T *removedItem = list.takeLast();
|
|
Animations::animDelete(removedItem, speed);
|
|
}
|
|
}
|
|
|
|
double DiveCartesianAxis::width() const
|
|
{
|
|
return labelWidth + labelSpaceHorizontal * dpr;
|
|
}
|
|
|
|
double DiveCartesianAxis::height() const
|
|
{
|
|
return labelHeight + labelSpaceVertical * dpr;
|
|
}
|
|
|
|
void DiveCartesianAxis::updateTicks(int animSpeed)
|
|
{
|
|
if (!changed && !printMode)
|
|
return;
|
|
QLineF m = line();
|
|
double stepsInRange = (max - min) / interval;
|
|
int steps = (int)stepsInRange;
|
|
double currValueText = min;
|
|
double currValueLine = min;
|
|
|
|
if (steps < 1)
|
|
return;
|
|
|
|
emptyList(labels, steps, animSpeed);
|
|
emptyList(lines, steps, animSpeed);
|
|
|
|
// Move the remaining grid lines / labels to their correct positions
|
|
// regarding the possible new values for the axis
|
|
qreal begin, stepSize;
|
|
if (orientation == TopToBottom) {
|
|
begin = m.y1();
|
|
stepSize = (m.y2() - m.y1());
|
|
} else if (orientation == BottomToTop) {
|
|
begin = m.y2();
|
|
stepSize = (m.y2() - m.y1());
|
|
} else if (orientation == LeftToRight) {
|
|
begin = m.x1();
|
|
stepSize = (m.x2() - m.x1());
|
|
} else /* if (orientation == RightToLeft) */ {
|
|
begin = m.x2();
|
|
stepSize = (m.x2() - m.x1());
|
|
}
|
|
stepSize /= stepsInRange;
|
|
|
|
for (int i = 0, count = labels.size(); i < count; i++, currValueText += interval) {
|
|
double childPos = (orientation == TopToBottom || orientation == LeftToRight) ?
|
|
begin + i * stepSize :
|
|
begin - i * stepSize;
|
|
|
|
labels[i]->set(textForValue(currValueText), colorForValue(currValueText));
|
|
switch (position) {
|
|
default:
|
|
case Position::Bottom:
|
|
Animations::moveTo(labels[i], animSpeed, childPos, rect.bottom() + labelSpaceVertical * dpr);
|
|
break;
|
|
case Position::Left:
|
|
Animations::moveTo(labels[i], animSpeed, rect.left() - labelSpaceHorizontal * dpr, childPos);
|
|
break;
|
|
case Position::Right:
|
|
Animations::moveTo(labels[i], animSpeed, rect.right() + labelSpaceHorizontal * dpr, childPos);
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (int i = 0, count = lines.size(); i < count; i++, currValueLine += interval) {
|
|
double childPos = (orientation == TopToBottom || orientation == LeftToRight) ?
|
|
begin + i * stepSize :
|
|
begin - i * stepSize;
|
|
|
|
if (position == Position::Bottom) {
|
|
// Fix size in case the scene changed
|
|
QLineF old = lines[i]->line();
|
|
lines[i]->setLine(old.x1(), old.y1(), old.x1(), old.y1() + rect.height());
|
|
Animations::moveTo(lines[i], animSpeed, childPos, rect.top());
|
|
} else {
|
|
// Fix size in case the scene changed
|
|
QLineF old = lines[i]->line();
|
|
lines[i]->setLine(old.x1(), old.y1(), old.x1() + rect.width(), old.y1());
|
|
Animations::moveTo(lines[i], animSpeed, rect.left(), childPos);
|
|
}
|
|
}
|
|
|
|
// Add the rest of the needed labels.
|
|
for (int i = labels.size(); i < steps; i++, currValueText += interval) {
|
|
double childPos;
|
|
if (orientation == TopToBottom || orientation == LeftToRight) {
|
|
childPos = begin + i * stepSize;
|
|
} else {
|
|
childPos = begin - i * stepSize;
|
|
}
|
|
int alignFlags = position == Position::Bottom ? Qt::AlignTop | Qt::AlignHCenter :
|
|
position == Position::Left ? Qt::AlignVCenter | Qt::AlignLeft:
|
|
Qt::AlignVCenter | Qt::AlignRight;
|
|
DiveTextItem *label = new DiveTextItem(dpr, labelScale, alignFlags, this);
|
|
label->set(textForValue(currValueText), colorForValue(currValueText));
|
|
label->setZValue(1);
|
|
labels.push_back(label);
|
|
switch (position) {
|
|
default:
|
|
case Position::Bottom:
|
|
label->setPos(scene.sceneRect().width() + 10, rect.bottom() + labelSpaceVertical * dpr); // position it outside of the scene;
|
|
Animations::moveTo(labels[i], animSpeed, childPos, rect.bottom() + labelSpaceVertical * dpr);
|
|
break;
|
|
case Position::Left:
|
|
label->setPos(rect.left() - labelSpaceHorizontal * dpr, scene.sceneRect().height() + 10);
|
|
Animations::moveTo(labels[i], animSpeed, rect.left() - labelSpaceHorizontal * dpr, childPos);
|
|
break;
|
|
case Position::Right:
|
|
label->setPos(rect.right() + labelSpaceHorizontal * dpr, scene.sceneRect().height() + 10);
|
|
Animations::moveTo(labels[i], animSpeed, rect.right() + labelSpaceHorizontal * dpr, childPos);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Add the rest of the needed grid lines.
|
|
for (int i = lines.size(); i < steps; i++, currValueText += interval) {
|
|
double childPos;
|
|
if (orientation == TopToBottom || orientation == LeftToRight) {
|
|
childPos = begin + i * stepSize;
|
|
} else {
|
|
childPos = begin - i * stepSize;
|
|
}
|
|
DiveLineItem *line = new DiveLineItem(this);
|
|
line->setPen(gridPen);
|
|
line->setZValue(0);
|
|
lines.push_back(line);
|
|
if (position == Position::Bottom) {
|
|
line->setLine(0.0, 0.0, 0.0, rect.height());
|
|
line->setPos(scene.sceneRect().width() + 10, rect.top()); // position it outside of the scene);
|
|
Animations::moveTo(line, animSpeed, childPos, rect.top());
|
|
} else {
|
|
line->setLine(0.0, 0.0, rect.width(), 0.0);
|
|
line->setPos(rect.left(), scene.sceneRect().height() + 10);
|
|
Animations::moveTo(line, animSpeed, rect.left(), childPos);
|
|
}
|
|
}
|
|
|
|
Q_FOREACH (DiveTextItem *item, labels)
|
|
item->setVisible(textVisibility);
|
|
Q_FOREACH (DiveLineItem *item, lines)
|
|
item->setVisible(lineVisibility);
|
|
changed = false;
|
|
}
|
|
|
|
void DiveCartesianAxis::setLine(const QLineF &line)
|
|
{
|
|
QGraphicsLineItem::setLine(line);
|
|
changed = true;
|
|
}
|
|
|
|
void DiveCartesianAxis::animateChangeLine(const QRectF &rectIn, int animSpeed)
|
|
{
|
|
rect = rectIn;
|
|
switch (position) {
|
|
case Position::Left:
|
|
setLine(QLineF(rect.topLeft(), rect.bottomLeft()));
|
|
break;
|
|
case Position::Right:
|
|
setLine(QLineF(rect.topRight(), rect.bottomRight()));
|
|
break;
|
|
case Position::Bottom:
|
|
default:
|
|
setLine(QLineF(rect.bottomLeft(), rect.bottomRight()));
|
|
break;
|
|
}
|
|
updateTicks(animSpeed);
|
|
sizeChanged();
|
|
}
|
|
|
|
double DiveCartesianAxis::Transform::to(double x) const
|
|
{
|
|
return a*x + b;
|
|
}
|
|
|
|
double DiveCartesianAxis::Transform::from(double y) const
|
|
{
|
|
return (y - b) / a;
|
|
}
|
|
|
|
QString DiveCartesianAxis::textForValue(double value) const
|
|
{
|
|
return QStringLiteral("%L1").arg(transform.to(value), 0, 'f', fractionalDigits);
|
|
}
|
|
|
|
void DiveCartesianAxis::setTickInterval(double i)
|
|
{
|
|
interval = i;
|
|
}
|
|
|
|
qreal DiveCartesianAxis::valueAt(const QPointF &p) const
|
|
{
|
|
double fraction;
|
|
QLineF m = line();
|
|
QPointF relativePosition = p;
|
|
relativePosition -= pos(); // normalize p based on the axis' offset on screen
|
|
|
|
if (orientation == LeftToRight || orientation == RightToLeft)
|
|
fraction = (relativePosition.x() - m.x1()) / (m.x2() - m.x1());
|
|
else
|
|
fraction = (relativePosition.y() - m.y1()) / (m.y2() - m.y1());
|
|
|
|
if (orientation == RightToLeft || orientation == BottomToTop)
|
|
fraction = 1 - fraction;
|
|
return fraction * (max - min) + min;
|
|
}
|
|
|
|
qreal DiveCartesianAxis::posAtValue(qreal value) const
|
|
{
|
|
QLineF m = line();
|
|
QPointF p = pos();
|
|
|
|
double size = max - min;
|
|
// unused for now:
|
|
// double distanceFromOrigin = value - min;
|
|
double percent = IS_FP_SAME(min, max) ? 0.0 : (value - min) / size;
|
|
|
|
|
|
double realSize = orientation == LeftToRight || orientation == RightToLeft ?
|
|
m.x2() - m.x1() :
|
|
m.y2() - m.y1();
|
|
|
|
// Inverted axis, just invert the percentage.
|
|
if (orientation == RightToLeft || orientation == BottomToTop)
|
|
percent = 1 - percent;
|
|
|
|
double retValue = realSize * percent;
|
|
double adjusted =
|
|
orientation == LeftToRight ? retValue + m.x1() + p.x() :
|
|
orientation == RightToLeft ? retValue + m.x1() + p.x() :
|
|
orientation == TopToBottom ? retValue + m.y1() + p.y() :
|
|
/* entation == BottomToTop */ retValue + m.y1() + p.y();
|
|
return adjusted;
|
|
}
|
|
|
|
double DiveCartesianAxis::maximum() const
|
|
{
|
|
return max;
|
|
}
|
|
|
|
double DiveCartesianAxis::minimum() const
|
|
{
|
|
return min;
|
|
}
|
|
|
|
std::pair<double, double> DiveCartesianAxis::screenMinMax() const
|
|
{
|
|
return position == Position::Bottom ? std::make_pair(rect.left(), rect.right())
|
|
: std::make_pair(rect.top(), rect.bottom());
|
|
}
|
|
|
|
QColor DepthAxis::colorForValue(double) const
|
|
{
|
|
return QColor(Qt::red);
|
|
}
|
|
|
|
QColor TimeAxis::colorForValue(double) const
|
|
{
|
|
return QColor(Qt::blue);
|
|
}
|
|
|
|
QString TimeAxis::textForValue(double value) const
|
|
{
|
|
int nr = lrint(value) / 60;
|
|
if (maximum() < 600)
|
|
return QString("%1:%2").arg(nr).arg((int)value % 60, 2, 10, QChar('0'));
|
|
return QString::number(nr);
|
|
}
|
|
|
|
// TODO: replace by real dynamic axis - this is just weird.
|
|
void TimeAxis::updateTicks(int animSpeed)
|
|
{
|
|
DiveCartesianAxis::updateTicks(animSpeed);
|
|
if (maximum() > 600) {
|
|
for (int i = 0; i < labels.count(); i++) {
|
|
labels[i]->setVisible(i % 2);
|
|
}
|
|
}
|
|
}
|
|
|
|
PartialGasPressureAxis::PartialGasPressureAxis(const DivePlotDataModel &model, Position position, int integralDigits, int fractionalDigits,
|
|
color_index_t gridColor, double dpr, double labelScale, bool printMode, bool isGrayscale,
|
|
ProfileScene &scene) :
|
|
DiveCartesianAxis(position, integralDigits, fractionalDigits, gridColor, dpr, labelScale, printMode, isGrayscale, scene),
|
|
model(model)
|
|
{
|
|
}
|
|
|
|
void PartialGasPressureAxis::update(int animSpeed)
|
|
{
|
|
bool showPhe = prefs.pp_graphs.phe;
|
|
bool showPn2 = prefs.pp_graphs.pn2;
|
|
bool showPo2 = prefs.pp_graphs.po2;
|
|
setVisible(showPhe || showPn2 || showPo2);
|
|
if (!model.rowCount())
|
|
return;
|
|
|
|
double max = showPhe ? model.pheMax() : -1;
|
|
if (showPn2 && model.pn2Max() > max)
|
|
max = model.pn2Max();
|
|
if (showPo2 && model.po2Max() > max)
|
|
max = model.po2Max();
|
|
|
|
qreal pp = floor(max * 10.0) / 10.0 + 0.2;
|
|
if (IS_FP_SAME(maximum(), pp))
|
|
return;
|
|
|
|
setBounds(0.0, pp);
|
|
setTickInterval(pp > 4 ? 0.5 : 0.25);
|
|
updateTicks(animSpeed);
|
|
}
|