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, ¢er );
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