1 /***************************************************************************
2                            qgsmapoverviewcanvas.cpp
3                       Map canvas subclassed for overview
4                               -------------------
5     begin                : 09/14/2005
6     copyright            : (C) 2005 by Martin Dobias
7     email                : won.der at centrum.sk
8  ***************************************************************************/
9 
10 /***************************************************************************
11  *                                                                         *
12  *   This program is free software; you can redistribute it and/or modify  *
13  *   it under the terms of the GNU General Public License as published by  *
14  *   the Free Software Foundation; either version 2 of the License, or     *
15  *   (at your option) any later version.                                   *
16  *                                                                         *
17  ***************************************************************************/
18 
19 #include "qgsmapcanvas.h"
20 #include "qgsmaplayer.h"
21 #include "qgsproject.h"
22 #include "qgsmapoverviewcanvas.h"
23 #include "qgsmaprenderersequentialjob.h"
24 #include "qgsmaptopixel.h"
25 #include "qgsprojectviewsettings.h"
26 
27 #include <QPainter>
28 #include <QPainterPath>
29 #include <QPaintEvent>
30 #include <QResizeEvent>
31 #include <QMouseEvent>
32 #include "qgslogger.h"
33 #include <limits>
34 
35 
QgsMapOverviewCanvas(QWidget * parent,QgsMapCanvas * mapCanvas)36 QgsMapOverviewCanvas::QgsMapOverviewCanvas( QWidget *parent, QgsMapCanvas *mapCanvas )
37   : QWidget( parent )
38   , mMapCanvas( mapCanvas )
39 
40 {
41   setAutoFillBackground( true );
42   setObjectName( QStringLiteral( "theOverviewCanvas" ) );
43   mPanningWidget = new QgsPanningWidget( this );
44 
45   mSettings.setTransformContext( mMapCanvas->mapSettings().transformContext() );
46   mSettings.setFlag( Qgis::MapSettingsFlag::DrawLabeling, false );
47 
48   connect( mMapCanvas, &QgsMapCanvas::extentsChanged, this, &QgsMapOverviewCanvas::drawExtentRect );
49   connect( mMapCanvas, &QgsMapCanvas::destinationCrsChanged, this, &QgsMapOverviewCanvas::destinationCrsChanged );
50   connect( mMapCanvas, &QgsMapCanvas::transformContextChanged, this, &QgsMapOverviewCanvas::transformContextChanged );
51 
52   connect( QgsProject::instance()->viewSettings(), &QgsProjectViewSettings::presetFullExtentChanged, this, &QgsMapOverviewCanvas::refresh );
53 }
54 
resizeEvent(QResizeEvent * e)55 void QgsMapOverviewCanvas::resizeEvent( QResizeEvent *e )
56 {
57   mPixmap = QPixmap();
58 
59   mSettings.setOutputSize( e->size() );
60 
61   updateFullExtent();
62 
63   refresh();
64 
65   QWidget::resizeEvent( e );
66 }
67 
showEvent(QShowEvent * e)68 void QgsMapOverviewCanvas::showEvent( QShowEvent *e )
69 {
70   refresh();
71   QWidget::showEvent( e );
72 }
73 
paintEvent(QPaintEvent * pe)74 void QgsMapOverviewCanvas::paintEvent( QPaintEvent *pe )
75 {
76   if ( !mPixmap.isNull() )
77   {
78     QPainter paint( this );
79     paint.drawPixmap( pe->rect().topLeft(), mPixmap, pe->rect() );
80   }
81 }
82 
83 
drawExtentRect()84 void QgsMapOverviewCanvas::drawExtentRect()
85 {
86   if ( !mMapCanvas ) return;
87 
88   const QgsRectangle &extent = mMapCanvas->extent();
89 
90   // show only when valid extent is set
91   if ( extent.isEmpty() || mSettings.visibleExtent().isEmpty() )
92   {
93     mPanningWidget->hide();
94     return;
95   }
96 
97   const QPolygonF &vPoly = mMapCanvas->mapSettings().visiblePolygon();
98   const QgsMapToPixel &cXf = mSettings.mapToPixel();
99   QVector< QPoint > pts;
100   pts.push_back( cXf.transform( QgsPointXY( vPoly[0] ) ).toQPointF().toPoint() );
101   pts.push_back( cXf.transform( QgsPointXY( vPoly[1] ) ).toQPointF().toPoint() );
102   pts.push_back( cXf.transform( QgsPointXY( vPoly[2] ) ).toQPointF().toPoint() );
103   pts.push_back( cXf.transform( QgsPointXY( vPoly[3] ) ).toQPointF().toPoint() );
104   mPanningWidget->setPolygon( QPolygon( pts ) );
105   mPanningWidget->show(); // show if hidden
106 }
107 
108 
mousePressEvent(QMouseEvent * e)109 void QgsMapOverviewCanvas::mousePressEvent( QMouseEvent *e )
110 {
111 //  if (mPanningWidget->isHidden())
112 //    return;
113 
114   // set offset in panning widget if inside it
115   // for better experience with panning :)
116   if ( mPanningWidget->geometry().contains( e->pos() ) )
117   {
118     mPanningCursorOffset = e->pos() - mPanningWidget->pos();
119   }
120   else
121   {
122     // use center of the panning widget if outside
123     const QSize s = mPanningWidget->size();
124     mPanningCursorOffset = QPoint( s.width() / 2, s.height() / 2 );
125   }
126   updatePanningWidget( e->pos() );
127 }
128 
129 
mouseReleaseEvent(QMouseEvent * e)130 void QgsMapOverviewCanvas::mouseReleaseEvent( QMouseEvent *e )
131 {
132 //  if (mPanningWidget->isHidden())
133 //    return;
134 
135   if ( e->button() == Qt::LeftButton )
136   {
137     // set new extent
138     const QgsMapToPixel &cXf = mSettings.mapToPixel();
139     const QRect rect = mPanningWidget->geometry();
140 
141     const QgsPointXY center = cXf.toMapCoordinates( rect.center() );
142     mMapCanvas->setCenter( center );
143     mMapCanvas->refresh();
144   }
145 }
146 
147 
wheelEvent(QWheelEvent * e)148 void QgsMapOverviewCanvas::wheelEvent( QWheelEvent *e )
149 {
150   double zoomFactor = e->angleDelta().y() > 0 ? 1. / mMapCanvas->zoomInFactor() : mMapCanvas->zoomOutFactor();
151 
152   // "Normal" mouse have an angle delta of 120, precision mouses provide data faster, in smaller steps
153   zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 120.0 * std::fabs( e->angleDelta().y() );
154 
155   if ( e->modifiers() & Qt::ControlModifier )
156   {
157     //holding ctrl while wheel zooming results in a finer zoom
158     zoomFactor = 1.0 + ( zoomFactor - 1.0 ) / 20.0;
159   }
160 
161   const double signedWheelFactor = e->angleDelta().y() > 0 ? 1 / zoomFactor : zoomFactor;
162 
163   const QgsMapToPixel &cXf = mSettings.mapToPixel();
164 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
165   QgsPointXY center = cXf.toMapCoordinates( e->pos().x(), e->pos().y() );
166   updatePanningWidget( QPoint( e->pos().x(), e->pos().y() ) );
167 #else
168   const QgsPointXY center = cXf.toMapCoordinates( e->position().x(), e->position().y() );
169   updatePanningWidget( QPoint( e->position().x(), e->position().y() ) );
170 #endif
171   mMapCanvas->zoomByFactor( signedWheelFactor, &center );
172 }
173 
mouseMoveEvent(QMouseEvent * e)174 void QgsMapOverviewCanvas::mouseMoveEvent( QMouseEvent *e )
175 {
176   // move with panning widget if tracking cursor
177   if ( ( e->buttons() & Qt::LeftButton ) == Qt::LeftButton )
178   {
179     updatePanningWidget( e->pos() );
180   }
181 }
182 
183 
updatePanningWidget(QPoint pos)184 void QgsMapOverviewCanvas::updatePanningWidget( QPoint pos )
185 {
186 //  if (mPanningWidget->isHidden())
187 //    return;
188   mPanningWidget->move( pos.x() - mPanningCursorOffset.x(), pos.y() - mPanningCursorOffset.y() );
189 }
190 
refresh()191 void QgsMapOverviewCanvas::refresh()
192 {
193   if ( !isVisible() )
194     return;
195 
196   updateFullExtent();
197 
198   if ( !mSettings.hasValidSettings() )
199   {
200     mPixmap = QPixmap();
201     update();
202     return; // makes no sense to render anything
203   }
204 
205   if ( mJob )
206   {
207     QgsDebugMsg( QStringLiteral( "oveview - canceling old" ) );
208     mJob->cancel();
209     QgsDebugMsg( QStringLiteral( "oveview - deleting old" ) );
210     delete mJob; // get rid of previous job (if any)
211   }
212 
213   QgsDebugMsg( QStringLiteral( "oveview - starting new" ) );
214 
215   // TODO: setup overview mode
216   mJob = new QgsMapRendererSequentialJob( mSettings );
217   connect( mJob, &QgsMapRendererJob::finished, this, &QgsMapOverviewCanvas::mapRenderingFinished );
218   mJob->start();
219 
220   setBackgroundColor( mMapCanvas->mapSettings().backgroundColor() );
221 
222   // schedule repaint
223   update();
224 
225   // update panning widget
226   drawExtentRect();
227 }
228 
mapRenderingFinished()229 void QgsMapOverviewCanvas::mapRenderingFinished()
230 {
231   QgsDebugMsg( QStringLiteral( "overview - finished" ) );
232   mPixmap = QPixmap::fromImage( mJob->renderedImage() );
233 
234   delete mJob;
235   mJob = nullptr;
236 
237   // schedule repaint
238   update();
239 }
240 
layerRepaintRequested(bool deferred)241 void QgsMapOverviewCanvas::layerRepaintRequested( bool deferred )
242 {
243   if ( !deferred )
244     refresh();
245 }
246 
247 
setBackgroundColor(const QColor & color)248 void QgsMapOverviewCanvas::setBackgroundColor( const QColor &color )
249 {
250   mSettings.setBackgroundColor( color );
251 
252   // set erase color
253   QPalette palette;
254   palette.setColor( backgroundRole(), color );
255   setPalette( palette );
256 }
257 
setLayers(const QList<QgsMapLayer * > & layers)258 void QgsMapOverviewCanvas::setLayers( const QList<QgsMapLayer *> &layers )
259 {
260   const auto oldLayers = mSettings.layers();
261   for ( QgsMapLayer *ml : oldLayers )
262   {
263     disconnect( ml, &QgsMapLayer::repaintRequested, this, &QgsMapOverviewCanvas::layerRepaintRequested );
264   }
265 
266   mSettings.setLayers( layers );
267 
268   const auto newLayers = mSettings.layers();
269   for ( QgsMapLayer *ml : newLayers )
270   {
271     connect( ml, &QgsMapLayer::repaintRequested, this, &QgsMapOverviewCanvas::layerRepaintRequested );
272   }
273 
274   updateFullExtent();
275 
276   refresh();
277 }
278 
updateFullExtent()279 void QgsMapOverviewCanvas::updateFullExtent()
280 {
281   QgsRectangle rect;
282   if ( !QgsProject::instance()->viewSettings()->presetFullExtent().isNull() )
283   {
284     const QgsReferencedRectangle extent = QgsProject::instance()->viewSettings()->fullExtent();
285     QgsCoordinateTransform ct( extent.crs(), mSettings.destinationCrs(), QgsProject::instance()->transformContext() );
286     ct.setBallparkTransformsAreAppropriate( true );
287     try
288     {
289       rect = ct.transformBoundingBox( extent );
290     }
291     catch ( QgsCsException & )
292     {
293     }
294   }
295 
296   if ( rect.isNull() )
297   {
298     if ( mSettings.hasValidSettings() )
299       rect = mSettings.fullExtent();
300     else
301       rect = mMapCanvas->projectExtent();
302   }
303 
304   // expand a bit to keep features on margin
305   rect.scale( 1.1 );
306 
307   mSettings.setExtent( rect );
308   drawExtentRect();
309 }
310 
destinationCrsChanged()311 void QgsMapOverviewCanvas::destinationCrsChanged()
312 {
313   mSettings.setDestinationCrs( mMapCanvas->mapSettings().destinationCrs() );
314 }
315 
transformContextChanged()316 void QgsMapOverviewCanvas::transformContextChanged()
317 {
318   mSettings.setTransformContext( mMapCanvas->mapSettings().transformContext() );
319 }
320 
layers() const321 QList<QgsMapLayer *> QgsMapOverviewCanvas::layers() const
322 {
323   return mSettings.layers();
324 }
325 
326 
327 /// @cond PRIVATE
328 
QgsPanningWidget(QWidget * parent)329 QgsPanningWidget::QgsPanningWidget( QWidget *parent )
330   : QWidget( parent )
331 {
332   setObjectName( QStringLiteral( "panningWidget" ) );
333   setMinimumSize( 5, 5 );
334   setAttribute( Qt::WA_NoSystemBackground );
335 }
336 
setPolygon(const QPolygon & p)337 void QgsPanningWidget::setPolygon( const QPolygon &p )
338 {
339   if ( p == mPoly ) return;
340   mPoly = p;
341 
342   //ensure polygon is closed
343   if ( mPoly.at( 0 ) != mPoly.at( mPoly.length() - 1 ) )
344     mPoly.append( mPoly.at( 0 ) );
345 
346   const QRect rect = p.boundingRect() + QMargins( 1, 1, 1, 1 );
347   setGeometry( rect );
348   update();
349 }
350 
paintEvent(QPaintEvent * pe)351 void QgsPanningWidget::paintEvent( QPaintEvent *pe )
352 {
353   Q_UNUSED( pe )
354 
355   QPainter p;
356 
357   p.begin( this );
358   const QPolygonF t = mPoly.translated( -mPoly.boundingRect().left() + 1, -mPoly.boundingRect().top() + 1 );
359 
360   // drawPolygon causes issues on windows - corners of path may be missing resulting in triangles being drawn
361   // instead of rectangles! (Same cause as #13343)
362   QPainterPath path;
363   path.addPolygon( t );
364 
365   QPen pen;
366   pen.setJoinStyle( Qt::MiterJoin );
367   pen.setColor( Qt::white );
368   pen.setWidth( 3 );
369   p.setPen( pen );
370   p.drawPath( path );
371   pen.setColor( Qt::red );
372   pen.setWidth( 1 );
373   p.setPen( pen );
374   p.drawPath( path );
375 
376   p.end();
377 }
378 
379 
380 
381 ///@endcond
382