QGIS API Documentation  2.17.0-Master (f49f7ce)
qgscomposerruler.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgscomposerruler.cpp
3  ---------------------
4  begin : January 2013
5  copyright : (C) 2013 by Marco Hugentobler
6  email : marco dot hugentobler at sourcepole dot ch
7  ***************************************************************************
8  * *
9  * This program is free software; you can redistribute it and/or modify *
10  * it under the terms of the GNU General Public License as published by *
11  * the Free Software Foundation; either version 2 of the License, or *
12  * (at your option) any later version. *
13  * *
14  ***************************************************************************/
15 #include "qgscomposerruler.h"
16 #include "qgscomposition.h"
17 #include "qgis.h"
18 #include <QDragEnterEvent>
19 #include <QGraphicsLineItem>
20 #include <QPainter>
21 #include <cmath>
22 
23 const int RULER_FONT_SIZE = 8;
24 const unsigned int COUNT_VALID_MULTIPLES = 3;
25 const unsigned int COUNT_VALID_MAGNITUDES = 5;
26 const int QgsComposerRuler::validScaleMultiples[] = {1, 2, 5};
27 const int QgsComposerRuler::validScaleMagnitudes[] = {1, 10, 100, 1000, 10000};
28 
30  : QWidget( nullptr )
31  , mDirection( d )
32  , mComposition( nullptr )
33  , mLineSnapItem( nullptr )
34  , mScaleMinPixelsWidth( 0 )
35 {
36  setMouseTracking( true );
37 
38  //calculate minimum size required for ruler text
39  mRulerFont = new QFont();
40  mRulerFont->setPointSize( RULER_FONT_SIZE );
41  mRulerFontMetrics = new QFontMetrics( *mRulerFont );
42 
43  //calculate ruler sizes and marker separations
44 
45  //minimum gap required between major ticks is 3 digits * 250%, based on appearance
46  mScaleMinPixelsWidth = mRulerFontMetrics->width( "000" ) * 2.5;
47  //minimum ruler height is twice the font height in pixels
48  mRulerMinSize = mRulerFontMetrics->height() * 1.5;
49 
50  mMinPixelsPerDivision = mRulerMinSize / 4;
51  //each small division must be at least 2 pixels apart
52  if ( mMinPixelsPerDivision < 2 )
53  mMinPixelsPerDivision = 2;
54 
55  mPixelsBetweenLineAndText = mRulerMinSize / 10;
56  mTextBaseline = mRulerMinSize / 1.667;
57  mMinSpacingVerticalLabels = mRulerMinSize / 5;
58 }
59 
61 {
62  delete mRulerFontMetrics;
63  delete mRulerFont;
64 }
65 
67 {
68  return QSize( mRulerMinSize, mRulerMinSize );
69 }
70 
72 {
73  Q_UNUSED( event );
74  if ( !mComposition )
75  {
76  return;
77  }
78 
79  QPainter p( this );
80 
81  QTransform t = mTransform.inverted();
82 
83  p.setFont( *mRulerFont );
84 
85  //find optimum scale for ruler (size of numbered divisions)
86  int magnitude = 1;
87  int multiple = 1;
88  int mmDisplay;
89  mmDisplay = optimumScale( mScaleMinPixelsWidth, magnitude, multiple );
90 
91  //find optimum number of small divisions
92  int numSmallDivisions = optimumNumberDivisions( mmDisplay, multiple );
93 
94  if ( mDirection == Horizontal )
95  {
96  if ( qgsDoubleNear( width(), 0 ) )
97  {
98  return;
99  }
100 
101  //start x-coordinate
102  double startX = t.map( QPointF( 0, 0 ) ).x();
103  double endX = t.map( QPointF( width(), 0 ) ).x();
104 
105  //start marker position in mm
106  double markerPos = ( floor( startX / mmDisplay ) + 1 ) * mmDisplay;
107 
108  //draw minor ticks marks which occur before first major tick
109  drawSmallDivisions( &p, markerPos, numSmallDivisions, -mmDisplay );
110 
111  while ( markerPos <= endX )
112  {
113  double pixelCoord = mTransform.map( QPointF( markerPos, 0 ) ).x();
114 
115  //draw large division and text
116  p.drawLine( pixelCoord, 0, pixelCoord, mRulerMinSize );
117  p.drawText( QPointF( pixelCoord + mPixelsBetweenLineAndText, mTextBaseline ), QString::number( markerPos ) );
118 
119  //draw small divisions
120  drawSmallDivisions( &p, markerPos, numSmallDivisions, mmDisplay, endX );
121 
122  markerPos += mmDisplay;
123  }
124  }
125  else //vertical
126  {
127  if ( qgsDoubleNear( height(), 0 ) )
128  {
129  return;
130  }
131 
132  double startY = t.map( QPointF( 0, 0 ) ).y(); //start position in mm (total including space between pages)
133  double endY = t.map( QPointF( 0, height() ) ).y(); //stop position in mm (total including space between pages)
134  int startPage = ( int )( startY / ( mComposition->paperHeight() + mComposition->spaceBetweenPages() ) );
135  if ( startPage < 0 )
136  {
137  startPage = 0;
138  }
139 
140  if ( startY < 0 )
141  {
142  double beforePageCoord = -mmDisplay;
143  double firstPageY = mTransform.map( QPointF( 0, 0 ) ).y();
144 
145  //draw negative rulers which fall before first page
146  while ( beforePageCoord > startY )
147  {
148  double pixelCoord = mTransform.map( QPointF( 0, beforePageCoord ) ).y();
149  p.drawLine( 0, pixelCoord, mRulerMinSize, pixelCoord );
150  //calc size of label
151  QString label = QString::number( beforePageCoord );
152  int labelSize = mRulerFontMetrics->width( label );
153 
154  //draw label only if it fits in before start of next page
155  if ( pixelCoord + labelSize + 8 < firstPageY )
156  {
157  drawRotatedText( &p, QPointF( mTextBaseline, pixelCoord + mMinSpacingVerticalLabels + labelSize ), label );
158  }
159 
160  //draw small divisions
161  drawSmallDivisions( &p, beforePageCoord, numSmallDivisions, mmDisplay );
162 
163  beforePageCoord -= mmDisplay;
164  }
165 
166  //draw minor ticks marks which occur before first major tick
167  drawSmallDivisions( &p, beforePageCoord + mmDisplay, numSmallDivisions, -mmDisplay, startY );
168  }
169 
170  int endPage = ( int )( endY / ( mComposition->paperHeight() + mComposition->spaceBetweenPages() ) );
171  if ( endPage > ( mComposition->numPages() - 1 ) )
172  {
173  endPage = mComposition->numPages() - 1;
174  }
175 
176  double nextPageStartPos = 0;
177  int nextPageStartPixel = 0;
178 
179  for ( int i = startPage; i <= endPage; ++i )
180  {
181  double pageCoord = 0; //page coordinate in mm
182  //total (composition) coordinate in mm, including space between pages
183  double totalCoord = i * ( mComposition->paperHeight() + mComposition->spaceBetweenPages() );
184 
185  //position of next page
186  if ( i < endPage )
187  {
188  //not the last page
189  nextPageStartPos = ( i + 1 ) * ( mComposition->paperHeight() + mComposition->spaceBetweenPages() );
190  nextPageStartPixel = mTransform.map( QPointF( 0, nextPageStartPos ) ).y();
191  }
192  else
193  {
194  //is the last page
195  nextPageStartPos = 0;
196  nextPageStartPixel = 0;
197  }
198 
199  while (( totalCoord < nextPageStartPos ) || (( nextPageStartPos == 0 ) && ( totalCoord <= endY ) ) )
200  {
201  double pixelCoord = mTransform.map( QPointF( 0, totalCoord ) ).y();
202  p.drawLine( 0, pixelCoord, mRulerMinSize, pixelCoord );
203  //calc size of label
204  QString label = QString::number( pageCoord );
205  int labelSize = mRulerFontMetrics->width( label );
206 
207  //draw label only if it fits in before start of next page
208  if (( pixelCoord + labelSize + 8 < nextPageStartPixel )
209  || ( nextPageStartPixel == 0 ) )
210  {
211  drawRotatedText( &p, QPointF( mTextBaseline, pixelCoord + mMinSpacingVerticalLabels + labelSize ), label );
212  }
213 
214  //draw small divisions
215  drawSmallDivisions( &p, totalCoord, numSmallDivisions, mmDisplay, nextPageStartPos );
216 
217  pageCoord += mmDisplay;
218  totalCoord += mmDisplay;
219  }
220  }
221  }
222 
223  //draw current marker pos
224  drawMarkerPos( &p );
225 }
226 
227 void QgsComposerRuler::drawMarkerPos( QPainter *painter )
228 {
229  //draw current marker pos in red
230  painter->setPen( QColor( Qt::red ) );
231  if ( mDirection == Horizontal )
232  {
233  painter->drawLine( mMarkerPos.x(), 0, mMarkerPos.x(), mRulerMinSize );
234  }
235  else
236  {
237  painter->drawLine( 0, mMarkerPos.y(), mRulerMinSize, mMarkerPos.y() );
238  }
239 }
240 
241 void QgsComposerRuler::drawRotatedText( QPainter *painter, QPointF pos, const QString &text )
242 {
243  painter->save();
244  painter->translate( pos.x(), pos.y() );
245  painter->rotate( 270 );
246  painter->drawText( 0, 0, text );
247  painter->restore();
248 }
249 
250 void QgsComposerRuler::drawSmallDivisions( QPainter *painter, double startPos, int numDivisions, double rulerScale, double maxPos )
251 {
252  if ( numDivisions == 0 )
253  return;
254 
255  //draw small divisions starting at startPos (in mm)
256  double smallMarkerPos = startPos;
257  double smallDivisionSpacing = rulerScale / numDivisions;
258 
259  double pixelCoord;
260 
261  //draw numDivisions small divisions
262  for ( int i = 0; i < numDivisions; ++i )
263  {
264  smallMarkerPos += smallDivisionSpacing;
265 
266  if ( maxPos > 0 && smallMarkerPos > maxPos )
267  {
268  //stop drawing current division position is past maxPos
269  return;
270  }
271 
272  //calculate pixelCoordinate of the current division
273  if ( mDirection == Horizontal )
274  {
275  pixelCoord = mTransform.map( QPointF( smallMarkerPos, 0 ) ).x();
276  }
277  else
278  {
279  pixelCoord = mTransform.map( QPointF( 0, smallMarkerPos ) ).y();
280  }
281 
282  //calculate height of small division line
283  double lineSize;
284  if (( numDivisions == 10 && i == 4 ) || ( numDivisions == 4 && i == 1 ) )
285  {
286  //if drawing the 5th line of 10 or drawing the 2nd line of 4, then draw it slightly longer
287  lineSize = mRulerMinSize / 1.5;
288  }
289  else
290  {
291  lineSize = mRulerMinSize / 1.25;
292  }
293 
294  //draw either horizontal or vertical line depending on ruler direction
295  if ( mDirection == Horizontal )
296  {
297  painter->drawLine( pixelCoord, lineSize, pixelCoord, mRulerMinSize );
298  }
299  else
300  {
301  painter->drawLine( lineSize, pixelCoord, mRulerMinSize, pixelCoord );
302  }
303  }
304 }
305 
306 int QgsComposerRuler::optimumScale( double minPixelDiff, int &magnitude, int &multiple )
307 {
308  //find optimal ruler display scale
309 
310  //loop through magnitudes and multiples to find optimum scale
311  for ( unsigned int magnitudeCandidate = 0; magnitudeCandidate < COUNT_VALID_MAGNITUDES; ++magnitudeCandidate )
312  {
313  for ( unsigned int multipleCandidate = 0; multipleCandidate < COUNT_VALID_MULTIPLES; ++multipleCandidate )
314  {
315  int candidateScale = validScaleMultiples[multipleCandidate] * validScaleMagnitudes[magnitudeCandidate];
316  //find pixel size for each step using this candidate scale
317  double pixelDiff = mTransform.map( QPointF( candidateScale, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
318  if ( pixelDiff > minPixelDiff )
319  {
320  //found the optimum major scale
321  magnitude = validScaleMagnitudes[magnitudeCandidate];
322  multiple = validScaleMultiples[multipleCandidate];
323  return candidateScale;
324  }
325  }
326  }
327 
328  return 100000;
329 }
330 
331 int QgsComposerRuler::optimumNumberDivisions( double rulerScale, int scaleMultiple )
332 {
333  //calculate size in pixels of each marked ruler unit
334  double largeDivisionSize = mTransform.map( QPointF( rulerScale, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
335 
336  //now calculate optimum small tick scale, depending on marked ruler units
337  QList<int> validSmallDivisions;
338  switch ( scaleMultiple )
339  {
340  case 1:
341  //numbers increase by 1 increment each time, eg 1, 2, 3 or 10, 20, 30
342  //so we can draw either 10, 5 or 2 small ticks and have each fall on a nice value
343  validSmallDivisions << 10 << 5 << 2;
344  break;
345  case 2:
346  //numbers increase by 2 increments each time, eg 2, 4, 6 or 20, 40, 60
347  //so we can draw either 10, 4 or 2 small ticks and have each fall on a nice value
348  validSmallDivisions << 10 << 4 << 2;
349  break;
350  case 5:
351  //numbers increase by 5 increments each time, eg 5, 10, 15 or 100, 500, 1000
352  //so we can draw either 10 or 5 small ticks and have each fall on a nice value
353  validSmallDivisions << 10 << 5;
354  break;
355  }
356 
357  //calculate the most number of small divisions we can draw without them being too close to each other
358  QList<int>::iterator divisions_it;
359  for ( divisions_it = validSmallDivisions.begin(); divisions_it != validSmallDivisions.end(); ++divisions_it )
360  {
361  //find pixel size for this small division
362  double candidateSize = largeDivisionSize / ( *divisions_it );
363  //check if this separation is more then allowed min separation
364  if ( candidateSize >= mMinPixelsPerDivision )
365  {
366  //found a good candidate, return it
367  return ( *divisions_it );
368  }
369  }
370 
371  //unable to find a good candidate
372  return 0;
373 }
374 
375 
377 {
378 #if 0
379  QString debug = QString::number( transform.dx() ) + ',' + QString::number( transform.dy() ) + ','
380  + QString::number( transform.m11() ) + ',' + QString::number( transform.m22() );
381 #endif
382  mTransform = transform;
383  update();
384 }
385 
387 {
388  //qWarning( "QgsComposerRuler::mouseMoveEvent" );
389  updateMarker( event->posF() );
390  setSnapLinePosition( event->posF() );
391 
392  //update cursor position in status bar
393  QPointF displayPos = mTransform.inverted().map( event->posF() );
394  if ( mDirection == Horizontal )
395  {
396  //mouse is over a horizontal ruler, so don't show a y coordinate
397  displayPos.setY( 0 );
398  }
399  else
400  {
401  //mouse is over a vertical ruler, so don't show an x coordinate
402  displayPos.setX( 0 );
403  }
404  emit cursorPosChanged( displayPos );
405 }
406 
408 {
409  Q_UNUSED( event );
410 
411  //remove snap line if coordinate under 0
412  QPointF pos = mTransform.inverted().map( event->pos() );
413  bool removeItem = false;
414  if ( mDirection == Horizontal )
415  {
416  removeItem = pos.x() < 0 ? true : false;
417  }
418  else
419  {
420  removeItem = pos.y() < 0 ? true : false;
421  }
422 
423  if ( removeItem )
424  {
425  mComposition->removeSnapLine( mLineSnapItem );
426  mSnappedItems.clear();
427  }
428  mLineSnapItem = nullptr;
429 }
430 
432 {
433  double x = 0;
434  double y = 0;
435  if ( mDirection == Horizontal )
436  {
437  x = mTransform.inverted().map( event->pos() ).x();
438  }
439  else //vertical
440  {
441  y = mTransform.inverted().map( event->pos() ).y();
442  }
443 
444  //horizontal ruler means vertical snap line
445  QGraphicsLineItem* line = mComposition->nearestSnapLine( mDirection != Horizontal, x, y, 10.0, mSnappedItems );
446  if ( !line )
447  {
448  //create new snap line
449  mLineSnapItem = mComposition->addSnapLine();
450  }
451  else
452  {
453  mLineSnapItem = line;
454  }
455 }
456 
457 void QgsComposerRuler::setSnapLinePosition( QPointF pos )
458 {
459  if ( !mLineSnapItem || !mComposition )
460  {
461  return;
462  }
463 
464  QPointF transformedPt = mTransform.inverted().map( pos );
465  if ( mDirection == Horizontal )
466  {
467  int numPages = mComposition->numPages();
468  double lineHeight = numPages * mComposition->paperHeight();
469  if ( numPages > 1 )
470  {
471  lineHeight += ( numPages - 1 ) * mComposition->spaceBetweenPages();
472  }
473  mLineSnapItem->setLine( QLineF( transformedPt.x(), 0, transformedPt.x(), lineHeight ) );
474  }
475  else //vertical
476  {
477  mLineSnapItem->setLine( QLineF( 0, transformedPt.y(), mComposition->paperWidth(), transformedPt.y() ) );
478  }
479 
480  //move snapped items together with the snap line
481  QList< QPair< QgsComposerItem*, QgsComposerItem::ItemPositionMode > >::const_iterator itemIt = mSnappedItems.constBegin();
482  for ( ; itemIt != mSnappedItems.constEnd(); ++itemIt )
483  {
484  if ( mDirection == Horizontal )
485  {
486  if ( itemIt->second == QgsComposerItem::MiddleLeft )
487  {
488  itemIt->first->setItemPosition( transformedPt.x(), itemIt->first->pos().y(), QgsComposerItem::UpperLeft );
489  }
490  else if ( itemIt->second == QgsComposerItem::Middle )
491  {
492  itemIt->first->setItemPosition( transformedPt.x(), itemIt->first->pos().y(), QgsComposerItem::UpperMiddle );
493  }
494  else
495  {
496  itemIt->first->setItemPosition( transformedPt.x(), itemIt->first->pos().y(), QgsComposerItem::UpperRight );
497  }
498  }
499  else
500  {
501  if ( itemIt->second == QgsComposerItem::UpperMiddle )
502  {
503  itemIt->first->setItemPosition( itemIt->first->pos().x(), transformedPt.y(), QgsComposerItem::UpperLeft );
504  }
505  else if ( itemIt->second == QgsComposerItem::Middle )
506  {
507  itemIt->first->setItemPosition( itemIt->first->pos().x(), transformedPt.y(), QgsComposerItem::MiddleLeft );
508  }
509  else
510  {
511  itemIt->first->setItemPosition( itemIt->first->pos().x(), transformedPt.y(), QgsComposerItem::LowerLeft );
512  }
513  }
514  }
515 }
void clear()
void mousePressEvent(QMouseEvent *event) override
double paperWidth() const
Width of paper item.
void setPointSize(int pointSize)
const unsigned int COUNT_VALID_MULTIPLES
const unsigned int COUNT_VALID_MAGNITUDES
qreal dx() const
qreal dy() const
QPoint map(const QPoint &point) const
int y() const
void save()
void rotate(qreal angle)
void drawLine(const QLineF &line)
double spaceBetweenPages() const
Returns the vertical space between pages in a composer view.
int numPages() const
Returns the number of pages in the composition.
bool qgsDoubleNear(double a, double b, double epsilon=4 *DBL_EPSILON)
Compare two doubles (but allow some difference)
Definition: qgis.h:353
void update()
QTransform inverted(bool *invertible) const
int x() const
int y() const
int width() const
void paintEvent(QPaintEvent *event) override
void setFont(const QFont &font)
QString number(int n, int base)
qreal x() const
qreal y() const
QSize minimumSizeHint() const override
void mouseMoveEvent(QMouseEvent *event) override
int x() const
void setPen(const QColor &color)
void removeSnapLine(QGraphicsLineItem *line)
Remove custom snap line (and delete the object)
QPointF posF() const
qreal m11() const
qreal m22() const
void setLine(const QLineF &line)
void setSceneTransform(const QTransform &transform)
QPoint pos() const
void mouseReleaseEvent(QMouseEvent *event) override
void drawText(const QPointF &position, const QString &text)
T & first()
QgsComposerRuler(QgsComposerRuler::Direction d)
iterator end()
int width(const QString &text, int len) const
void restore()
void cursorPosChanged(QPointF)
Is emitted when mouse cursor coordinates change.
int height() const
QGraphicsLineItem * nearestSnapLine(const bool horizontal, const double x, const double y, const double tolerance, QList< QPair< QgsComposerItem *, QgsComposerItem::ItemPositionMode > > &snappedItems) const
Get nearest snap line.
double paperHeight() const
Height of paper item.
void setY(int y)
void translate(const QPointF &offset)
void setMouseTracking(bool enable)
void updateMarker(QPointF pos)
const QPoint & pos() const
const_iterator constEnd() const
QGraphicsLineItem * addSnapLine()
Add a custom snap line (can be horizontal or vertical)
const_iterator constBegin() const
const int RULER_FONT_SIZE
virtual bool event(QEvent *event)
iterator begin()
int height() const