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