subsurface/profile-widget/divecartesianaxis.cpp

513 lines
16 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: GPL-2.0
#include "profile-widget/divecartesianaxis.h"
#include "profile-widget/divetextitem.h"
#include "core/profile.h"
#include "core/qthelper.h"
#include "core/subsurface-float.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)
{
dataMin = min = minimum;
dataMax = max = maximum;
}
DiveCartesianAxis::DiveCartesianAxis(Position position, bool inverted, int integralDigits, int fractionalDigits, color_index_t gridColor,
QColor textColor, bool textVisible, bool linesVisible,
double dpr, double labelScale, bool printMode, bool isGrayscale, ProfileScene &scene) :
printMode(printMode),
position(position),
inverted(inverted),
fractionalDigits(fractionalDigits),
textColor(textColor),
scene(scene),
min(0),
max(0),
textVisibility(textVisible),
lineVisibility(linesVisible),
gridIsMultipleOfThree(false),
labelScale(labelScale),
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');
}
std::tie(labelWidth, labelHeight) = DiveTextItem::getLabelSize(dpr, labelScale, label);
// The axis is implemented by a line, which gives the position.
// If we don't show labels or grid, we don't want to show the line,
// as this gives a strange artifact. TODO: Don't derive from a
// line object in the first place.
setVisible(textVisible || linesVisible);
}
DiveCartesianAxis::~DiveCartesianAxis()
{
}
void DiveCartesianAxis::setTransform(double a, double b)
{
transform.a = a;
transform.b = b;
}
double DiveCartesianAxis::width() const
{
return labelWidth + labelSpaceHorizontal * dpr;
}
double DiveCartesianAxis::height() const
{
return labelHeight + labelSpaceVertical * dpr;
}
double DiveCartesianAxis::horizontalOverhang() const
{
return labelWidth / 2.0;
}
int DiveCartesianAxis::getMinLabelDistance(const DiveCartesianAxis &timeAxis) const
{
// For the plot not being to crowded we want at least two
// labels to fit between each pair of displayed labels.
// May need some optimization.
QLineF m = timeAxis.line();
double interval = labelWidth * 3.0 * (timeAxis.maximum() - timeAxis.minimum()) / (m.x2() - m.x1());
return int(ceil(interval));
}
static double sensibleInterval(double inc, int decimals, bool is_time_axis, bool is_multiple_of_three)
{
if (is_time_axis && inc < 60.0) {
// for time axes and less than one hour increments, round to
// 1, 2, 3, 4, 5, 6 or 12 parts of an hour or a minute
// (that is 60, 30, 20, 15, 12, 10 or 5 min/sec).
bool fraction_of_hour = inc > 1.0;
if (fraction_of_hour)
inc /= 60.0;
inc = inc <= 1.0 / 12.0 ? 1.0 / 12.0 :
inc <= 1.0 / 6.0 ? 1.0 / 6.0 :
1.0 / floor(1.0/inc);
if (fraction_of_hour)
inc *= 60.0;
return inc;
}
// Use full decimal increments
double digits = floor(log10(inc));
int digits_int = lrint(digits);
// Don't do increments smaller than the displayed decimals.
if (digits_int < -decimals) {
digits_int = -decimals;
digits = static_cast<double>(digits_int);
}
double digits_factor = pow(10.0, digits);
int inc_int = std::max((int)ceil(inc / digits_factor), 1);
if (is_multiple_of_three)
{
// Do increments quantized to 3. In general: 1, 3, 6, 15
if (inc_int > 6)
inc_int = 15;
else if (inc_int > 3)
inc_int = 6;
else if (inc_int == 2)
inc_int = 3;
} else {
// Do "nice" increments of the leading digit. In general: 1, 2, 4, 5.
if (inc_int > 5)
inc_int = 10;
if (inc_int == 3)
inc_int = 4;
}
inc = inc_int * digits_factor;
return inc;
}
void DiveCartesianAxis::updateTicks(int animSpeed)
{
if (dataMax - dataMin < 1e-5)
return;
if (!textVisibility && !lineVisibility)
return; // Nothing to display...
// Remember the old range for animations.
double dataMaxOld = dataMax;
double dataMinOld = dataMin;
// Guess the number of tick marks.
QLineF m = line();
double spaceNeeded = position == Position::Bottom ? labelWidth * 3.0 / 2.0
: labelHeight * 2.0;
double size = position == Position::Bottom ? fabs(m.x2() - m.x1())
: fabs(m.y2() - m.y1());
int numTicks = lrint(size / spaceNeeded);
numTicks = std::clamp(numTicks, 2, 50);
double stepValue = (dataMax - dataMin) / numTicks;
// Round the interval to a sensible size in display units
double intervalDisplay = stepValue * transform.a;
intervalDisplay = sensibleInterval(intervalDisplay, fractionalDigits, position == Position::Bottom, gridIsMultipleOfThree);
// Choose full multiples of the interval as minumum and maximum values
double minDisplay = transform.to(dataMin);
double maxDisplay = transform.to(dataMax);
// The time axis is special: use the full width in that case.
// Other axes round to the next "nice" number
double firstDisplay, lastDisplay;
double firstValue;
if (position == Position::Bottom) {
firstDisplay = ceil(minDisplay / intervalDisplay * (1.0 - 1e-5)) * intervalDisplay;
lastDisplay = floor(maxDisplay / intervalDisplay * (1.0 + 1e-5)) * intervalDisplay;
firstValue = transform.from(firstDisplay);
} else {
firstDisplay = floor(minDisplay / intervalDisplay * (1.0 + 1e-5)) * intervalDisplay;
lastDisplay = ceil(maxDisplay / intervalDisplay * (1.0 - 1e-5)) * intervalDisplay;
min = transform.from(firstDisplay);
max = transform.from(lastDisplay);
firstValue = min;
}
numTicks = lrint((lastDisplay - firstDisplay) / intervalDisplay) + 1;
numTicks = std::max(numTicks, 0);
if (numTicks == 0)
return;
double internalToScreen = size / (max - min);
stepValue = position == Position::Bottom ?
intervalDisplay / transform.a : // special case for time axis.
numTicks > 1 ? (max - min) / (numTicks - 1) : 0;
double stepScreen = stepValue * internalToScreen;
// The ticks of the time axis don't necessarily start at the beginning.
double offsetScreen = position == Position::Bottom ?
(firstValue - min) * internalToScreen : 0.0;
// Move the remaining grid lines / labels to their correct positions
// regarding the possible new values for the axis.
double firstPosScreen = position == Position::Bottom ?
(inverted ? m.x2() - offsetScreen : m.x1() + offsetScreen) :
(inverted ? m.y1() + offsetScreen : m.y2() - offsetScreen);
updateLabels(numTicks, firstPosScreen, firstValue, stepScreen, stepValue, animSpeed, dataMinOld, dataMaxOld);
}
QPointF DiveCartesianAxis::labelPos(double pos) const
{
return position == Position::Bottom ? QPointF(pos, rect.bottom() + labelSpaceVertical * dpr) :
position == Position::Left ? QPointF(rect.left() - labelSpaceHorizontal * dpr, pos) :
QPointF(rect.right() + labelSpaceHorizontal * dpr, pos);
}
QLineF DiveCartesianAxis::linePos(double pos) const
{
return position == Position::Bottom ? QLineF(pos, rect.top(), pos, rect.bottom()) :
QLineF(rect.left(), pos, rect.right(), pos);
}
void DiveCartesianAxis::updateLabel(Label &label, double opacityEnd, double pos) const
{
label.opacityStart = label.label ? label.label->opacity()
: label.line->opacity();
label.opacityEnd = opacityEnd;
if (label.label) {
label.labelPosStart = label.label->pos();
label.labelPosEnd = labelPos(pos);
// For the time-axis, the format might change from "mm" to "mm:ss",
// or vice versa. Currently, we don't animate that, i.e. it will
// switch instantaneously.
if (position == Position::Bottom)
label.label->set(textForValue(label.value), textColor);
}
if (label.line) {
label.lineStart = label.line->line();
label.lineEnd = linePos(pos);
}
}
DiveCartesianAxis::Label DiveCartesianAxis::createLabel(double value, double pos, double dataMinOld, double dataMaxOld, int animSpeed, bool noLabel)
{
Label label { value, 0.0, 1.0 };
double posStart = posAtValue(value, dataMaxOld, dataMinOld);
if (textVisibility && !noLabel) {
label.labelPosStart = labelPos(posStart);
label.labelPosEnd = labelPos(pos);
int alignFlags = position == Position::Bottom ? Qt::AlignTop | Qt::AlignHCenter :
position == Position::Left ? Qt::AlignVCenter | Qt::AlignLeft:
Qt::AlignVCenter | Qt::AlignRight;
label.label = std::make_unique<DiveTextItem>(dpr, labelScale, alignFlags, this);
label.label->set(textForValue(value), textColor);
label.label->setZValue(1);
label.label->setPos(animSpeed <= 0 ? label.labelPosEnd : label.labelPosStart);
label.label->setOpacity(animSpeed <= 0 ? 1.0 : 0.0);
}
if (lineVisibility) {
label.lineStart = linePos(posStart);
label.lineEnd = linePos(pos);
label.line = std::make_unique<DiveLineItem>(this);
label.line->setPen(gridPen);
label.line->setZValue(0);
label.line->setLine(animSpeed <= 0 ? label.lineEnd : label.lineStart);
label.line->setOpacity(animSpeed <= 0 ? 1.0 : 0.0);
}
return label;
}
void DiveCartesianAxis::updateLabels(int numTicks, double firstPosScreen, double firstValue, double stepScreen, double stepValue,
int animSpeed, double dataMinOld, double dataMaxOld)
{
if (animSpeed <= 0)
labels.clear(); // No animation? Simply redo the labels.
std::vector<Label> newLabels;
newLabels.reserve(numTicks);
auto actOld = labels.begin();
double value = firstValue;
for (int i = 0; i < numTicks; i++, value += stepValue) {
// Check if we already got that label. If we find unused labels, mark them for deletion.
// Labels to be deleted are recognized by an end-opacity of 0.0.
// Note: floating point comparisons should be fine owing to our rounding to integers above.
for ( ; actOld != labels.end() && actOld->value < value; ++actOld) {
double pos = posAtValue(actOld->value);
updateLabel(*actOld, 0.0, pos);
newLabels.push_back(std::move(*actOld));
}
double pos = ((position == Position::Bottom) != inverted) ?
firstPosScreen + i * stepScreen :
firstPosScreen - i * stepScreen;
if (actOld != labels.end() && actOld->value == value) {
// Update label, but don't delete it
updateLabel(*actOld, 1.0, pos);
newLabels.push_back(std::move(*actOld));
++actOld;
} else {
// This is horrible: for the depth axis, we don't want to show the first label (0).
// We recognize this by the fact that the depth axis is the only "inverted" axis.
// This really should be replaced by a general flag to avoid surprises!
bool noLabel = inverted && i == 0;
// Create new label
newLabels.push_back(createLabel(value, pos, dataMinOld, dataMaxOld, animSpeed, noLabel));
}
}
// If there are any labels left, mark them for deletion.
for ( ; actOld != labels.end(); ++actOld) {
double pos = posAtValue(actOld->value);
updateLabel(*actOld, 0.0, pos);
newLabels.push_back(std::move(*actOld));
}
labels = std::move(newLabels);
}
// Arithmetics with lines. Needed for animations. Operates pointwise.
static QLineF operator-(const QLineF &l1, const QLineF &l2)
{
return QLineF(l1.p1() - l2.p1(), l1.p2() - l2.p2());
}
static QLineF operator+(const QLineF &l1, const QLineF &l2)
{
return QLineF(l1.p1() + l2.p1(), l1.p2() + l2.p2());
}
static QLineF operator*(double f, const QLineF &l)
{
return QLineF(f*l.p1(), f*l.p2());
}
// Helper template: get point in interval (0.0: start, 1.0: end)
template <typename T>
T mid(const T &start, const T &end, double fraction)
{
return start + fraction * (end - start);
}
void DiveCartesianAxis::anim(double fraction)
{
if (fraction == 1.0) {
// The animation has finished.
// Remove labels that have been marked for deletion by setting the opacity to 0.0.
// Use the erase-remove idiom (yes, it is a weird idiom).
labels.erase(std::remove_if(labels.begin(), labels.end(),
[](const Label &l) { return l.opacityEnd == 0.0; }),
labels.end());
}
for (Label &label: labels) {
double opacity = mid(label.opacityStart, label.opacityEnd, fraction);
if (label.label) {
label.label->setOpacity(opacity);
label.label->setPos(mid(label.labelPosStart, label.labelPosEnd, fraction));
}
if (label.line) {
label.line->setOpacity(opacity);
label.line->setLine(mid(label.lineStart, label.lineEnd, fraction));
}
}
}
void DiveCartesianAxis::setPosition(const QRectF &rectIn)
{
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;
}
}
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
{
if (position == Position::Bottom) {
// The bottom axis is the time axis and that needs special treatment.
int nr = lrint(value) / 60;
if (maximum() - minimum() < 600.0)
return QString("%1:%2").arg(nr).arg((int)value % 60, 2, 10, QChar('0'));
return QString::number(nr);
} else {
return QStringLiteral("%L1").arg(transform.to(value), 0, 'f', fractionalDigits);
}
}
double DiveCartesianAxis::valueAt(const QPointF &p) const
{
QLineF m = line();
QPointF relativePosition = p;
relativePosition -= pos(); // normalize p based on the axis' offset on screen
double fraction = position == Position::Bottom ?
(relativePosition.x() - m.x1()) / (m.x2() - m.x1()) :
(relativePosition.y() - m.y1()) / (m.y2() - m.y1());
if ((position == Position::Bottom) == inverted)
fraction = 1.0 - fraction;
return fraction * (max - min) + min;
}
double DiveCartesianAxis::deltaToValue(double delta) const
{
QLineF m = line();
double screenSize = position == Position::Bottom ? m.x2() - m.x1()
: m.y2() - m.y1();
double axisSize = max - min;
double res = delta * axisSize / screenSize;
return ((position == Position::Bottom) == inverted) ? -res : res;
}
double DiveCartesianAxis::posAtValue(double value, double max, double min) const
{
QLineF m = line();
double screenFrom = position == Position::Bottom ? m.x1() : m.y1();
double screenTo = position == Position::Bottom ? m.x2() : m.y2();
if (nearly_equal(min, max))
return (screenFrom + screenTo) / 2.0;
if ((position == Position::Bottom) == inverted)
std::swap(screenFrom, screenTo);
return (value - min) / (max - min) *
(screenTo - screenFrom) + screenFrom;
}
double DiveCartesianAxis::posAtValue(double value) const
{
return posAtValue(value, max, min);
}
static std::pair<double, double> getLineFromTo(const QLineF &l, bool horizontal)
{
if (horizontal)
return std::make_pair(l.x1(), l.x2());
else
return std::make_pair(l.y1(), l.y2());
}
double DiveCartesianAxis::screenPosition(double pos) const
{
if ((position == Position::Bottom) == inverted)
pos = 1.0 - pos;
auto [from, to] = getLineFromTo(line(), position == Position::Bottom);
return (to - from) * pos + from;
}
double DiveCartesianAxis::pointInRange(double pos) const
{
auto [from, to] = getLineFromTo(line(), position == Position::Bottom);
if (from > to)
std::swap(from, to);
return pos >= from && pos <= to;
}
double DiveCartesianAxis::maximum() const
{
return max;
}
double DiveCartesianAxis::minimum() const
{
return min;
}
void DiveCartesianAxis::setGridIsMultipleOfThree(bool arg1)
{
gridIsMultipleOfThree = arg1;
}
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());
}