QGIS API Documentation  3.0.2-Girona (307d082)
qgslayoutsnapper.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgslayoutsnapper.cpp
3  --------------------
4  begin : July 2017
5  copyright : (C) 2017 by Nyall Dawson
6  email : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 /***************************************************************************
9  * *
10  * This program is free software; you can redistribute it and/or modify *
11  * it under the terms of the GNU General Public License as published by *
12  * the Free Software Foundation; either version 2 of the License, or *
13  * (at your option) any later version. *
14  * *
15  ***************************************************************************/
16 
17 #include "qgslayoutsnapper.h"
18 #include "qgslayout.h"
19 #include "qgsreadwritecontext.h"
20 #include "qgsproject.h"
22 #include "qgssettings.h"
23 
25  : mLayout( layout )
26 {
27  QgsSettings s;
28  mTolerance = s.value( QStringLiteral( "LayoutDesigner/defaultSnapTolerancePixels" ), 5, QgsSettings::Gui ).toInt();
29 }
30 
32 {
33  return mLayout;
34 }
35 
37 {
38  mTolerance = snapTolerance;
39 }
40 
41 void QgsLayoutSnapper::setSnapToGrid( bool enabled )
42 {
43  mSnapToGrid = enabled;
44 }
45 
47 {
48  mSnapToGuides = enabled;
49 }
50 
52 {
53  mSnapToItems = enabled;
54 }
55 
56 QPointF QgsLayoutSnapper::snapPoint( QPointF point, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine, QGraphicsLineItem *verticalSnapLine,
57  const QList< QgsLayoutItem * > *ignoreItems ) const
58 {
59  snapped = false;
60 
61  // highest priority - guides
62  bool snappedXToGuides = false;
63  double newX = snapPointToGuides( point.x(), Qt::Vertical, scaleFactor, snappedXToGuides );
64  if ( snappedXToGuides )
65  {
66  snapped = true;
67  point.setX( newX );
68  if ( verticalSnapLine )
69  verticalSnapLine->setVisible( false );
70  }
71  bool snappedYToGuides = false;
72  double newY = snapPointToGuides( point.y(), Qt::Horizontal, scaleFactor, snappedYToGuides );
73  if ( snappedYToGuides )
74  {
75  snapped = true;
76  point.setY( newY );
77  if ( horizontalSnapLine )
78  horizontalSnapLine->setVisible( false );
79  }
80 
81  bool snappedXToItems = false;
82  bool snappedYToItems = false;
83  if ( !snappedXToGuides )
84  {
85  newX = snapPointToItems( point.x(), Qt::Horizontal, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedXToItems, verticalSnapLine );
86  if ( snappedXToItems )
87  {
88  snapped = true;
89  point.setX( newX );
90  }
91  }
92  if ( !snappedYToGuides )
93  {
94  newY = snapPointToItems( point.y(), Qt::Vertical, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedYToItems, horizontalSnapLine );
95  if ( snappedYToItems )
96  {
97  snapped = true;
98  point.setY( newY );
99  }
100  }
101 
102  bool snappedXToGrid = false;
103  bool snappedYToGrid = false;
104  QPointF res = snapPointToGrid( point, scaleFactor, snappedXToGrid, snappedYToGrid );
105  if ( snappedXToGrid && !snappedXToGuides && !snappedXToItems )
106  {
107  snapped = true;
108  point.setX( res.x() );
109  }
110  if ( snappedYToGrid && !snappedYToGuides && !snappedYToItems )
111  {
112  snapped = true;
113  point.setY( res.y() );
114  }
115 
116  return point;
117 }
118 
119 QRectF QgsLayoutSnapper::snapRect( const QRectF &rect, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine, QGraphicsLineItem *verticalSnapLine, const QList<QgsLayoutItem *> *ignoreItems ) const
120 {
121  snapped = false;
122  QRectF snappedRect = rect;
123 
124  QList< double > xCoords;
125  xCoords << rect.left() << rect.center().x() << rect.right();
126  QList< double > yCoords;
127  yCoords << rect.top() << rect.center().y() << rect.bottom();
128 
129  // highest priority - guides
130  bool snappedXToGuides = false;
131  double deltaX = snapPointsToGuides( xCoords, Qt::Vertical, scaleFactor, snappedXToGuides );
132  if ( snappedXToGuides )
133  {
134  snapped = true;
135  snappedRect.translate( deltaX, 0 );
136  if ( verticalSnapLine )
137  verticalSnapLine->setVisible( false );
138  }
139  bool snappedYToGuides = false;
140  double deltaY = snapPointsToGuides( yCoords, Qt::Horizontal, scaleFactor, snappedYToGuides );
141  if ( snappedYToGuides )
142  {
143  snapped = true;
144  snappedRect.translate( 0, deltaY );
145  if ( horizontalSnapLine )
146  horizontalSnapLine->setVisible( false );
147  }
148 
149  bool snappedXToItems = false;
150  bool snappedYToItems = false;
151  if ( !snappedXToGuides )
152  {
153  deltaX = snapPointsToItems( xCoords, Qt::Horizontal, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedXToItems, verticalSnapLine );
154  if ( snappedXToItems )
155  {
156  snapped = true;
157  snappedRect.translate( deltaX, 0 );
158  }
159  }
160  if ( !snappedYToGuides )
161  {
162  deltaY = snapPointsToItems( yCoords, Qt::Vertical, scaleFactor, ignoreItems ? *ignoreItems : QList< QgsLayoutItem * >(), snappedYToItems, horizontalSnapLine );
163  if ( snappedYToItems )
164  {
165  snapped = true;
166  snappedRect.translate( 0, deltaY );
167  }
168  }
169 
170  bool snappedXToGrid = false;
171  bool snappedYToGrid = false;
172  QList< QPointF > points;
173  points << rect.topLeft() << rect.topRight() << rect.bottomLeft() << rect.bottomRight();
174  QPointF res = snapPointsToGrid( points, scaleFactor, snappedXToGrid, snappedYToGrid );
175  if ( snappedXToGrid && !snappedXToGuides && !snappedXToItems )
176  {
177  snapped = true;
178  snappedRect.translate( res.x(), 0 );
179  }
180  if ( snappedYToGrid && !snappedYToGuides && !snappedYToItems )
181  {
182  snapped = true;
183  snappedRect.translate( 0, res.y() );
184  }
185 
186  return snappedRect;
187 }
188 
189 QPointF QgsLayoutSnapper::snapPointToGrid( QPointF point, double scaleFactor, bool &snappedX, bool &snappedY ) const
190 {
191  QPointF delta = snapPointsToGrid( QList< QPointF >() << point, scaleFactor, snappedX, snappedY );
192  return point + delta;
193 }
194 
195 QPointF QgsLayoutSnapper::snapPointsToGrid( const QList<QPointF> &points, double scaleFactor, bool &snappedX, bool &snappedY ) const
196 {
197  snappedX = false;
198  snappedY = false;
199  if ( !mLayout || !mSnapToGrid )
200  {
201  return QPointF( 0, 0 );
202  }
203  const QgsLayoutGridSettings &grid = mLayout->gridSettings();
204  if ( grid.resolution().length() <= 0 )
205  return QPointF( 0, 0 );
206 
207  double deltaX = 0;
208  double deltaY = 0;
209  double smallestDiffX = DBL_MAX;
210  double smallestDiffY = DBL_MAX;
211  for ( QPointF point : points )
212  {
213  //calculate y offset to current page
214  QPointF pagePoint = mLayout->pageCollection()->positionOnPage( point );
215 
216  double yPage = pagePoint.y(); //y-coordinate relative to current page
217  double yAtTopOfPage = mLayout->pageCollection()->page( mLayout->pageCollection()->pageNumberForPoint( point ) )->pos().y();
218 
219  //snap x coordinate
220  double gridRes = mLayout->convertToLayoutUnits( grid.resolution() );
221  QPointF gridOffset = mLayout->convertToLayoutUnits( grid.offset() );
222  int xRatio = static_cast< int >( ( point.x() - gridOffset.x() ) / gridRes + 0.5 ); //NOLINT
223  int yRatio = static_cast< int >( ( yPage - gridOffset.y() ) / gridRes + 0.5 ); //NOLINT
224 
225  double xSnapped = xRatio * gridRes + gridOffset.x();
226  double ySnapped = yRatio * gridRes + gridOffset.y() + yAtTopOfPage;
227 
228  double currentDiffX = std::fabs( xSnapped - point.x() );
229  if ( currentDiffX < smallestDiffX )
230  {
231  smallestDiffX = currentDiffX;
232  deltaX = xSnapped - point.x();
233  }
234 
235  double currentDiffY = std::fabs( ySnapped - point.y() );
236  if ( currentDiffY < smallestDiffY )
237  {
238  smallestDiffY = currentDiffY;
239  deltaY = ySnapped - point.y();
240  }
241  }
242 
243  //convert snap tolerance from pixels to layout units
244  double alignThreshold = mTolerance / scaleFactor;
245 
246  QPointF delta( 0, 0 );
247  if ( smallestDiffX <= alignThreshold )
248  {
249  //snap distance is inside of tolerance
250  snappedX = true;
251  delta.setX( deltaX );
252  }
253  if ( smallestDiffY <= alignThreshold )
254  {
255  //snap distance is inside of tolerance
256  snappedY = true;
257  delta.setY( deltaY );
258  }
259 
260  return delta;
261 }
262 
263 double QgsLayoutSnapper::snapPointToGuides( double original, Qt::Orientation orientation, double scaleFactor, bool &snapped ) const
264 {
265  double delta = snapPointsToGuides( QList< double >() << original, orientation, scaleFactor, snapped );
266  return original + delta;
267 }
268 
269 double QgsLayoutSnapper::snapPointsToGuides( const QList<double> &points, Qt::Orientation orientation, double scaleFactor, bool &snapped ) const
270 {
271  snapped = false;
272  if ( !mLayout || !mSnapToGuides )
273  {
274  return 0;
275  }
276 
277  //convert snap tolerance from pixels to layout units
278  double alignThreshold = mTolerance / scaleFactor;
279 
280  double bestDelta = 0;
281  double smallestDiff = DBL_MAX;
282 
283  for ( double p : points )
284  {
285  Q_FOREACH ( QgsLayoutGuide *guide, mLayout->guides().guides( orientation ) )
286  {
287  double guidePos = guide->layoutPosition();
288  double diff = std::fabs( p - guidePos );
289  if ( diff < smallestDiff )
290  {
291  smallestDiff = diff;
292  bestDelta = guidePos - p;
293  }
294  }
295  }
296 
297  if ( smallestDiff <= alignThreshold )
298  {
299  snapped = true;
300  return bestDelta;
301  }
302  else
303  {
304  return 0;
305  }
306 }
307 
308 double QgsLayoutSnapper::snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped,
309  QGraphicsLineItem *snapLine ) const
310 {
311  double delta = snapPointsToItems( QList< double >() << original, orientation, scaleFactor, ignoreItems, snapped, snapLine );
312  return original + delta;
313 }
314 
315 double QgsLayoutSnapper::snapPointsToItems( const QList<double> &points, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped, QGraphicsLineItem *snapLine ) const
316 {
317  snapped = false;
318  if ( !mLayout || !mSnapToItems )
319  {
320  if ( snapLine )
321  snapLine->setVisible( false );
322  return 0;
323  }
324 
325  double alignThreshold = mTolerance / scaleFactor;
326 
327  double bestDelta = 0;
328  double smallestDiff = DBL_MAX;
329  double closest = 0;
330  const QList<QGraphicsItem *> itemList = mLayout->items();
331  QList< double > currentCoords;
332  for ( QGraphicsItem *item : itemList )
333  {
334  QgsLayoutItem *currentItem = dynamic_cast< QgsLayoutItem *>( item );
335  if ( !currentItem || ignoreItems.contains( currentItem ) )
336  continue;
337  if ( currentItem->type() == QgsLayoutItemRegistry::LayoutGroup )
338  continue; // don't snap to group bounds, instead we snap to group item bounds
339  if ( !currentItem->isVisible() )
340  continue; // don't snap to invisible items
341 
342  QRectF itemRect;
343  if ( dynamic_cast<const QgsLayoutItemPage *>( currentItem ) )
344  {
345  //if snapping to paper use the paper item's rect rather then the bounding rect,
346  //since we want to snap to the page edge and not any outlines drawn around the page
347  itemRect = currentItem->mapRectToScene( currentItem->rect() );
348  }
349  else
350  {
351  itemRect = currentItem->mapRectToScene( currentItem->rectWithFrame() );
352  }
353 
354  currentCoords.clear();
355  switch ( orientation )
356  {
357  case Qt::Horizontal:
358  {
359  currentCoords << itemRect.left();
360  currentCoords << itemRect.right();
361  currentCoords << itemRect.center().x();
362  break;
363  }
364 
365  case Qt::Vertical:
366  {
367  currentCoords << itemRect.top();
368  currentCoords << itemRect.center().y();
369  currentCoords << itemRect.bottom();
370  break;
371  }
372  }
373 
374  for ( double val : qgis::as_const( currentCoords ) )
375  {
376  for ( double p : points )
377  {
378  double dist = std::fabs( p - val );
379  if ( dist <= alignThreshold && dist < smallestDiff )
380  {
381  snapped = true;
382  smallestDiff = dist;
383  bestDelta = val - p;
384  closest = val;
385  }
386  }
387  }
388  }
389 
390  if ( snapLine )
391  {
392  if ( snapped )
393  {
394  snapLine->setVisible( true );
395  switch ( orientation )
396  {
397  case Qt::Vertical:
398  {
399  snapLine->setLine( QLineF( -100000, closest, 100000, closest ) );
400  break;
401  }
402 
403  case Qt::Horizontal:
404  {
405  snapLine->setLine( QLineF( closest, -100000, closest, 100000 ) );
406  break;
407  }
408  }
409  }
410  else
411  {
412  snapLine->setVisible( false );
413  }
414  }
415 
416  return bestDelta;
417 }
418 
419 
420 bool QgsLayoutSnapper::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
421 {
422  QDomElement element = document.createElement( QStringLiteral( "Snapper" ) );
423 
424  element.setAttribute( QStringLiteral( "tolerance" ), mTolerance );
425  element.setAttribute( QStringLiteral( "snapToGrid" ), mSnapToGrid );
426  element.setAttribute( QStringLiteral( "snapToGuides" ), mSnapToGuides );
427  element.setAttribute( QStringLiteral( "snapToItems" ), mSnapToItems );
428 
429  parentElement.appendChild( element );
430  return true;
431 }
432 
433 bool QgsLayoutSnapper::readXml( const QDomElement &e, const QDomDocument &, const QgsReadWriteContext & )
434 {
435  QDomElement element = e;
436  if ( element.nodeName() != QStringLiteral( "Snapper" ) )
437  {
438  element = element.firstChildElement( QStringLiteral( "Snapper" ) );
439  }
440 
441  if ( element.nodeName() != QStringLiteral( "Snapper" ) )
442  {
443  return false;
444  }
445 
446  mTolerance = element.attribute( QStringLiteral( "tolerance" ), QStringLiteral( "5" ) ).toInt();
447  mSnapToGrid = element.attribute( QStringLiteral( "snapToGrid" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
448  mSnapToGuides = element.attribute( QStringLiteral( "snapToGuides" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
449  mSnapToItems = element.attribute( QStringLiteral( "snapToItems" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
450  return true;
451 }
double snapPointsToItems(const QList< double > &points, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped, QGraphicsLineItem *snapLine=nullptr) const
Snaps a set of points to the item bounds.
void setSnapToGrid(bool enabled)
Sets whether snapping to grid is enabled.
The class is used as a container of context for various read/write operations on other objects...
QgsLayoutGuideCollection & guides()
Returns a reference to the layout&#39;s guide collection, which manages page snap guides.
Definition: qgslayout.cpp:381
QgsLayoutSnapper(QgsLayout *layout)
Constructor for QgsLayoutSnapper, attached to the specified layout.
Base class for graphical items within a QgsLayout.
int type() const override
Returns a unique graphics item type identifier.
This class is a composition of two QSettings instances:
Definition: qgssettings.h:57
int pageNumberForPoint(QPointF point) const
Returns the page number corresponding to a point in the layout (in layout units). ...
virtual QRectF rectWithFrame() const
Returns the item&#39;s rectangular bounds, including any bleed caused by the item&#39;s frame.
Contains the configuration for a single snap guide used by a layout.
double snapPointsToGuides(const QList< double > &points, Qt::Orientation orientation, double scaleFactor, bool &snapped) const
Snaps a set of points to the guides.
QPointF snapPointToGrid(QPointF point, double scaleFactor, bool &snappedX, bool &snappedY) const
Snaps a layout coordinate point to the grid.
double layoutPosition() const
Returns the guide&#39;s position in absolute layout units.
void setSnapToGuides(bool enabled)
Sets whether snapping to guides is enabled.
double convertToLayoutUnits(const QgsLayoutMeasurement &measurement) const
Converts a measurement into the layout&#39;s native units.
Definition: qgslayout.cpp:325
QgsLayoutItemPage * page(int pageNumber)
Returns a specific page (by pageNumber) from the collection.
bool writeXml(QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context) const override
Stores the snapper&#39;s state in a DOM element.
QgsLayoutMeasurement resolution() const
Returns the page/snap grid resolution.
QgsLayoutPageCollection * pageCollection()
Returns a pointer to the layout&#39;s page collection, which stores and manages page items in the layout...
Definition: qgslayout.cpp:455
QgsLayoutGridSettings & gridSettings()
Returns a reference to the layout&#39;s grid settings, which stores settings relating to grid appearance...
Definition: qgslayout.h:418
QRectF snapRect(const QRectF &rect, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine=nullptr, QGraphicsLineItem *verticalSnapLine=nullptr, const QList< QgsLayoutItem * > *ignoreItems=nullptr) const
Snaps a layout coordinate rect.
QPointF snapPointsToGrid(const QList< QPointF > &points, double scaleFactor, bool &snappedX, bool &snappedY) const
Snaps a set of points to the grid.
QgsLayout * layout() override
Returns the layout the object belongs to.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:49
Contains settings relating to the appearance, spacing and offset for layout grids.
bool readXml(const QDomElement &gridElement, const QDomDocument &document, const QgsReadWriteContext &context) override
Sets the snapper&#39;s state from a DOM element.
void setSnapTolerance(const int snapTolerance)
Sets the snap tolerance (in pixels) to use when snapping.
QPointF snapPoint(QPointF point, double scaleFactor, bool &snapped, QGraphicsLineItem *horizontalSnapLine=nullptr, QGraphicsLineItem *verticalSnapLine=nullptr, const QList< QgsLayoutItem * > *ignoreItems=nullptr) const
Snaps a layout coordinate point.
QPointF positionOnPage(QPointF point) const
Returns the position within a page of a point in the layout (in layout units).
QList< QgsLayoutGuide *> guides()
Returns a list of all guides contained in the collection.
QgsLayoutPoint offset() const
Returns the offset of the page/snap grid.
double length() const
Returns the length of the measurement.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), const Section section=NoSection) const
Returns the value for setting key.
double snapPointToGuides(double original, Qt::Orientation orientation, double scaleFactor, bool &snapped) const
Snaps an original layout coordinate to the guides.
int snapTolerance() const
Returns the snap tolerance (in pixels) to use when snapping.
double snapPointToItems(double original, Qt::Orientation orientation, double scaleFactor, const QList< QgsLayoutItem * > &ignoreItems, bool &snapped, QGraphicsLineItem *snapLine=nullptr) const
Snaps an original layout coordinate to the item bounds.
void setSnapToItems(bool enabled)
Sets whether snapping to items is enabled.