QGIS API Documentation  3.13.0-Master (b73bd58cfb)
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 = std::numeric_limits<double>::max();
210  double smallestDiffY = std::numeric_limits<double>::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 = std::numeric_limits<double>::max();
282 
283  for ( double p : points )
284  {
285  const auto constGuides = mLayout->guides().guides( orientation );
286  for ( QgsLayoutGuide *guide : constGuides )
287  {
288  double guidePos = guide->layoutPosition();
289  double diff = std::fabs( p - guidePos );
290  if ( diff < smallestDiff )
291  {
292  smallestDiff = diff;
293  bestDelta = guidePos - p;
294  }
295  }
296  }
297 
298  if ( smallestDiff <= alignThreshold )
299  {
300  snapped = true;
301  return bestDelta;
302  }
303  else
304  {
305  return 0;
306  }
307 }
308 
309 double QgsLayoutSnapper::snapPointToItems( double original, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped,
310  QGraphicsLineItem *snapLine ) const
311 {
312  double delta = snapPointsToItems( QList< double >() << original, orientation, scaleFactor, ignoreItems, snapped, snapLine );
313  return original + delta;
314 }
315 
316 double QgsLayoutSnapper::snapPointsToItems( const QList<double> &points, Qt::Orientation orientation, double scaleFactor, const QList<QgsLayoutItem *> &ignoreItems, bool &snapped, QGraphicsLineItem *snapLine ) const
317 {
318  snapped = false;
319  if ( !mLayout || !mSnapToItems )
320  {
321  if ( snapLine )
322  snapLine->setVisible( false );
323  return 0;
324  }
325 
326  double alignThreshold = mTolerance / scaleFactor;
327 
328  double bestDelta = 0;
329  double smallestDiff = std::numeric_limits<double>::max();
330  double closest = 0;
331  const QList<QGraphicsItem *> itemList = mLayout->items();
332  QList< double > currentCoords;
333  for ( QGraphicsItem *item : itemList )
334  {
335  QgsLayoutItem *currentItem = dynamic_cast< QgsLayoutItem *>( item );
336  if ( !currentItem || ignoreItems.contains( currentItem ) )
337  continue;
338  if ( currentItem->type() == QgsLayoutItemRegistry::LayoutGroup )
339  continue; // don't snap to group bounds, instead we snap to group item bounds
340  if ( !currentItem->isVisible() )
341  continue; // don't snap to invisible items
342 
343  QRectF itemRect;
344  if ( dynamic_cast<const QgsLayoutItemPage *>( currentItem ) )
345  {
346  //if snapping to paper use the paper item's rect rather then the bounding rect,
347  //since we want to snap to the page edge and not any outlines drawn around the page
348  itemRect = currentItem->mapRectToScene( currentItem->rect() );
349  }
350  else
351  {
352  itemRect = currentItem->mapRectToScene( currentItem->rectWithFrame() );
353  }
354 
355  currentCoords.clear();
356  switch ( orientation )
357  {
358  case Qt::Horizontal:
359  {
360  currentCoords << itemRect.left();
361  currentCoords << itemRect.right();
362  currentCoords << itemRect.center().x();
363  break;
364  }
365 
366  case Qt::Vertical:
367  {
368  currentCoords << itemRect.top();
369  currentCoords << itemRect.center().y();
370  currentCoords << itemRect.bottom();
371  break;
372  }
373  }
374 
375  for ( double val : qgis::as_const( currentCoords ) )
376  {
377  for ( double p : points )
378  {
379  double dist = std::fabs( p - val );
380  if ( dist <= alignThreshold && dist < smallestDiff )
381  {
382  snapped = true;
383  smallestDiff = dist;
384  bestDelta = val - p;
385  closest = val;
386  }
387  }
388  }
389  }
390 
391  if ( snapLine )
392  {
393  if ( snapped )
394  {
395  snapLine->setVisible( true );
396  switch ( orientation )
397  {
398  case Qt::Vertical:
399  {
400  snapLine->setLine( QLineF( -100000, closest, 100000, closest ) );
401  break;
402  }
403 
404  case Qt::Horizontal:
405  {
406  snapLine->setLine( QLineF( closest, -100000, closest, 100000 ) );
407  break;
408  }
409  }
410  }
411  else
412  {
413  snapLine->setVisible( false );
414  }
415  }
416 
417  return bestDelta;
418 }
419 
420 
421 bool QgsLayoutSnapper::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
422 {
423  QDomElement element = document.createElement( QStringLiteral( "Snapper" ) );
424 
425  element.setAttribute( QStringLiteral( "tolerance" ), mTolerance );
426  element.setAttribute( QStringLiteral( "snapToGrid" ), mSnapToGrid );
427  element.setAttribute( QStringLiteral( "snapToGuides" ), mSnapToGuides );
428  element.setAttribute( QStringLiteral( "snapToItems" ), mSnapToItems );
429 
430  parentElement.appendChild( element );
431  return true;
432 }
433 
434 bool QgsLayoutSnapper::readXml( const QDomElement &e, const QDomDocument &, const QgsReadWriteContext & )
435 {
436  QDomElement element = e;
437  if ( element.nodeName() != QStringLiteral( "Snapper" ) )
438  {
439  element = element.firstChildElement( QStringLiteral( "Snapper" ) );
440  }
441 
442  if ( element.nodeName() != QStringLiteral( "Snapper" ) )
443  {
444  return false;
445  }
446 
447  mTolerance = element.attribute( QStringLiteral( "tolerance" ), QStringLiteral( "5" ) ).toInt();
448  mSnapToGrid = element.attribute( QStringLiteral( "snapToGrid" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
449  mSnapToGuides = element.attribute( QStringLiteral( "snapToGuides" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
450  mSnapToItems = element.attribute( QStringLiteral( "snapToItems" ), QStringLiteral( "0" ) ) != QLatin1String( "0" );
451  return true;
452 }
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:384
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:61
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.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
QPointF snapPointToGrid(QPointF point, double scaleFactor, bool &snappedX, bool &snappedY) const
Snaps a layout coordinate point to the grid.
void setSnapToGuides(bool enabled)
Sets whether snapping to guides is enabled.
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:458
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.
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.
double snapPointToGuides(double original, Qt::Orientation orientation, double scaleFactor, bool &snapped) const
Snaps an original layout coordinate to the guides.
void setSnapTolerance(int snapTolerance)
Sets the snap tolerance (in pixels) to use when snapping.
int snapTolerance() const
Returns the snap tolerance (in pixels) to use when snapping.
double convertToLayoutUnits(QgsLayoutMeasurement measurement) const
Converts a measurement into the layout&#39;s native units.
Definition: qgslayout.cpp:328
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.