mirror of
https://github.com/subsurface/subsurface.git
synced 2024-11-30 22:20:21 +00:00
7aacde3169
All data access is now directly via the plot_info structure owned by the ProfileScene itself. Also removes DivePercentageItem::hColumn, which was an artifact from the DivePlotDataModel. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
466 lines
14 KiB
C++
466 lines
14 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 "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),
|
|
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);
|
|
}
|
|
|
|
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)
|
|
{
|
|
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);
|
|
// 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);
|
|
|
|
// 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 = textVisibility ? label.label->opacity()
|
|
: label.line->opacity();
|
|
label.opacityEnd = opacityEnd;
|
|
if (textVisibility) {
|
|
label.labelPosStart = label.label->pos();
|
|
label.labelPosEnd = labelPos(pos);
|
|
}
|
|
if (lineVisibility) {
|
|
label.lineStart = label.line->line();
|
|
label.lineEnd = linePos(pos);
|
|
}
|
|
}
|
|
|
|
DiveCartesianAxis::Label DiveCartesianAxis::createLabel(double value, double pos, double dataMinOld, double dataMaxOld, int animSpeed)
|
|
{
|
|
Label label { value, 0.0, 1.0 };
|
|
double posStart = posAtValue(value, dataMaxOld, dataMinOld);
|
|
if (textVisibility) {
|
|
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 {
|
|
// Create new label
|
|
newLabels.push_back(createLabel(value, pos, dataMinOld, dataMaxOld, animSpeed));
|
|
}
|
|
}
|
|
|
|
// 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::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 (IS_FP_SAME(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;
|
|
}
|
|
|
|
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());
|
|
}
|