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