mirror of
https://github.com/subsurface/subsurface.git
synced 2024-11-27 20:58:47 +00:00
edf2a2f4f4
When zoomed in, the profile position was moved by hovering with the mouse. What a horrible user experience. This is especially useless if we want to implement an interactive profile on mobile. Instead, let the user start the panning with a mouse click. The code is somewhat nasty, because the position is given as a real in the [0,1] range, which represents all possible positions from completely to the left to completely to the right. This commit also removes the restriction that the planner handles can only be moved when fully zoomed out. It is not completely clear what the implications are. Let's see. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
512 lines
16 KiB
C++
512 lines
16 KiB
C++
// 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());
|
|
}
|