QGIS API Documentation  3.4.15-Madeira (e83d02e274)
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  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 = std::numeric_limits<double>::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 }
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...
QgsLayoutPoint offset() const
Returns the offset of the page/snap grid.
QgsLayoutGuideCollection & guides()
Returns a reference to the layout&#39;s guide collection, which manages page snap guides.
Definition: qgslayout.cpp:382
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:58
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.
double length() const
Returns the length of the measurement.
QgsLayoutMeasurement resolution() const
Returns the page/snap grid resolution.
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), Section section=NoSection) const
Returns the value for setting key.
void setSnapToGuides(bool enabled)
Sets whether snapping to guides is enabled.
QPointF snapPointsToGrid(const QList< QPointF > &points, double scaleFactor, bool &snappedX, bool &snappedY) const
Snaps a set of points to the grid.
QPointF snapPointToGrid(QPointF point, double scaleFactor, bool &snappedX, bool &snappedY) const
Snaps a layout coordinate point to the grid.
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.
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.
QgsLayoutPageCollection * pageCollection()
Returns a pointer to the layout&#39;s page collection, which stores and manages page items in the layout...
Definition: qgslayout.cpp:456
QgsLayoutGridSettings & gridSettings()
Returns a reference to the layout&#39;s grid settings, which stores settings relating to grid appearance...
Definition: qgslayout.h:418
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
QPointF positionOnPage(QPointF point) const
Returns the position within a page of a point in the layout (in layout units).
int pageNumberForPoint(QPointF point) const
Returns the page number corresponding to a point in the layout (in layout units). ...
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.
double snapPointToGuides(double original, Qt::Orientation orientation, double scaleFactor, bool &snapped) const
Snaps an original layout coordinate to the guides.
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.
QList< QgsLayoutGuide * > guides()
Returns a list of all guides contained in the collection.
virtual QRectF rectWithFrame() const
Returns the item&#39;s rectangular bounds, including any bleed caused by the item&#39;s frame.
int snapTolerance() const
Returns the snap tolerance (in pixels) to use when snapping.
void setSnapTolerance(int snapTolerance)
Sets the snap tolerance (in pixels) to use when snapping.
double layoutPosition() const
Returns the guide&#39;s position in absolute layout units.
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.
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.
double convertToLayoutUnits(QgsLayoutMeasurement measurement) const
Converts a measurement into the layout&#39;s native units.
Definition: qgslayout.cpp:326
void setSnapToItems(bool enabled)
Sets whether snapping to items is enabled.