QGIS API Documentation  2.3.0-Master
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
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( 0 ),
18  mLineSnapItem( 0 ),
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 seperations
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 
36  //each small division must be at least 2 pixels apart
37  if ( mMinPixelsPerDivision < 2 )
39 
41  mTextBaseline = mRulerMinSize / 1.667;
43 }
44 
46 {
47  delete mRulerFontMetrics;
48  delete mRulerFont;
49 }
50 
52 {
53  return QSize( mRulerMinSize, mRulerMinSize );
54 }
55 
56 void QgsComposerRuler::paintEvent( QPaintEvent* event )
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  //draw small divisions starting at startPos (in mm)
238  double smallMarkerPos = startPos;
239  double smallDivisionSpacing = rulerScale / numDivisions;
240 
241  double pixelCoord;
242 
243  //draw numDivisions small divisions
244  for ( int i = 0; i < numDivisions; ++i )
245  {
246  smallMarkerPos += smallDivisionSpacing;
247 
248  if ( maxPos > 0 && smallMarkerPos > maxPos )
249  {
250  //stop drawing current division position is past maxPos
251  return;
252  }
253 
254  //calculate pixelCoordinate of the current division
255  if ( mDirection == Horizontal )
256  {
257  pixelCoord = mTransform.map( QPointF( smallMarkerPos, 0 ) ).x();
258  }
259  else
260  {
261  pixelCoord = mTransform.map( QPointF( 0, smallMarkerPos ) ).y();
262  }
263 
264  //calculate height of small division line
265  double lineSize;
266  if (( numDivisions == 10 && i == 4 ) || ( numDivisions == 4 && i == 1 ) )
267  {
268  //if drawing the 5th line of 10 or drawing the 2nd line of 4, then draw it slightly longer
269  lineSize = mRulerMinSize / 1.5;
270  }
271  else
272  {
273  lineSize = mRulerMinSize / 1.25;
274  }
275 
276  //draw either horizontal or vertical line depending on ruler direction
277  if ( mDirection == Horizontal )
278  {
279  painter->drawLine( pixelCoord, lineSize, pixelCoord, mRulerMinSize );
280  }
281  else
282  {
283  painter->drawLine( lineSize, pixelCoord, mRulerMinSize, pixelCoord );
284  }
285  }
286 }
287 
288 int QgsComposerRuler::optimumScale( double minPixelDiff, int &magnitude, int &multiple )
289 {
290  //find optimal ruler display scale
291 
292  //loop through magnitudes and multiples to find optimum scale
293  for ( unsigned int magnitudeCandidate = 0; magnitudeCandidate < COUNT_VALID_MAGNITUDES; ++magnitudeCandidate )
294  {
295  for ( unsigned int multipleCandidate = 0; multipleCandidate < COUNT_VALID_MULTIPLES; ++multipleCandidate )
296  {
297  int candidateScale = validScaleMultiples[multipleCandidate] * validScaleMagnitudes[magnitudeCandidate];
298  //find pixel size for each step using this candidate scale
299  double pixelDiff = mTransform.map( QPointF( candidateScale, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
300  if ( pixelDiff > minPixelDiff )
301  {
302  //found the optimum major scale
303  magnitude = validScaleMagnitudes[magnitudeCandidate];
304  multiple = validScaleMultiples[multipleCandidate];
305  return candidateScale;
306  }
307  }
308  }
309 
310  return 100000;
311 }
312 
313 int QgsComposerRuler::optimumNumberDivisions( double rulerScale, int scaleMultiple )
314 {
315  //calculate size in pixels of each marked ruler unit
316  double largeDivisionSize = mTransform.map( QPointF( rulerScale, 0 ) ).x() - mTransform.map( QPointF( 0, 0 ) ).x();
317 
318  //now calculate optimum small tick scale, depending on marked ruler units
319  QList<int> validSmallDivisions;
320  switch ( scaleMultiple )
321  {
322  case 1:
323  //numbers increase by 1 increment each time, eg 1, 2, 3 or 10, 20, 30
324  //so we can draw either 10, 5 or 2 small ticks and have each fall on a nice value
325  validSmallDivisions << 10 << 5 << 2;
326  break;
327  case 2:
328  //numbers increase by 2 increments each time, eg 2, 4, 6 or 20, 40, 60
329  //so we can draw either 10, 4 or 2 small ticks and have each fall on a nice value
330  validSmallDivisions << 10 << 4 << 2;
331  break;
332  case 5:
333  //numbers increase by 5 increments each time, eg 5, 10, 15 or 100, 500, 1000
334  //so we can draw either 10 or 5 small ticks and have each fall on a nice value
335  validSmallDivisions << 10 << 5;
336  break;
337  }
338 
339  //calculate the most number of small divisions we can draw without them being too close to each other
340  QList<int>::iterator divisions_it;
341  for ( divisions_it = validSmallDivisions.begin(); divisions_it != validSmallDivisions.end(); ++divisions_it )
342  {
343  //find pixel size for this small division
344  double candidateSize = largeDivisionSize / ( *divisions_it );
345  //check if this seperation is more then allowed min seperation
346  if ( candidateSize >= mMinPixelsPerDivision )
347  {
348  //found a good candidate, return it
349  return ( *divisions_it );
350  }
351  }
352 
353  //unable to find a good candidate
354  return 0;
355 }
356 
357 
358 void QgsComposerRuler::setSceneTransform( const QTransform& transform )
359 {
360  QString debug = QString::number( transform.dx() ) + "," + QString::number( transform.dy() ) + ","
361  + QString::number( transform.m11() ) + "," + QString::number( transform.m22() );
362  mTransform = transform;
363  update();
364 }
365 
366 void QgsComposerRuler::mouseMoveEvent( QMouseEvent* event )
367 {
368  //qWarning( "QgsComposerRuler::mouseMoveEvent" );
369  updateMarker( event->posF() );
370  setSnapLinePosition( event->posF() );
371 
372  //update cursor position in status bar
373  QPointF displayPos = mTransform.inverted().map( event->posF() );
374  if ( mDirection == Horizontal )
375  {
376  //mouse is over a horizontal ruler, so don't show a y coordinate
377  displayPos.setY( 0 );
378  }
379  else
380  {
381  //mouse is over a vertical ruler, so don't show an x coordinate
382  displayPos.setX( 0 );
383  }
384  emit cursorPosChanged( displayPos );
385 }
386 
387 void QgsComposerRuler::mouseReleaseEvent( QMouseEvent* event )
388 {
389  Q_UNUSED( event );
390 
391  //remove snap line if coordinate under 0
392  QPointF pos = mTransform.inverted().map( event->pos() );
393  bool removeItem = false;
394  if ( mDirection == Horizontal )
395  {
396  removeItem = pos.x() < 0 ? true : false;
397  }
398  else
399  {
400  removeItem = pos.y() < 0 ? true : false;
401  }
402 
403  if ( removeItem )
404  {
406  mSnappedItems.clear();
407  }
408  mLineSnapItem = 0;
409 }
410 
411 void QgsComposerRuler::mousePressEvent( QMouseEvent* event )
412 {
413  double x = 0;
414  double y = 0;
415  if ( mDirection == Horizontal )
416  {
417  x = mTransform.inverted().map( event->pos() ).x();
418  }
419  else //vertical
420  {
421  y = mTransform.inverted().map( event->pos() ).y();
422  }
423 
424  //horizontal ruler means vertical snap line
425  QGraphicsLineItem* line = mComposition->nearestSnapLine( mDirection != Horizontal, x, y, 10.0, mSnappedItems );
426  if ( !line )
427  {
428  //create new snap line
430  }
431  else
432  {
433  mLineSnapItem = line;
434  }
435 }
436 
437 void QgsComposerRuler::setSnapLinePosition( const QPointF& pos )
438 {
439  if ( !mLineSnapItem || !mComposition )
440  {
441  return;
442  }
443 
444  QPointF transformedPt = mTransform.inverted().map( pos );
445  if ( mDirection == Horizontal )
446  {
447  int numPages = mComposition->numPages();
448  double lineHeight = numPages * mComposition->paperHeight();
449  if ( numPages > 1 )
450  {
451  lineHeight += ( numPages - 1 ) * mComposition->spaceBetweenPages();
452  }
453  mLineSnapItem->setLine( QLineF( transformedPt.x(), 0, transformedPt.x(), lineHeight ) );
454  }
455  else //vertical
456  {
457  mLineSnapItem->setLine( QLineF( 0, transformedPt.y(), mComposition->paperWidth(), transformedPt.y() ) );
458  }
459 
460  //move snapped items together with the snap line
461  QList< QPair< QgsComposerItem*, QgsComposerItem::ItemPositionMode > >::iterator itemIt = mSnappedItems.begin();
462  for ( ; itemIt != mSnappedItems.end(); ++itemIt )
463  {
464  if ( mDirection == Horizontal )
465  {
466  if ( itemIt->second == QgsComposerItem::MiddleLeft )
467  {
468  itemIt->first->setItemPosition( transformedPt.x(), itemIt->first->pos().y(), QgsComposerItem::UpperLeft );
469  }
470  else if ( itemIt->second == QgsComposerItem::Middle )
471  {
472  itemIt->first->setItemPosition( transformedPt.x(), itemIt->first->pos().y(), QgsComposerItem::UpperMiddle );
473  }
474  else
475  {
476  itemIt->first->setItemPosition( transformedPt.x(), itemIt->first->pos().y(), QgsComposerItem::UpperRight );
477  }
478  }
479  else
480  {
481  if ( itemIt->second == QgsComposerItem::UpperMiddle )
482  {
483  itemIt->first->setItemPosition( itemIt->first->pos().x(), transformedPt.y(), QgsComposerItem::UpperLeft );
484  }
485  else if ( itemIt->second == QgsComposerItem::Middle )
486  {
487  itemIt->first->setItemPosition( itemIt->first->pos().x(), transformedPt.y(), QgsComposerItem::MiddleLeft );
488  }
489  else
490  {
491  itemIt->first->setItemPosition( itemIt->first->pos().x(), transformedPt.y(), QgsComposerItem::LowerLeft );
492  }
493  }
494  }
495 }
QGraphicsLineItem * mLineSnapItem
double paperWidth() const
Returns width of paper item.
const unsigned int COUNT_VALID_MULTIPLES
static const int validScaleMagnitudes[]
const unsigned int COUNT_VALID_MAGNITUDES
int optimumNumberDivisions(double rulerScale, int scaleMultiple)
void updateMarker(const QPointF &pos)
QGraphicsLineItem * nearestSnapLine(bool horizontal, double x, double y, double tolerance, QList< QPair< QgsComposerItem *, QgsComposerItem::ItemPositionMode > > &snappedItems)
Get nearest snap line.
double spaceBetweenPages() const
int numPages() const
Note: added in version 1.9.
bool qgsDoubleNear(double a, double b, double epsilon=4 *DBL_EPSILON)
Definition: qgis.h:324
int optimumScale(double minPixelDiff, int &magnitude, int &multiple)
QgsComposition * mComposition
void setSnapLinePosition(const QPointF &pos)
void mouseReleaseEvent(QMouseEvent *event)
void drawMarkerPos(QPainter *painter)
void removeSnapLine(QGraphicsLineItem *line)
Remove custom snap line (and delete the object)
static const int validScaleMultiples[]
QList< QPair< QgsComposerItem *, QgsComposerItem::ItemPositionMode > > mSnappedItems
void setSceneTransform(const QTransform &transform)
void drawRotatedText(QPainter *painter, QPointF pos, const QString &text)
QgsComposerRuler(QgsComposerRuler::Direction d)
QSize minimumSizeHint() const
QTransform mTransform
void mouseMoveEvent(QMouseEvent *event)
void mousePressEvent(QMouseEvent *event)
QFontMetrics * mRulerFontMetrics
void cursorPosChanged(QPointF)
Is emitted when mouse cursor coordinates change.
double paperHeight() const
Returns height of paper item.
void drawSmallDivisions(QPainter *painter, double startPos, int numDivisions, double rulerScale, double maxPos=0)
QGraphicsLineItem * addSnapLine()
Add a custom snap line (can be horizontal or vertical)
const int RULER_FONT_SIZE
void paintEvent(QPaintEvent *event)