From f984b2775f0a329d5c3f6d777fcbe9524ea3e5d1 Mon Sep 17 00:00:00 2001 From: speillet Date: Sat, 8 Feb 2020 01:17:25 +0100 Subject: [PATCH] Merge master into fix_mixed_layer_oracle --- external/mdal/mdal.cpp | 2 +- .../layout/qgslayoutitempicture.sip.in | 2 +- .../layout/qgslayouttable.sip.in | 2 +- .../layout/qgslayouttablecolumn.sip.in | 2 +- .../qgsdatadefinedsizelegend.sip.in | 2 +- python/core/auto_generated/qgsproject.sip.in | 2 +- .../auto_generated/qgsrendercontext.sip.in | 29 ++++- .../gui/auto_generated/qgscolorbutton.sip.in | 11 ++ python/gui/auto_generated/qgsmapcanvas.sip.in | 7 ++ .../testdata/expected/mbg_rect_field.gfs | 52 -------- .../testdata/expected/mbg_rect_field.gml | 26 ++-- .../testdata/expected/oriented_bounds.gfs | 57 --------- .../testdata/expected/oriented_bounds.gml | 45 +++---- resources/qgis_global_settings.ini | 4 + src/app/gps/qgsgpsbearingitem.cpp | 5 +- src/app/gps/qgsgpsbearingitem.h | 8 +- src/app/gps/qgsgpsinformationwidget.cpp | 39 +++++- src/app/gps/qgsgpsinformationwidget.h | 7 ++ src/app/qgisapp.cpp | 4 + src/core/auth/qgsauthcertutils.cpp | 5 + src/core/expression/qgsexpressionnodeimpl.cpp | 5 +- src/core/geometry/qgsgeometry.cpp | 2 +- .../layertree/qgslayertreemodellegendnode.cpp | 4 +- src/core/layout/qgslayoutitempicture.h | 2 +- src/core/layout/qgslayoutitemscalebar.h | 2 +- src/core/layout/qgslayouttable.h | 4 +- src/core/layout/qgslayouttablecolumn.h | 2 +- src/core/qgsdatadefinedsizelegend.cpp | 58 +++++---- src/core/qgsdatadefinedsizelegend.h | 2 +- src/core/qgsproject.h | 4 +- src/core/qgsrendercontext.h | 33 ++++- src/core/qgsvectorlayer.cpp | 13 ++ src/core/qgsvirtuallayerdefinition.cpp | 113 +++++++++++++++--- src/core/symbology/qgsmarkersymbollayer.cpp | 16 ++- .../attributetable/qgsattributetablemodel.cpp | 8 +- src/gui/qgscolorbutton.cpp | 1 + src/gui/qgscolorbutton.h | 9 ++ src/gui/qgsmapcanvas.cpp | 9 ++ src/gui/qgsmapcanvas.h | 6 + src/gui/qgsmaptoolpan.cpp | 1 - src/gui/qgsnewhttpconnection.cpp | 100 ++++++++++++++-- src/gui/qgsquerybuilder.cpp | 17 ++- .../qgstableeditorformattingwidget.cpp | 10 ++ src/providers/oracle/qgsoracleprovider.cpp | 6 +- src/providers/wcs/qgswcsprovider.cpp | 14 ++- src/server/qgsserver.cpp | 15 ++- src/server/qgsserviceregistry.cpp | 2 +- src/ui/qgsgpsinformationwidgetbase.ui | 16 +++ tests/src/core/testqgsexpression.cpp | 16 +++ tests/src/core/testqgsgeometry.cpp | 30 +++++ tests/src/python/test_provider_oracle.py | 35 +++++- tests/src/python/test_qgscolorbutton.py | 33 +++++ tests/src/python/test_qgsgeometry.py | 10 +- .../src/python/test_qgssymbollayer_readsld.py | 5 + .../expected_basic_bottom.png | Bin 5473 -> 5456 bytes .../expected_crowded/expected_crowded.png | Bin 7272 -> 7791 bytes ...ted_legend_data_defined_size_collapsed.png | Bin 13449 -> 13304 bytes ...egend_data_defined_size_collapsed_mask.png | Bin 9817 -> 0 bytes 58 files changed, 653 insertions(+), 261 deletions(-) delete mode 100644 python/plugins/processing/tests/testdata/expected/mbg_rect_field.gfs delete mode 100644 python/plugins/processing/tests/testdata/expected/oriented_bounds.gfs delete mode 100644 tests/testdata/control_images/legend/expected_legend_data_defined_size_collapsed/expected_legend_data_defined_size_collapsed_mask.png diff --git a/external/mdal/mdal.cpp b/external/mdal/mdal.cpp index b0b23a634f76..eeb669aac71b 100644 --- a/external/mdal/mdal.cpp +++ b/external/mdal/mdal.cpp @@ -22,7 +22,7 @@ static MDAL_Status sLastStatus; const char *MDAL_Version() { - return "0.4.95"; + return "0.5.0"; } MDAL_Status MDAL_LastStatus() diff --git a/python/core/auto_generated/layout/qgslayoutitempicture.sip.in b/python/core/auto_generated/layout/qgslayoutitempicture.sip.in index 86059a250ad7..09b824e4c16c 100644 --- a/python/core/auto_generated/layout/qgslayoutitempicture.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitempicture.sip.in @@ -153,7 +153,7 @@ Sets the ``offset`` added to the picture's rotation from a map's North. ResizeMode resizeMode() const; %Docstring -Returns the resize mode used for drawing the picture within the composer +Returns the resize mode used for drawing the picture within the layout item's frame. .. seealso:: :py:func:`setResizeMode` diff --git a/python/core/auto_generated/layout/qgslayouttable.sip.in b/python/core/auto_generated/layout/qgslayouttable.sip.in index 261544b9d82b..fe5ea319407b 100644 --- a/python/core/auto_generated/layout/qgslayouttable.sip.in +++ b/python/core/auto_generated/layout/qgslayouttable.sip.in @@ -22,7 +22,7 @@ typedef QVector< QgsLayoutTableColumn * > QgsLayoutTableColumns; class QgsLayoutTableStyle { %Docstring -Styling option for a composer table cell +Styling option for a layout table cell .. versionadded:: 3.0 %End diff --git a/python/core/auto_generated/layout/qgslayouttablecolumn.sip.in b/python/core/auto_generated/layout/qgslayouttablecolumn.sip.in index 4b51ca059c95..e86ba612af17 100644 --- a/python/core/auto_generated/layout/qgslayouttablecolumn.sip.in +++ b/python/core/auto_generated/layout/qgslayouttablecolumn.sip.in @@ -199,7 +199,7 @@ Sets the sort ``rank`` for the column. If the sort rank is > 0 then the column will be sorted in the table. The sort rank specifies the priority given to the column when the table is sorted by multiple columns, with lower sort ranks having higher priority. This property is only used when the column -is part of a :py:class:`QgsComposerAttributeTable`. +is part of a :py:class:`QgsLayoutItemAttributeTable`. If the sort ``rank`` is <= 0 then the column is not being sorted. .. note:: diff --git a/python/core/auto_generated/qgsdatadefinedsizelegend.sip.in b/python/core/auto_generated/qgsdatadefinedsizelegend.sip.in index 44c59adea575..925c5fbdf92c 100644 --- a/python/core/auto_generated/qgsdatadefinedsizelegend.sip.in +++ b/python/core/auto_generated/qgsdatadefinedsizelegend.sip.in @@ -150,7 +150,7 @@ Updates the list of classes, source symbol and title label from given symbol and Generates legend symbol items according to the configuration %End - void drawCollapsedLegend( QgsRenderContext &context, QSize *outputSize /Out/ = 0, int *labelXOffset /Out/ = 0 ) const; + void drawCollapsedLegend( QgsRenderContext &context, QSizeF *outputSize /Out/ = 0, double *labelXOffset /Out/ = 0 ) const; %Docstring Draw the legend if using LegendOneNodeForAll and optionally output size of the legend and x offset of labels (in painter units). If the painter in context is ``None``, it only does size calculation without actual rendering. diff --git a/python/core/auto_generated/qgsproject.sip.in b/python/core/auto_generated/qgsproject.sip.in index 128a52445d55..a468424d3f92 100644 --- a/python/core/auto_generated/qgsproject.sip.in +++ b/python/core/auto_generated/qgsproject.sip.in @@ -575,7 +575,7 @@ an absolute path. QgsLayoutManager *layoutManager(); %Docstring -Returns the project's layout manager, which manages compositions within +Returns the project's layout manager, which manages print layouts, atlases and reports within the project. .. versionadded:: 3.0 diff --git a/python/core/auto_generated/qgsrendercontext.sip.in b/python/core/auto_generated/qgsrendercontext.sip.in index 258d2f498953..069ff056ea1c 100644 --- a/python/core/auto_generated/qgsrendercontext.sip.in +++ b/python/core/auto_generated/qgsrendercontext.sip.in @@ -282,11 +282,15 @@ of any faster raster shortcuts. bool useAdvancedEffects() const; %Docstring Returns ``True`` if advanced effects such as blend modes such be used + +.. seealso:: :py:func:`setUseAdvancedEffects` %End void setUseAdvancedEffects( bool enabled ); %Docstring Used to enable or disable advanced effects such as blend modes + +.. seealso:: :py:func:`useAdvancedEffects` %End bool drawEditingInformation() const; @@ -479,9 +483,16 @@ Sets whether vector selections should be shown in the rendered map bool useRenderingOptimization() const; %Docstring Returns ``True`` if the rendering optimization (geometry simplification) can be executed + +.. seealso:: :py:func:`setUseRenderingOptimization` %End void setUseRenderingOptimization( bool enabled ); +%Docstring +Sets whether the rendering optimization (geometry simplification) should be executed + +.. seealso:: :py:func:`useRenderingOptimization` +%End const QgsVectorSimplifyMethod &vectorSimplifyMethod() const; %Docstring @@ -565,22 +576,36 @@ Gets the filter feature provider used for additional filtering of rendered featu %Docstring Sets the segmentation tolerance applied when rendering curved geometries -:param tolerance: the segmentation tolerance* +:param tolerance: the segmentation tolerance + +.. seealso:: :py:func:`segmentationTolerance` + +.. seealso:: :py:func:`segmentationToleranceType` %End + double segmentationTolerance() const; %Docstring Gets the segmentation tolerance applied when rendering curved geometries + +.. seealso:: :py:func:`setSegmentationTolerance` %End void setSegmentationToleranceType( QgsAbstractGeometry::SegmentationToleranceType type ); %Docstring Sets segmentation tolerance type (maximum angle or maximum difference between curve and approximation) -:param type: the segmentation tolerance typename* +:param type: the segmentation tolerance typename + +.. seealso:: :py:func:`segmentationToleranceType` + +.. seealso:: :py:func:`segmentationTolerance` %End + QgsAbstractGeometry::SegmentationToleranceType segmentationToleranceType() const; %Docstring Gets segmentation tolerance type (maximum angle or maximum difference between curve and approximation) + +.. seealso:: :py:func:`setSegmentationToleranceType` %End diff --git a/python/gui/auto_generated/qgscolorbutton.sip.in b/python/gui/auto_generated/qgscolorbutton.sip.in index 6de8b753450d..13f086f34bf4 100644 --- a/python/gui/auto_generated/qgscolorbutton.sip.in +++ b/python/gui/auto_generated/qgscolorbutton.sip.in @@ -415,6 +415,8 @@ Sets color to null. .. seealso:: :py:func:`setToNoColor` +.. seealso:: :py:func:`cleared` + .. versionadded:: 2.16 %End @@ -448,6 +450,15 @@ Emitted when the button is clicked, if the button's behavior is set to SignalOnl .. seealso:: :py:func:`setBehavior` .. seealso:: :py:func:`behavior` +%End + + void cleared(); +%Docstring +Emitted when the color is cleared (set to null). + +.. seealso:: :py:func:`setToNull` + +.. versionadded:: 3.12 %End void unlinked(); diff --git a/python/gui/auto_generated/qgsmapcanvas.sip.in b/python/gui/auto_generated/qgsmapcanvas.sip.in index 12a29d83f51f..d9dd45d8d617 100644 --- a/python/gui/auto_generated/qgsmapcanvas.sip.in +++ b/python/gui/auto_generated/qgsmapcanvas.sip.in @@ -1020,6 +1020,13 @@ This signal will be emitted during a pan operation as the user moves the map, giving the total distance and bearing between the map position at the start of the pan and the current pan position. +.. versionadded:: 3.12 +%End + + void tapAndHoldGestureOccurred( const QgsPointXY &mapPoint, QTapAndHoldGesture *gesture ); +%Docstring +Emitted whenever a tap and hold ``gesture`` occurs at the specified map point. + .. versionadded:: 3.12 %End diff --git a/python/plugins/processing/tests/testdata/expected/mbg_rect_field.gfs b/python/plugins/processing/tests/testdata/expected/mbg_rect_field.gfs deleted file mode 100644 index e9cc8449a238..000000000000 --- a/python/plugins/processing/tests/testdata/expected/mbg_rect_field.gfs +++ /dev/null @@ -1,52 +0,0 @@ - - - mbg_rect_field - mbg_rect_field - - 3 - EPSG:4326 - - 4 - -2.05660 - 9.16296 - -3.00000 - 6.08868 - - - id - id - Integer - - - name - name - String - 2 - - - width - width - Real - - - height - height - Real - - - angle - angle - Real - - - area - area - Real - - - perimeter - perimeter - Real - - - diff --git a/python/plugins/processing/tests/testdata/expected/mbg_rect_field.gml b/python/plugins/processing/tests/testdata/expected/mbg_rect_field.gml index 0f39371ea181..07a223afb66d 100644 --- a/python/plugins/processing/tests/testdata/expected/mbg_rect_field.gml +++ b/python/plugins/processing/tests/testdata/expected/mbg_rect_field.gml @@ -1,13 +1,13 @@ -2.056603773584904-3 - 9.1629558541266816.088675623800386 + 9.1629558541266826.489909219550769 @@ -25,26 +25,26 @@ - 5.24145873320538,-0.054510556621882 5.24145873320538,-1.05451055662188 7.24145873320538,-1.05451055662188 7.24145873320538,-0.054510556621882 5.24145873320538,-0.054510556621882 + 5.24145873320538,-1.05451055662188 6.24145873320538,-2.05451055662188 7.24145873320538,-1.05451055662188 6.24145873320538,-0.054510556621882 5.24145873320538,-1.05451055662188 1 dd - 1.000000 - 2.000000 - 90.000000 + 1.414214 + 1.414214 + 45.000000 2.000000 - 6.000000 + 5.656854 - 2.0,6.08867562380039 2.0,4.4236084452975 5.1725527831094,4.4236084452975 5.1725527831094,6.08867562380039 2.0,6.08867562380039 + 5.34748915030878,4.7278689643513 5.06593031585745,6.48990921955077 1.84418704183259,5.97510243323637 2.12574587628392,4.2130621780369 5.34748915030878,4.7278689643513 2 bb - 1.665067 - 3.172553 - 90.000000 - 5.282514 - 9.675240 + 1.784394 + 3.262615 + 80.921378 + 5.821790 + 10.094017 diff --git a/python/plugins/processing/tests/testdata/expected/oriented_bounds.gfs b/python/plugins/processing/tests/testdata/expected/oriented_bounds.gfs deleted file mode 100644 index 4f4d91bbfc2f..000000000000 --- a/python/plugins/processing/tests/testdata/expected/oriented_bounds.gfs +++ /dev/null @@ -1,57 +0,0 @@ - - - oriented_bounds - oriented_bounds - - 3 - EPSG:4326 - - 6 - -1.00000 - 10.04414 - -3.27034 - 6.44118 - - - intval - intval - Integer - - - floatval - floatval - Real - - - width - width - Real - - - height - height - Real - - - angle - angle - Real - - - area - area - Real - - - perimeter - perimeter - Real - - - name - name - String - 5 - - - diff --git a/python/plugins/processing/tests/testdata/expected/oriented_bounds.gml b/python/plugins/processing/tests/testdata/expected/oriented_bounds.gml index f9a6aea471dd..9381e504923a 100644 --- a/python/plugins/processing/tests/testdata/expected/oriented_bounds.gml +++ b/python/plugins/processing/tests/testdata/expected/oriented_bounds.gml @@ -1,21 +1,22 @@ - -1-3.270344827586206 - 10.044137931034486.441176470588236 + -1-3.270344827586211 + 10.04413793103456.468552677345371 - + - 6,-3 7.69811320754717,0.056603773584906 3.80943396226415,2.21698113207547 2.11132075471698,-0.839622641509436 6,-3 + 6.0,-3.0 7.69811320754717,0.056603773584905 3.80943396226415,2.21698113207547 2.11132075471698,-0.839622641509436 6.0,-3.0 120 -100291.43213 + 3.496629 4.448489 119.054604 @@ -25,34 +26,35 @@ - 4.11764705882353,3.52941176470588 6.0,4.0 5.72941176470588,5.08235294117647 3.84705882352941,4.61176470588235 4.11764705882353,3.52941176470588 + 5.6332734,5.0792647 3.84029,4.4700214 4.2070166,3.3907567 6,4 5.6332734,5.0792647 -33 0 Aaaaa - 1.940285 - 1.115664 - 165.963757 - 2.164706 - 6.111898 + 1.893665 + 1.139869 + 161.232595 + 2.158529 + 6.067067 - -1.0,3.0 -1.0,-1.0 3.0,-1.0 3.0,3.0 -1.0,3.0 + -1,-1 3,-1 3,3 -1,3 -1,-1 33 44.12346 aaaaa 4.000000 4.000000 - 90.000000 + 0.000000 16.000000 16.000000 - 6.4,-3.0 9.64413793103449,-3.27034482758621 10.0441379310345,1.52965517241379 6.8,1.8 6.4,-3.0 + 6.4,-3 9.6441379,-3.2703448 10.0441379,1.5296552 6.8,1.8 6.4,-3 0 + ASDF 3.255383 4.816638 @@ -63,19 +65,20 @@ - 1.36470588235294,4.94117647058824 2.1,4.5 3.0,6.0 2.26470588235294,6.44117647058824 1.36470588235294,4.94117647058824 + 1.1679275,5.4220069 1.6,4.8 3.1065959,5.8465458 2.6745233,6.4685527 1.1679275,5.4220069 + 0.123 bbaaa - 0.857493 - 1.749286 - 30.963757 - 1.500000 - 5.213557 + 0.757350 + 1.834418 + 55.214507 + 1.389297 + 5.183536 - 6,-3 7.69811320754717,0.056603773584906 3.80943396226415,2.21698113207547 2.11132075471698,-0.839622641509436 6,-3 + 6.0,-3.0 7.69811320754717,0.056603773584905 3.80943396226415,2.21698113207547 2.11132075471698,-0.839622641509436 6.0,-3.0 2 3.33 elim diff --git a/resources/qgis_global_settings.ini b/resources/qgis_global_settings.ini index 60eca6dc5d5a..78e12efd247d 100644 --- a/resources/qgis_global_settings.ini +++ b/resources/qgis_global_settings.ini @@ -54,6 +54,10 @@ maxRecentProjects=20 # notification to be shown when the task completes. minTaskLengthForSystemNotification=5 +# Whether to show the distance panned message in the status bar after a map pan operation +# occurs. Set to false to disable the message. +showPanDistanceInStatusBar=true + # Whether to prompt users for a selection when multiple possible transformation paths exist # when transforming coordinates. If false, a reasonable choice will be estimated by default # without asking users. If true, users are always required to make this choice themselves. diff --git a/src/app/gps/qgsgpsbearingitem.cpp b/src/app/gps/qgsgpsbearingitem.cpp index c67f3f358457..877a56f8425a 100644 --- a/src/app/gps/qgsgpsbearingitem.cpp +++ b/src/app/gps/qgsgpsbearingitem.cpp @@ -40,13 +40,14 @@ QgsGpsBearingItem::QgsGpsBearingItem( QgsMapCanvas *mapCanvas ) void QgsGpsBearingItem::setGpsPosition( const QgsPointXY &point ) { + mCenterWGS84 = point; //transform to map crs if ( mMapCanvas ) { QgsCoordinateTransform t( mWgs84CRS, mMapCanvas->mapSettings().destinationCrs(), QgsProject::instance() ); try { - mCenter = t.transform( point ); + mCenter = t.transform( mCenterWGS84 ); } catch ( QgsCsException &e ) //silently ignore transformation exceptions { @@ -69,7 +70,7 @@ void QgsGpsBearingItem::setGpsBearing( double bearing ) void QgsGpsBearingItem::updatePosition() { - setGpsPosition( mCenter ); + setGpsPosition( mCenterWGS84 ); } void QgsGpsBearingItem::updateLine() diff --git a/src/app/gps/qgsgpsbearingitem.h b/src/app/gps/qgsgpsbearingitem.h index 2cd3b6637497..1fa62c601f40 100644 --- a/src/app/gps/qgsgpsbearingitem.h +++ b/src/app/gps/qgsgpsbearingitem.h @@ -34,6 +34,9 @@ class QgsGpsBearingItem : public QObject, public QgsMapCanvasLineSymbolItem public: explicit QgsGpsBearingItem( QgsMapCanvas *mapCanvas ); + /** + * Point is in WGS84 + */ void setGpsPosition( const QgsPointXY &point ); void setGpsBearing( double bearing ); @@ -41,9 +44,12 @@ class QgsGpsBearingItem : public QObject, public QgsMapCanvasLineSymbolItem protected: - //! coordinates of the point in the center + //! coordinates of the point in the center (map units) QgsPointXY mCenter; + //! coordinates of the point in the center (WGS84) + QgsPointXY mCenterWGS84; + private: void updateLine(); diff --git a/src/app/gps/qgsgpsinformationwidget.cpp b/src/app/gps/qgsgpsinformationwidget.cpp index 9d091c34429f..fd2fc87d3d90 100644 --- a/src/app/gps/qgsgpsinformationwidget.cpp +++ b/src/app/gps/qgsgpsinformationwidget.cpp @@ -81,6 +81,8 @@ QgsGpsInformationWidget::QgsGpsInformationWidget( QgsMapCanvas *mapCanvas, QWidg Q_ASSERT( mMapCanvas ); // precondition setupUi( this ); connect( mConnectButton, &QPushButton::toggled, this, &QgsGpsInformationWidget::mConnectButton_toggled ); + connect( mRecenterButton, &QPushButton::clicked, this, &QgsGpsInformationWidget::recenter ); + connect( mConnectButton, &QAbstractButton::toggled, mRecenterButton, &QWidget::setEnabled ); connect( mBtnTrackColor, &QgsColorButton::colorChanged, this, &QgsGpsInformationWidget::trackColorChanged ); connect( mSpinTrackWidth, qgis::overload< int >::of( &QSpinBox::valueChanged ), this, &QgsGpsInformationWidget::mSpinTrackWidth_valueChanged ); connect( mBtnPosition, &QToolButton::clicked, this, &QgsGpsInformationWidget::mBtnPosition_clicked ); @@ -94,6 +96,9 @@ QgsGpsInformationWidget::QgsGpsInformationWidget( QgsMapCanvas *mapCanvas, QWidg connect( mBtnResetFeature, &QToolButton::clicked, this, &QgsGpsInformationWidget::mBtnResetFeature_clicked ); connect( mBtnLogFile, &QPushButton::clicked, this, &QgsGpsInformationWidget::mBtnLogFile_clicked ); connect( mMapCanvas, &QgsMapCanvas::xyCoordinates, this, &QgsGpsInformationWidget::cursorCoordinateChanged ); + connect( mMapCanvas, &QgsMapCanvas::tapAndHoldGestureOccurred, this, &QgsGpsInformationWidget::tapAndHold ); + + mRecenterButton->setEnabled( false ); mWgs84CRS = QgsCoordinateReferenceSystem::fromOgcWmsCrs( QStringLiteral( "EPSG:4326" ) ); @@ -551,6 +556,20 @@ void QgsGpsInformationWidget::mConnectButton_toggled( bool flag ) } } +void QgsGpsInformationWidget::recenter() +{ + try + { + const QgsPointXY center = mCanvasToWgs84Transform.transform( mLastGpsPosition, QgsCoordinateTransform::ReverseTransform ); + mMapCanvas->setCenter( center ); + mMapCanvas->refresh(); + } + catch ( QgsCsException & ) + { + + } +} + void QgsGpsInformationWidget::connectGps() { // clear position page fields to give better indication that something happened (or didn't happen) @@ -1513,12 +1532,12 @@ void QgsGpsInformationWidget::updateGpsDistanceStatusMessage() const double distance = mDistanceCalculator.convertLengthMeasurement( mDistanceCalculator.measureLine( QVector< QgsPointXY >() << mLastCursorPosWgs84 << mLastGpsPosition ), QgsProject::instance()->distanceUnits() ); - const double bearing = 180 * mDistanceCalculator.bearing( mLastCursorPosWgs84, mLastGpsPosition ) / M_PI; + const double bearing = 180 * mDistanceCalculator.bearing( mLastGpsPosition, mLastCursorPosWgs84 ) / M_PI; const int distanceDecimalPlaces = QgsSettings().value( QStringLiteral( "qgis/measure/decimalplaces" ), "3" ).toInt(); const QString distanceString = QgsDistanceArea::formatDistance( distance, distanceDecimalPlaces, QgsProject::instance()->distanceUnits() ); const QString bearingString = mBearingNumericFormat->formatDouble( bearing, QgsNumericFormatContext() ); - QgisApp::instance()->statusBarIface()->showMessage( tr( "Distance to GPS location %1 (%2)" ).arg( distanceString, bearingString ), 2000 ); + QgisApp::instance()->statusBarIface()->showMessage( tr( "%1 (%2) from GPS location" ).arg( distanceString, bearingString ), 2000 ); } void QgsGpsInformationWidget::updateTimestampDestinationFields( QgsMapLayer *mapLayer ) @@ -1555,6 +1574,22 @@ void QgsGpsInformationWidget::updateTimestampDestinationFields( QgsMapLayer *map mPopulatingFields = false; } +void QgsGpsInformationWidget::tapAndHold( const QgsPointXY &mapPoint, QTapAndHoldGesture * ) +{ + if ( !mNmea ) + return; + + try + { + mLastCursorPosWgs84 = mCanvasToWgs84Transform.transform( mapPoint ); + updateGpsDistanceStatusMessage(); + } + catch ( QgsCsException & ) + { + + } +} + void QgsGpsInformationWidget::switchAcquisition() { if ( mAcquisitionInterval > 0 ) diff --git a/src/app/gps/qgsgpsinformationwidget.h b/src/app/gps/qgsgpsinformationwidget.h index 11e4cfed5b63..0d448b4effe0 100644 --- a/src/app/gps/qgsgpsinformationwidget.h +++ b/src/app/gps/qgsgpsinformationwidget.h @@ -54,8 +54,14 @@ class APP_EXPORT QgsGpsInformationWidget: public QgsPanelWidget, private Ui::Qgs public: QgsGpsInformationWidget( QgsMapCanvas *mapCanvas, QWidget *parent = nullptr ); ~QgsGpsInformationWidget() override; + + public slots: + void tapAndHold( const QgsPointXY &mapPoint, QTapAndHoldGesture *gesture ); + + private slots: void mConnectButton_toggled( bool flag ); + void recenter(); void displayGPSInformation( const QgsGpsInformation &info ); void logNmeaSentence( const QString &nmeaString ); // added to handle 'raw' data void updateCloseFeatureButton( QgsMapLayer *lyr ); @@ -87,6 +93,7 @@ class APP_EXPORT QgsGpsInformationWidget: public QgsPanelWidget, private Ui::Qgs * Updates compatible fields for timestamp recording */ void updateTimestampDestinationFields( QgsMapLayer *mapLayer ); + private: enum FixStatus //GPS status { diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index ef2f16b09982..6b8be89e0424 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -13044,6 +13044,10 @@ void QgisApp::showRotation() void QgisApp::showPanMessage( double distance, QgsUnitTypes::DistanceUnit unit, double bearing ) { + const bool showMessage = QgsSettings().value( QStringLiteral( "showPanDistanceInStatusBar" ), true, QgsSettings::App ).toBool(); + if ( !showMessage ) + return; + const double distanceInProjectUnits = distance * QgsUnitTypes::fromUnitToUnitFactor( unit, QgsProject::instance()->distanceUnits() ); const int distanceDecimalPlaces = QgsSettings().value( QStringLiteral( "qgis/measure/decimalplaces" ), "3" ).toInt(); const QString distanceString = QgsDistanceArea::formatDistance( distanceInProjectUnits, distanceDecimalPlaces, QgsProject::instance()->distanceUnits() ); diff --git a/src/core/auth/qgsauthcertutils.cpp b/src/core/auth/qgsauthcertutils.cpp index a579cfda72c0..04dc117dd40f 100644 --- a/src/core/auth/qgsauthcertutils.cpp +++ b/src/core/auth/qgsauthcertutils.cpp @@ -228,6 +228,11 @@ QSslKey QgsAuthCertUtils::keyFromFile( const QString &keypath, case QSsl::KeyAlgorithm::Opaque: *algtype = QStringLiteral( "opaque" ); break; +#if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) + case QSsl::KeyAlgorithm::Dh: + *algtype = QStringLiteral( "dh" ); + break; +#endif } } return clientkey; diff --git a/src/core/expression/qgsexpressionnodeimpl.cpp b/src/core/expression/qgsexpressionnodeimpl.cpp index 22fed9de6e51..80362ee6b2b2 100644 --- a/src/core/expression/qgsexpressionnodeimpl.cpp +++ b/src/core/expression/qgsexpressionnodeimpl.cpp @@ -807,8 +807,11 @@ QVariant QgsExpressionNodeInOperator::evalNode( QgsExpression *parent, const Qgs { bool equal = false; // check whether they are equal - if ( QgsExpressionUtils::isDoubleSafe( v1 ) && QgsExpressionUtils::isDoubleSafe( v2 ) ) + if ( ( v1.type() != QVariant::String || v2.type() != QVariant::String ) && + QgsExpressionUtils::isDoubleSafe( v1 ) && QgsExpressionUtils::isDoubleSafe( v2 ) ) { + // do numeric comparison if both operators can be converted to numbers, + // and they aren't both string double f1 = QgsExpressionUtils::getDoubleValue( v1, parent ); ENSURE_NO_EVAL_ERROR; double f2 = QgsExpressionUtils::getDoubleValue( v2, parent ); diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp index 0f7ddd983934..38f0b4653a50 100644 --- a/src/core/geometry/qgsgeometry.cpp +++ b/src/core/geometry/qgsgeometry.cpp @@ -1000,7 +1000,7 @@ QgsGeometry QgsGeometry::orientedMinimumBoundingBox( double &area, double &angle height = bounds.height(); } - pt2 = pt1; + pt1 = pt2; } QgsGeometry minBounds = QgsGeometry::fromRect( minRect ); diff --git a/src/core/layertree/qgslayertreemodellegendnode.cpp b/src/core/layertree/qgslayertreemodellegendnode.cpp index 8a82bba8e11f..2153765031db 100644 --- a/src/core/layertree/qgslayertreemodellegendnode.cpp +++ b/src/core/layertree/qgslayertreemodellegendnode.cpp @@ -1134,8 +1134,8 @@ QgsLayerTreeModelLegendNode::ItemMetrics QgsDataDefinedSizeLegendNode::draw( con ddsLegend.setFont( settings.style( QgsLegendStyle::SymbolLabel ).font() ); ddsLegend.setTextColor( settings.fontColor() ); - QSize contentSize; - int labelXOffset; + QSizeF contentSize; + double labelXOffset; ddsLegend.drawCollapsedLegend( context, &contentSize, &labelXOffset ); if ( ctx && ctx->painter ) diff --git a/src/core/layout/qgslayoutitempicture.h b/src/core/layout/qgslayoutitempicture.h index 15979a8ea92a..ae9a7e7b64e3 100644 --- a/src/core/layout/qgslayoutitempicture.h +++ b/src/core/layout/qgslayoutitempicture.h @@ -155,7 +155,7 @@ class CORE_EXPORT QgsLayoutItemPicture: public QgsLayoutItem void setNorthOffset( double offset ); /** - * Returns the resize mode used for drawing the picture within the composer + * Returns the resize mode used for drawing the picture within the layout * item's frame. * \see setResizeMode() */ diff --git a/src/core/layout/qgslayoutitemscalebar.h b/src/core/layout/qgslayoutitemscalebar.h index b72052bb6c89..354bc040c12e 100644 --- a/src/core/layout/qgslayoutitemscalebar.h +++ b/src/core/layout/qgslayoutitemscalebar.h @@ -514,7 +514,7 @@ class CORE_EXPORT QgsLayoutItemScaleBar: public QgsLayoutItem //! Calculates with of a segment in mm and stores it in mSegmentMillimeters void refreshSegmentMillimeters(); - //! Returns diagonal of composer map in selected units (map units / meters / feet / nautical miles) + //! Returns diagonal of layout map in selected units (map units / meters / feet / nautical miles) double mapWidth() const; QgsScaleBarRenderer::ScaleBarContext createScaleContext() const; diff --git a/src/core/layout/qgslayouttable.h b/src/core/layout/qgslayouttable.h index 4811bc156ed9..7bef39a5fdc9 100644 --- a/src/core/layout/qgslayouttable.h +++ b/src/core/layout/qgslayouttable.h @@ -31,7 +31,7 @@ class QgsLayoutTableColumn; /** * \ingroup core * List of QVariants, representing a the contents of a single row in - * a QgsComposerTable + * a QgsLayoutTable * \since QGIS 3.0 */ typedef QVector< QVariant > QgsLayoutTableRow; @@ -60,7 +60,7 @@ typedef QVector< QgsLayoutTableColumn * > QgsLayoutTableColumns; /** * \ingroup core * \class QgsLayoutTableStyle - * \brief Styling option for a composer table cell + * \brief Styling option for a layout table cell * \since QGIS 3.0 */ diff --git a/src/core/layout/qgslayouttablecolumn.h b/src/core/layout/qgslayouttablecolumn.h index 81a80d08b7ac..62d156b1ba1a 100644 --- a/src/core/layout/qgslayouttablecolumn.h +++ b/src/core/layout/qgslayouttablecolumn.h @@ -173,7 +173,7 @@ class CORE_EXPORT QgsLayoutTableColumn : public QObject * will be sorted in the table. The sort rank specifies the priority given to the * column when the table is sorted by multiple columns, with lower sort ranks * having higher priority. This property is only used when the column - * is part of a QgsComposerAttributeTable. + * is part of a QgsLayoutItemAttributeTable. * If the sort \a rank is <= 0 then the column is not being sorted. * * \note only applicable when used in a QgsLayoutItemAttributeTable diff --git a/src/core/qgsdatadefinedsizelegend.cpp b/src/core/qgsdatadefinedsizelegend.cpp index 05864e62ed52..d26d7c04f804 100644 --- a/src/core/qgsdatadefinedsizelegend.cpp +++ b/src/core/qgsdatadefinedsizelegend.cpp @@ -141,12 +141,12 @@ QgsLegendSymbolList QgsDataDefinedSizeLegend::legendSymbolList() const } -void QgsDataDefinedSizeLegend::drawCollapsedLegend( QgsRenderContext &context, QSize *outputSize, int *labelXOffset ) const +void QgsDataDefinedSizeLegend::drawCollapsedLegend( QgsRenderContext &context, QSizeF *outputSize, double *labelXOffset ) const { if ( mType != LegendCollapsed || mSizeClasses.isEmpty() || !mSymbol ) { if ( outputSize ) - *outputSize = QSize(); + *outputSize = QSizeF(); if ( labelXOffset ) *labelXOffset = 0; return; @@ -170,78 +170,76 @@ void QgsDataDefinedSizeLegend::drawCollapsedLegend( QgsRenderContext &context, Q // make sure we draw bigger symbols first std::sort( classes.begin(), classes.end(), []( const SizeClass & a, const SizeClass & b ) { return a.size > b.size; } ); - int hLengthLine = std::round( context.convertToPainterUnits( hLengthLineMM, QgsUnitTypes::RenderMillimeters ) ); - int hSpaceLineText = std::round( context.convertToPainterUnits( hSpaceLineTextMM, QgsUnitTypes::RenderMillimeters ) ); + double hLengthLine = context.convertToPainterUnits( hLengthLineMM, QgsUnitTypes::RenderMillimeters ); + double hSpaceLineText = context.convertToPainterUnits( hSpaceLineTextMM, QgsUnitTypes::RenderMillimeters ); int dpm = std::round( context.scaleFactor() * 1000 ); // scale factor = dots per millimeter // get font metrics - we need a temporary image just to get the metrics right for the given DPI QImage tmpImg( QSize( 1, 1 ), QImage::Format_ARGB32_Premultiplied ); tmpImg.setDotsPerMeterX( dpm ); tmpImg.setDotsPerMeterY( dpm ); - QFontMetrics fm( mFont, &tmpImg ); - int textHeight = fm.height(); - int leading = fm.leading(); - int minTextDistY = textHeight + leading; + QFontMetricsF fm( mFont, &tmpImg ); + double textHeight = fm.height(); + double leading = fm.leading(); + double minTextDistY = textHeight + leading; // // determine layout of the rendered elements // // find out how wide the text will be - int maxTextWidth = 0; + double maxTextWidth = 0; for ( const SizeClass &c : qgis::as_const( classes ) ) { - int w = fm.width( c.label ); - if ( w > maxTextWidth ) - maxTextWidth = w; + maxTextWidth = std::max( maxTextWidth, fm.boundingRect( c.label ).width() ); } // add extra width needed to handle varying rendering of font weight maxTextWidth += 1; // find out size of the largest symbol double largestSize = classes.at( 0 ).size; - int outputLargestSize = std::round( context.convertToPainterUnits( largestSize, s->sizeUnit(), s->sizeMapUnitScale() ) ); + double outputLargestSize = context.convertToPainterUnits( largestSize, s->sizeUnit(), s->sizeMapUnitScale() ); // find out top Y coordinate for individual symbol sizes - QList symbolTopY; + QList symbolTopY; for ( const SizeClass &c : qgis::as_const( classes ) ) { - int outputSymbolSize = std::round( context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() ) ); + double outputSymbolSize = context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() ); switch ( mVAlign ) { case AlignCenter: - symbolTopY << std::round( outputLargestSize / 2 - outputSymbolSize / 2 ); + symbolTopY << outputLargestSize / 2 - outputSymbolSize / 2; break; case AlignBottom: - symbolTopY << std::round( outputLargestSize - outputSymbolSize ); + symbolTopY << outputLargestSize - outputSymbolSize; break; } } // determine Y coordinate of texts: ideally they should be at the same level as symbolTopY // but we need to avoid overlapping texts, so adjust the vertical positions - int middleIndex = 0; // classes.count() / 2; // will get the ideal position - QList textCenterY; - int lastY = symbolTopY[middleIndex]; + double middleIndex = 0; // classes.count() / 2; // will get the ideal position + QList textCenterY; + double lastY = symbolTopY[middleIndex]; textCenterY << lastY; for ( int i = middleIndex + 1; i < classes.count(); ++i ) { - int symbolY = symbolTopY[i]; + double symbolY = symbolTopY[i]; if ( symbolY - lastY < minTextDistY ) symbolY = lastY + minTextDistY; textCenterY << symbolY; lastY = symbolY; } - int textTopY = textCenterY.first() - textHeight / 2; - int textBottomY = textCenterY.last() + textHeight / 2; - int totalTextHeight = textBottomY - textTopY; + double textTopY = textCenterY.first() - textHeight / 2; + double textBottomY = textCenterY.last() + textHeight / 2; + double totalTextHeight = textBottomY - textTopY; - int fullWidth = outputLargestSize + hLengthLine + hSpaceLineText + maxTextWidth; - int fullHeight = std::max( static_cast< int >( std::round( outputLargestSize ) ) - textTopY, totalTextHeight ); + double fullWidth = outputLargestSize + hLengthLine + hSpaceLineText + maxTextWidth; + double fullHeight = std::max( outputLargestSize - textTopY, totalTextHeight ); if ( outputSize ) - *outputSize = QSize( fullWidth, fullHeight ); + *outputSize = QSizeF( fullWidth, fullHeight ); if ( labelXOffset ) *labelXOffset = outputLargestSize + hLengthLine + hSpaceLineText; @@ -262,7 +260,7 @@ void QgsDataDefinedSizeLegend::drawCollapsedLegend( QgsRenderContext &context, Q { s->setSize( c.size ); - int outputSymbolSize = std::round( context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() ) ); + double outputSymbolSize = context.convertToPainterUnits( c.size, s->sizeUnit(), s->sizeMapUnitScale() ); double tx = ( outputLargestSize - outputSymbolSize ) / 2; p->save(); @@ -305,10 +303,10 @@ QImage QgsDataDefinedSizeLegend::collapsedLegendImage( QgsRenderContext &context return QImage(); // find out the size first - QSize contentSize; + QSizeF contentSize; drawCollapsedLegend( context, &contentSize ); - int padding = std::round( context.convertToPainterUnits( paddingMM, QgsUnitTypes::RenderMillimeters ) ); + double padding = context.convertToPainterUnits( paddingMM, QgsUnitTypes::RenderMillimeters ); int dpm = std::round( context.scaleFactor() * 1000 ); // scale factor = dots per millimeter QImage img( contentSize.width() + padding * 2, contentSize.height() + padding * 2, QImage::Format_ARGB32_Premultiplied ); diff --git a/src/core/qgsdatadefinedsizelegend.h b/src/core/qgsdatadefinedsizelegend.h index 074ba1f5cd52..16fc1a37fe04 100644 --- a/src/core/qgsdatadefinedsizelegend.h +++ b/src/core/qgsdatadefinedsizelegend.h @@ -133,7 +133,7 @@ class CORE_EXPORT QgsDataDefinedSizeLegend * If the painter in context is NULLPTR, it only does size calculation without actual rendering. * Does nothing if legend is not configured as collapsed. */ - void drawCollapsedLegend( QgsRenderContext &context, QSize *outputSize SIP_OUT = nullptr, int *labelXOffset SIP_OUT = nullptr ) const; + void drawCollapsedLegend( QgsRenderContext &context, QSizeF *outputSize SIP_OUT = nullptr, double *labelXOffset SIP_OUT = nullptr ) const; //! Returns output image that would be shown in the legend. Returns invalid image if legend is not configured as collapsed. QImage collapsedLegendImage( QgsRenderContext &context, const QColor &backgroundColor = Qt::transparent, double paddingMM = 1 ) const; diff --git a/src/core/qgsproject.h b/src/core/qgsproject.h index 73018e1059dc..b66859c5782a 100644 --- a/src/core/qgsproject.h +++ b/src/core/qgsproject.h @@ -565,7 +565,7 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera QgsRelationManager *relationManager() const; /** - * Returns the project's layout manager, which manages compositions within + * Returns the project's layout manager, which manages print layouts, atlases and reports within * the project. * \note not available in Python bindings * \since QGIS 3.0 @@ -573,7 +573,7 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera const QgsLayoutManager *layoutManager() const SIP_SKIP; /** - * Returns the project's layout manager, which manages compositions within + * Returns the project's layout manager, which manages print layouts, atlases and reports within * the project. * \since QGIS 3.0 */ diff --git a/src/core/qgsrendercontext.h b/src/core/qgsrendercontext.h index 4665e8596162..ee64b15d1e89 100644 --- a/src/core/qgsrendercontext.h +++ b/src/core/qgsrendercontext.h @@ -331,11 +331,15 @@ class CORE_EXPORT QgsRenderContext /** * Returns TRUE if advanced effects such as blend modes such be used + * + * \see setUseAdvancedEffects() */ bool useAdvancedEffects() const; /** * Used to enable or disable advanced effects such as blend modes + * + * \see useAdvancedEffects() */ void setUseAdvancedEffects( bool enabled ); @@ -519,9 +523,16 @@ class CORE_EXPORT QgsRenderContext /** * Returns TRUE if the rendering optimization (geometry simplification) can be executed + * + * \see setUseRenderingOptimization() */ bool useRenderingOptimization() const; + /** + * Sets whether the rendering optimization (geometry simplification) should be executed + * + * \see useRenderingOptimization() + */ void setUseRenderingOptimization( bool enabled ); /** @@ -597,16 +608,30 @@ class CORE_EXPORT QgsRenderContext /** * Sets the segmentation tolerance applied when rendering curved geometries - \param tolerance the segmentation tolerance*/ + * \param tolerance the segmentation tolerance + * \see segmentationTolerance() + * \see segmentationToleranceType() + */ void setSegmentationTolerance( double tolerance ) { mSegmentationTolerance = tolerance; } - //! Gets the segmentation tolerance applied when rendering curved geometries + + /** + * Gets the segmentation tolerance applied when rendering curved geometries + * \see setSegmentationTolerance() + */ double segmentationTolerance() const { return mSegmentationTolerance; } /** * Sets segmentation tolerance type (maximum angle or maximum difference between curve and approximation) - \param type the segmentation tolerance typename*/ + * \param type the segmentation tolerance typename + * \see segmentationToleranceType() + * \see segmentationTolerance() + */ void setSegmentationToleranceType( QgsAbstractGeometry::SegmentationToleranceType type ) { mSegmentationToleranceType = type; } - //! Gets segmentation tolerance type (maximum angle or maximum difference between curve and approximation) + + /** + * Gets segmentation tolerance type (maximum angle or maximum difference between curve and approximation) + * \see setSegmentationToleranceType() + */ QgsAbstractGeometry::SegmentationToleranceType segmentationToleranceType() const { return mSegmentationToleranceType; } // Conversions diff --git a/src/core/qgsvectorlayer.cpp b/src/core/qgsvectorlayer.cpp index 98bff3390361..fcf0de3672a5 100644 --- a/src/core/qgsvectorlayer.cpp +++ b/src/core/qgsvectorlayer.cpp @@ -4618,6 +4618,10 @@ bool QgsVectorLayer::readSldTextSymbolizer( const QDomNode &node, QgsPalLayerSet if ( !pointPlacementElem.isNull() ) { settings.placement = QgsPalLayerSettings::OverPoint; + if ( geometryType() == QgsWkbTypes::LineGeometry ) + { + settings.placement = QgsPalLayerSettings::Line; + } QDomElement displacementElem = pointPlacementElem.firstChildElement( QStringLiteral( "Displacement" ) ); if ( !displacementElem.isNull() ) @@ -4683,6 +4687,15 @@ bool QgsVectorLayer::readSldTextSymbolizer( const QDomNode &node, QgsPalLayerSet } } } + else + { + // PointPlacement + QDomElement linePlacementElem = labelPlacementElem.firstChildElement( QStringLiteral( "LinePlacement" ) ); + if ( !linePlacementElem.isNull() ) + { + settings.placement = QgsPalLayerSettings::Line; + } + } } // read vendor options diff --git a/src/core/qgsvirtuallayerdefinition.cpp b/src/core/qgsvirtuallayerdefinition.cpp index 76c2a9021ec8..96b611e354d7 100644 --- a/src/core/qgsvirtuallayerdefinition.cpp +++ b/src/core/qgsvirtuallayerdefinition.cpp @@ -168,59 +168,142 @@ QgsVirtualLayerDefinition QgsVirtualLayerDefinition::fromUrl( const QUrl &url ) return def; } +// Mega ewwww. all this is taken from Qt's QUrl::addEncodedQueryItem compatibility helper. +// (I can't see any way to port the below code to NOT require this without breaking +// existing projects.) + +inline char toHexUpper( uint value ) noexcept +{ + return "0123456789ABCDEF"[value & 0xF]; +} + +static inline ushort encodeNibble( ushort c ) +{ + return ushort( toHexUpper( c ) ); +} + +bool qt_is_ascii( const char *&ptr, const char *end ) noexcept +{ + while ( ptr + 4 <= end ) + { + quint32 data = qFromUnaligned( ptr ); + if ( data &= 0x80808080U ) + { +#if Q_BYTE_ORDER == Q_BIG_ENDIAN + uint idx = qCountLeadingZeroBits( data ); +#else + uint idx = qCountTrailingZeroBits( data ); +#endif + ptr += idx / 8; + return false; + } + ptr += 4; + } + while ( ptr != end ) + { + if ( quint8( *ptr ) & 0x80 ) + return false; + ++ptr; + } + return true; +} + +QString fromEncodedComponent_helper( const QByteArray &ba ) +{ + if ( ba.isNull() ) + return QString(); + // scan ba for anything above or equal to 0x80 + // control points below 0x20 are fine in QString + const char *in = ba.constData(); + const char *const end = ba.constEnd(); + if ( qt_is_ascii( in, end ) ) + { + // no non-ASCII found, we're safe to convert to QString + return QString::fromLatin1( ba, ba.size() ); + } + // we found something that we need to encode + QByteArray intermediate = ba; + intermediate.resize( ba.size() * 3 - ( in - ba.constData() ) ); + uchar *out = reinterpret_cast( intermediate.data() + ( in - ba.constData() ) ); + for ( ; in < end; ++in ) + { + if ( *in & 0x80 ) + { + // encode + *out++ = '%'; + *out++ = encodeNibble( uchar( *in ) >> 4 ); + *out++ = encodeNibble( uchar( *in ) & 0xf ); + } + else + { + // keep + *out++ = uchar( *in ); + } + } + // now it's safe to call fromLatin1 + return QString::fromLatin1( intermediate, out - reinterpret_cast( intermediate.data() ) ); +} + QUrl QgsVirtualLayerDefinition::toUrl() const { QUrl url; if ( !filePath().isEmpty() ) url = QUrl::fromLocalFile( filePath() ); + QUrlQuery urlQuery( url ); + const auto constSourceLayers = sourceLayers(); for ( const QgsVirtualLayerDefinition::SourceLayer &l : constSourceLayers ) { if ( l.isReferenced() ) - url.addQueryItem( QStringLiteral( "layer_ref" ), QStringLiteral( "%1:%2" ).arg( l.reference(), l.name() ) ); + urlQuery.addQueryItem( QStringLiteral( "layer_ref" ), QStringLiteral( "%1:%2" ).arg( l.reference(), l.name() ) ); else - url.addEncodedQueryItem( "layer", QStringLiteral( "%1:%4:%2:%3" ) // the order is important, since the 4th argument may contain '%2' as well - .arg( l.provider(), - QString( QUrl::toPercentEncoding( l.name() ) ), - l.encoding(), - QString( QUrl::toPercentEncoding( l.source() ) ) ).toUtf8() ); + // if you can find a way to port this away from fromEncodedComponent_helper without breaking existing projects, + // please do so... this is GROSS! + urlQuery.addQueryItem( fromEncodedComponent_helper( "layer" ), + fromEncodedComponent_helper( QStringLiteral( "%1:%4:%2:%3" ) // the order is important, since the 4th argument may contain '%2' as well + .arg( l.provider(), + QString( QUrl::toPercentEncoding( l.name() ) ), + l.encoding(), + QString( QUrl::toPercentEncoding( l.source() ) ) ).toUtf8() ) ); } if ( !query().isEmpty() ) { - url.addQueryItem( QStringLiteral( "query" ), query() ); + urlQuery.addQueryItem( QStringLiteral( "query" ), query() ); } if ( !uid().isEmpty() ) - url.addQueryItem( QStringLiteral( "uid" ), uid() ); + urlQuery.addQueryItem( QStringLiteral( "uid" ), uid() ); if ( geometryWkbType() == QgsWkbTypes::NoGeometry ) - url.addQueryItem( QStringLiteral( "nogeometry" ), QString() ); + urlQuery.addQueryItem( QStringLiteral( "nogeometry" ), QString() ); else if ( !geometryField().isEmpty() ) { if ( hasDefinedGeometry() ) - url.addQueryItem( QStringLiteral( "geometry" ), QStringLiteral( "%1:%2:%3" ).arg( geometryField() ). arg( geometryWkbType() ).arg( geometrySrid() ).toUtf8() ); + urlQuery.addQueryItem( QStringLiteral( "geometry" ), QStringLiteral( "%1:%2:%3" ).arg( geometryField() ). arg( geometryWkbType() ).arg( geometrySrid() ).toUtf8() ); else - url.addQueryItem( QStringLiteral( "geometry" ), geometryField() ); + urlQuery.addQueryItem( QStringLiteral( "geometry" ), geometryField() ); } const auto constFields = fields(); for ( const QgsField &f : constFields ) { if ( f.type() == QVariant::Int ) - url.addQueryItem( QStringLiteral( "field" ), f.name() + ":int" ); + urlQuery.addQueryItem( QStringLiteral( "field" ), f.name() + ":int" ); else if ( f.type() == QVariant::Double ) - url.addQueryItem( QStringLiteral( "field" ), f.name() + ":real" ); + urlQuery.addQueryItem( QStringLiteral( "field" ), f.name() + ":real" ); else if ( f.type() == QVariant::String ) - url.addQueryItem( QStringLiteral( "field" ), f.name() + ":text" ); + urlQuery.addQueryItem( QStringLiteral( "field" ), f.name() + ":text" ); } if ( isLazy() ) { - url.addQueryItem( QStringLiteral( "lazy" ), QString() ); + urlQuery.addQueryItem( QStringLiteral( "lazy" ), QString() ); } + url.setQuery( urlQuery ); + return url; } diff --git a/src/core/symbology/qgsmarkersymbollayer.cpp b/src/core/symbology/qgsmarkersymbollayer.cpp index 4cded2b073a9..b8f5d082d185 100644 --- a/src/core/symbology/qgsmarkersymbollayer.cpp +++ b/src/core/symbology/qgsmarkersymbollayer.cpp @@ -1233,8 +1233,6 @@ void QgsSimpleMarkerSymbolLayer::drawMarker( QPainter *p, QgsSymbolRenderContext bool QgsSimpleMarkerSymbolLayer::writeDxf( QgsDxfExport &e, double mmMapUnitScaleFactor, const QString &layerName, QgsSymbolRenderContext &context, QPointF shift ) const { - Q_UNUSED( mmMapUnitScaleFactor ) - //data defined size? double size = mSize; @@ -1260,6 +1258,12 @@ bool QgsSimpleMarkerSymbolLayer::writeDxf( QgsDxfExport &e, double mmMapUnitScal size *= e.mapUnitScaleFactor( e.symbologyScale(), mSizeUnit, e.mapUnits(), context.renderContext().mapToPixel().mapUnitsPerPixel() ); } + + if ( mSizeUnit == QgsUnitTypes::RenderMillimeters ) + { + size *= mmMapUnitScaleFactor; + } + if ( mSizeUnit == QgsUnitTypes::RenderMapUnits ) { e.clipValueToMapUnitScale( size, mSizeMapUnitScale, context.renderContext().scaleFactor() ); @@ -3036,7 +3040,11 @@ void QgsFontMarkerSymbolLayer::startRender( QgsSymbolRenderContext &context ) mFont.setPixelSize( std::max( 2, static_cast< int >( std::round( sizePixels ) ) ) ); delete mFontMetrics; mFontMetrics = new QFontMetrics( mFont ); +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) mChrWidth = mFontMetrics->width( mString ); +#else + mChrWidth = mFontMetrics->horizontalAdvance( mString ); +#endif mChrOffset = QPointF( mChrWidth / 2.0, -mFontMetrics->ascent() / 2.0 ); mOrigSize = mSize; // save in case the size would be data defined @@ -3067,7 +3075,11 @@ QString QgsFontMarkerSymbolLayer::characterToRender( QgsSymbolRenderContext &con stringToRender = mDataDefinedProperties.valueAsString( QgsSymbolLayer::PropertyCharacter, context.renderContext().expressionContext(), mString ); if ( stringToRender != mString ) { +#if QT_VERSION < QT_VERSION_CHECK(5, 11, 0) charWidth = mFontMetrics->width( stringToRender ); +#else + charWidth = mFontMetrics->horizontalAdvance( stringToRender ); +#endif charOffset = QPointF( charWidth / 2.0, -mFontMetrics->ascent() / 2.0 ); } } diff --git a/src/gui/attributetable/qgsattributetablemodel.cpp b/src/gui/attributetable/qgsattributetablemodel.cpp index 257bf7727fce..bc2302c3cc5b 100644 --- a/src/gui/attributetable/qgsattributetablemodel.cpp +++ b/src/gui/attributetable/qgsattributetablemodel.cpp @@ -711,14 +711,14 @@ QVariant QgsAttributeTableModel::data( const QModelIndex &index, int role ) cons { mExpressionContext.setFeature( mFeat ); QList styles; - if ( mRowStylesMap.contains( index.row() ) ) + if ( mRowStylesMap.contains( mFeat.id() ) ) { - styles = mRowStylesMap[index.row()]; + styles = mRowStylesMap[mFeat.id()]; } else { styles = QgsConditionalStyle::matchingConditionalStyles( layer()->conditionalStyles()->rowStyles(), QVariant(), mExpressionContext ); - mRowStylesMap.insert( index.row(), styles ); + mRowStylesMap.insert( mFeat.id(), styles ); } QgsConditionalStyle rowstyle = QgsConditionalStyle::compressStyles( styles ); @@ -756,7 +756,7 @@ bool QgsAttributeTableModel::setData( const QModelIndex &index, const QVariant & if ( !layer()->isModified() ) return false; - mRowStylesMap.remove( index.row() ); + mRowStylesMap.remove( mFeat.id() ); return true; } diff --git a/src/gui/qgscolorbutton.cpp b/src/gui/qgscolorbutton.cpp index 4f901d259adc..bbb35b72cb69 100644 --- a/src/gui/qgscolorbutton.cpp +++ b/src/gui/qgscolorbutton.cpp @@ -160,6 +160,7 @@ void QgsColorButton::setToDefaultColor() void QgsColorButton::setToNull() { setColor( QColor() ); + emit cleared(); } void QgsColorButton::unlink() diff --git a/src/gui/qgscolorbutton.h b/src/gui/qgscolorbutton.h index 834577e5b15a..c3c9277b585b 100644 --- a/src/gui/qgscolorbutton.h +++ b/src/gui/qgscolorbutton.h @@ -362,6 +362,7 @@ class GUI_EXPORT QgsColorButton : public QToolButton * Sets color to null. * \see setToDefaultColor() * \see setToNoColor() + * \see cleared() * \since QGIS 2.16 */ void setToNull(); @@ -393,6 +394,14 @@ class GUI_EXPORT QgsColorButton : public QToolButton */ void colorClicked( const QColor &color ); + /** + * Emitted when the color is cleared (set to null). + * + * \see setToNull() + * \since QGIS 3.12 + */ + void cleared(); + /** * Emitted when the color is unlinked, e.g. when it was previously set to link * to a project color and is now no longer linked. diff --git a/src/gui/qgsmapcanvas.cpp b/src/gui/qgsmapcanvas.cpp index 3fe9bda5f025..82021580ec20 100644 --- a/src/gui/qgsmapcanvas.cpp +++ b/src/gui/qgsmapcanvas.cpp @@ -194,6 +194,7 @@ QgsMapCanvas::QgsMapCanvas( QWidget *parent ) // Enable touch event on Windows. // Qt on Windows needs to be told it can take touch events or else it ignores them. grabGesture( Qt::PinchGesture ); + grabGesture( Qt::TapAndHoldGesture ); viewport()->setAttribute( Qt::WA_AcceptTouchEvents ); #endif @@ -2337,6 +2338,14 @@ bool QgsMapCanvas::event( QEvent *e ) { if ( e->type() == QEvent::Gesture ) { + if ( QTapAndHoldGesture *tapAndHoldGesture = qobject_cast< QTapAndHoldGesture * >( static_cast( e )->gesture( Qt::TapAndHoldGesture ) ) ) + { + QPointF pos = tapAndHoldGesture->position(); + pos = mapFromGlobal( QPoint( pos.x(), pos.y() ) ); + QgsPointXY mapPoint = getCoordinateTransform()->toMapCoordinates( pos.x(), pos.y() ); + emit tapAndHoldGestureOccurred( mapPoint, tapAndHoldGesture ); + } + // call handler of current map tool if ( mMapTool ) { diff --git a/src/gui/qgsmapcanvas.h b/src/gui/qgsmapcanvas.h index ceb62e767fc2..a0bea0712bd2 100644 --- a/src/gui/qgsmapcanvas.h +++ b/src/gui/qgsmapcanvas.h @@ -916,6 +916,12 @@ class GUI_EXPORT QgsMapCanvas : public QGraphicsView */ void panDistanceBearingChanged( double distance, QgsUnitTypes::DistanceUnit unit, double bearing ); + /** + * Emitted whenever a tap and hold \a gesture occurs at the specified map point. + * \since QGIS 3.12 + */ + void tapAndHoldGestureOccurred( const QgsPointXY &mapPoint, QTapAndHoldGesture *gesture ); + protected: bool event( QEvent *e ) override; diff --git a/src/gui/qgsmaptoolpan.cpp b/src/gui/qgsmaptoolpan.cpp index b9ff52e77e4f..fba96b5b4d8d 100644 --- a/src/gui/qgsmaptoolpan.cpp +++ b/src/gui/qgsmaptoolpan.cpp @@ -115,7 +115,6 @@ bool QgsMapToolPan::gestureEvent( QGestureEvent *event ) if ( QTouchDevice::devices().isEmpty() ) return true; // no touch support - qDebug() << "gesture " << event; if ( QGesture *gesture = event->gesture( Qt::PinchGesture ) ) { mPinching = true; diff --git a/src/gui/qgsnewhttpconnection.cpp b/src/gui/qgsnewhttpconnection.cpp index 2fe905a9c52a..f5b144257ba1 100644 --- a/src/gui/qgsnewhttpconnection.cpp +++ b/src/gui/qgsnewhttpconnection.cpp @@ -337,29 +337,109 @@ void QgsNewHttpConnection::updateServiceSpecificSettings() cbxWfsFeaturePaging->setChecked( pagingEnabled ); } -QUrl QgsNewHttpConnection::urlTrimmed() const + + +// Mega ewwww. all this is taken from Qt's QUrl::setEncodedPath compatibility helper. +// (I can't see any way to port the below code to NOT require this). + +inline char toHexUpper( uint value ) noexcept { + return "0123456789ABCDEF"[value & 0xF]; +} +static inline ushort encodeNibble( ushort c ) +{ + return ushort( toHexUpper( c ) ); +} + +bool qt_is_ascii( const char *&ptr, const char *end ) noexcept +{ + while ( ptr + 4 <= end ) + { + quint32 data = qFromUnaligned( ptr ); + if ( data &= 0x80808080U ) + { +#if Q_BYTE_ORDER == Q_BIG_ENDIAN + uint idx = qCountLeadingZeroBits( data ); +#else + uint idx = qCountTrailingZeroBits( data ); +#endif + ptr += idx / 8; + return false; + } + ptr += 4; + } + while ( ptr != end ) + { + if ( quint8( *ptr ) & 0x80 ) + return false; + ++ptr; + } + return true; +} + +QString fromEncodedComponent_helper( const QByteArray &ba ) +{ + if ( ba.isNull() ) + return QString(); + // scan ba for anything above or equal to 0x80 + // control points below 0x20 are fine in QString + const char *in = ba.constData(); + const char *const end = ba.constEnd(); + if ( qt_is_ascii( in, end ) ) + { + // no non-ASCII found, we're safe to convert to QString + return QString::fromLatin1( ba, ba.size() ); + } + // we found something that we need to encode + QByteArray intermediate = ba; + intermediate.resize( ba.size() * 3 - ( in - ba.constData() ) ); + uchar *out = reinterpret_cast( intermediate.data() + ( in - ba.constData() ) ); + for ( ; in < end; ++in ) + { + if ( *in & 0x80 ) + { + // encode + *out++ = '%'; + *out++ = encodeNibble( uchar( *in ) >> 4 ); + *out++ = encodeNibble( uchar( *in ) & 0xf ); + } + else + { + // keep + *out++ = uchar( *in ); + } + } + // now it's safe to call fromLatin1 + return QString::fromLatin1( intermediate, out - reinterpret_cast( intermediate.data() ) ); +} + + +QUrl QgsNewHttpConnection::urlTrimmed() const +{ QUrl url( txtUrl->text().trimmed() ); - const QList< QPair > &items = url.encodedQueryItems(); - QHash< QString, QPair > params; - for ( QList< QPair >::const_iterator it = items.constBegin(); it != items.constEnd(); ++it ) + QUrlQuery query( url ); + const QList > items = query.queryItems( QUrl::FullyEncoded ); + QHash< QString, QPair > params; + for ( const QPair &it : items ) { - params.insert( QString( it->first ).toUpper(), *it ); + params.insert( it.first.toUpper(), it ); } if ( params[QStringLiteral( "SERVICE" )].second.toUpper() == "WMS" || params[QStringLiteral( "SERVICE" )].second.toUpper() == "WFS" || params[QStringLiteral( "SERVICE" )].second.toUpper() == "WCS" ) { - url.removeEncodedQueryItem( params[QStringLiteral( "SERVICE" )].first ); - url.removeEncodedQueryItem( params[QStringLiteral( "REQUEST" )].first ); - url.removeEncodedQueryItem( params[QStringLiteral( "FORMAT" )].first ); + query.removeQueryItem( params.value( QStringLiteral( "SERVICE" ) ).first ); + query.removeQueryItem( params.value( QStringLiteral( "REQUEST" ) ).first ); + query.removeQueryItem( params.value( QStringLiteral( "FORMAT" ) ).first ); } - if ( url.encodedPath().isEmpty() ) + url.setQuery( query ); + + if ( url.path( QUrl::FullyEncoded ).isEmpty() ) { - url.setEncodedPath( "/" ); + url.setPath( fromEncodedComponent_helper( "/" ) ); } return url; } diff --git a/src/gui/qgsquerybuilder.cpp b/src/gui/qgsquerybuilder.cpp index 7c230563547f..a4372d6027fb 100644 --- a/src/gui/qgsquerybuilder.cpp +++ b/src/gui/qgsquerybuilder.cpp @@ -218,9 +218,20 @@ void QgsQueryBuilder::test() { mUseUnfilteredLayer->setDisabled( mLayer->subsetString().isEmpty() ); - QMessageBox::information( this, - tr( "Query Result" ), - tr( "The where clause returned %n row(s).", "returned test rows", mLayer->featureCount() ) ); + const long featureCount { mLayer->featureCount() }; + // Check for errors + if ( featureCount < 0 ) + { + QMessageBox::warning( this, + tr( "Query Result" ), + tr( "An error occurred when executing the query, please check the expression syntax." ) ); + } + else + { + QMessageBox::information( this, + tr( "Query Result" ), + tr( "The where clause returned %n row(s).", "returned test rows", featureCount ) ); + } } else if ( mLayer->dataProvider()->hasErrors() ) { diff --git a/src/gui/tableeditor/qgstableeditorformattingwidget.cpp b/src/gui/tableeditor/qgstableeditorformattingwidget.cpp index 2e855463f277..0effd17152ae 100644 --- a/src/gui/tableeditor/qgstableeditorformattingwidget.cpp +++ b/src/gui/tableeditor/qgstableeditorformattingwidget.cpp @@ -43,11 +43,21 @@ QgsTableEditorFormattingWidget::QgsTableEditorFormattingWidget( QWidget *parent if ( !mBlockSignals ) emit foregroundColorChanged( mTextColorButton->color() ); } ); + connect( mTextColorButton, &QgsColorButton::cleared, this, [ = ] + { + if ( !mBlockSignals ) + emit foregroundColorChanged( QColor() ); + } ); connect( mBackgroundColorButton, &QgsColorButton::colorChanged, this, [ = ] { if ( !mBlockSignals ) emit backgroundColorChanged( mBackgroundColorButton->color() ); } ); + connect( mBackgroundColorButton, &QgsColorButton::cleared, this, [ = ] + { + if ( !mBlockSignals ) + emit backgroundColorChanged( QColor() ); + } ); connect( mFormatNumbersCheckBox, &QCheckBox::stateChanged, this, [ = ]( int state ) { diff --git a/src/providers/oracle/qgsoracleprovider.cpp b/src/providers/oracle/qgsoracleprovider.cpp index 816e566b9d7c..064dd7d2caaa 100644 --- a/src/providers/oracle/qgsoracleprovider.cpp +++ b/src/providers/oracle/qgsoracleprovider.cpp @@ -423,11 +423,9 @@ QString QgsOracleUtils::whereClause( QgsFeatureId featureId, const QgsFields &fi case PktRowId: case PktFidMap: { - QVariant pkValsVariant = sharedData->lookupKey( featureId ); - if ( !pkValsVariant.isNull() ) + QVariantList pkVals = sharedData->lookupKey( featureId ); + if ( !pkVals.isEmpty() ) { - QList pkVals = pkValsVariant.toList(); - if ( primaryKeyType == PktFidMap ) { Q_ASSERT( pkVals.size() == primaryKeyAttrs.size() ); diff --git a/src/providers/wcs/qgswcsprovider.cpp b/src/providers/wcs/qgswcsprovider.cpp index df3f93d23238..7f1ee0135a8b 100644 --- a/src/providers/wcs/qgswcsprovider.cpp +++ b/src/providers/wcs/qgswcsprovider.cpp @@ -582,13 +582,12 @@ bool QgsWcsProvider::readBlock( int bandNo, QgsRectangle const &viewExtent, int !qgsDoubleNearSig( cacheExtent.xMaximum(), viewExtent.xMaximum(), 10 ) || !qgsDoubleNearSig( cacheExtent.yMaximum(), viewExtent.yMaximum(), 10 ) ) { + // Just print a message so user is aware of a server side issue but don't left + // the tile blank so we can deal with eventually misconfigured WCS server + // https://github.com/qgis/QGIS/issues/33339 + QgsDebugMsg( QStringLiteral( "cacheExtent and viewExtent differ" ) ); QgsMessageLog::logMessage( tr( "Received coverage has wrong extent %1 (expected %2)" ).arg( cacheExtent.toString(), viewExtent.toString() ), tr( "WCS" ) ); - // We are doing all possible to avoid this situation, - // If it happens, it would be possible to rescale the portion we get - // to only part of the data block, but it is better to left it - // blank, so that the problem may be discovered in its origin. - return false; } } @@ -810,7 +809,10 @@ void QgsWcsProvider::getCache( int bandNo, QgsRectangle const &viewExtent, int QgsDebugMsg( QStringLiteral( "%1 bytes received" ).arg( mCachedData.size() ) ); if ( mCachedData.isEmpty() ) { - QgsMessageLog::logMessage( tr( "No data received" ), tr( "WCS" ) ); + if ( !feedback || !feedback->isCanceled() ) + { + QgsMessageLog::logMessage( tr( "No data received" ), tr( "WCS" ) ); + } clearCache(); return; } diff --git a/src/server/qgsserver.cpp b/src/server/qgsserver.cpp index cd53dd62ad5a..83e8c2d1058e 100644 --- a/src/server/qgsserver.cpp +++ b/src/server/qgsserver.cpp @@ -290,11 +290,14 @@ bool QgsServer::init() void QgsServer::putenv( const QString &var, const QString &val ) { -#ifdef _MSC_VER - _putenv_s( var.toStdString().c_str(), val.toStdString().c_str() ); -#else - setenv( var.toStdString().c_str(), val.toStdString().c_str(), 1 ); -#endif + if ( val.isEmpty() ) + { + qunsetenv( var.toUtf8().data() ); + } + else + { + qputenv( var.toUtf8().data(), val.toUtf8() ); + } sSettings()->load( var ); } @@ -420,7 +423,7 @@ void QgsServer::handleRequest( QgsServerRequest &request, QgsServerResponse &res { responseDecorator.write( ex ); QString format; - QgsMessageLog::logMessage( ex.formatResponse( format ), QStringLiteral( "Server" ), Qgis::Info ); + QgsMessageLog::logMessage( ex.formatResponse( format ), QStringLiteral( "Server" ), Qgis::Warning ); } catch ( QgsException &ex ) { diff --git a/src/server/qgsserviceregistry.cpp b/src/server/qgsserviceregistry.cpp index 8321350c3253..19a002b72f1c 100644 --- a/src/server/qgsserviceregistry.cpp +++ b/src/server/qgsserviceregistry.cpp @@ -128,7 +128,7 @@ void QgsServiceRegistry::registerService( QgsService *service ) return; } - QgsMessageLog::logMessage( QStringLiteral( "Adding service %1 %2" ).arg( name, version ) ); + QgsMessageLog::logMessage( QStringLiteral( "Adding service %1 %2" ).arg( name, version ), QStringLiteral( "Server" ), Qgis::Info ); mServices.insert( key, std::shared_ptr( service ) ); // Check the default version diff --git a/src/ui/qgsgpsinformationwidgetbase.ui b/src/ui/qgsgpsinformationwidgetbase.ui index f91eb59186d7..af779cfee19b 100644 --- a/src/ui/qgsgpsinformationwidgetbase.ui +++ b/src/ui/qgsgpsinformationwidgetbase.ui @@ -1414,6 +1414,22 @@ gray = no data + + + + + 0 + 0 + + + + Recenter + + + false + + + diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 2a13e2423287..6166add9a1fd 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -671,10 +671,26 @@ class TestQgsExpression: public QObject QTest::newRow( "in 2" ) << "1 in (1,null,3)" << false << QVariant( 1 ); QTest::newRow( "in 3" ) << "1 in (null,2,3)" << false << QVariant(); QTest::newRow( "in 4" ) << "null in (1,2,3)" << false << QVariant(); + QTest::newRow( "in 5" ) << "'a' in (1,2,3)" << false << QVariant( 0 ); + QTest::newRow( "in 6" ) << "'a' in (1,'a',3)" << false << QVariant( 1 ); + QTest::newRow( "in 7" ) << "'b' in (1,'a',3)" << false << QVariant( 0 ); + QTest::newRow( "in 8" ) << "1.2 in (1,2,3)" << false << QVariant( 0 ); + QTest::newRow( "in 9" ) << "'010080383000187224' in ('010080383000187219','010080383000187218','010080383000187223')" << false << QVariant( 0 ); + QTest::newRow( "in 10" ) << "'010080383000187219' in ('010080383000187219','010080383000187218','010080383000187223')" << false << QVariant( 1 ); + QTest::newRow( "in 11" ) << "'010080383000187218' in ('010080383000187219','010080383000187218','010080383000187223')" << false << QVariant( 1 ); + QTest::newRow( "in 12" ) << "'010080383000187223' in ('010080383000187219','010080383000187218','010080383000187223')" << false << QVariant( 1 ); QTest::newRow( "not in 1" ) << "1 not in (1,2,3)" << false << QVariant( 0 ); QTest::newRow( "not in 2" ) << "1 not in (1,null,3)" << false << QVariant( 0 ); QTest::newRow( "not in 3" ) << "1 not in (null,2,3)" << false << QVariant(); QTest::newRow( "not in 4" ) << "null not in (1,2,3)" << false << QVariant(); + QTest::newRow( "not in 5" ) << "'a' not in (1,2,3)" << false << QVariant( 1 ); + QTest::newRow( "not in 6" ) << "'a' not in (1,'a',3)" << false << QVariant( 0 ); + QTest::newRow( "not in 7" ) << "'b' not in (1,'a',3)" << false << QVariant( 1 ); + QTest::newRow( "not in 8" ) << "1.2 not in (1,2,3)" << false << QVariant( 1 ); + QTest::newRow( "not in 9" ) << "'010080383000187224' not in ('010080383000187219','010080383000187218','010080383000187223')" << false << QVariant( 1 ); + QTest::newRow( "not in 10" ) << "'010080383000187219' not in ('010080383000187219','010080383000187218','010080383000187223')" << false << QVariant( 0 ); + QTest::newRow( "not in 11" ) << "'010080383000187218' not in ('010080383000187219','010080383000187218','010080383000187223')" << false << QVariant( 0 ); + QTest::newRow( "not in 12" ) << "'010080383000187223' not in ('010080383000187219','010080383000187218','010080383000187223')" << false << QVariant( 0 ); // regexp, like QTest::newRow( "like 1" ) << "'hello' like '%ll_'" << false << QVariant( 1 ); diff --git a/tests/src/core/testqgsgeometry.cpp b/tests/src/core/testqgsgeometry.cpp index 0dbe18e1578b..63c862b68ea0 100644 --- a/tests/src/core/testqgsgeometry.cpp +++ b/tests/src/core/testqgsgeometry.cpp @@ -152,6 +152,7 @@ class TestQgsGeometry : public QObject void reshapeGeometryLineMerge(); void createCollectionOfType(); + void orientedMinimumBoundingBox( ); void minimalEnclosingCircle( ); void splitGeometry(); void snappedToGrid(); @@ -17405,6 +17406,35 @@ void TestQgsGeometry::createCollectionOfType() QVERIFY( dynamic_cast< QgsMultiSurface *>( collect.get() ) ); } +void TestQgsGeometry::orientedMinimumBoundingBox() +{ + QgsGeometry geomTest; + QgsGeometry result, resultTest; + // empty + result = geomTest.orientedMinimumBoundingBox( ); + QVERIFY( result.isEmpty() ); + + // oriented rectangle + geomTest = QgsGeometry::fromWkt( QStringLiteral( " Polygon(0 0, 5 5, -2.07106781186547462 12.07106781186547551, -7.07106781186547462 7.07106781186547551, 0 0) " ) ); + result = geomTest.orientedMinimumBoundingBox( ); + QgsPolygonXY geomXY, resultXY; + geomXY = geomTest.asPolygon(); + resultXY = result.asPolygon(); + + QCOMPARE( geomXY.count(), resultXY.count() ); + // can't strictly compare, use tolerance + for ( int i = 0 ; i < geomXY.count() ; ++i ) + { + QVERIFY( geomXY.at( 0 ).at( i ).compare( resultXY.at( 0 ).at( i ), 1E-8 ) ); + } + + // Issue https://github.com/qgis/QGIS/issues/33532 + geomTest = QgsGeometry::fromWkt( QStringLiteral( " Polygon ((264 -525, 248 -521, 244 -519, 233 -508, 231 -504, 210 -445, 196 -396, 180 -332, 178 -322, 176 -310, 174 -296, 174 -261, 176 -257, 178 -255, 183 -251, 193 -245, 197 -243, 413 -176, 439 -168, 447 -166, 465 -164, 548 -164, 552 -166, 561 -175, 567 -187, 602 -304, 618 -379, 618 -400, 616 -406, 612 -414, 606 -420, 587 -430, 575 -436, 547 -446, 451 -474, 437 -478, 321 -511, 283 -521, 275 -523, 266 -525, 264 -525)) " ) ); + result = geomTest.orientedMinimumBoundingBox( ); + QString resultTestWKT = QStringLiteral( "Polygon ((153.56814721430669124 -251.04910547836090018, 236.58928384252226351 -536.38483199428605985, 635.81698325140541783 -420.2257453523852746, 552.79584662318995925 -134.89001883646011493, 153.56814721430669124 -251.04910547836090018))" ); + QCOMPARE( result.asWkt(), resultTestWKT ); + +} void TestQgsGeometry::minimalEnclosingCircle() { QgsGeometry geomTest; diff --git a/tests/src/python/test_provider_oracle.py b/tests/src/python/test_provider_oracle.py index d2a1dafbda74..0c2a79474927 100644 --- a/tests/src/python/test_provider_oracle.py +++ b/tests/src/python/test_provider_oracle.py @@ -718,8 +718,12 @@ def testIdentityCommit(self): vl.dataProvider().deleteFeatures([features[-1].id()]) def testFilterSimpleMultiGeom(self): - print("laaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", self.dbconn + ' sslmode=disable key=\'pk\' srid=4326 type=POLYGON table="QGIS"."MIX_SIMPLE_MULTI_POLYGON" (GEOM) sql=', - 'test', 'oracle') + """ + Check that we load separatly + simple and multi geometry stored + is a same table + https://github.com/qgis/QGIS/issues/32521 + """ vl = QgsVectorLayer( self.dbconn + ' sslmode=disable key=\'pk\' srid=4326 type=POLYGON table="QGIS"."MIX_SIMPLE_MULTI_POLYGON" (GEOM) sql=', 'test', 'oracle') @@ -734,6 +738,33 @@ def testFilterSimpleMultiGeom(self): self.assertTrue(vl.isValid()) self.assertEqual(vl.featureCount(), 1) + def testGetFeatureFidInvalid(self): + """ + Get feature with an invalid fid + https://github.com/qgis/QGIS/issues/31626 + """ + self.execSQLCommand('DROP TABLE "QGIS"."TABLE_QGIS_ISSUE_FLOAT_KEY"', ignore_errors=True) + self.execSQLCommand("""CREATE TABLE "QGIS"."TABLE_QGIS_ISSUE_FLOAT_KEY" (CODE NUMBER PRIMARY KEY, DESCRIPTION VARCHAR2(25))""") + self.execSQLCommand("""INSERT INTO "QGIS"."TABLE_QGIS_ISSUE_FLOAT_KEY" VALUES(1000,'Desc for 1st record')""") + self.execSQLCommand("""INSERT INTO "QGIS"."TABLE_QGIS_ISSUE_FLOAT_KEY" VALUES(2000,'Desc for 2nd record')""") + self.execSQLCommand("""CREATE OR REPLACE VIEW "QGIS"."VIEW_QGIS_ISSUE_FLOAT_KEY" AS SELECT * FROM "QGIS"."TABLE_QGIS_ISSUE_FLOAT_KEY" """) + + vl = QgsVectorLayer( + self.dbconn + ' sslmode=disable key=\'CODE\' table="QGIS"."VIEW_QGIS_ISSUE_FLOAT_KEY" sql=', + 'test', 'oracle') + + # feature are not loaded yet, mapping between CODE and fid is not built so feature + # is invalid + feature = vl.getFeature(2) + self.assertTrue(not feature.isValid()) + + # load all features + for f in vl.getFeatures(): + pass + + feature = vl.getFeature(2) + self.assertTrue(feature.isValid()) + if __name__ == '__main__': unittest.main() diff --git a/tests/src/python/test_qgscolorbutton.py b/tests/src/python/test_qgscolorbutton.py index 1f7cc94d7888..56c0bea0ce5c 100644 --- a/tests/src/python/test_qgscolorbutton.py +++ b/tests/src/python/test_qgscolorbutton.py @@ -39,6 +39,39 @@ def testClearingColors(self): # ensure that only the alpha channel has changed - not the other color components self.assertEqual(button.color(), QColor(255, 100, 200, 0)) + def testNulling(self): + """ + Test clearing colors to null + """ + + # start with a valid color + button = QgsColorButton() + button.setAllowOpacity(True) + button.setColor(QColor(255, 100, 200, 255)) + self.assertEqual(button.color(), QColor(255, 100, 200, 255)) + + spy_changed = QSignalSpy(button.colorChanged) + spy_cleared = QSignalSpy(button.cleared) + + button.setColor(QColor(50, 100, 200, 255)) + self.assertEqual(button.color(), QColor(50, 100, 200, 255)) + self.assertEqual(len(spy_changed), 1) + self.assertEqual(len(spy_cleared), 0) + + # now set to null + button.setToNull() + + self.assertEqual(button.color(), QColor()) + self.assertEqual(len(spy_changed), 2) + self.assertEqual(len(spy_cleared), 1) + + button.setToNull() + self.assertEqual(button.color(), QColor()) + # should not be refired, the color wasn't changed + self.assertEqual(len(spy_changed), 2) + # SHOULD be refired + self.assertEqual(len(spy_cleared), 2) + def testLinkProjectColor(self): """ Test linking to a project color diff --git a/tests/src/python/test_qgsgeometry.py b/tests/src/python/test_qgsgeometry.py index b0d1b135d247..6cea93258b2f 100644 --- a/tests/src/python/test_qgsgeometry.py +++ b/tests/src/python/test_qgsgeometry.py @@ -4465,15 +4465,15 @@ def testMinimumOrientedBoundingBox(self): # polygon polygon = QgsGeometry.fromWkt('Polygon((-0.1 -1.3, 2.1 1, 3 2.8, 6.7 0.2, 3 -1.8, 0.3 -2.7, -0.1 -1.3))') bbox, area, angle, width, height = polygon.orientedMinimumBoundingBox() - exp = 'Polygon ((-0.94905660 -1.571698, 2.3817055 -4.580453, 6.7000000 0.1999999, 3.36923 3.208754, -0.949056 -1.57169))' + exp = 'Polygon ((2.63653329463248109 -4.65542585423934607, 6.70000000000000284 0.19999999999999485, 3.24436595086289614 3.09199224252241889, -0.81910075450462605 -1.76343361171692159, 2.63653329463248109 -4.65542585423934607))' result = bbox.asWkt() self.assertTrue(compareWkt(result, exp, 0.00001), "Oriented MBBR: mismatch Expected:\n{}\nGot:\n{}\n".format(exp, result)) - self.assertAlmostEqual(area, 28.9152, places=3) - self.assertAlmostEqual(angle, 42.0922, places=3) - self.assertAlmostEqual(width, 4.4884, places=3) - self.assertAlmostEqual(height, 6.4420, places=3) + self.assertAlmostEqual(area, 28.5300, places=3) + self.assertAlmostEqual(angle, 129.9257, places=3) + self.assertAlmostEqual(width, 6.3314, places=3) + self.assertAlmostEqual(height, 4.5061, places=3) def testOrthogonalize(self): empty = QgsGeometry() diff --git a/tests/src/python/test_qgssymbollayer_readsld.py b/tests/src/python/test_qgssymbollayer_readsld.py index 6a1e3565e069..b1e23b4329b9 100644 --- a/tests/src/python/test_qgssymbollayer_readsld.py +++ b/tests/src/python/test_qgssymbollayer_readsld.py @@ -444,7 +444,12 @@ def testLineOpacity(): self.assertEqual(format.size(), 18) self.assertEqual(format.sizeUnit(), QgsUnitTypes.RenderPixels) + # the layer contains lines + # from qgis.core import QgsWkbTypes + # self.assertEqual(layer.geometryType(), QgsWkbTypes.LineGeometry) + # the placement should be QgsPalLayerSettings.Line self.assertEqual(settings.placement, QgsPalLayerSettings.AroundPoint) + self.assertEqual(settings.xOffset, 1) self.assertEqual(settings.yOffset, 0) self.assertEqual(settings.offsetUnits, QgsUnitTypes.RenderPixels) diff --git a/tests/testdata/control_images/data_defined_size_legend/expected_basic_bottom/expected_basic_bottom.png b/tests/testdata/control_images/data_defined_size_legend/expected_basic_bottom/expected_basic_bottom.png index 1f72c33c119d69d70f2decc54720a4f631c45152..3d99ed42a247fb3a637eb77eec2b3c0e48c7b1a8 100644 GIT binary patch literal 5456 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

2^Zfx1>}S6Te5uJN|5HA_#HF5_ z3QgN3jW^ymxM3Pmn`jClB#9(XJWb#Zn86HBJSQ>fEN8X3Z)Z5Al=|Rf0!N)``*^| zi%sCGj56^vRo(p>+utTOy}~Od&e8UaX7nN!nPA*>gB#v>8{3OU9_fCEFvO_N_ONFJ zBTO?g>V_H2kRkh=&yDKE2`BNpUu&$JXv!n6o7RSKm8*H=z0T`Kvab^>Sdm4R@e0Fo z$dMp1&4{csk9d8SXD@TvNS+c&G|i8hmMmh?$9!xg<5^@y(D6RI$#_K6aKdxQF@~FS zoU_ButS0IN5oC#FCYi=Co7Xp67+NsS_PVjUYns z1s@J`I0_95+>UeH z^bdvF(^1qx3<@Jmzzq;G$e7Ahp{yB$sZ14c*7s>n&tjG>wn7=y0}U5aF~kTsVOEaZ z%$I7P#xw!H#HTnFLySYbG@IF3WEpNJndDWhGTe@VX#Q@-5$B5QOT1*D^(v|~ z7D$>KEwofoWw@PP?5d{Pa61N~Ia3{Qh$h_ZCqN?|yf(vz&6F;xUl!9{4%p_!e&& z@A*`pVFx=lv)O#64fgO9cLN(PaG{h^{j3Wz05JiE^eDv?XOq2-b;5>Z2t7ZV-_7oA zY}3zrZxL02Lht=#wy|wDyM>L%KzbH+3RBcKT=cMR_yEK-=$)ylq=peD?li2|8L zfCUyZWV)WUkcA?y)Z7i)mb+PDb%6^@S*nxu2~7BgZ*-5!)O^D?6PTdWDT`U0O}2zC zSJMy?9cmaSFkvlg-{U>)PqD^2fPjl!)IBiMah~(R1nab9#AY^cWTQX+#*4}jbSq3< z&T!67}|FBoaxCCAP){fkqNZ3?k@BG%S|e%3>D(mfxEA>ZPfv>B^NWhYlSA92^{CVq)|# zqPgx@KeEUQCOC9^_b-}(D-jw|KLrZY{}SdV7(c*g5t=Q`IbsOV%f7Pn$E2=9~Ra3iaP^>CG zqdN1B>K&O%rZOg-Kcl0gqn+Ka#fW8PWiw~a{MWz!rR#qb!e(kKYgwB?##_AARmx6c zk{r4AOXj`Ei*NIG=e+!pAOFH%zT`_S?bK60gBd^YgT{A@Dc;7mFv33I11$|2gQ}{k zxVX5zd-no!=FCY>*14+QD53)AIJcCg!31j@jHsvO?q%<2M)zpyGd^?1xvR;LU-8u) z?kr|jSW*}K_o4o|#ub8`a%0stQ$pMnCNZ+94LrHRQ*zQ(n& zjEy9+^9&9PEY5N6J>L7C?|U?LjAOwBclB;C!P?BdJjSuDZ2f^BIxBjVqtlrlNMHd4 zVksIO9ld}5{`~y>TeogyWo5<3$E%Nfd3ot!%~+Y*iW22I)_uiSF~lrjL5FuYvss30 z1uMEby_VW}%Ix`!|_8L+Q+{6%-_y-accqF50P52x3^DE zPY((T($$*L*Mu3wpwIZMkite9!wLU_FWN5w{x0uIk*;R7krrnBkssHvW-x=>D_Te0 zK@L8{Gb&Wqx&Al)CVF*eY;5f0$&;O(od*mUU~6mZ?d|=>8*k*~_uT#csw$v>4SU*VP3BYET<<>+;;#}fNAPq)yLLrxlL$2mTWSuw;IGUuRI zAdaYN>Z#wyzLT6xA!R({0|*Etu#(Ed94@DPDpRL1O+9j%&-~x>`_989^|JeiI0T&M zyd1e4`3OeLXZ}!zW|5Ucj(V+>2qKoSMBG{XCPPe7+mk^?8fm9EB|)MWi;0zCQSP zX_Cfw1 Xmt@B}IJxdJ00000NkvXXu0mjf{rIfV diff --git a/tests/testdata/control_images/data_defined_size_legend/expected_crowded/expected_crowded.png b/tests/testdata/control_images/data_defined_size_legend/expected_crowded/expected_crowded.png index 9281a723bea44beb6c31fe6556087f2b0fe17719..c8e9959271a97ccbd787e21d30ffc1cdb7e34c3b 100644 GIT binary patch literal 7791 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 literal 7272 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 diff --git a/tests/testdata/control_images/legend/expected_legend_data_defined_size_collapsed/expected_legend_data_defined_size_collapsed.png b/tests/testdata/control_images/legend/expected_legend_data_defined_size_collapsed/expected_legend_data_defined_size_collapsed.png index ad7476ca992a2df8d75883eede6f99914157309d..64331385a0c260b7bfcdd7b0c3ece8010cd45471 100644 GIT binary patch literal 13304 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 QiV<<|_(N_g2|i$x0d7o6JJU3X#2!k)6FhR@VJ= zf3NTVaQ|>$7jfmB^M1Wwujljee2jOPsjV+ zHClu3BoZ=TqhQuYIWS&Jtl`q%M3?_T4-mc~aZHLOEV`}LYTmTCB)!bX+EerA%|b9C zx|u~kIW$m#n)|Erz>q=Fna{=8*Y^18zO*>{RPEi)pDWHio;`!^y`DYGp5kfCqzJ5l zn0bfs5=1}@{)cx|8Lx1X7;*pqFaK|Df*NB6_MTV0YQ6@Mx&LvOVQ@TVYU+CL9?@b? ziYSF+_vz{Bf5S;s6=!Zs-Bi?+l%EMxQc{{P((b$OEh#ZESzB2#GG&|`Zpqx&*433m zqp2gMk9>T5l0!p&KQ7B$8!EG*Yv zF%#ol`5YX4qaZ&&9WEXc@4fx!O6Q^zH5C<6b&WnZ{AA4RXS1Q9p}8B-^mu-LekITF zfF^W&M<5K1(ko8rw&0J9iW-3HDygVEH87yR@;N6b=ZnA8$j}g)wjBqfbdN-rDWPtDLUIwOOLEneP%mX#7wR8rDxQubv z_O|C8!XL06A8|vSn#Xru9Uptnp2%MB$F50Fzd}bxH#RjDq*oj~v(^6l(<8^_9y%La z+w?DAR0&`HHB9WfUOE~MZ>p~sJ38~bhIJ)x*m`|)6O}vIWL<^I?0=lu|HFv)F;mPP zLTs}=^+9D=(ZRt1Q+3^x-R#9y!wI(lwbIyfG=szqL{V{ZknIcfINLYFS<7pcnIEo}@Y$#9;8k z(8T-v0ZoY5&TGY5;VCJX%Qb%S;$|X`;m`4k7iG1zZ-aua$Hm8|S62(hCVq6(d-g07 zcif2gb4?AEUNQO1(a%Q+MC18>=>rLg=<{>Gh2>>aTie^7o}SImsS%{O*F920q)=;>S8~#H(8iO%g&!++(s~V+$M^x zeGmCiKp-nSI>P3WGE$J2_c1>|zsuUleWx0I0VGlpHbl3S0#Q&}nmapd>?D%nviOVa zvf%ss`-?_7$?!Z`4nDQa-a%+QeJX{>D=UjBC@fr=uJ<-xL>Yv`Ta2Im_2y(_!^T+f zQnqgS)p_pSgX)a!x6Fd|*ZQ4#jjXx6gTv_W>m$_E(CEvQiT(=<*DZa@H!?G$f<`wP zy@Mk_cXf54(Uo7n%3QsAbqIsujJtjOCT;&N4-d~EoE8~5Iqu?jgHlyL6O%U7|3{ z)~Q<8@X*knGzqvi0fTsiNV&GA<`XoUBVPV!a?vT<)@)0se*N~XThJfdNP>Kgw+#*A z>+9=gb$aX%`1r8G&~G+2Y$+q91s*;$uagf^H!z5jINymT!p!*WDUI0(LJbGoHaaik zH=Hi=;Q3#i%v=Pc(nGg5lO_D=C5}5)nyY6`aO4zUt3odfIZ{WT0&;vSLGrb=H6rs~KyEc|2?>ek`#nA_ib_gBx3&4g zZSg|_HfI{sh9y5QEm`2+2+&YdW5iZwiXkNGpP0}?9|Is@zZOa9`NAhmw`#_hWa;Ge zem{hyGA4#-b#pT(I5@Z{xiLLmk`U9}jCG%ji?Dz28#20`M6Kc5w-0txwPY6Q$CQj+ z zai4UXF4^S8w#UmgBp*9HyWQi~@;v>F$4 zC8n(G0n+c366$8Q@`Qwha;CZ3T9KiV zkvH=~LMh6Z@ZI)-JzhR8G11iBokCPp^cyq~4BI;~vY%DiuElnmv9Yl|28ikD!AH^) zkW)}N?yqWIzTDH3Nx7QEoYMBSKbb1HZwbpHe`r*TeG4k0W<$lkeJ)^mtwtxS! zInv5xW@aup)dapvc1i16bP7s&>Ea@AcloWkTb)<|&!eX!7>oiMjURHC@llcn2MYpq zoQ&fN%s>O^1+0n6UccTzWol|_!9^rLD+^7iVQae)x zoOY)XZ(k4|1)pCejGcQ-8JyZB*nSIV=BPKmBdC@Q+@Lo=a#(G8UGkpl)skNh*|D*X zBT~iO!7yMe#?>ES+FrijtH*M(-kZXGYx+7ur&tT|s&`q++?+*TL7{(SBq3&xo0~hM zun-S72unR*z_em%b#-<7Y|EdaSKS(#(ecgEgBE{>aA38K8JCV9KsNoTmXBanu(g9_W9*m*#Oe<<1x{ zALLt5P_PYbh zl7gz|e^@~EHm9?-OiHS~?2BIUwNoB^dkNUTNUup%{h10RW>~iEeZ$>7WOT)g-sZLa zxsl>$owQ8%mV47eep>{Lt_A6H_sSnK(nUl?;f})MIlo;DylyjT8W~n z{1Tts8J3rq7vHYavQtfmr2te|9-GM_utO-f_x+a4iCys{rfI^?!SJebzsUBc_p9cH zwJtbNksm&MkhM7c5V)y>?4558!n-N5;v5A0N&lOJr0nnbp1R%QXJ27sUw0+)d0WsT8SMv8d+K4z_M>{H@;N5X;4Z5{cR;`Q1ae^p{A;e z?6Upbw+!?+jH>i5i>3eT>AZ9Y-n@%Q*_SWxc6Y3;tv~l$HtU$E!C|X-=;Q0$v&9WlPpdaC<9T^c>X+Pm&O97UjFcE za@R@%4&2S%Dm|sMvom(O+?^vx9IkYB&#)1z+RzI`;1*bun7 zYJCaFwzjrHuB)Vij~=N3GLM*l2Lyz%w4JZwiwoGm#?Ae+*YAK|QBg6=yrj(;@o$d( zXeZjA_TsqLe|zMaJ&SL7YAXFU(%)uDo-O{DX>5tLmlt(>d^~hQ3$Us8B0s&S_vpEJ zc;+)1&e(u_`m;Pwbf5N=`9MGfK%bG7b&a;&*xdYjlM!LsarcYuHNy*PpwD(=$+rOW z8-b;G07;34HoJN+76*p+_OXxg=)tF%(;JuF45u}p=PW|Z$bIh-M4E@K?PA3PpL!+a3`8HCV;ycJ_AklzAhaK9k*0#1mzbXa~0emr@vh(l|z`Bo*kAe5eEX~2-mAaHa1ibWGT3UbH z>KqT&$G;uQu?(@3?*#MG>E^{6l-}&!8=IJrU%lp5N9R<7v^^iN{O{lZ*QDO>toF_V z99=|2#GlSs)(3~-RRyuA^jE{ypk#i6)~T(lGp-o_^8I^6WaMoZH#dtVlWh&?$4me( zF)=Zm4Vu~m;AzR?z9NgYw4|$XQJN_OWUDTD?;cX$h7n#?Q(ddeL+rU)uCQczj-AiSxgcQQjf5RS)56&N z3rJ;^Mw}D@10D`1^Zk2bP#;!QTwWvcF#L>o-zn!RSW3ipl2&J^XTTQ8pj9YJYJ5)w zK|=>+w|EI=s&-JE7BL);6|ZXO=zMTF=!OYZ$vrnVI4A|;3a&wXZo+T-~5s7p#-u@d+g z$4U9|o!wVV{`c>v9Zx>y{RQ$77-2vd^^dCLMrm*3i_9 z->(+p5OuKW+i;U9!uF?x_aIVxLgj-1r@6Sfn=J)?ZUhN&1F&C#=C!c6F=GQ5`n`bMi_`f_s-qN?6H+q5^uC z6)e+i77My=JdinnNt30{?vWZTTGVM$_G4{dTBx36c6GQA+FmAgp6h9c86VX3B}a5{ z;LSE&TwIjoPBgW$VlUYhNx?_$dS%%o7%q zKPx2GMn(u^Z*Fp{bh;|@f0cOGuvnK$Xe%^*#2L8A6-;iPT*y7@B&2R4jtxyScW@vS zPvgl>|A)pr0=-v)BWc38`s}klx60pQ7&yFEtUf@5;i%qo66&3%t(LM`zo2%0 zbAPtCx3sSBd!=h|?7(N;Y&>e?F%qEOsp<8_pE*DHYAiqvxW zt9UP5^iWStO|AJ%{L^n~uln<`-=Bmy3iI;r6b{SFXYO;Uufcb#U4)Sn4(IWUVHXm9?HTLjF` z3@PO1=tv~^>9$3M@NAqmOc(lS8J#Rva^MrlUIr17Pj6pjR#cG8oGv{IYLqGzIXi5; zAp1A``Z0o&lauIq^8YoAj=K+I0-B8Uo05iyhd+G!6pmR=E-_wj0k9)BX}$zoz9dyc zLmuqUU%yhoTgB5;N#Ef7CiLI{Es1) zOxvEHWLRa6?feX+&h-e!2rn-$qGCJ(46eT}A{rVRS;ZwB@wfp20bej1ILC=_Dj+7W zr!EuFz5-JyK&w#6qd}30_Czx!OWjJ}*X;5r6%1-Eer|jP-3mU7hL)Bzh+A5G8NJg1 zb8~Z$1}hU)FSE1?J6BwCre;zUnPrh1+%d9qe#R=fbcLEJbH3vW8XeYEBE_>R78Oaf z_%|JC6NzHPUII#93XA;wF!;0y;uzKnMSJ8%>Plk$d}RTNiSM1*&bfaNooOi!7c2{h zKe`N|1e{?31_sKo0Od6ClrkQh#!JTmJ~BnZKNW7^sd%b-K64N%Ms?lzvNpKOH3BEc z#@GS{!LS5YMRJbsJCjbx%9OFPVl^>2$()g!uHx4yvF=q)zM7@MAy=%$;DyN7pgx@E zj5l6CBiE}uW23N{v zvZoYHFGei>lp`LEUN}5yiAt1lIK1M=pBVS)JD|hnVJxY4|8?U6E z%gaQz%z`*l5b1paqW@Ly`JzW??i8G;N2-bls~k7c;EU>-8l2SS^kM5kC#EnoQ}>K) z!c7AD$EBkTqC{6!{B`J~Z@OQjyz*iY#R(?gAUmZ%c)o5>RcMf+9t~eZ>tYQgdp4ygrUG=c5t_Z8pE< zf8pJIHIVwh&!T~f_ga8oPy3y01agjHFyT*gm1jj~j+L8FEW+EP8Ri==&Uk)xb?ty& z6W%*=<5dpd|Mi(E)Nc9dhagaxQIOe)!s>lPr3^RGLRn~ zj{ZzDdwT+4ZSD68eV9&n6B`4YjAT99ax~`Xr6_OR02|E&^lJ9}0&z&S4)UcQtzZf- zRg`tG{ykk?-KGu9SAM6vLV3U8rs2cYV;F0GrK9k#Vcis8CBN(FJn%Rlw2WS!H(dX^jtt<4|ntaXAI{H9*KG!bP>;zG3HUd~}!yJOj^LDtGY7HAhG#K37+h zgMe0AIjWIIiBpH6-w&BeO#6bH5SywKhi5iGLUQW{!N#-jq`+Eo2s~5^ht;-ztx)PI zwLfr5X{o)_KamIW1@x#~ZxH!aoWkJcU4mE@oL;gT2oVV^5n=zyu+;g1CsdfYzQwx{pyo__6YC0$7c*to3}_wEK8D7I26{&s9u5wa zr>8L3YW&aZO_kJ9^t7~r%DGYypNt)3XS0x!lK$yV;+NBu&Q@Psi2Lm^4))MLy30Xs zawjW0`#P|wOZTt9r04yApN0<(1Yx_;X+1`~XtH_mw}1f?B>R1mGAmG9cy+d+pgAt}Gw&)jHtBnqdov$bsk-blM-(V7GK3vK)C z!3L(e8cN)RxPmK>;qzMVmU%m%s=#+;gB`!7TWrSAp384r6I+EB0PwncC_^6G%er zlpV!6(6yodeo3G?#*6s!2PvCF742;%EDjOz3w7VWCyR)P1fql7>Qba-WGejV+J^@w z7{$B~xc>91e`nslbqE)I;=v1^#>mJ>2LAg{S8*o+8$lYVqKse`P{fk=Vw<)O!F!j2 zLD-e--X2uhsO#$P4zyEc+w2xTz6tp8TtP!aL!{tBs_E(`Z4NGzdezedodO6o#%#EW zPTD8pcg>M@KYT_QduaNDj`CAX{2l376L{aXcXL%?$vchD-g(?7q_2*YA_R%AJn9q^KS{Z4_nI)i)5AW=;qUa)={O!7BM>M#e8K zC3RIfMI^}ZY*icaDk=*9``G~oY|8e!tp?7x3LP$RK$=;_cW`{R0~i9K3P&LXw9yo0 z6@K4I2(XY>T-*i}bJjtqZ+bck8dBiVqj_k~4>oZ>=pz zq3A;h7@FaPNtQvD1OJT%ZHL&G_mg+Y-*Mz9RFHX|bg3xtM)ND5FLXjL6O zy=EX7CHgg3QS%Nf11!dAqVAhU78dtxT6wi8DJg$!ZkF^ZK?p4gH+7j4&F%qaUB*h# z46tz^94(lX-``D3H?0^~)zAoxk0%3vJ{_*3l8SuYGW!?|I1rFfN9pSMgxJ@>B%E~- z0W6BDEs(p6QNdJ#a{XaZkq(p{e8FbCVu%Z3SyjGNR9yXviG(B~E+N69W~4?*E9WC5 zm4E{#zy+YlF0K1lxrWrxH?M%9feU~@ z)=U3c!AtG8L<22QK9h~+q;hSf3pOZi2e;@g1nqd7W6rc8aA;#2Zr}8|UEJL>A>am^ zfN06g#;6 z44Q=hr%F4Jfp#U@AW*LD(Fd)Xqbph1xLEZ&_Sa%bxs;qpc!I2FD2Rb_w z!r=i+0U3iva7E$X?uNIa{ztYz3=B>k6KGO+r~x4J@A`O!u6-AYiEinS*IN4eiX@w_$?EjN9adQ}DbWTtUA7x3IX_57`VEDJc|v23+eh!YvEBU7s*nrr9d1O2&p^d>X*q6 zJL%iXw7SoqU+HsGT;2$LDio@S`zG!F;o(!*OORRp2M2B=n7>&QWnSaq6!44yWThYm zKZD8zuIT|h7iM?U9um@J0(gi38ew*JHY+m{^ltyq&^5?1;5iZqWliCs0_jX5AitNu zq1~`c_Wfus{6lK0@}4FiF(Yfx_cTcKPmkIqe%kw=JezFrrGW$rmc&aP$xC?)%CZeY zaTo%3juscq0g1t&X))t#vJr%TUoU%l)K7nTS6o%~a2pv7>2+mQRnzhwz^2)80o6(8 zKU2mjw~dL1pxd&HcxB<#UEnzxtW@c2*i@q4JqBb~B!FTI0@7nh!}yO|VYT;yc^3 zEnLHKF}GNyJ8n5T7u`TpMwl6+EJCsFnFX^M{qHYF5~(BGt<0#3EO=H7A@@WXU4k-t G^M3&2<`O#q