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