482 lines
15 KiB
C++
482 lines
15 KiB
C++
// Ruler class implementation
|
|
//
|
|
// A QWidget displaying a ruler and allowing user zooming and shifting
|
|
//
|
|
// QATSH Copyright 2009 Jean-Philippe MEURET <jpmeuret@free.fr>
|
|
|
|
#include <iostream>
|
|
|
|
#include <QtGui/QPainter>
|
|
#include <QtGui/QPalette>
|
|
#include <QtGui/QFontMetrics>
|
|
#include <QtGui/QResizeEvent>
|
|
#include <QtGui/QWheelEvent>
|
|
|
|
#include "ATSMath.h"
|
|
#include "Ruler.h"
|
|
#include "RulerSpec.h"
|
|
|
|
|
|
Ruler::Ruler(QWidget *parent)
|
|
: QWidget(parent), _bShifting(false), _dZoomFactor(1.0), _dValueOffset(0.0)
|
|
{
|
|
//std::cout << "Ruler::Ruler : viewRect = "
|
|
// << width() << "x" << height() << std::endl;
|
|
}
|
|
|
|
void Ruler::onSpecChanged(const RulerSpec* pSpec)
|
|
{
|
|
_pSpec = pSpec;
|
|
|
|
// Check and fix value offset / zoom factor in case they are no longer
|
|
// compatible with new ruler spec.
|
|
const double dScaleMinValue = _pSpec->minValue();
|
|
const double dScaleMaxValue = _pSpec->maxValue();
|
|
const double dScaleLength = dScaleMaxValue - dScaleMinValue;
|
|
const double dViewMaxValue =
|
|
dScaleMinValue + _dValueOffset + dScaleLength / _dZoomFactor;
|
|
if (dViewMaxValue > dScaleMaxValue)
|
|
_dValueOffset = dScaleMaxValue - dScaleLength / _dZoomFactor;
|
|
if (_dValueOffset < 0.0)
|
|
{
|
|
_dZoomFactor = 1.0;
|
|
_dValueOffset = 0.0;
|
|
}
|
|
|
|
// Force a resize, in order for each ruler-driven view
|
|
// to be notified of new offset and zoom factor.
|
|
resize(size());
|
|
}
|
|
|
|
void Ruler::zoom(double dRelativeAnchor, double dRelativeFactor)
|
|
{
|
|
const QPoint qpAnchorPoint((int)(width() * dRelativeAnchor),
|
|
(int)(height() * dRelativeAnchor));
|
|
zoom(qpAnchorPoint, dRelativeFactor);
|
|
}
|
|
|
|
void Ruler::zoomAll()
|
|
{
|
|
// Back to unzoomed aspect.
|
|
_dZoomFactor = 1.0;
|
|
_dValueOffset = 0.0;
|
|
|
|
// Redraw the ruler.
|
|
repaint();
|
|
|
|
// Post zoom event properties for other views to apply it.
|
|
const int nViewLength = _pSpec->orientation() == RulerSpec::eHorizontal ? width() : height();
|
|
propagateZoomShift(nViewLength);
|
|
}
|
|
|
|
// Event handlers =============================================================
|
|
void Ruler::mousePressEvent(QMouseEvent *pqEvent)
|
|
{
|
|
if (pqEvent->button() == Qt::LeftButton)
|
|
{
|
|
_bShifting = true;
|
|
_qpLastShiftingPos = pqEvent->pos();
|
|
}
|
|
}
|
|
|
|
void Ruler::mouseMoveEvent(QMouseEvent *pqEvent)
|
|
{
|
|
if ((pqEvent->buttons() & Qt::LeftButton) && _bShifting)
|
|
{
|
|
shift(pqEvent->pos());
|
|
_qpLastShiftingPos = pqEvent->pos();
|
|
}
|
|
}
|
|
|
|
void Ruler::mouseReleaseEvent(QMouseEvent *pqEvent)
|
|
{
|
|
if (pqEvent->button() == Qt::LeftButton && _bShifting)
|
|
{
|
|
_bShifting = false;
|
|
shift(pqEvent->pos());
|
|
}
|
|
}
|
|
|
|
void Ruler::wheelEvent(QWheelEvent *pqEvent)
|
|
{
|
|
//std::cout << "Ruler::zoom : delta=" << pqEvent->delta() << std::endl;
|
|
zoom(pqEvent->pos(), pow(2.0, -pqEvent->delta() / 240.0));
|
|
}
|
|
|
|
void Ruler::resizeEvent(QResizeEvent *pqEvent)
|
|
{
|
|
// std::cout << "Ruler::resizeEvent(" << pqEvent->size().width()
|
|
// << "x" << pqEvent->size().height() << ")" << std::endl;
|
|
|
|
resize(pqEvent->size());
|
|
}
|
|
|
|
// Actually draw the ruler (triggered after a call repaint()).
|
|
void Ruler::paintEvent(QPaintEvent *pqEvent)
|
|
{
|
|
Q_UNUSED(pqEvent);
|
|
|
|
QPainter qPainter(this);
|
|
|
|
draw(qPainter);
|
|
}
|
|
|
|
|
|
// ===========================================================================
|
|
void Ruler::propagateZoomShift(int nViewLength)
|
|
{
|
|
// Get ruler spec.
|
|
const double dScaleMinValue = _pSpec->minValue();
|
|
const double dScaleMaxValue = _pSpec->maxValue();
|
|
const double dScaleLength = dScaleMaxValue - dScaleMinValue;
|
|
|
|
const bool bPositiveDir = _pSpec->direction() == RulerSpec::ePositive;
|
|
const bool bHorizontal = _pSpec->orientation() == RulerSpec::eHorizontal;
|
|
const bool bInverted = (bPositiveDir && !bHorizontal) || (!bPositiveDir && bHorizontal);
|
|
|
|
//
|
|
double dAbsoluteFactor = _dZoomFactor * nViewLength / dScaleLength;
|
|
if (bInverted)
|
|
dAbsoluteFactor = -dAbsoluteFactor;
|
|
|
|
//
|
|
double dAbsoluteShiftValue = - dAbsoluteFactor;
|
|
if (bInverted)
|
|
dAbsoluteShiftValue *= _dValueOffset + dScaleLength / _dZoomFactor;
|
|
else
|
|
dAbsoluteShiftValue *= _dValueOffset;
|
|
|
|
// Post zoom event properties for other views to apply it.
|
|
emit zoomShift(dAbsoluteFactor, dAbsoluteShiftValue);
|
|
}
|
|
|
|
void Ruler::resize(const QSize& qSize)
|
|
{
|
|
// Get ruler spec.
|
|
const bool bHorizontal = _pSpec->orientation() == RulerSpec::eHorizontal;
|
|
|
|
// Get new view length.
|
|
const int nViewLength = bHorizontal ? qSize.width() : qSize.height();
|
|
|
|
// Post zoom event properties for other views to apply it.
|
|
propagateZoomShift(nViewLength);
|
|
}
|
|
|
|
void Ruler::zoom(const QPoint& qpAnchorPos, double dRelativeFactor)
|
|
{
|
|
// Get ruler spec.
|
|
const double dScaleMinValue = _pSpec->minValue();
|
|
const double dScaleMaxValue = _pSpec->maxValue();
|
|
const double dScaleLength = dScaleMaxValue - dScaleMinValue;
|
|
|
|
const bool bPositiveDir = _pSpec->direction() == RulerSpec::ePositive;
|
|
const bool bHorizontal = _pSpec->orientation() == RulerSpec::eHorizontal;
|
|
const bool bInverted = (bPositiveDir && !bHorizontal) || (!bPositiveDir && bHorizontal);
|
|
|
|
// Keep from zooming under 1.0 factor.
|
|
if (_dZoomFactor == 1.0 && dRelativeFactor < 1.0)
|
|
return;
|
|
|
|
// Determine new absolute zoom factor (>= 1.0).
|
|
const double dOldZoomFactor = _dZoomFactor;
|
|
_dZoomFactor *= dRelativeFactor;
|
|
|
|
if (_dZoomFactor < 1.01)
|
|
{
|
|
// Prevent zoom factor from being < 1, force it to 1 when positive but close enough,
|
|
// and force offset to 0 in such cases (to fix errors due to repeated * and /)
|
|
_dZoomFactor = 1.0;
|
|
_dValueOffset = 0.0;
|
|
dRelativeFactor = _dZoomFactor / dOldZoomFactor;
|
|
}
|
|
|
|
// Determine the new view offset (but keep it inside [0, scaleMax-viewMaxVal+viewMinVal]).
|
|
const int nViewLength = bHorizontal ? width() : height();
|
|
|
|
const double dOldPixelValue = dScaleLength / dOldZoomFactor / nViewLength;
|
|
const double dNewPixelValue = dScaleLength / _dZoomFactor / nViewLength;
|
|
|
|
int nValue = bHorizontal ? qpAnchorPos.x() : qpAnchorPos.y();
|
|
if (bInverted)
|
|
nValue = nViewLength - 1 - nValue;
|
|
|
|
_dValueOffset += nValue * (dOldPixelValue - dNewPixelValue);
|
|
if (_dValueOffset < 0 || _dValueOffset + dScaleLength / _dZoomFactor > dScaleMaxValue)
|
|
_dValueOffset = 0;
|
|
// Note: No need to update _dZoomFactor, as already forced to >= 1.0
|
|
|
|
// Redraw the ruler.
|
|
repaint();
|
|
|
|
// std::cout << "Ruler::wheelEvent(relFact=" << dRelativeFactor
|
|
// << ", anchor=" << nValue
|
|
// << ") : zoomFactor=" << _dZoomFactor
|
|
// << ", offsetValue=" << _dValueOffset << std::endl;
|
|
|
|
// Post zoom event properties for other views to apply it.
|
|
propagateZoomShift(nViewLength);
|
|
}
|
|
|
|
void Ruler::shift(const QPoint& qpNewPos)
|
|
{
|
|
// Get ruler spec.
|
|
const double dScaleMinValue = _pSpec->minValue();
|
|
const double dScaleMaxValue = _pSpec->maxValue();
|
|
const double dScaleLength = dScaleMaxValue - dScaleMinValue;
|
|
|
|
const bool bPositiveDir = _pSpec->direction() == RulerSpec::ePositive;
|
|
const bool bHorizontal = _pSpec->orientation() == RulerSpec::eHorizontal;
|
|
const bool bInverted = (bPositiveDir && !bHorizontal) || (!bPositiveDir && bHorizontal);
|
|
|
|
// Compute view properties.
|
|
const int nViewLength = bHorizontal ? width() : height();
|
|
const double dPixelValue = dScaleLength / _dZoomFactor / nViewLength;
|
|
|
|
// Compute the shifting amount
|
|
int nShift;
|
|
if (bHorizontal)
|
|
nShift = _qpLastShiftingPos.x() - qpNewPos.x();
|
|
else
|
|
nShift = _qpLastShiftingPos.y() - qpNewPos.y();
|
|
if (bInverted)
|
|
nShift = - nShift;
|
|
|
|
// Update current offset from scale origin.
|
|
_dValueOffset += nShift * dPixelValue;
|
|
if (_dValueOffset < 0)
|
|
_dValueOffset = 0;
|
|
const double dNewViewMaxValue =
|
|
dScaleMinValue + _dValueOffset + dScaleLength / _dZoomFactor;
|
|
if (dNewViewMaxValue > dScaleMaxValue)
|
|
_dValueOffset = dScaleMaxValue - nViewLength * dPixelValue;
|
|
//_dValueOffset = dScaleMaxValue - dScaleLength / _dZoomFactor;
|
|
|
|
// Redraw the scale.
|
|
repaint();
|
|
|
|
// std::cout << "Ruler::shift : shift=" << nShift
|
|
// << ", offsetValue=" << _dValueOffset
|
|
// << ", absFactor=" << _dZoomFactor
|
|
// << ", pixValue=" << dPixelValue << std::endl;
|
|
|
|
// Post zoom event properties for other views to apply it.
|
|
propagateZoomShift(nViewLength);
|
|
}
|
|
|
|
//===================================================================================
|
|
// Draw the scale according to the ruler spec and current zoom factor and translation
|
|
static const int nUnitPrefixMinIndex = -3;
|
|
static const int nUnitPrefixMaxIndex = +3;
|
|
static const int nUnitPrefixes = nUnitPrefixMaxIndex - nUnitPrefixMinIndex + 1;
|
|
static const char* pszUnitPrefix[nUnitPrefixes] = { "n", "u", "m", "", "k", "M", "g" };
|
|
|
|
void Ruler::draw(QPainter& qPainter)
|
|
{
|
|
qPainter.setPen(Qt::black);
|
|
|
|
// Get ruler specs.
|
|
const bool bPositiveDir = _pSpec->direction() == RulerSpec::ePositive;
|
|
const bool bPositiveSide = _pSpec->marksSide() == RulerSpec::ePositiveSide;
|
|
const bool bHorizontal = _pSpec->orientation() == RulerSpec::eHorizontal;
|
|
|
|
typedef enum { eNorth = 0, eWest, eSouth, eEast } EOrientation;
|
|
const EOrientation eOrient =
|
|
(bHorizontal ? (bPositiveSide ? eNorth : eSouth) : (bPositiveSide ? eEast : eWest));
|
|
|
|
const double dScaleMinValue = _pSpec->minValue();
|
|
const double dScaleMaxValue = _pSpec->maxValue();
|
|
const double dScaleLength = dScaleMaxValue - dScaleMinValue;
|
|
|
|
// Get widget dimensions.
|
|
const int nViewLength = bHorizontal ? width() : height();
|
|
const int nViewThickness = bHorizontal ? height() : width();
|
|
|
|
|
|
// Draw the main line of the ruler.
|
|
switch (eOrient)
|
|
{
|
|
case eNorth:
|
|
qPainter.drawLine(0, 0, nViewLength - 1, 0);
|
|
break;
|
|
|
|
case eSouth:
|
|
qPainter.drawLine(0, nViewThickness - 1, nViewLength - 1, nViewThickness - 1);
|
|
break;
|
|
|
|
case eWest:
|
|
qPainter.drawLine(0, 0, 0, nViewLength - 1);
|
|
break;
|
|
|
|
case eEast:
|
|
qPainter.drawLine(nViewThickness - 1, 0, nViewThickness - 1, nViewLength - 1);
|
|
break;
|
|
}
|
|
|
|
// Determine the value interval of the view
|
|
double dViewMinValue = dScaleMinValue + _dValueOffset;
|
|
double dViewMaxValue = dViewMinValue + dScaleLength / _dZoomFactor;
|
|
|
|
// Determine the resolution of a pixel in the view
|
|
const double dPixelValue = dScaleLength / _dZoomFactor / nViewLength;
|
|
|
|
//std::cout << "Ruler::draw(orient=" << eOrient << ", dir=" << _pSpec->direction()
|
|
// << ") : viewMinValue=" << dViewMinValue
|
|
// << ", viewMaxValue=" << dViewMaxValue
|
|
// << ", pixelValue=" << dPixelValue << std::endl;
|
|
|
|
// Determine the distance between main scale marks
|
|
// (criteria = at least 5 pixels between 1st secondary scale marks).
|
|
const int nMinValueStep = 5;
|
|
const double dLevelDivisor = _pSpec->levelDivisor();
|
|
double dValueStep = _pSpec->baseStep();
|
|
//while (dValueStep >= nMinValueStep * dLevelDivisor * dPixelValue)
|
|
while (2 * dValueStep > dScaleLength / _dZoomFactor)
|
|
{
|
|
dValueStep /= dLevelDivisor;
|
|
}
|
|
|
|
// Determine the lowest value of any main scale mark visible in the view
|
|
double dFirstMarkValue =
|
|
dScaleMinValue + _pSpec->baseOffset()
|
|
+ dValueStep
|
|
* ceil((dViewMinValue - dScaleMinValue - _pSpec->baseOffset()) / dValueStep);
|
|
|
|
//std::cout << "Ruler::draw(main) : markLength=" << nViewThickness
|
|
// << ", valueStep=" << dValueStep
|
|
// << ", 1stMarkValue=" << dFirstMarkValue << std::endl;
|
|
|
|
// Draw the main scale marks and associated labels
|
|
//const QFont qFont("Tahoma", 7); // Windows very good.
|
|
//qPainter.setFont(qFont);
|
|
const QFontMetrics qFontMtrx = qPainter.fontMetrics();
|
|
const bool bInverted = (bPositiveDir && !bHorizontal) || (!bPositiveDir && bHorizontal);
|
|
double dValue = dFirstMarkValue;
|
|
int nValue;
|
|
while (dValue <= dViewMaxValue)
|
|
{
|
|
// Mark coordinate (pixel) in the "long" direction of the view.
|
|
nValue = (int)round((dValue - dViewMinValue) / dPixelValue);
|
|
if (nValue == nViewLength)
|
|
nValue--;
|
|
if (bInverted)
|
|
nValue = nViewLength - 1 - nValue;
|
|
|
|
switch (eOrient)
|
|
{
|
|
case eNorth:
|
|
case eSouth:
|
|
qPainter.drawLine(nValue, 0, nValue, nViewThickness - 1);
|
|
break;
|
|
|
|
case eWest:
|
|
case eEast:
|
|
qPainter.drawLine(0, nValue, nViewThickness - 1, nValue);
|
|
break;
|
|
}
|
|
|
|
// Convert value to a user-friendly label string.
|
|
const char* pszLabelSign = dValue > 0.0 ? "" : "-";
|
|
double dLabelValue = fabs(dValue);
|
|
QString qsLabel("0");
|
|
if (dValue != 0.0)
|
|
{
|
|
const int nUnitPrefixIndex = (int)(floor(log10(dLabelValue))) / 3;
|
|
if (nUnitPrefixIndex < nUnitPrefixMinIndex || nUnitPrefixIndex > nUnitPrefixMaxIndex)
|
|
qsLabel = QString("%1").arg(dValue, 0, 'g', 3);
|
|
else
|
|
{
|
|
dLabelValue /= pow(10.0, 3*nUnitPrefixIndex);
|
|
qsLabel = QString("%1%2%3").arg(pszLabelSign).arg(dLabelValue, 0, 'g', 3)
|
|
.arg(pszUnitPrefix[nUnitPrefixIndex - nUnitPrefixMinIndex]);
|
|
}
|
|
}
|
|
|
|
// Draw the mark label.
|
|
const QSize qsLabelSize = qFontMtrx.size(Qt::TextSingleLine, qsLabel);
|
|
const int nLabeWidth = qsLabelSize.width();
|
|
const int nLabeHeight = qsLabelSize.height();
|
|
|
|
nValue += (bHorizontal ? (bInverted ? -3 - nLabeWidth : 3) : -3);
|
|
|
|
switch (eOrient)
|
|
{
|
|
case eNorth:
|
|
qPainter.drawText(nValue, nViewThickness - 1, qsLabel);
|
|
break;
|
|
|
|
case eSouth:
|
|
qPainter.drawText(nValue, nLabeHeight, qsLabel);
|
|
break;
|
|
|
|
case eWest:
|
|
qPainter.drawText(nViewThickness - nLabeWidth, nValue, qsLabel);
|
|
break;
|
|
|
|
case eEast:
|
|
qPainter.drawText(0, nValue, qsLabel);
|
|
break;
|
|
}
|
|
|
|
// Next mark.
|
|
dValue += dValueStep;
|
|
}
|
|
|
|
// Draw the secondary scale marks
|
|
const QList<double>& qlSecondDivisors = _pSpec->secondaryDivisors();
|
|
const QList<double>& qlSecondSizes = _pSpec->secondaryMarksSizes();
|
|
for (int nSecondInd = 0; nSecondInd < qlSecondDivisors.size(); nSecondInd++)
|
|
{
|
|
const int nMarkLength = (int)round(qlSecondSizes[nSecondInd] * nViewThickness);
|
|
dValueStep /= qlSecondDivisors[nSecondInd];
|
|
|
|
// Don't draw secondary scales under a certain inter-mark distance threshold.
|
|
if (dValueStep < nMinValueStep * dPixelValue)
|
|
break;
|
|
|
|
dFirstMarkValue =
|
|
dScaleMinValue + _pSpec->baseOffset()
|
|
+ dValueStep
|
|
* ceil((dViewMinValue - dScaleMinValue - _pSpec->baseOffset()) / dValueStep);
|
|
|
|
//dFirstMarkValue = remainder(dFirstMarkValue - dViewMinValue, dValueStep);
|
|
dValue = dFirstMarkValue;
|
|
|
|
//std::cout << "Ruler::draw(secondary) : markLength=" << nMarkLength
|
|
// << ", valueStep=" << dValueStep
|
|
// << ", 1stMarkValue=" << dFirstMarkValue << std::endl;
|
|
|
|
while (dValue <= dViewMaxValue)
|
|
{
|
|
// mark coordinate (pixel) in the "long" direction of the view.
|
|
nValue = (int)round((dValue - dViewMinValue) / dPixelValue);
|
|
if (nValue == nViewLength)
|
|
nValue--;
|
|
if (bInverted)
|
|
nValue = nViewLength - 1 - nValue;
|
|
|
|
switch (eOrient)
|
|
{
|
|
case eNorth:
|
|
qPainter.drawLine(nValue, 0, nValue, nMarkLength - 1);
|
|
break;
|
|
|
|
case eSouth:
|
|
qPainter.drawLine(nValue, nViewThickness - nMarkLength,
|
|
nValue, nViewThickness - 1);
|
|
break;
|
|
|
|
case eWest:
|
|
qPainter.drawLine(0, nValue, nMarkLength - 1, nValue);
|
|
break;
|
|
|
|
case eEast:
|
|
qPainter.drawLine(nViewThickness - nMarkLength, nValue,
|
|
nViewThickness - 1, nValue);
|
|
break;
|
|
}
|
|
|
|
dValue += dValueStep;
|
|
}
|
|
}
|
|
}
|