// 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(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 = label.label ? label.label->opacity() : label.line->opacity(); label.opacityEnd = opacityEnd; if (label.label) { label.labelPosStart = label.label->pos(); label.labelPosEnd = labelPos(pos); } 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(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(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