Skip to content

Commit

Permalink
[composer] Improve appearance of rulers, add small tick marks to ruler (
Browse files Browse the repository at this point in the history
fix #5656)
  • Loading branch information
nyalldawson committed Dec 29, 2013
1 parent d02be61 commit 097a195
Show file tree
Hide file tree
Showing 2 changed files with 267 additions and 31 deletions.
277 changes: 246 additions & 31 deletions src/gui/qgscomposerruler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
#include <cmath>

const int RULER_MIN_SIZE = 20;
const int COUNT_VALID_MULTIPLES = 3;
const int COUNT_VALID_MAGNITUDES = 5;
const int QgsComposerRuler::validScaleMultiples[] = {1, 2, 5};
const int QgsComposerRuler::validScaleMagnitudes[] = {1, 10, 100, 1000, 10000};

QgsComposerRuler::QgsComposerRuler( QgsComposerRuler::Direction d ): QWidget( 0 ), mDirection( d ), mComposition( 0 ), mLineSnapItem( 0 )
{
Expand Down Expand Up @@ -34,20 +38,22 @@ void QgsComposerRuler::paintEvent( QPaintEvent* event )

QTransform t = mTransform.inverted();

//find optimal ruler display scale (steps of 1, 10 or 50)
double pixelDiff1 = mTransform.map( QPointF( 1, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
double pixelDiff10 = mTransform.map( QPointF( 10, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
//double pixelDiff50 = mTransform.map( QPointF( 50, 0 ) ).x() - mTransform.map( QPointF( 5, 0 ) ).x();
//calculate minimum size required for ruler text
QFont rulerFont = p.font();
rulerFont.setPointSize( 8 );
QFontMetrics rulerFontMetrics( rulerFont );
//minimum gap required between major ticks is 3 digits * 250%
double minFontPixelsWidth = rulerFontMetrics.width( "000" ) * 2.5;
p.setFont( rulerFont );

double mmDisplay = 50.0;
if ( pixelDiff1 > 25 )
{
mmDisplay = 1.0;
}
else if ( pixelDiff10 > 25 )
{
mmDisplay = 10.0;
}
//find optimum scale for ruler (size of numbered divisions)
int magnitude = 1;
int multiple = 1;
int mmDisplay;
mmDisplay = optimumScale( minFontPixelsWidth, magnitude, multiple );

//find optimum number of small divisions
int numSmallDivisions = optimumNumberDivisions( mmDisplay, multiple );

if ( mDirection == Horizontal )
{
Expand All @@ -60,20 +66,25 @@ void QgsComposerRuler::paintEvent( QPaintEvent* event )
double startX = t.map( QPointF( 0, 0 ) ).x();
double endX = t.map( QPointF( width(), 0 ) ).x();

double markerPos = ( floor( startX / mmDisplay ) + 1 ) * mmDisplay; //marker position in mm
//start marker position in mm
double markerPos = ( floor( startX / mmDisplay ) + 1 ) * mmDisplay;

//draw minor ticks marks which occur before first major tick
drawSmallDivisions( &p, markerPos, numSmallDivisions, -mmDisplay );

while ( markerPos <= endX )
{
if ( markerPos >= 0 && markerPos <= mComposition->paperWidth() ) //todo: need to know paper size
{
double pixelCoord = mTransform.map( QPointF( markerPos, 0 ) ).x();
p.drawLine( pixelCoord, 0, pixelCoord, RULER_MIN_SIZE );
p.drawText( QPointF( pixelCoord + 2, RULER_MIN_SIZE / 2.0 ), QString::number(( int )( markerPos ) ) );
}
double pixelCoord = mTransform.map( QPointF( markerPos, 0 ) ).x();

//draw large division and text
p.drawLine( pixelCoord, 0, pixelCoord, RULER_MIN_SIZE );
p.drawText( QPointF( pixelCoord + 2, RULER_MIN_SIZE / 2.0 + 2 ), QString::number( markerPos ) );

//draw small divisions
drawSmallDivisions( &p, markerPos, numSmallDivisions, mmDisplay, endX );

markerPos += mmDisplay;
}

p.setPen( QColor( Qt::red ) );
p.drawLine( mMarkerPos.x(), 0, mMarkerPos.x(), RULER_MIN_SIZE );
}
else //vertical
{
Expand All @@ -89,36 +100,240 @@ void QgsComposerRuler::paintEvent( QPaintEvent* event )
{
startPage = 0;
}

if ( startY < 0 )
{
double beforePageCoord = 0;
double firstPageY = mTransform.map( QPointF( 0, 0 ) ).y();

//draw negative rulers which fall before first page
while ( beforePageCoord > startY )
{
double pixelCoord = mTransform.map( QPointF( 0, beforePageCoord ) ).y();
p.drawLine( 0, pixelCoord, RULER_MIN_SIZE, pixelCoord );
//calc size of label
QString label = QString::number( beforePageCoord );
int labelSize = rulerFontMetrics.width( label );

//draw label only if it fits in before start of next page
if ( pixelCoord + labelSize + 8 < firstPageY )
{
drawRotatedText( &p, QPointF( RULER_MIN_SIZE / 2.0 + 2.0, pixelCoord + 4.0 + labelSize ), label );
}

//draw small divisions
drawSmallDivisions( &p, beforePageCoord, numSmallDivisions, mmDisplay );

beforePageCoord -= mmDisplay;
}

//draw minor ticks marks which occur before first major tick
drawSmallDivisions( &p, beforePageCoord + mmDisplay, numSmallDivisions, -mmDisplay, startY );
}

int endPage = ( int )( endY / ( mComposition->paperHeight() + mComposition->spaceBetweenPages() ) );
if ( endPage > ( mComposition->numPages() - 1 ) )
{
endPage = mComposition->numPages() - 1;
}

double nextPageStartPos = 0;
int nextPageStartPixel = 0;

for ( int i = startPage; i <= endPage; ++i )
{
double pageCoord = 0; //page coordinate in mm
//total (composition) coordinate in mm, including space between pages
double totalCoord = i * ( mComposition->paperHeight() + mComposition->spaceBetweenPages() );
while ( pageCoord < mComposition->paperHeight() )

//position of next page
if ( i < endPage )
{
//not the last page
nextPageStartPos = ( i + 1 ) * ( mComposition->paperHeight() + mComposition->spaceBetweenPages() );
nextPageStartPixel = mTransform.map( QPointF( 0, nextPageStartPos ) ).y();
}
else
{
//is the last page
nextPageStartPos = 0;
nextPageStartPixel = 0;
}

while (( totalCoord < nextPageStartPos ) || (( nextPageStartPos == 0 ) && ( totalCoord <= endY ) ) )
{
if ( totalCoord > endY )
{
break;
}
double pixelCoord = mTransform.map( QPointF( 0, totalCoord ) ).y();
p.drawLine( 0, pixelCoord, RULER_MIN_SIZE, pixelCoord );
p.drawText( QPointF( 0, pixelCoord - 2.0 ), QString::number( pageCoord ) );
//calc size of label
QString label = QString::number( pageCoord );
int labelSize = rulerFontMetrics.width( label );

//draw label only if it fits in before start of next page
if (( pixelCoord + labelSize + 8 < nextPageStartPixel )
|| ( nextPageStartPixel == 0 ) )
{
drawRotatedText( &p, QPointF( RULER_MIN_SIZE / 2.0 + 2.0, pixelCoord + 4.0 + labelSize ), label );
}

//draw small divisions
drawSmallDivisions( &p, totalCoord, numSmallDivisions, mmDisplay, nextPageStartPos );

pageCoord += mmDisplay;
totalCoord += mmDisplay;
}
}
}

//draw current marker pos
drawMarkerPos( &p );
}

p.setPen( QColor( Qt::red ) );
p.drawLine( 0, mMarkerPos.y(), RULER_MIN_SIZE, mMarkerPos.y() );
void QgsComposerRuler::drawMarkerPos( QPainter *painter )
{
//draw current marker pos in red
painter->setPen( QColor( Qt::red ) );
if ( mDirection == Horizontal )
{
painter->drawLine( mMarkerPos.x(), 0, mMarkerPos.x(), RULER_MIN_SIZE );
}
else
{
painter->drawLine( 0, mMarkerPos.y(), RULER_MIN_SIZE, mMarkerPos.y() );
}
}

void QgsComposerRuler::drawRotatedText( QPainter *painter, QPointF pos, const QString &text )
{
painter->save();
painter->translate( pos.x(), pos.y() );
painter->rotate( 270 );
painter->drawText( 0, 0, text );
painter->restore();
}

void QgsComposerRuler::drawSmallDivisions( QPainter *painter, double startPos, int numDivisions, double rulerScale, double maxPos )
{
//draw small divisions starting at startPos (in mm)
double smallMarkerPos = startPos;
double smallDivisionSpacing = rulerScale / numDivisions;

double pixelCoord;

//draw numDivisions small divisions
for ( int i = 0; i < numDivisions; ++i )
{
smallMarkerPos += smallDivisionSpacing;

if ( maxPos > 0 && smallMarkerPos > maxPos )
{
//stop drawing current division position is past maxPos
return;
}

//calculate pixelCoordinate of the current division
if ( mDirection == Horizontal )
{
pixelCoord = mTransform.map( QPointF( smallMarkerPos, 0 ) ).x();
}
else
{
pixelCoord = mTransform.map( QPointF( 0, smallMarkerPos ) ).y();
}

//calculate height of small division line
double lineSize;
if (( numDivisions == 10 && i == 4 ) || ( numDivisions == 4 && i == 1 ) )
{
//if drawing the 5th line of 10 or drawing the 2nd line of 4, then draw it slightly longer
lineSize = RULER_MIN_SIZE / 1.5;
}
else
{
lineSize = RULER_MIN_SIZE / 1.25;
}

//draw either horizontal or vertical line depending on ruler direction
if ( mDirection == Horizontal )
{
painter->drawLine( pixelCoord, lineSize, pixelCoord, RULER_MIN_SIZE );
}
else
{
painter->drawLine( lineSize, pixelCoord, RULER_MIN_SIZE, pixelCoord );
}
}
}

int QgsComposerRuler::optimumScale( double minPixelDiff, int &magnitude, int &multiple )
{
//find optimal ruler display scale

//loop through magnitudes and multiples to find optimum scale
for ( unsigned int magnitudeCandidate = 0; magnitudeCandidate < COUNT_VALID_MAGNITUDES; ++magnitudeCandidate )
{
for ( unsigned int multipleCandidate = 0; multipleCandidate < COUNT_VALID_MULTIPLES; ++multipleCandidate )
{
int candidateScale = validScaleMultiples[multipleCandidate] * validScaleMagnitudes[magnitudeCandidate];
//find pixel size for each step using this candidate scale
double pixelDiff = mTransform.map( QPointF( candidateScale, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
if ( pixelDiff > minPixelDiff )
{
//found the optimum major scale
magnitude = validScaleMagnitudes[magnitudeCandidate];
multiple = validScaleMultiples[multipleCandidate];
return candidateScale;
}
}
}

return 100000;
}

int QgsComposerRuler::optimumNumberDivisions( double rulerScale, int scaleMultiple )
{
//calculate size in pixels of each marked ruler unit
double largeDivisionSize = mTransform.map( QPointF( rulerScale, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();

//now calculate optimum small tick scale, depending on marked ruler units
QList<int> validSmallDivisions;
switch ( scaleMultiple )
{
case 1:
//numbers increase by 1 increment each time, eg 1, 2, 3 or 10, 20, 30
//so we can draw either 10, 5 or 2 small ticks and have each fall on a nice value
validSmallDivisions << 10 << 5 << 2;
break;
case 2:
//numbers increase by 2 increments each time, eg 2, 4, 6 or 20, 40, 60
//so we can draw either 10, 4 or 2 small ticks and have each fall on a nice value
validSmallDivisions << 10 << 4 << 2;
break;
case 5:
//numbers increase by 5 increments each time, eg 5, 10, 15 or 100, 500, 1000
//so we can draw either 10 or 5 small ticks and have each fall on a nice value
validSmallDivisions << 10 << 5;
break;
}

//calculate the most number of small divisions we can draw without them being too close to each other
QList<int>::iterator divisions_it;
for ( divisions_it = validSmallDivisions.begin(); divisions_it != validSmallDivisions.end(); ++divisions_it )
{
//find pixel size for this small division
double candidateSize = largeDivisionSize / ( *divisions_it );
//small divisions must be seperated by at least 4 pixels
if ( candidateSize >= 4 )
{
//found a good candidate, return it
return ( *divisions_it );
}
}

//unable to find a good candidate
return 0;
}


void QgsComposerRuler::setSceneTransform( const QTransform& transform )
{
QString debug = QString::number( transform.dx() ) + "," + QString::number( transform.dy() ) + ","
Expand Down
21 changes: 21 additions & 0 deletions src/gui/qgscomposerruler.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class GUI_EXPORT QgsComposerRuler: public QWidget
void mousePressEvent( QMouseEvent* event );

private:
static const int validScaleMultiples[];
static const int validScaleMagnitudes[];

Direction mDirection;
QTransform mTransform;
QPointF mMarkerPos;
Expand All @@ -46,6 +49,24 @@ class GUI_EXPORT QgsComposerRuler: public QWidget

void setSnapLinePosition( const QPointF& pos );

//calculate optimum labeled units for ruler so that labels are a good distance apart
int optimumScale( double minPixelDiff, int &magnitude, int &multiple );
//calculate number of small divisions for each ruler unit, ensuring that they
//are sufficiently spaced
int optimumNumberDivisions( double rulerScale, int scaleMultiple );

//draws vertical text on a painter
void drawRotatedText( QPainter *painter, QPointF pos, const QString &text );

/* Draws small ruler divisions
* Starting at startPos in mm, for numDivisions divisions, with major division spacing of rulerScale (in mm)
* Stop drawing if position exceeds maxPos
*/
void drawSmallDivisions( QPainter *painter, double startPos, int numDivisions, double rulerScale, double maxPos = 0 );

//draw current marker pos on ruler
void drawMarkerPos( QPainter *painter );

signals:
/**Is emitted when mouse cursor coordinates change*/
void cursorPosChanged( QPointF );
Expand Down

0 comments on commit 097a195

Please sign in to comment.