diff --git a/.ci/azure-pipelines/azure-pipelines.yml b/.ci/azure-pipelines/azure-pipelines.yml index 78ee121ee649..4ea567b391e4 100644 --- a/.ci/azure-pipelines/azure-pipelines.yml +++ b/.ci/azure-pipelines/azure-pipelines.yml @@ -1,7 +1,7 @@ variables: LR: release-3_12 LTR: release-3_10 - CTEST_CUSTOM_TESTS_IGNORE: "ProcessingGdalAlgorithmsRasterTest;ProcessingGdalAlgorithmsVectorTest;ProcessingGrass7AlgorithmsImageryTest;ProcessingGrass7AlgorithmsRasterTest;ProcessingGrass7AlgorithmsVectorTest;ProcessingGuiTest;ProcessingOtbAlgorithmsTest;ProcessingQgisAlgorithmsTestPt1;ProcessingQgisAlgorithmsTestPt2;ProcessingQgisAlgorithmsTestPt3;ProcessingQgisAlgorithmsTestPt4;ProcessingScriptUtilsTest;PyQgsAnnotation;PyQgsAppStartup;PyQgsAuthManagerOAuth2OWSTest;PyQgsAuthManagerPasswordOWSTest;PyQgsAuthManagerPKIOWSTest;PyQgsAuthManagerProxy;PyQgsAuthSettingsWidget;PyQgsAuxiliaryStorage;PyQgsBlockingNetworkRequest;PyQgsExifTools;PyQgsFileDownloader;PyQgsFileUtils;PyQgsGeometryTest;PyQgsImageCache;PyQgsImportIntoPostGIS;PyQgsLayoutAtlas;PyQgsLayoutLegend;PyQgsLayoutMap;PyQgsLayoutMapGrid;PyQgsMapLayer;PyQgsOfflineEditingWFS;PyQgsOGRProvider;PyQgsOGRProviderGpkg;PyQgsOGRProviderSqlite;PyQgsPalLabelingCanvas;PyQgsPalLabelingLayout;PyQgsPalLabelingPlacement;PyQgsPointDisplacementRenderer;PyQgsProject;PyQgsProviderConnectionGpkg;PyQgsProviderConnectionPostgres;PyQgsPythonProvider;PyQgsRasterFileWriter;PyQgsRasterLayer;PyQgsSelectiveMasking;PyQgsServerAccessControlWMSGetlegendgraphic;PyQgsServerApi;PyQgsServerCacheManager;PyQgsServerLocaleOverride;PyQgsServerSecurity;PyQgsServerSettings;PyQgsServerWMS;PyQgsServerWMSDimension;PyQgsServerWMSGetFeatureInfo;PyQgsServerWMSGetLegendGraphic;PyQgsServerWMSGetMap;PyQgsServerWMSGetPrint;PyQgsServerWMTS;PyQgsSettings;PyQgsShapefileProvider;PyQgsSpatialiteProvider;PyQgsSvgCache;PyQgsSymbolLayer;PyQgsTaskManager;PyQgsTextRenderer;PyQgsVectorFileWriter;PyQgsVectorLayer;PyQgsVectorLayerUtils;PyQgsVirtualLayerProvider;PyQgsWFSProviderGUI;PyQgsZipUtils;qgis_3drenderingtest;qgis_alignrastertest;qgis_arcgisrestutilstest;qgis_banned_keywords;qgis_browsermodeltest;qgis_callouttest;qgis_compositionconvertertest;qgis_coordinatereferencesystemtest;qgis_datadefinedsizelegendtest;qgis_datumtransformdialog;qgis_diagramtest;qgis_doxygen_order;qgis_dxfexporttest;qgis_expressiontest;qgis_filedownloader;qgis_geometrycheckstest;qgis_geometrytest;qgis_geonodeconnectiontest;qgis_grassprovidertest7;qgis_imagecachetest;qgis_invertedpolygonrenderertest;qgis_labelingenginetest;qgis_layerdefinitiontest;qgis_layout3dmaptest;qgis_layouthtmltest;qgis_layoutlabeltest;qgis_layoutmapgridtest;qgis_layoutmaptest;qgis_layoutpicturetest;qgis_layoutscalebartest;qgis_layouttabletest;qgis_legendrenderertest;qgis_licenses;qgis_maprendererjobtest;qgis_maprotationtest;qgis_mapsettingsutilstest;qgis_maptooladdfeatureline;qgis_mimedatautilstest;qgis_networkaccessmanagertest;qgis_openclutilstest;qgis_painteffecttest;qgis_pallabelingtest;qgis_processingtest;qgis_projecttest;qgis_qgisappclipboard;qgis_rasterlayersaveasdialog;qgis_shellcheck;qgis_sipify;qgis_sip_include;qgis_sip_uptodate;qgis_spelling;qgis_styletest;qgis_svgcachetest;qgis_taskmanagertest;qgis_transformdialog;qgis_vectorfilewritertest;qgis_wcsprovidertest;qgis_ziplayertest;qgis_meshcalculator;qgis_pointlocatortest;PyQgsExpressionBuilderWidget;PyQgsDatumTransform;qgis_vertextool;PyQgsCoordinateOperationWidget;PyQgsProviderConnectionSpatialite;qgis_maptoolsplitpartstest" + CTEST_CUSTOM_TESTS_IGNORE: "ProcessingGdalAlgorithmsRasterTest;ProcessingGdalAlgorithmsVectorTest;ProcessingGrass7AlgorithmsImageryTest;ProcessingGrass7AlgorithmsRasterTest;ProcessingGrass7AlgorithmsVectorTest;ProcessingGuiTest;ProcessingOtbAlgorithmsTest;ProcessingQgisAlgorithmsTestPt1;ProcessingQgisAlgorithmsTestPt2;ProcessingQgisAlgorithmsTestPt3;ProcessingQgisAlgorithmsTestPt4;ProcessingScriptUtilsTest;PyQgsAnnotation;PyQgsAppStartup;PyQgsAuthManagerOAuth2OWSTest;PyQgsAuthManagerPasswordOWSTest;PyQgsAuthManagerPKIOWSTest;PyQgsAuthManagerProxy;PyQgsAuthSettingsWidget;PyQgsAuxiliaryStorage;PyQgsBlockingNetworkRequest;PyQgsExifTools;PyQgsFileDownloader;PyQgsFileUtils;PyQgsGeometryTest;PyQgsImageCache;PyQgsImportIntoPostGIS;PyQgsLayoutAtlas;PyQgsLayoutLegend;PyQgsLayoutMap;PyQgsLayoutMapGrid;PyQgsMapLayer;PyQgsOfflineEditingWFS;PyQgsOGRProvider;PyQgsOGRProviderGpkg;PyQgsOGRProviderSqlite;PyQgsPalLabelingCanvas;PyQgsPalLabelingLayout;PyQgsPalLabelingPlacement;PyQgsPointDisplacementRenderer;PyQgsProject;PyQgsProviderConnectionGpkg;PyQgsProviderConnectionPostgres;PyQgsPythonProvider;PyQgsRasterFileWriter;PyQgsRasterLayer;PyQgsSelectiveMasking;PyQgsServerAccessControlWMSGetlegendgraphic;PyQgsServerApi;PyQgsServerCacheManager;PyQgsServerLocaleOverride;PyQgsServerSecurity;PyQgsServerSettings;PyQgsServerWMS;PyQgsServerWMSDimension;PyQgsServerWMSGetFeatureInfo;PyQgsServerWMSGetLegendGraphic;PyQgsServerWMSGetMap;PyQgsServerWMSGetPrint;PyQgsServerWMTS;PyQgsSettings;PyQgsShapefileProvider;PyQgsSpatialiteProvider;PyQgsSvgCache;PyQgsSymbolLayer;PyQgsTaskManager;PyQgsTextRenderer;PyQgsVectorFileWriter;PyQgsVectorLayer;PyQgsVectorLayerUtils;PyQgsVirtualLayerProvider;PyQgsWFSProviderGUI;PyQgsZipUtils;qgis_3drenderingtest;qgis_alignrastertest;qgis_arcgisrestutilstest;qgis_banned_keywords;qgis_browsermodeltest;qgis_callouttest;qgis_compositionconvertertest;qgis_coordinatereferencesystemtest;qgis_datadefinedsizelegendtest;qgis_datumtransformdialog;qgis_diagramtest;qgis_doxygen_order;qgis_dxfexporttest;qgis_expressiontest;qgis_filedownloader;qgis_geometrycheckstest;qgis_geometrytest;qgis_geonodeconnectiontest;qgis_grassprovidertest7;qgis_imagecachetest;qgis_invertedpolygonrenderertest;qgis_labelingenginetest;qgis_layerdefinitiontest;qgis_layout3dmaptest;qgis_layouthtmltest;qgis_layoutlabeltest;qgis_layoutmapgridtest;qgis_layoutmaptest;qgis_layoutpicturetest;qgis_layoutscalebartest;qgis_layouttabletest;qgis_legendrenderertest;qgis_licenses;qgis_maprendererjobtest;qgis_maprotationtest;qgis_mapsettingsutilstest;qgis_maptooladdfeatureline;qgis_mimedatautilstest;qgis_networkaccessmanagertest;qgis_openclutilstest;qgis_painteffecttest;qgis_pallabelingtest;qgis_processingtest;qgis_projecttest;qgis_qgisappclipboard;qgis_rasterlayersaveasdialog;qgis_shellcheck;qgis_sipify;qgis_sip_include;qgis_sip_uptodate;qgis_spelling;qgis_styletest;qgis_svgcachetest;qgis_taskmanagertest;qgis_transformdialog;qgis_vectorfilewritertest;qgis_wcsprovidertest;qgis_ziplayertest;qgis_meshcalculator;qgis_pointlocatortest;PyQgsExpressionBuilderWidget;PyQgsDatumTransform;qgis_vertextool;PyQgsCoordinateOperationWidget;PyQgsProviderConnectionSpatialite;qgis_maptoolsplitpartstest;qgis_vectortilelayertest" Agent.Source.Git.ShallowFetchDepth: 120 trigger: diff --git a/CMakeLists.txt b/CMakeLists.txt index c7f145d52578..2c76dd89c3f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -331,6 +331,14 @@ IF(WITH_CORE) MESSAGE (SEND_ERROR "sqlite3 dependency was not found!") ENDIF (NOT SQLITE3_FOUND) + FIND_PACKAGE(Protobuf REQUIRED) # for decoding of vector tiles in MVT format + MESSAGE(STATUS "Found Protobuf: ${Protobuf_LIBRARIES}") + IF (NOT Protobuf_PROTOC_EXECUTABLE) + MESSAGE (SEND_ERROR "Protobuf library's 'protoc' tool was not found!") + ENDIF () + FIND_PACKAGE(ZLIB REQUIRED) # for decompression of vector tiles in MBTiles file + MESSAGE(STATUS "Found zlib: ${ZLIB_LIBRARIES}") + # optional IF (WITH_POSTGRESQL) FIND_PACKAGE(Postgres) # PostgreSQL provider diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 78ab8d1d2be1..5513bd8d3448 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -99,6 +99,7 @@ IF(WITH_APIDOC) ${CMAKE_SOURCE_DIR}/src/core/scalebar ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/core/validity + ${CMAKE_SOURCE_DIR}/src/core/vectortile ${CMAKE_SOURCE_DIR}/src/gui ${CMAKE_SOURCE_DIR}/src/gui/auth ${CMAKE_SOURCE_DIR}/src/gui/attributetable diff --git a/images/images.qrc b/images/images.qrc index 06a9872019bb..1fe5dee12ebd 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -513,6 +513,7 @@ themes/default/mIconTimerPause.svg themes/default/mIconTreeView.svg themes/default/mIconVector.svg + themes/default/mIconVectorTileLayer.svg themes/default/mIconVirtualLayer.svg themes/default/mIconWcs.svg themes/default/mIconWfs.svg diff --git a/images/themes/default/mIconVectorTileLayer.svg b/images/themes/default/mIconVectorTileLayer.svg new file mode 100644 index 000000000000..fa10e503b7d5 --- /dev/null +++ b/images/themes/default/mIconVectorTileLayer.svg @@ -0,0 +1,195 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index e87ac9aad67a..101a12d656fd 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -123,6 +123,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/core/classification ${CMAKE_SOURCE_DIR}/src/core/validity + ${CMAKE_SOURCE_DIR}/src/core/vectortile ${CMAKE_SOURCE_DIR}/src/plugins ${CMAKE_SOURCE_DIR}/external ${CMAKE_SOURCE_DIR}/external/nlohmann diff --git a/python/core/auto_additions/qgsmaplayer.py b/python/core/auto_additions/qgsmaplayer.py index 4535f58d3d3d..fde3f16868da 100644 --- a/python/core/auto_additions/qgsmaplayer.py +++ b/python/core/auto_additions/qgsmaplayer.py @@ -9,7 +9,9 @@ QgsMapLayer.PluginLayer.__doc__ = "" QgsMapLayer.MeshLayer = QgsMapLayerType.MeshLayer QgsMapLayer.MeshLayer.__doc__ = "Added in 3.2" -QgsMapLayerType.__doc__ = 'Types of layers that can be added to a map\n\n.. versionadded:: 3.8\n\n' + '* ``VectorLayer``: ' + QgsMapLayerType.VectorLayer.__doc__ + '\n' + '* ``RasterLayer``: ' + QgsMapLayerType.RasterLayer.__doc__ + '\n' + '* ``PluginLayer``: ' + QgsMapLayerType.PluginLayer.__doc__ + '\n' + '* ``MeshLayer``: ' + QgsMapLayerType.MeshLayer.__doc__ +QgsMapLayer.VectorTileLayer = QgsMapLayerType.VectorTileLayer +QgsMapLayer.VectorTileLayer.__doc__ = "Added in 3.14" +QgsMapLayerType.__doc__ = 'Types of layers that can be added to a map\n\n.. versionadded:: 3.8\n\n' + '* ``VectorLayer``: ' + QgsMapLayerType.VectorLayer.__doc__ + '\n' + '* ``RasterLayer``: ' + QgsMapLayerType.RasterLayer.__doc__ + '\n' + '* ``PluginLayer``: ' + QgsMapLayerType.PluginLayer.__doc__ + '\n' + '* ``MeshLayer``: ' + QgsMapLayerType.MeshLayer.__doc__ + '\n' + '* ``VectorTileLayer``: ' + QgsMapLayerType.VectorTileLayer.__doc__ # -- QgsMapLayer.LayerFlag.baseClass = QgsMapLayer QgsMapLayer.LayerFlags.baseClass = QgsMapLayer diff --git a/python/core/auto_generated/qgsdataitem.sip.in b/python/core/auto_generated/qgsdataitem.sip.in index 4edac4ac530a..c44ca229b69e 100644 --- a/python/core/auto_generated/qgsdataitem.sip.in +++ b/python/core/auto_generated/qgsdataitem.sip.in @@ -479,7 +479,8 @@ Item that represents a layer that can be opened with one of the providers Database, Table, Plugin, - Mesh + Mesh, + VectorTile }; @@ -573,6 +574,10 @@ Use QgsDataItemGuiProvider.deleteLayer instead static QIcon iconMesh(); %Docstring Returns icon for mesh layer type +%End + static QIcon iconVectorTile(); +%Docstring +Returns icon for vector tile layer %End virtual QString layerName() const; diff --git a/python/core/auto_generated/qgsmaplayer.sip.in b/python/core/auto_generated/qgsmaplayer.sip.in index 480915b632e3..fa8de3a8987d 100644 --- a/python/core/auto_generated/qgsmaplayer.sip.in +++ b/python/core/auto_generated/qgsmaplayer.sip.in @@ -19,7 +19,8 @@ enum class QgsMapLayerType VectorLayer, RasterLayer, PluginLayer, - MeshLayer + MeshLayer, + VectorTileLayer }; class QgsMapLayer : QObject @@ -53,6 +54,9 @@ This is the base class for all map layer types (vector, raster). case QgsMapLayerType::MeshLayer: sipType = sipType_QgsMeshLayer; break; + case QgsMapLayerType::VectorTileLayer: + sipType = sipType_QgsVectorTileLayer; + break; default: sipType = nullptr; break; @@ -1419,7 +1423,7 @@ Sets the coordinate transform context to ``transformContext`` SIP_PYOBJECT __repr__(); %MethodCode - QString str = QStringLiteral( "" ).arg( sipCpp->name(), sipCpp->dataProvider()->name() ); + QString str = QStringLiteral( "" ).arg( sipCpp->name(), sipCpp->dataProvider() ? sipCpp->dataProvider()->name() : QStringLiteral( "Invalid" ) ); sipRes = PyUnicode_FromString( str.toUtf8().constData() ); %End diff --git a/python/core/auto_generated/qgsmaplayerproxymodel.sip.in b/python/core/auto_generated/qgsmaplayerproxymodel.sip.in index 6f07901d899a..cd78381587d9 100644 --- a/python/core/auto_generated/qgsmaplayerproxymodel.sip.in +++ b/python/core/auto_generated/qgsmaplayerproxymodel.sip.in @@ -34,6 +34,7 @@ The QgsMapLayerProxyModel class provides an easy to use model to display the lis PluginLayer, WritableLayer, MeshLayer, + VectorTileLayer, All }; typedef QFlags Filters; diff --git a/python/core/auto_generated/qgstiles.sip.in b/python/core/auto_generated/qgstiles.sip.in new file mode 100644 index 000000000000..befad4c77ee3 --- /dev/null +++ b/python/core/auto_generated/qgstiles.sip.in @@ -0,0 +1,144 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgstiles.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + +class QgsTileXYZ +{ +%Docstring +Stores coordinates of a tile in a tile matrix set. Tile matrix is identified +by the zoomLevel(), and the position within tile matrix is given by column() +and row(). + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgstiles.h" +%End + public: + QgsTileXYZ( int tc = -1, int tr = -1, int tz = -1 ); +%Docstring +Constructs a tile identifier from given column, row and zoom level indices +%End + + int column() const; +%Docstring +Returns tile's column index (X) +%End + int row() const; +%Docstring +Returns tile's row index (Y) +%End + int zoomLevel() const; +%Docstring +Returns tile's zoom level (Z) +%End + + QString toString() const; +%Docstring +Returns tile coordinates in a formatted string +%End + +}; + + +class QgsTileRange +{ +%Docstring +Range of tiles in a tile matrix to be rendered. The selection is rectangular, +given by start/end row and column numbers. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgstiles.h" +%End + public: + QgsTileRange( int c1 = -1, int c2 = -1, int r1 = -1, int r2 = -1 ); +%Docstring +Constructs a range of tiles from given span of columns and rows +%End + bool isValid() const; +%Docstring +Returns whether the range is valid (when all row/column numbers are not negative) +%End + + int startColumn() const; +%Docstring +Returns index of the first column in the range +%End + int endColumn() const; +%Docstring +Returns index of the last column in the range +%End + int startRow() const; +%Docstring +Returns index of the first row in the range +%End + int endRow() const; +%Docstring +Returns index of the last row in the range +%End + +}; + + +class QgsTileMatrix +{ +%Docstring +Defines a matrix of tiles for a single zoom level: it is defined by its size (width * height) +and map extent that it covers. + +Please note that we follow the XYZ convention of X/Y axes, i.e. top-left tile has [0,0] coordinate +(which is different from TMS convention where bottom-left tile has [0,0] coordinate). + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgstiles.h" +%End + public: + + static QgsTileMatrix fromWebMercator( int mZoomLevel ); +%Docstring +Returns a tile matrix for the usual web mercator +%End + + QgsRectangle tileExtent( QgsTileXYZ id ) const; +%Docstring +Returns extent of the given tile in this matrix +%End + + QgsPointXY tileCenter( QgsTileXYZ id ) const; +%Docstring +Returns center of the given tile in this matrix +%End + + QgsTileRange tileRangeFromExtent( const QgsRectangle &mExtent ); +%Docstring +Returns tile range that fully covers the given extent +%End + + QPointF mapToTileCoordinates( const QgsPointXY &mapPoint ) const; +%Docstring +Returns row/column coordinates (floating point number) from the given point in map coordinates +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/qgstiles.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/vectortile/qgsvectortilebasicrenderer.sip.in b/python/core/auto_generated/vectortile/qgsvectortilebasicrenderer.sip.in new file mode 100644 index 000000000000..30b7a0883cc8 --- /dev/null +++ b/python/core/auto_generated/vectortile/qgsvectortilebasicrenderer.sip.in @@ -0,0 +1,198 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilebasicrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsVectorTileBasicRendererStyle +{ +%Docstring +Definition of map rendering of a subset of vector tile data. The subset of data is defined by: +1. sub-layer name +2. geometry type (a single sub-layer may have multiple geometry types) +3. filter expression + +Renering is determined by the associated symbol (QgsSymbol). Symbol has to be of the same +type as the chosen geometryType() - i.e. QgsMarkerSymbol for points, QgsLineSymbol for linestrings +and QgsFillSymbol for polygons. + +It is possible to further constrain when this style is applied by setting a range of allowed +zoom levels, or by disabling it. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsvectortilebasicrenderer.h" +%End + public: + QgsVectorTileBasicRendererStyle( const QString &stName = QString(), const QString &laName = QString(), QgsWkbTypes::GeometryType geomType = QgsWkbTypes::UnknownGeometry ); +%Docstring +Constructs a style object +%End + QgsVectorTileBasicRendererStyle( const QgsVectorTileBasicRendererStyle &other ); +%Docstring +Constructs a style object as a copy of another style +%End + ~QgsVectorTileBasicRendererStyle(); + + void setStyleName( const QString &name ); +%Docstring +Sets human readable name of this style +%End + QString styleName() const; +%Docstring +Returns human readable name of this style +%End + + void setLayerName( const QString &name ); +%Docstring +Sets name of the sub-layer to render (empty layer means that all layers match) +%End + QString layerName() const; +%Docstring +Returns name of the sub-layer to render (empty layer means that all layers match) +%End + + void setGeometryType( QgsWkbTypes::GeometryType geomType ); +%Docstring +Sets type of the geometry that will be used (point / line / polygon) +%End + QgsWkbTypes::GeometryType geometryType() const; +%Docstring +Returns type of the geometry that will be used (point / line / polygon) +%End + + void setFilterExpression( const QString &expr ); +%Docstring +Sets filter expression (empty filter means that all features match) +%End + QString filterExpression() const; +%Docstring +Returns filter expression (empty filter means that all features match) +%End + + void setSymbol( QgsSymbol *sym /Transfer/ ); +%Docstring +Sets symbol for rendering. Takes ownership of the symbol. +%End + QgsSymbol *symbol() const; +%Docstring +Returns symbol for rendering +%End + + void setEnabled( bool enabled ); +%Docstring +Sets whether this style is enabled (used for rendering) +%End + bool isEnabled() const; +%Docstring +Returns whether this style is enabled (used for rendering) +%End + + void setMinZoomLevel( int minZoom ); +%Docstring +Sets minimum zoom level index (negative number means no limit) +%End + int minZoomLevel() const; +%Docstring +Returns minimum zoom level index (negative number means no limit) +%End + + void setMaxZoomLevel( int maxZoom ); +%Docstring +Sets maximum zoom level index (negative number means no limit) +%End + int maxZoomLevel() const; +%Docstring +Returns maxnimum zoom level index (negative number means no limit) +%End + + bool isActive( int zoomLevel ) const; +%Docstring +Returns whether the style is active at given zoom level (also checks "enabled" flag) +%End + + void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const; +%Docstring +Writes object content to given DOM element +%End + void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); +%Docstring +Reads object content from given DOM element +%End + +}; + + +class QgsVectorTileBasicRenderer : QgsVectorTileRenderer +{ +%Docstring +The default vector tile renderer implementation. It has an ordered list of "styles", +each defines a rendering rule. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsvectortilebasicrenderer.h" +%End + public: + QgsVectorTileBasicRenderer(); +%Docstring +Constructs renderer with no styles +%End + + virtual QString type() const; + + virtual QgsVectorTileBasicRenderer *clone() const /Factory/; + + virtual void startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ); + + virtual void stopRender( QgsRenderContext &context ); + + virtual void renderTile( const QgsVectorTileRendererData &tile, QgsRenderContext &context ); + + virtual void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const; + + virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); + + + void setStyles( const QList &styles ); +%Docstring +Sets list of styles of the renderer +%End + QList styles() const; +%Docstring +Returns list of styles of the renderer +%End + + static QList simpleStyle( + const QColor &polygonFillColor, const QColor &polygonStrokeColor, double polygonStrokeWidth, + const QColor &lineStrokeColor, double lineStrokeWidth, + const QColor &pointFillColor, const QColor &pointStrokeColor, double pointSize ); +%Docstring +Returns a list of styles to render all layers with the given fill/stroke colors, stroke widths and marker sizes +%End + + static QList simpleStyleWithRandomColors(); +%Docstring +Returns a list of styles to render all layers, using random colors +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilebasicrenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/vectortile/qgsvectortilelayer.sip.in b/python/core/auto_generated/vectortile/qgsvectortilelayer.sip.in new file mode 100644 index 000000000000..196d732ff912 --- /dev/null +++ b/python/core/auto_generated/vectortile/qgsvectortilelayer.sip.in @@ -0,0 +1,146 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilelayer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + +class QgsVectorTileLayer : QgsMapLayer +{ +%Docstring +Implements a map layer that is dedicated to rendering of vector tiles. +Vector tiles compared to "ordinary" vector layers are pre-processed data +optimized for fast rendering. A dataset is provided with a series of zoom levels +for different map scales. Each zoom level has a matrix of tiles that contain +actual data. A single vector tile may be a a file stored on a local drive, +requested over HTTP request or retrieved from a database. + +Content of a vector tile is divided into one or more named sub-layers. Each such +sub-layer may contain many features which consist of geometry and attributes. +Contrary to traditional vector layers, these sub-layers do not need to have a rigid +schema where geometry type and attributes are the same for all features. A single +sub-layer may have multiple geometry types in a single tile or have some attributes +defined only at particular zoom levels. + +Vector tile layer currently does not use the concept of data providers that other +layer types use. The process of rendering of vector tiles looks like this: + ++--------+ +------+ +---------+ +| DATA | | RAW | | DECODED | +| | --> LOADER --> | | --> DECODER --> | | --> RENDERER +| SOURCE | | TILE | | TILE | ++--------+ +------+ +---------+ + +Data source is a place from where tiles are fetched from (URL for HTTP access, local +files, MBTiles file, GeoPackage file or others. Loader (QgsVectorTileLoader) class +takes care of loading data from the data source. The "raw tile" data is just a blob +(QByteArray) that is encoded in some way. There are multiple ways how vector tiles +are encoded just like there are different formats how to store images. For example, +tiles can be encoded using Mapbox Vector Tiles (MVT) format or in GeoJSON. Decoder +(QgsVectorTileDecoder) takes care of decoding raw tile data into QgsFeature objects. +A decoded tile is essentially an array of vector features for each sub-layer found +in the tile - this is what vector tile renderer (QgsVectorTileRenderer) expects +and does the map rendering. + +To construct a vector tile layer, it is best to use QgsDataSourceUri class and set +the following parameters to get a valid encoded URI: +- "type" - what kind of data source will be used +- "url" - URL or path of the data source (specific to each data source type, see below) + +Currently supported data source types: +- "xyz" - the "url" should be a template like http://example.com/{z}/{x}/{y}.pbf where +{x},{y},{z} will be replaced by tile coordinates +- "mbtiles" - tiles read from a MBTiles file (a SQLite database) + +Currently supported decoders: +- MVT - following Mapbox Vector Tiles specification + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsvectortilelayer.h" +%End + public: + explicit QgsVectorTileLayer( const QString &path = QString(), const QString &baseName = QString() ); +%Docstring +Constructs a new vector tile layer +%End + ~QgsVectorTileLayer(); + + + virtual QgsVectorTileLayer *clone() const /Factory/; + + + virtual QgsMapLayerRenderer *createMapRenderer( QgsRenderContext &rendererContext ) /Factory/; + + virtual bool readXml( const QDomNode &layerNode, QgsReadWriteContext &context ); + + virtual bool writeXml( QDomNode &layerNode, QDomDocument &doc, const QgsReadWriteContext &context ) const; + + virtual bool readSymbology( const QDomNode &node, QString &errorMessage, + QgsReadWriteContext &context, StyleCategories categories = AllStyleCategories ); + + virtual bool writeSymbology( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, + StyleCategories categories = AllStyleCategories ) const; + + virtual void setTransformContext( const QgsCoordinateTransformContext &transformContext ); + + + QString sourceType() const; +%Docstring +Returns type of the data source +%End + QString sourcePath() const; +%Docstring +Returns URL/path of the data source (syntax different to each data source type) +%End + + int sourceMinZoom() const; +%Docstring +Returns minimum zoom level at which source has any valid tiles (negative = unconstrained) +%End + int sourceMaxZoom() const; +%Docstring +Returns maximum zoom level at which source has any valid tiles (negative = unconstrained) +%End + + + void setRenderer( QgsVectorTileRenderer *r /Transfer/ ); +%Docstring +Sets renderer for the map layer. + +.. note:: + + Takes ownership of the passed renderer +%End + QgsVectorTileRenderer *renderer() const; +%Docstring +Returns currently assigned renderer +%End + + void setTileBorderRenderingEnabled( bool enabled ); +%Docstring +Sets whether to render also borders of tiles (useful for debugging) +%End + bool isTileBorderRenderingEnabled() const; +%Docstring +Returns whether to render also borders of tiles (useful for debugging) +%End + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilelayer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/vectortile/qgsvectortilerenderer.sip.in b/python/core/auto_generated/vectortile/qgsvectortilerenderer.sip.in new file mode 100644 index 000000000000..de0b552eca55 --- /dev/null +++ b/python/core/auto_generated/vectortile/qgsvectortilerenderer.sip.in @@ -0,0 +1,133 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilerenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + + + +class QgsVectorTileRendererData +{ +%Docstring +Contains decoded features of a single vector tile and any other data necessary +for rendering of it. + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsvectortilerenderer.h" +%End + public: + explicit QgsVectorTileRendererData( QgsTileXYZ id ); +%Docstring +Constructs the object +%End + + QgsTileXYZ id() const; +%Docstring +Returns coordinates of the tile +%End + + void setTilePolygon( QPolygon polygon ); +%Docstring +Sets polygon of the tile +%End + QPolygon tilePolygon() const; +%Docstring +Returns polygon (made out of four corners of the tile) in screen coordinates calculated from render context +%End + + QStringList layers() const; +%Docstring +Returns list of layer names present in the tile +%End + QVector layerFeatures( const QString &layerName ) const; +%Docstring +Returns list of all features within a single sub-layer +%End + +}; + +class QgsVectorTileRenderer +{ +%Docstring +Abstract base class for all vector tile renderer implementations. + +For rendering it is expected that client code calls: +1. startRender() to prepare renderer +2. renderTile() for each tile +3. stopRender() to clean up renderer and free resources + +.. versionadded:: 3.14 +%End + +%TypeHeaderCode +#include "qgsvectortilerenderer.h" +%End +%ConvertToSubClassCode + + const QString type = sipCpp->type(); + + if ( type == QStringLiteral( "basic" ) ) + sipType = sipType_QgsVectorTileBasicRenderer; + else + sipType = 0; +%End + public: + virtual ~QgsVectorTileRenderer(); + + virtual QString type() const = 0; +%Docstring +Returns unique type name of the renderer implementation +%End + + virtual QgsVectorTileRenderer *clone() const = 0 /Factory/; +%Docstring +Returns a clone of the renderer +%End + + virtual void startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ) = 0; +%Docstring +Initializes rendering. It should be paired with a stopRender() call. +%End + + + virtual void stopRender( QgsRenderContext &context ) = 0; +%Docstring +Finishes rendering and cleans up any resources +%End + + virtual void renderTile( const QgsVectorTileRendererData &tile, QgsRenderContext &context ) = 0; +%Docstring +Renders given vector tile. Must be called between startRender/stopRender. +%End + + virtual void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const = 0; +%Docstring +Writes renderer's properties to given XML element +%End + virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ) = 0; +%Docstring +Reads renderer's properties from given XML element +%End + virtual void resolveReferences( const QgsProject &project ); +%Docstring +Resolves references to other objects - second phase of loading - after readXml() +%End + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/vectortile/qgsvectortilerenderer.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 542b47c86979..8191000c84c8 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -205,6 +205,7 @@ %Include auto_generated/qgstessellator.sip %Include auto_generated/qgstestutils.sip %Include auto_generated/qgstextrenderer.sip +%Include auto_generated/qgstiles.sip %Include auto_generated/qgstolerance.sip %Include auto_generated/qgstracer.sip %Include auto_generated/qgstrackedvectorlayertools.sip @@ -530,6 +531,9 @@ %Include auto_generated/validity/qgsabstractvaliditycheck.sip %Include auto_generated/validity/qgsvaliditycheckcontext.sip %Include auto_generated/validity/qgsvaliditycheckregistry.sip +%Include auto_generated/vectortile/qgsvectortilebasicrenderer.sip +%Include auto_generated/vectortile/qgsvectortilelayer.sip +%Include auto_generated/vectortile/qgsvectortilerenderer.sip %Include auto_generated/gps/qgsqtlocationconnection.sip %Include auto_generated/gps/qgsgpsconnectionregistry.sip %Include auto_generated/symbology/qgsmasksymbollayer.sip diff --git a/python/plugins/db_manager/db_manager_plugin.py b/python/plugins/db_manager/db_manager_plugin.py index 049d4a77e854..5e137a6d7a01 100644 --- a/python/plugins/db_manager/db_manager_plugin.py +++ b/python/plugins/db_manager/db_manager_plugin.py @@ -85,7 +85,7 @@ def unload(self): def onLayerWasAdded(self, aMapLayer): # Be able to update every Db layer from Postgres, Spatialite and Oracle - if hasattr(aMapLayer, 'dataProvider') and aMapLayer.dataProvider().name() in ['postgres', 'spatialite', 'oracle']: + if hasattr(aMapLayer, 'dataProvider') and aMapLayer.dataProvider() and aMapLayer.dataProvider().name() in ['postgres', 'spatialite', 'oracle']: self.iface.addCustomActionForLayer(self.layerAction, aMapLayer) # virtual has QUrl source # url = QUrl(QUrl.fromPercentEncoding(l.source())) diff --git a/scripts/astyle.sh b/scripts/astyle.sh index 7217f9acee60..29565ee2eff5 100755 --- a/scripts/astyle.sh +++ b/scripts/astyle.sh @@ -105,7 +105,7 @@ astyleit() { for f in "$@"; do case "$f" in - src/plugins/grass/qtermwidget/*|external/o2/*|external/qt-unix-signals/*|external/rtree/*|external/astyle/*|external/kdbush/*|external/poly2tri/*|external/wintoast/*|external/qt3dextra-headers/*|external/meshOptimizer/*|python/ext-libs/*|ui_*.py|*.astyle|tests/testdata/*|editors/*) + src/plugins/grass/qtermwidget/*|external/o2/*|external/qt-unix-signals/*|external/rtree/*|external/astyle/*|external/kdbush/*|external/poly2tri/*|external/wintoast/*|external/qt3dextra-headers/*|external/meshOptimizer/*|external/mapbox-vector-tile/*|python/ext-libs/*|ui_*.py|*.astyle|tests/testdata/*|editors/*) echo -ne "$f skipped $elcr" continue ;; diff --git a/src/analysis/processing/qgsalgorithmfilterbygeometry.cpp b/src/analysis/processing/qgsalgorithmfilterbygeometry.cpp index 0bfc7f286c1e..52e8f101ea30 100644 --- a/src/analysis/processing/qgsalgorithmfilterbygeometry.cpp +++ b/src/analysis/processing/qgsalgorithmfilterbygeometry.cpp @@ -303,6 +303,7 @@ QVariantMap QgsFilterByLayerTypeAlgorithm::processAlgorithm( const QVariantMap & case QgsMapLayerType::PluginLayer: case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: break; } diff --git a/src/analysis/processing/qgsalgorithmpackage.cpp b/src/analysis/processing/qgsalgorithmpackage.cpp index f7d214385b4a..1461a494dde8 100644 --- a/src/analysis/processing/qgsalgorithmpackage.cpp +++ b/src/analysis/processing/qgsalgorithmpackage.cpp @@ -177,6 +177,12 @@ QVariantMap QgsPackageAlgorithm::processAlgorithm( const QVariantMap ¶meters feedback->pushDebugInfo( QObject::tr( "Packaging mesh layers is not supported." ) ); errored = true; break; + + case QgsMapLayerType::VectorTileLayer: + //not supported + feedback->pushDebugInfo( QObject::tr( "Packaging vector tile layers is not supported." ) ); + errored = true; + break; } } diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 4796088b48d1..af5ca1a46dbe 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -404,6 +404,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/core/effects ${CMAKE_SOURCE_DIR}/src/core/validity + ${CMAKE_SOURCE_DIR}/src/core/vectortile ${CMAKE_SOURCE_DIR}/src/gui ${CMAKE_SOURCE_DIR}/src/gui/attributeformconfig ${CMAKE_SOURCE_DIR}/src/gui/symbology diff --git a/src/app/browser/qgsinbuiltdataitemproviders.cpp b/src/app/browser/qgsinbuiltdataitemproviders.cpp index de79dfaa8d47..6d6898a26f9c 100644 --- a/src/app/browser/qgsinbuiltdataitemproviders.cpp +++ b/src/app/browser/qgsinbuiltdataitemproviders.cpp @@ -447,6 +447,7 @@ void QgsLayerItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *men case QgsMapLayerType::PluginLayer: case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: break; } } ); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 8a0bbc4afb12..65f3b6f3afb7 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -8325,6 +8325,7 @@ QString QgisApp::saveAsFile( QgsMapLayer *layer, const bool onlySelected, const return saveAsVectorFileGeneral( qobject_cast( layer ), true, onlySelected, defaultToAddToMap ); case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: return QString(); } @@ -14006,6 +14007,10 @@ void QgisApp::activateDeactivateLayerRelatedActions( QgsMapLayer *layer ) mActionIdentify->setEnabled( true ); break; + case QgsMapLayerType::VectorTileLayer: + // TODO + break; + case QgsMapLayerType::PluginLayer: break; @@ -14958,6 +14963,12 @@ void QgisApp::showLayerProperties( QgsMapLayer *mapLayer, const QString &page ) break; } + case QgsMapLayerType::VectorTileLayer: + { + // TODO + break; + } + case QgsMapLayerType::PluginLayer: { QgsPluginLayer *pl = qobject_cast( mapLayer ); diff --git a/src/app/qgsidentifyresultsdialog.cpp b/src/app/qgsidentifyresultsdialog.cpp index 617219d98f7f..1449961c5271 100644 --- a/src/app/qgsidentifyresultsdialog.cpp +++ b/src/app/qgsidentifyresultsdialog.cpp @@ -497,6 +497,10 @@ void QgsIdentifyResultsDialog::addFeature( const QgsMapToolIdentify::IdentifyRes addFeature( qobject_cast( result.mLayer ), result.mLabel, result.mAttributes, result.mDerivedAttributes ); break; + case QgsMapLayerType::VectorTileLayer: + // TODO + break; + case QgsMapLayerType::PluginLayer: break; } diff --git a/src/app/qgslayerstylingwidget.cpp b/src/app/qgslayerstylingwidget.cpp index 1ab0b863bbb1..0653dc46234a 100644 --- a/src/app/qgslayerstylingwidget.cpp +++ b/src/app/qgslayerstylingwidget.cpp @@ -225,6 +225,12 @@ void QgsLayerStylingWidget::setLayer( QgsMapLayer *layer ) break; } + case QgsMapLayerType::VectorTileLayer: + { + // TODO + break; + } + case QgsMapLayerType::PluginLayer: break; } @@ -600,6 +606,12 @@ void QgsLayerStylingWidget::updateCurrentWidgetLayer() break; } + case QgsMapLayerType::VectorTileLayer: + { + // TODO + break; + } + case QgsMapLayerType::PluginLayer: { mStackedWidget->setCurrentIndex( mNotSupportedPage ); @@ -724,6 +736,9 @@ bool QgsLayerStyleManagerWidgetFactory::supportsLayer( QgsMapLayer *layer ) cons case QgsMapLayerType::MeshLayer: return true; + case QgsMapLayerType::VectorTileLayer: + return false; // TODO + case QgsMapLayerType::PluginLayer: return false; } diff --git a/src/app/qgslayertreeviewtemporalindicator.cpp b/src/app/qgslayertreeviewtemporalindicator.cpp index b616de741661..3b975fe40917 100644 --- a/src/app/qgslayertreeviewtemporalindicator.cpp +++ b/src/app/qgslayertreeviewtemporalindicator.cpp @@ -56,6 +56,7 @@ void QgsLayerTreeViewTemporalIndicatorProvider::onIndicatorClicked( const QModel case QgsMapLayerType::VectorLayer: case QgsMapLayerType::MeshLayer: case QgsMapLayerType::PluginLayer: + case QgsMapLayerType::VectorTileLayer: break; } } diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 5b8b552b1daa..eb2c98e50d2e 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -312,6 +312,7 @@ SET(QGIS_CORE_SRCS qgsmapunitscale.cpp qgsmargins.cpp qgsmaskidprovider.cpp + qgsmbtilesreader.cpp qgsmessagelog.cpp qgsmessageoutput.cpp qgsmimedatautils.cpp @@ -395,6 +396,7 @@ SET(QGIS_CORE_SRCS qgstessellator.cpp qgstextrenderer.cpp qgstilecache.cpp + qgstiles.cpp qgstolerance.cpp qgstracer.cpp qgstranslationcontext.cpp @@ -635,6 +637,13 @@ SET(QGIS_CORE_SRCS validity/qgsvaliditycheckcontext.cpp validity/qgsvaliditycheckregistry.cpp + vectortile/qgsvectortilebasicrenderer.cpp + vectortile/qgsvectortilelayer.cpp + vectortile/qgsvectortilelayerrenderer.cpp + vectortile/qgsvectortileloader.cpp + vectortile/qgsvectortilemvtdecoder.cpp + vectortile/qgsvectortileutils.cpp + ${CMAKE_CURRENT_BINARY_DIR}/qgsexpression_texts.cpp qgsuserprofile.cpp @@ -833,6 +842,7 @@ SET(QGIS_CORE_HDRS qgsmapunitscale.h qgsmargins.h qgsmaskidprovider.h + qgsmbtilesreader.h qgsmessagelog.h qgsmessageoutput.h qgsmimedatautils.h @@ -924,6 +934,7 @@ SET(QGIS_CORE_HDRS qgstextrenderer.h qgsthreadingutils.h qgstilecache.h + qgstiles.h qgstolerance.h qgstracer.h qgstrackedvectorlayertools.h @@ -1319,6 +1330,14 @@ SET(QGIS_CORE_HDRS validity/qgsabstractvaliditycheck.h validity/qgsvaliditycheckcontext.h validity/qgsvaliditycheckregistry.h + + vectortile/qgsvectortilebasicrenderer.h + vectortile/qgsvectortilelayer.h + vectortile/qgsvectortilelayerrenderer.h + vectortile/qgsvectortileloader.h + vectortile/qgsvectortilemvtdecoder.h + vectortile/qgsvectortilerenderer.h + vectortile/qgsvectortileutils.h ) SET(QGIS_CORE_PRIVATE_HDRS @@ -1362,6 +1381,18 @@ IF(NOT MSVC) ENDIF () ENDIF(NOT MSVC) +# Generate cpp+header file from .proto file using "protoc" tool (to support MVT encoding of vector tiles) +protobuf_generate_cpp(VECTOR_TILE_PROTO_SRCS VECTOR_TILE_PROTO_HDRS vectortile/vector_tile.proto) +SET(QGIS_CORE_SRCS ${QGIS_CORE_SRCS} ${VECTOR_TILE_PROTO_SRCS}) +SET(QGIS_CORE_HDRS ${QGIS_CORE_HDRS} ${VECTOR_TILE_PROTO_HDRS}) +IF (MSVC) + SET_SOURCE_FILES_PROPERTIES(${VECTOR_TILE_PROTO_SRCS} vectortile/qgsvectortilemvtdecoder.cpp PROPERTIES COMPILE_DEFINITIONS PROTOBUF_USE_DLLS) +ELSE (MSVC) + # automatically generated file produces warnings (unused-parameter, unused-variable, misleading-indentation) + SET_SOURCE_FILES_PROPERTIES(${VECTOR_TILE_PROTO_SRCS} PROPERTIES COMPILE_FLAGS -w) +ENDIF (MSVC) + + # install headers # install qgsconfig.h and plugin.h here so they can get into # the OS X framework target @@ -1408,6 +1439,7 @@ INCLUDE_DIRECTORIES( symbology mesh validity + vectortile ${CMAKE_SOURCE_DIR}/external ${CMAKE_SOURCE_DIR}/external/nlohmann ${CMAKE_SOURCE_DIR}/external/kdbush/include @@ -1415,6 +1447,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/external/poly2tri ${CMAKE_SOURCE_DIR}/external/rtree/include ${CMAKE_SOURCE_DIR}/external/meshOptimizer + ${CMAKE_SOURCE_DIR}/external/mapbox-vector-tile ) INCLUDE_DIRECTORIES(SYSTEM @@ -1429,6 +1462,8 @@ INCLUDE_DIRECTORIES(SYSTEM ${QCA_INCLUDE_DIR} ${QTKEYCHAIN_INCLUDE_DIR} ${Qt5SerialPort_INCLUDE_DIRS} + ${Protobuf_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIRS} ) @@ -1563,6 +1598,8 @@ TARGET_LINK_LIBRARIES(qgis_core ${SQLITE3_LIBRARY} ${SPATIALITE_LIBRARY} ${LIBZIP_LIBRARY} + ${Protobuf_LITE_LIBRARY} + ${ZLIB_LIBRARIES} ) IF (FORCE_STATIC_PROVIDERS) diff --git a/src/core/expression/qgsexpressionfunction.cpp b/src/core/expression/qgsexpressionfunction.cpp index 49f044d9237a..6930b3bdef1e 100644 --- a/src/core/expression/qgsexpressionfunction.cpp +++ b/src/core/expression/qgsexpressionfunction.cpp @@ -4697,6 +4697,8 @@ static QVariant fcnGetLayerProperty( const QVariantList &values, const QgsExpres return QCoreApplication::translate( "expressions", "Raster" ); case QgsMapLayerType::MeshLayer: return QCoreApplication::translate( "expressions", "Mesh" ); + case QgsMapLayerType::VectorTileLayer: + return QCoreApplication::translate( "expressions", "Vector Tile" ); case QgsMapLayerType::PluginLayer: return QCoreApplication::translate( "expressions", "Plugin" ); } diff --git a/src/core/layertree/qgslayertreemodel.cpp b/src/core/layertree/qgslayertreemodel.cpp index 84dfaeb122af..909078f1f116 100644 --- a/src/core/layertree/qgslayertreemodel.cpp +++ b/src/core/layertree/qgslayertreemodel.cpp @@ -203,6 +203,9 @@ QVariant QgsLayerTreeModel::data( const QModelIndex &index, int role ) const case QgsMapLayerType::MeshLayer: return QgsLayerItem::iconMesh(); + case QgsMapLayerType::VectorTileLayer: + return QgsLayerItem::iconVectorTile(); + case QgsMapLayerType::VectorLayer: case QgsMapLayerType::PluginLayer: break; diff --git a/src/core/processing/qgsprocessingutils.cpp b/src/core/processing/qgsprocessingutils.cpp index fe7056ad88c0..53b5ada59908 100644 --- a/src/core/processing/qgsprocessingutils.cpp +++ b/src/core/processing/qgsprocessingutils.cpp @@ -33,6 +33,7 @@ #include "qgsmeshlayer.h" #include "qgsreferencedgeometry.h" #include "qgsrasterfilewriter.h" +#include "qgsvectortilelayer.h" QList QgsProcessingUtils::compatibleRasterLayers( QgsProject *project, bool sort ) { @@ -152,6 +153,8 @@ QgsMapLayer *QgsProcessingUtils::mapLayerFromStore( const QString &string, QgsMa return true; case QgsMapLayerType::MeshLayer: return !canUseLayer( qobject_cast< QgsMeshLayer * >( layer ) ); + case QgsMapLayerType::VectorTileLayer: + return !canUseLayer( qobject_cast< QgsVectorTileLayer * >( layer ) ); } return true; } ), layers.end() ); @@ -404,6 +407,11 @@ bool QgsProcessingUtils::canUseLayer( const QgsMeshLayer *layer ) return layer && layer->dataProvider(); } +bool QgsProcessingUtils::canUseLayer( const QgsVectorTileLayer *layer ) +{ + return layer && layer->isValid(); +} + bool QgsProcessingUtils::canUseLayer( const QgsRasterLayer *layer ) { return layer && layer->isValid(); diff --git a/src/core/processing/qgsprocessingutils.h b/src/core/processing/qgsprocessingutils.h index e8eb2a09a81c..f1ac1d8406cf 100644 --- a/src/core/processing/qgsprocessingutils.h +++ b/src/core/processing/qgsprocessingutils.h @@ -34,6 +34,7 @@ class QgsMapLayerStore; class QgsProcessingFeedback; class QgsProcessingFeatureSource; class QgsProcessingAlgorithm; +class QgsVectorTileLayer; #include #include @@ -383,6 +384,7 @@ class CORE_EXPORT QgsProcessingUtils private: static bool canUseLayer( const QgsRasterLayer *layer ); static bool canUseLayer( const QgsMeshLayer *layer ); + static bool canUseLayer( const QgsVectorTileLayer *layer ); static bool canUseLayer( const QgsVectorLayer *layer, const QList< int > &sourceTypes = QList< int >() ); diff --git a/src/core/qgsdataitem.cpp b/src/core/qgsdataitem.cpp index 8e4b80c5f3b4..85d3de6394a9 100644 --- a/src/core/qgsdataitem.cpp +++ b/src/core/qgsdataitem.cpp @@ -80,6 +80,11 @@ QIcon QgsLayerItem::iconMesh() return QgsApplication::getThemeIcon( QStringLiteral( "/mIconMeshLayer.svg" ) ); } +QIcon QgsLayerItem::iconVectorTile() +{ + return QgsApplication::getThemeIcon( QStringLiteral( "/mIconVectorTileLayer.svg" ) ); +} + QIcon QgsLayerItem::iconDefault() { return QgsApplication::getThemeIcon( QStringLiteral( "/mIconLayer.png" ) ); @@ -643,6 +648,9 @@ QgsMapLayerType QgsLayerItem::mapLayerType() const case QgsLayerItem::Mesh: return QgsMapLayerType::MeshLayer; + case QgsLayerItem::VectorTile: + return QgsMapLayerType::VectorTileLayer; + case QgsLayerItem::Plugin: return QgsMapLayerType::PluginLayer; @@ -693,6 +701,8 @@ QgsLayerItem::LayerType QgsLayerItem::typeFromMapLayer( QgsMapLayer *layer ) return Plugin; case QgsMapLayerType::MeshLayer: return Mesh; + case QgsMapLayerType::VectorTileLayer: + return VectorTile; } return Vector; // no warnings } @@ -778,6 +788,7 @@ QgsMimeDataUtils::Uri QgsLayerItem::mimeUri() const case Raster: case Plugin: case Mesh: + case VectorTile: break; } break; @@ -787,6 +798,9 @@ QgsMimeDataUtils::Uri QgsLayerItem::mimeUri() const case QgsMapLayerType::MeshLayer: u.layerType = QStringLiteral( "mesh" ); break; + case QgsMapLayerType::VectorTileLayer: + u.layerType = QStringLiteral( "vector-tile" ); + break; case QgsMapLayerType::PluginLayer: u.layerType = QStringLiteral( "plugin" ); break; diff --git a/src/core/qgsdataitem.h b/src/core/qgsdataitem.h index 637f8d3cb1c3..e697ecfdf151 100644 --- a/src/core/qgsdataitem.h +++ b/src/core/qgsdataitem.h @@ -506,7 +506,8 @@ class CORE_EXPORT QgsLayerItem : public QgsDataItem Database, Table, Plugin, //!< Added in 2.10 - Mesh //!< Added in 3.2 + Mesh, //!< Added in 3.2 + VectorTile //!< Added in 3.14 }; Q_ENUM( LayerType ) @@ -595,6 +596,8 @@ class CORE_EXPORT QgsLayerItem : public QgsDataItem static QIcon iconDefault(); //! Returns icon for mesh layer type static QIcon iconMesh(); + //! Returns icon for vector tile layer + static QIcon iconVectorTile(); //! \returns the layer name virtual QString layerName() const { return name(); } diff --git a/src/core/qgsmaplayer.h b/src/core/qgsmaplayer.h index 90f65cc83e0d..b19cab3a817f 100644 --- a/src/core/qgsmaplayer.h +++ b/src/core/qgsmaplayer.h @@ -69,7 +69,8 @@ enum class QgsMapLayerType SIP_MONKEYPATCH_SCOPEENUM_UNNEST( QgsMapLayer, LayerT VectorLayer, RasterLayer, PluginLayer, - MeshLayer //!< Added in 3.2 + MeshLayer, //!< Added in 3.2 + VectorTileLayer //!< Added in 3.14 }; /** @@ -108,6 +109,9 @@ class CORE_EXPORT QgsMapLayer : public QObject case QgsMapLayerType::MeshLayer: sipType = sipType_QgsMeshLayer; break; + case QgsMapLayerType::VectorTileLayer: + sipType = sipType_QgsVectorTileLayer; + break; default: sipType = nullptr; break; @@ -1273,7 +1277,7 @@ class CORE_EXPORT QgsMapLayer : public QObject #ifdef SIP_RUN SIP_PYOBJECT __repr__(); % MethodCode - QString str = QStringLiteral( "" ).arg( sipCpp->name(), sipCpp->dataProvider()->name() ); + QString str = QStringLiteral( "" ).arg( sipCpp->name(), sipCpp->dataProvider() ? sipCpp->dataProvider()->name() : QStringLiteral( "Invalid" ) ); sipRes = PyUnicode_FromString( str.toUtf8().constData() ); % End #endif diff --git a/src/core/qgsmaplayermodel.cpp b/src/core/qgsmaplayermodel.cpp index a4e2c5a78f07..fc51b44514ef 100644 --- a/src/core/qgsmaplayermodel.cpp +++ b/src/core/qgsmaplayermodel.cpp @@ -367,6 +367,11 @@ QIcon QgsMapLayerModel::iconForLayer( QgsMapLayer *layer ) return QgsLayerItem::iconMesh(); } + case QgsMapLayerType::VectorTileLayer: + { + return QgsLayerItem::iconVectorTile(); + } + case QgsMapLayerType::VectorLayer: { QgsVectorLayer *vl = qobject_cast( layer ); diff --git a/src/core/qgsmaplayerproxymodel.cpp b/src/core/qgsmaplayerproxymodel.cpp index 4dd20727fa97..485f61695d9f 100644 --- a/src/core/qgsmaplayerproxymodel.cpp +++ b/src/core/qgsmaplayerproxymodel.cpp @@ -116,6 +116,7 @@ bool QgsMapLayerProxyModel::acceptsLayer( QgsMapLayer *layer ) const if ( ( mFilters.testFlag( RasterLayer ) && layer->type() == QgsMapLayerType::RasterLayer ) || ( mFilters.testFlag( VectorLayer ) && layer->type() == QgsMapLayerType::VectorLayer ) || ( mFilters.testFlag( MeshLayer ) && layer->type() == QgsMapLayerType::MeshLayer ) || + ( mFilters.testFlag( VectorTileLayer ) && layer->type() == QgsMapLayerType::VectorTileLayer ) || ( mFilters.testFlag( PluginLayer ) && layer->type() == QgsMapLayerType::PluginLayer ) ) return true; diff --git a/src/core/qgsmaplayerproxymodel.h b/src/core/qgsmaplayerproxymodel.h index 2383bd9a94f0..8261dc911f02 100644 --- a/src/core/qgsmaplayerproxymodel.h +++ b/src/core/qgsmaplayerproxymodel.h @@ -51,7 +51,8 @@ class CORE_EXPORT QgsMapLayerProxyModel : public QSortFilterProxyModel PluginLayer = 32, WritableLayer = 64, MeshLayer = 128, //!< QgsMeshLayer \since QGIS 3.6 - All = RasterLayer | VectorLayer | PluginLayer | MeshLayer + VectorTileLayer = 256, //!< QgsVectorTileLayer \since QGIS 3.14 + All = RasterLayer | VectorLayer | PluginLayer | MeshLayer | VectorTileLayer }; Q_DECLARE_FLAGS( Filters, Filter ) Q_FLAG( Filters ) diff --git a/src/core/qgsmaprendererjob.cpp b/src/core/qgsmaprendererjob.cpp index 68c951a3beed..05969df628ae 100644 --- a/src/core/qgsmaprendererjob.cpp +++ b/src/core/qgsmaprendererjob.cpp @@ -906,6 +906,7 @@ bool QgsMapRendererJob::needTemporaryImage( QgsMapLayer *ml ) } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } diff --git a/src/providers/wms/qgsmbtilesreader.cpp b/src/core/qgsmbtilesreader.cpp similarity index 100% rename from src/providers/wms/qgsmbtilesreader.cpp rename to src/core/qgsmbtilesreader.cpp diff --git a/src/providers/wms/qgsmbtilesreader.h b/src/core/qgsmbtilesreader.h similarity index 69% rename from src/providers/wms/qgsmbtilesreader.h rename to src/core/qgsmbtilesreader.h index 279e89a6e3e9..a08ded825d9c 100644 --- a/src/providers/wms/qgsmbtilesreader.h +++ b/src/core/qgsmbtilesreader.h @@ -16,29 +16,44 @@ #ifndef QGSMBTILESREADER_H #define QGSMBTILESREADER_H +#include "qgis_core.h" #include "sqlite3.h" #include "qgssqliteutils.h" +#define SIP_NO_FILE + class QImage; class QgsRectangle; -class QgsMBTilesReader +/** + * \ingroup core + * Utility class for reading MBTiles files (which are SQLite3 databases). + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsMBTilesReader { public: + //! Contructs MBTiles reader (but it does not open the file yet) explicit QgsMBTilesReader( const QString &filename ); + //! Tries to open the file, returns true on success bool open(); + //! Returns whether the MBTiles file is currently opened bool isOpen() const; + //! Requests metadata value for the given key QString metadataValue( const QString &key ); - //! given in WGS 84 (if available) + //! Returns bounding box from metadata, given in WGS 84 (if available) QgsRectangle extent(); + //! Returns raw tile data for given tile QByteArray tileData( int z, int x, int y ); + //! Returns tile decoded as a raster image (if stored in a known format like JPG or PNG) QImage tileDataAsImage( int z, int x, int y ); private: diff --git a/src/core/qgsmimedatautils.cpp b/src/core/qgsmimedatautils.cpp index 58fea21faebd..08fba7866d2f 100644 --- a/src/core/qgsmimedatautils.cpp +++ b/src/core/qgsmimedatautils.cpp @@ -91,6 +91,12 @@ QgsMimeDataUtils::Uri::Uri( QgsMapLayer *layer ) break; } + case QgsMapLayerType::VectorTileLayer: + { + layerType = QStringLiteral( "vector-tile" ); + break; + } + case QgsMapLayerType::PluginLayer: { // plugin layers do not have a standard way of storing their URI... diff --git a/src/core/qgsproject.cpp b/src/core/qgsproject.cpp index 99d10c5b336c..415319c89e80 100644 --- a/src/core/qgsproject.cpp +++ b/src/core/qgsproject.cpp @@ -60,6 +60,7 @@ #include "qgsprojectviewsettings.h" #include "qgsprojectdisplaysettings.h" #include "qgsprojecttimesettings.h" +#include "qgsvectortilelayer.h" #include #include @@ -1062,6 +1063,10 @@ bool QgsProject::addLayer( const QDomElement &layerElem, QList &broken { mapLayer = qgis::make_unique(); } + else if ( type == QLatin1String( "vector-tile" ) ) + { + mapLayer = qgis::make_unique(); + } else if ( type == QLatin1String( "plugin" ) ) { QString typeName = layerElem.attribute( QStringLiteral( "name" ) ); diff --git a/src/core/qgstiles.cpp b/src/core/qgstiles.cpp new file mode 100644 index 000000000000..3beade14fca8 --- /dev/null +++ b/src/core/qgstiles.cpp @@ -0,0 +1,83 @@ +/*************************************************************************** + qgstiles.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstiles.h" + +#include "qgslogger.h" + +QgsTileMatrix QgsTileMatrix::fromWebMercator( int zoomLevel ) +{ + int numTiles = static_cast( pow( 2, zoomLevel ) ); // assuming we won't ever go over 30 zoom levels + double z0xMin = -20037508.3427892, z0yMin = -20037508.3427892; + double z0xMax = 20037508.3427892, z0yMax = 20037508.3427892; + double s0 = 559082264.0287178; // scale denominator at zoom level 0 of GoogleCRS84Quad + + QgsTileMatrix tm; + tm.mZoomLevel = zoomLevel; + tm.mMatrixWidth = numTiles; + tm.mMatrixHeight = numTiles; + tm.mTileXSpan = ( z0xMax - z0xMin ) / tm.mMatrixWidth; + tm.mTileYSpan = ( z0yMax - z0yMin ) / tm.mMatrixHeight; + tm.mExtent = QgsRectangle( z0xMin, z0yMin, z0xMax, z0yMax ); + tm.mScaleDenom = s0 / pow( 2, zoomLevel ); + return tm; +} + +QgsRectangle QgsTileMatrix::tileExtent( QgsTileXYZ id ) const +{ + double xMin = mExtent.xMinimum() + mTileXSpan * id.column(); + double xMax = xMin + mTileXSpan; + double yMax = mExtent.yMaximum() - mTileYSpan * id.row(); + double yMin = yMax - mTileYSpan; + return QgsRectangle( xMin, yMin, xMax, yMax ); +} + +QgsPointXY QgsTileMatrix::tileCenter( QgsTileXYZ id ) const +{ + double x = mExtent.xMinimum() + mTileXSpan / 2 * id.column(); + double y = mExtent.yMaximum() - mTileYSpan / 2 * id.row(); + return QgsPointXY( x, y ); +} + +QgsTileRange QgsTileMatrix::tileRangeFromExtent( const QgsRectangle &r ) +{ + double x0 = qBound( mExtent.xMinimum(), r.xMinimum(), mExtent.xMaximum() ); + double y0 = qBound( mExtent.yMinimum(), r.yMinimum(), mExtent.yMaximum() ); + double x1 = qBound( mExtent.xMinimum(), r.xMaximum(), mExtent.xMaximum() ); + double y1 = qBound( mExtent.yMinimum(), r.yMaximum(), mExtent.yMaximum() ); + if ( x0 >= x1 || y0 >= y1 ) + return QgsTileRange(); // nothing to display + + double tileX1 = ( x0 - mExtent.xMinimum() ) / mTileXSpan; + double tileX2 = ( x1 - mExtent.xMinimum() ) / mTileXSpan; + double tileY1 = ( mExtent.yMaximum() - y1 ) / mTileYSpan; + double tileY2 = ( mExtent.yMaximum() - y0 ) / mTileYSpan; + + QgsDebugMsgLevel( QStringLiteral( "Tile range of edges [%1,%2] - [%3,%4]" ).arg( tileX1 ).arg( tileY1 ).arg( tileX2 ).arg( tileY2 ), 2 ); + + // figure out tile range from zoom + int startColumn = qBound( 0, static_cast( floor( tileX1 ) ), mMatrixWidth - 1 ); + int endColumn = qBound( 0, static_cast( floor( tileX2 ) ), mMatrixWidth - 1 ); + int startRow = qBound( 0, static_cast( floor( tileY1 ) ), mMatrixHeight - 1 ); + int endRow = qBound( 0, static_cast( floor( tileY2 ) ), mMatrixHeight - 1 ); + return QgsTileRange( startColumn, endColumn, startRow, endRow ); +} + +QPointF QgsTileMatrix::mapToTileCoordinates( const QgsPointXY &mapPoint ) const +{ + double dx = mapPoint.x() - mExtent.xMinimum(); + double dy = mExtent.yMaximum() - mapPoint.y(); + return QPointF( dx / mTileXSpan, dy / mTileYSpan ); +} diff --git a/src/core/qgstiles.h b/src/core/qgstiles.h new file mode 100644 index 000000000000..86641487ad38 --- /dev/null +++ b/src/core/qgstiles.h @@ -0,0 +1,138 @@ +/*************************************************************************** + qgstiles.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSTILES_H +#define QGSTILES_H + +#include "qgis_core.h" +#include "qgis_sip.h" + +#include "qgsrectangle.h" + +/** + * \ingroup core + * Stores coordinates of a tile in a tile matrix set. Tile matrix is identified + * by the zoomLevel(), and the position within tile matrix is given by column() + * and row(). + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsTileXYZ +{ + public: + //! Constructs a tile identifier from given column, row and zoom level indices + QgsTileXYZ( int tc = -1, int tr = -1, int tz = -1 ) + : mColumn( tc ), mRow( tr ), mZoomLevel( tz ) + { + } + + //! Returns tile's column index (X) + int column() const { return mColumn; } + //! Returns tile's row index (Y) + int row() const { return mRow; } + //! Returns tile's zoom level (Z) + int zoomLevel() const { return mZoomLevel; } + + //! Returns tile coordinates in a formatted string + QString toString() const { return QStringLiteral( "X=%1 Y=%2 Z=%3" ).arg( mColumn ).arg( mRow ).arg( mZoomLevel ); } + + private: + int mColumn; + int mRow; + int mZoomLevel; +}; + + +/** + * \ingroup core + * Range of tiles in a tile matrix to be rendered. The selection is rectangular, + * given by start/end row and column numbers. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsTileRange +{ + public: + //! Constructs a range of tiles from given span of columns and rows + QgsTileRange( int c1 = -1, int c2 = -1, int r1 = -1, int r2 = -1 ) + : mStartColumn( c1 ), mEndColumn( c2 ), mStartRow( r1 ), mEndRow( r2 ) {} + + //! Returns whether the range is valid (when all row/column numbers are not negative) + bool isValid() const { return mStartColumn >= 0 && mEndColumn >= 0 && mStartRow >= 0 && mEndRow >= 0; } + + //! Returns index of the first column in the range + int startColumn() const { return mStartColumn; } + //! Returns index of the last column in the range + int endColumn() const { return mEndColumn; } + //! Returns index of the first row in the range + int startRow() const { return mStartRow; } + //! Returns index of the last row in the range + int endRow() const { return mEndRow; } + + private: + int mStartColumn; + int mEndColumn; + int mStartRow; + int mEndRow; +}; + + +/** + * \ingroup core + * Defines a matrix of tiles for a single zoom level: it is defined by its size (width * height) + * and map extent that it covers. + * + * Please note that we follow the XYZ convention of X/Y axes, i.e. top-left tile has [0,0] coordinate + * (which is different from TMS convention where bottom-left tile has [0,0] coordinate). + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsTileMatrix +{ + public: + + //! Returns a tile matrix for the usual web mercator + static QgsTileMatrix fromWebMercator( int mZoomLevel ); + + //! Returns extent of the given tile in this matrix + QgsRectangle tileExtent( QgsTileXYZ id ) const; + + //! Returns center of the given tile in this matrix + QgsPointXY tileCenter( QgsTileXYZ id ) const; + + //! Returns tile range that fully covers the given extent + QgsTileRange tileRangeFromExtent( const QgsRectangle &mExtent ); + + //! Returns row/column coordinates (floating point number) from the given point in map coordinates + QPointF mapToTileCoordinates( const QgsPointXY &mapPoint ) const; + + private: + //! Zoom level index associated with the tile matrix + int mZoomLevel; + //! Number of columns of the tile matrix + int mMatrixWidth; + //! Number of rows of the tile matrix + int mMatrixHeight; + //! Matrix extent in map units in the CRS of tile matrix set + QgsRectangle mExtent; + //! Scale denominator of the map scale associated with the tile matrix + double mScaleDenom; + //! Width of a single tile in map units (derived from extent and matrix size) + double mTileXSpan; + //! Height of a single tile in map units (derived from extent and matrix size) + double mTileYSpan; +}; + +#endif // QGSTILES_H diff --git a/src/core/vectortile/qgsvectortilebasicrenderer.cpp b/src/core/vectortile/qgsvectortilebasicrenderer.cpp new file mode 100644 index 000000000000..14bc8d7851d1 --- /dev/null +++ b/src/core/vectortile/qgsvectortilebasicrenderer.cpp @@ -0,0 +1,289 @@ +/*************************************************************************** + qgsvectortilebasicrenderer.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsvectortilebasicrenderer.h" + +#include "qgsapplication.h" +#include "qgscolorschemeregistry.h" +#include "qgsexpressioncontextutils.h" +#include "qgsfillsymbollayer.h" +#include "qgslinesymbollayer.h" +#include "qgsmarkersymbollayer.h" +#include "qgssymbollayerutils.h" +#include "qgsvectortileutils.h" + + +QgsVectorTileBasicRendererStyle::QgsVectorTileBasicRendererStyle( const QString &stName, const QString &laName, QgsWkbTypes::GeometryType geomType ) + : mStyleName( stName ) + , mLayerName( laName ) + , mGeometryType( geomType ) +{ +} + +QgsVectorTileBasicRendererStyle::QgsVectorTileBasicRendererStyle( const QgsVectorTileBasicRendererStyle &other ) +{ + operator=( other ); +} + +QgsVectorTileBasicRendererStyle &QgsVectorTileBasicRendererStyle::operator=( const QgsVectorTileBasicRendererStyle &other ) +{ + mStyleName = other.mStyleName; + mLayerName = other.mLayerName; + mGeometryType = other.mGeometryType; + mSymbol.reset( other.mSymbol ? other.mSymbol->clone() : nullptr ); + mEnabled = other.mEnabled; + mExpression = other.mExpression; + mMinZoomLevel = other.mMinZoomLevel; + mMaxZoomLevel = other.mMaxZoomLevel; + return *this; +} + +QgsVectorTileBasicRendererStyle::~QgsVectorTileBasicRendererStyle() = default; + +void QgsVectorTileBasicRendererStyle::setSymbol( QgsSymbol *sym ) +{ + mSymbol.reset( sym ); +} + +void QgsVectorTileBasicRendererStyle::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const +{ + elem.setAttribute( QStringLiteral( "name" ), mStyleName ); + elem.setAttribute( QStringLiteral( "layer" ), mLayerName ); + elem.setAttribute( QStringLiteral( "geometry" ), mGeometryType ); + elem.setAttribute( QStringLiteral( "enabled" ), mEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + elem.setAttribute( QStringLiteral( "expression" ), mExpression ); + elem.setAttribute( QStringLiteral( "min-zoom" ), mMinZoomLevel ); + elem.setAttribute( QStringLiteral( "max-zoom" ), mMaxZoomLevel ); + + QDomDocument doc = elem.ownerDocument(); + QgsSymbolMap symbols; + symbols[QStringLiteral( "0" )] = mSymbol.get(); + QDomElement symbolsElem = QgsSymbolLayerUtils::saveSymbols( symbols, QStringLiteral( "symbols" ), doc, context ); + elem.appendChild( symbolsElem ); +} + +void QgsVectorTileBasicRendererStyle::readXml( const QDomElement &elem, const QgsReadWriteContext &context ) +{ + mStyleName = elem.attribute( QStringLiteral( "name" ) ); + mLayerName = elem.attribute( QStringLiteral( "layer" ) ); + mGeometryType = static_cast( elem.attribute( QStringLiteral( "geometry" ) ).toInt() ); + mEnabled = elem.attribute( QStringLiteral( "enabled" ) ).toInt(); + mExpression = elem.attribute( QStringLiteral( "expression" ) ); + mMinZoomLevel = elem.attribute( QStringLiteral( "min-zoom" ) ).toInt(); + mMaxZoomLevel = elem.attribute( QStringLiteral( "max-zoom" ) ).toInt(); + + mSymbol.reset(); + QDomElement symbolsElem = elem.firstChildElement( QStringLiteral( "symbols" ) ); + if ( !symbolsElem.isNull() ) + { + QgsSymbolMap symbolMap = QgsSymbolLayerUtils::loadSymbols( symbolsElem, context ); + if ( !symbolMap.contains( QStringLiteral( "0" ) ) ) + { + mSymbol.reset( symbolMap.take( QStringLiteral( "0" ) ) ); + } + } +} + +//////// + + +QgsVectorTileBasicRenderer::QgsVectorTileBasicRenderer() +{ +} + +QString QgsVectorTileBasicRenderer::type() const +{ + return QStringLiteral( "basic" ); +} + +QgsVectorTileBasicRenderer *QgsVectorTileBasicRenderer::clone() const +{ + QgsVectorTileBasicRenderer *r = new QgsVectorTileBasicRenderer; + r->mStyles = mStyles; + r->mStyles.detach(); // make a deep copy to make sure symbols get cloned + return r; +} + +void QgsVectorTileBasicRenderer::startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ) +{ + Q_UNUSED( context ) + Q_UNUSED( tileRange ) + // figure out required fields for different layers + for ( const QgsVectorTileBasicRendererStyle &layerStyle : qgis::as_const( mStyles ) ) + { + if ( layerStyle.isActive( tileZoom ) && !layerStyle.filterExpression().isEmpty() ) + { + QgsExpression expr( layerStyle.filterExpression() ); + mRequiredFields[layerStyle.layerName()].unite( expr.referencedColumns() ); + } + } +} + +QMap > QgsVectorTileBasicRenderer::usedAttributes( const QgsRenderContext & ) +{ + return mRequiredFields; +} + +void QgsVectorTileBasicRenderer::stopRender( QgsRenderContext &context ) +{ + Q_UNUSED( context ) +} + +void QgsVectorTileBasicRenderer::renderTile( const QgsVectorTileRendererData &tile, QgsRenderContext &context ) +{ + const QgsVectorTileFeatures tileData = tile.features(); + int zoomLevel = tile.id().zoomLevel(); + + for ( const QgsVectorTileBasicRendererStyle &layerStyle : qgis::as_const( mStyles ) ) + { + if ( !layerStyle.isActive( zoomLevel ) ) + continue; + + QgsFields fields = QgsVectorTileUtils::makeQgisFields( mRequiredFields[layerStyle.layerName()] ); + + QgsExpressionContextScope *scope = new QgsExpressionContextScope( QObject::tr( "Layer" ) ); // will be deleted by popper + scope->setFields( fields ); + QgsExpressionContextScopePopper popper( context.expressionContext(), scope ); + + QgsExpression filterExpression( layerStyle.filterExpression() ); + filterExpression.prepare( &context.expressionContext() ); + + QgsSymbol *sym = layerStyle.symbol(); + sym->startRender( context, QgsFields() ); + if ( layerStyle.layerName().isEmpty() ) + { + // matching all layers + for ( QString layerName : tileData.keys() ) + { + for ( const QgsFeature &f : tileData[layerName] ) + { + scope->setFeature( f ); + if ( filterExpression.isValid() && !filterExpression.evaluate( &context.expressionContext() ).toBool() ) + continue; + + if ( QgsWkbTypes::geometryType( f.geometry().wkbType() ) == layerStyle.geometryType() ) + sym->renderFeature( f, context ); + } + } + } + else if ( tileData.contains( layerStyle.layerName() ) ) + { + // matching one particular layer + for ( const QgsFeature &f : tileData[layerStyle.layerName()] ) + { + scope->setFeature( f ); + if ( filterExpression.isValid() && !filterExpression.evaluate( &context.expressionContext() ).toBool() ) + continue; + + if ( QgsWkbTypes::geometryType( f.geometry().wkbType() ) == layerStyle.geometryType() ) + sym->renderFeature( f, context ); + } + } + sym->stopRender( context ); + } +} + +void QgsVectorTileBasicRenderer::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const +{ + QDomDocument doc = elem.ownerDocument(); + QDomElement elemStyles = doc.createElement( QStringLiteral( "styles" ) ); + for ( const QgsVectorTileBasicRendererStyle &layerStyle : mStyles ) + { + QDomElement elemStyle = doc.createElement( QStringLiteral( "style" ) ); + layerStyle.writeXml( elemStyle, context ); + elemStyles.appendChild( elemStyle ); + } + elem.appendChild( elemStyles ); +} + +void QgsVectorTileBasicRenderer::readXml( const QDomElement &elem, const QgsReadWriteContext &context ) +{ + mStyles.clear(); + + QDomElement elemStyles = elem.firstChildElement( QStringLiteral( "styles" ) ); + QDomElement elemStyle = elemStyles.firstChildElement( QStringLiteral( "style" ) ); + while ( !elemStyle.isNull() ) + { + QgsVectorTileBasicRendererStyle layerStyle; + layerStyle.readXml( elemStyle, context ); + mStyles.append( layerStyle ); + } +} + +void QgsVectorTileBasicRenderer::setStyles( const QList &styles ) +{ + mStyles = styles; +} + +QList QgsVectorTileBasicRenderer::styles() const +{ + return mStyles; +} + +QList QgsVectorTileBasicRenderer::simpleStyleWithRandomColors() +{ + QColor polygonFillColor = QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(); + QColor polygonStrokeColor = polygonFillColor; + polygonFillColor.setAlpha( 100 ); + double polygonStrokeWidth = DEFAULT_LINE_WIDTH; + + QColor lineStrokeColor = QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(); + double lineStrokeWidth = DEFAULT_LINE_WIDTH; + + QColor pointFillColor = QgsApplication::colorSchemeRegistry()->fetchRandomStyleColor(); + QColor pointStrokeColor = pointFillColor; + pointFillColor.setAlpha( 100 ); + double pointSize = DEFAULT_POINT_SIZE; + + return simpleStyle( polygonFillColor, polygonStrokeColor, polygonStrokeWidth, + lineStrokeColor, lineStrokeWidth, + pointFillColor, pointStrokeColor, pointSize ); +} + +QList QgsVectorTileBasicRenderer::simpleStyle( + const QColor &polygonFillColor, const QColor &polygonStrokeColor, double polygonStrokeWidth, + const QColor &lineStrokeColor, double lineStrokeWidth, + const QColor &pointFillColor, const QColor &pointStrokeColor, double pointSize ) +{ + QgsSimpleFillSymbolLayer *fillSymbolLayer = new QgsSimpleFillSymbolLayer(); + fillSymbolLayer->setFillColor( polygonFillColor ); + fillSymbolLayer->setStrokeColor( polygonStrokeColor ); + fillSymbolLayer->setStrokeWidth( polygonStrokeWidth ); + QgsFillSymbol *fillSymbol = new QgsFillSymbol( QgsSymbolLayerList() << fillSymbolLayer ); + + QgsSimpleLineSymbolLayer *lineSymbolLayer = new QgsSimpleLineSymbolLayer; + lineSymbolLayer->setColor( lineStrokeColor ); + lineSymbolLayer->setWidth( lineStrokeWidth ); + QgsLineSymbol *lineSymbol = new QgsLineSymbol( QgsSymbolLayerList() << lineSymbolLayer ); + + QgsSimpleMarkerSymbolLayer *markerSymbolLayer = new QgsSimpleMarkerSymbolLayer; + markerSymbolLayer->setFillColor( pointFillColor ); + markerSymbolLayer->setStrokeColor( pointStrokeColor ); + markerSymbolLayer->setSize( pointSize ); + QgsMarkerSymbol *markerSymbol = new QgsMarkerSymbol( QgsSymbolLayerList() << markerSymbolLayer ); + + QgsVectorTileBasicRendererStyle st1( QStringLiteral( "Polygons" ), QString(), QgsWkbTypes::PolygonGeometry ); + st1.setSymbol( fillSymbol ); + + QgsVectorTileBasicRendererStyle st2( QStringLiteral( "Lines" ), QString(), QgsWkbTypes::LineGeometry ); + st2.setSymbol( lineSymbol ); + + QgsVectorTileBasicRendererStyle st3( QStringLiteral( "Points" ), QString(), QgsWkbTypes::PointGeometry ); + st3.setSymbol( markerSymbol ); + + QList lst; + lst << st1 << st2 << st3; + return lst; +} diff --git a/src/core/vectortile/qgsvectortilebasicrenderer.h b/src/core/vectortile/qgsvectortilebasicrenderer.h new file mode 100644 index 000000000000..1062551e5013 --- /dev/null +++ b/src/core/vectortile/qgsvectortilebasicrenderer.h @@ -0,0 +1,169 @@ +/*************************************************************************** + qgsvectortilebasicrenderer.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSVECTORTILEBASICRENDERER_H +#define QGSVECTORTILEBASICRENDERER_H + +#include "qgis_core.h" +#include "qgis_sip.h" + +#include "qgsvectortilerenderer.h" + +class QgsLineSymbol; +class QgsFillSymbol; +class QgsMarkerSymbol; + +class QgsSymbol; + +/** + * \ingroup core + * Definition of map rendering of a subset of vector tile data. The subset of data is defined by: + * 1. sub-layer name + * 2. geometry type (a single sub-layer may have multiple geometry types) + * 3. filter expression + * + * Renering is determined by the associated symbol (QgsSymbol). Symbol has to be of the same + * type as the chosen geometryType() - i.e. QgsMarkerSymbol for points, QgsLineSymbol for linestrings + * and QgsFillSymbol for polygons. + * + * It is possible to further constrain when this style is applied by setting a range of allowed + * zoom levels, or by disabling it. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsVectorTileBasicRendererStyle +{ + public: + //! Constructs a style object + QgsVectorTileBasicRendererStyle( const QString &stName = QString(), const QString &laName = QString(), QgsWkbTypes::GeometryType geomType = QgsWkbTypes::UnknownGeometry ); + //! Constructs a style object as a copy of another style + QgsVectorTileBasicRendererStyle( const QgsVectorTileBasicRendererStyle &other ); + QgsVectorTileBasicRendererStyle &operator=( const QgsVectorTileBasicRendererStyle &other ); + ~QgsVectorTileBasicRendererStyle(); + + //! Sets human readable name of this style + void setStyleName( const QString &name ) { mStyleName = name; } + //! Returns human readable name of this style + QString styleName() const { return mStyleName; } + + //! Sets name of the sub-layer to render (empty layer means that all layers match) + void setLayerName( const QString &name ) { mLayerName = name; } + //! Returns name of the sub-layer to render (empty layer means that all layers match) + QString layerName() const { return mLayerName; } + + //! Sets type of the geometry that will be used (point / line / polygon) + void setGeometryType( QgsWkbTypes::GeometryType geomType ) { mGeometryType = geomType; } + //! Returns type of the geometry that will be used (point / line / polygon) + QgsWkbTypes::GeometryType geometryType() const { return mGeometryType; } + + //! Sets filter expression (empty filter means that all features match) + void setFilterExpression( const QString &expr ) { mExpression = expr; } + //! Returns filter expression (empty filter means that all features match) + QString filterExpression() const { return mExpression; } + + //! Sets symbol for rendering. Takes ownership of the symbol. + void setSymbol( QgsSymbol *sym SIP_TRANSFER ); + //! Returns symbol for rendering + QgsSymbol *symbol() const { return mSymbol.get(); } + + //! Sets whether this style is enabled (used for rendering) + void setEnabled( bool enabled ) { mEnabled = enabled; } + //! Returns whether this style is enabled (used for rendering) + bool isEnabled() const { return mEnabled; } + + //! Sets minimum zoom level index (negative number means no limit) + void setMinZoomLevel( int minZoom ) { mMinZoomLevel = minZoom; } + //! Returns minimum zoom level index (negative number means no limit) + int minZoomLevel() const { return mMinZoomLevel; } + + //! Sets maximum zoom level index (negative number means no limit) + void setMaxZoomLevel( int maxZoom ) { mMaxZoomLevel = maxZoom; } + //! Returns maxnimum zoom level index (negative number means no limit) + int maxZoomLevel() const { return mMaxZoomLevel; } + + //! Returns whether the style is active at given zoom level (also checks "enabled" flag) + bool isActive( int zoomLevel ) const + { + return mEnabled && ( mMinZoomLevel == -1 || zoomLevel >= mMinZoomLevel ) && ( mMaxZoomLevel == -1 || zoomLevel <= mMaxZoomLevel ); + } + + //! Writes object content to given DOM element + void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const; + //! Reads object content from given DOM element + void readXml( const QDomElement &elem, const QgsReadWriteContext &context ); + + private: + QString mStyleName; + QString mLayerName; + QgsWkbTypes::GeometryType mGeometryType; + std::unique_ptr mSymbol; + bool mEnabled = true; + QString mExpression; + int mMinZoomLevel = -1; + int mMaxZoomLevel = -1; +}; + + +/** + * \ingroup core + * The default vector tile renderer implementation. It has an ordered list of "styles", + * each defines a rendering rule. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsVectorTileBasicRenderer : public QgsVectorTileRenderer +{ + public: + //! Constructs renderer with no styles + QgsVectorTileBasicRenderer(); + + QString type() const override; + QgsVectorTileBasicRenderer *clone() const override SIP_FACTORY; + void startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ) override; + QMap > usedAttributes( const QgsRenderContext & ) override SIP_SKIP; + void stopRender( QgsRenderContext &context ) override; + void renderTile( const QgsVectorTileRendererData &tile, QgsRenderContext &context ) override; + void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const override; + void readXml( const QDomElement &elem, const QgsReadWriteContext &context ) override; + + //! Sets list of styles of the renderer + void setStyles( const QList &styles ); + //! Returns list of styles of the renderer + QList styles() const; + + //! Returns a list of styles to render all layers with the given fill/stroke colors, stroke widths and marker sizes + static QList simpleStyle( + const QColor &polygonFillColor, const QColor &polygonStrokeColor, double polygonStrokeWidth, + const QColor &lineStrokeColor, double lineStrokeWidth, + const QColor &pointFillColor, const QColor &pointStrokeColor, double pointSize ); + + //! Returns a list of styles to render all layers, using random colors + static QList simpleStyleWithRandomColors(); + + private: + void setDefaultStyle(); + + private: + //! List of rendering styles + QList mStyles; + + // temporary bits + + //! Names of required fields for each sub-layer (only valid between startRender/stopRender calls) + QMap > mRequiredFields; + +}; + +#endif // QGSVECTORTILEBASICRENDERER_H diff --git a/src/core/vectortile/qgsvectortilelayer.cpp b/src/core/vectortile/qgsvectortilelayer.cpp new file mode 100644 index 000000000000..7528de135f74 --- /dev/null +++ b/src/core/vectortile/qgsvectortilelayer.cpp @@ -0,0 +1,183 @@ +/*************************************************************************** + qgsvectortilelayer.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsvectortilelayer.h" + +#include "qgslogger.h" +#include "qgsvectortilelayerrenderer.h" +#include "qgsmbtilesreader.h" +#include "qgsvectortilebasicrenderer.h" +#include "qgsvectortileloader.h" + +#include "qgsdatasourceuri.h" + + +QgsVectorTileLayer::QgsVectorTileLayer( const QString &uri, const QString &baseName ) + : QgsMapLayer( QgsMapLayerType::VectorTileLayer, baseName ) +{ + mDataSource = uri; + + QgsDataSourceUri dsUri; + dsUri.setEncodedUri( uri ); + + mSourceType = dsUri.param( QStringLiteral( "type" ) ); + mSourcePath = dsUri.param( QStringLiteral( "url" ) ); + if ( mSourceType == QStringLiteral( "xyz" ) ) + { + // online tiles + mSourceMinZoom = 0; + mSourceMaxZoom = 14; + + if ( dsUri.hasParam( QStringLiteral( "zmin" ) ) ) + mSourceMinZoom = dsUri.param( QStringLiteral( "zmin" ) ).toInt(); + if ( dsUri.hasParam( QStringLiteral( "zmax" ) ) ) + mSourceMaxZoom = dsUri.param( QStringLiteral( "zmax" ) ).toInt(); + + setExtent( QgsRectangle( -20037508.3427892, -20037508.3427892, 20037508.3427892, 20037508.3427892 ) ); + } + else if ( mSourceType == QStringLiteral( "mbtiles" ) ) + { + QgsMBTilesReader reader( mSourcePath ); + if ( !reader.open() ) + { + QgsDebugMsg( QStringLiteral( "failed to open MBTiles file: " ) + mSourcePath ); + return; + } + + QgsDebugMsgLevel( QStringLiteral( "name: " ) + reader.metadataValue( QStringLiteral( "name" ) ), 2 ); + bool minZoomOk, maxZoomOk; + int minZoom = reader.metadataValue( QStringLiteral( "minzoom" ) ).toInt( &minZoomOk ); + int maxZoom = reader.metadataValue( QStringLiteral( "maxzoom" ) ).toInt( &maxZoomOk ); + if ( minZoomOk ) + mSourceMinZoom = minZoom; + if ( maxZoomOk ) + mSourceMaxZoom = maxZoom; + QgsDebugMsgLevel( QStringLiteral( "zoom range: %1 - %2" ).arg( mSourceMinZoom ).arg( mSourceMaxZoom ), 2 ); + + QgsRectangle r = reader.extent(); + // TODO: reproject to EPSG:3857 + setExtent( r ); + } + else + { + QgsDebugMsg( QStringLiteral( "Unknown source type: " ) + mSourceType ); + return; + } + + setCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:3857" ) ) ); + setValid( true ); + + // set a default renderer + QgsVectorTileBasicRenderer *renderer = new QgsVectorTileBasicRenderer; + renderer->setStyles( QgsVectorTileBasicRenderer::simpleStyleWithRandomColors() ); + setRenderer( renderer ); +} + +QgsVectorTileLayer::~QgsVectorTileLayer() = default; + + +QgsVectorTileLayer *QgsVectorTileLayer::clone() const +{ + QgsVectorTileLayer *layer = new QgsVectorTileLayer( source(), name() ); + layer->setRenderer( renderer() ? renderer()->clone() : nullptr ); + return layer; +} + +QgsMapLayerRenderer *QgsVectorTileLayer::createMapRenderer( QgsRenderContext &rendererContext ) +{ + return new QgsVectorTileLayerRenderer( this, rendererContext ); +} + +bool QgsVectorTileLayer::readXml( const QDomNode &layerNode, QgsReadWriteContext &context ) +{ + QString errorMsg; + return readSymbology( layerNode, errorMsg, context ); +} + +bool QgsVectorTileLayer::writeXml( QDomNode &layerNode, QDomDocument &doc, const QgsReadWriteContext &context ) const +{ + QDomElement mapLayerNode = layerNode.toElement(); + mapLayerNode.setAttribute( QStringLiteral( "type" ), QStringLiteral( "vector-tile" ) ); + + QString errorMsg; + return writeSymbology( layerNode, doc, errorMsg, context ); +} + +bool QgsVectorTileLayer::readSymbology( const QDomNode &node, QString &errorMessage, QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories ) +{ + QDomElement elem = node.toElement(); + + readCommonStyle( elem, context, categories ); + + QDomElement elemRenderer = elem.firstChildElement( QStringLiteral( "renderer" ) ); + if ( elemRenderer.isNull() ) + { + errorMessage = tr( "Missing tag" ); + return false; + } + QString rendererType = elemRenderer.attribute( QStringLiteral( "type" ) ); + QgsVectorTileRenderer *r = nullptr; + if ( rendererType == QStringLiteral( "basic" ) ) + r = new QgsVectorTileBasicRenderer; + else + { + errorMessage = tr( "Unknown renderer type: " ) + rendererType; + return false; + } + + r->readXml( elemRenderer, context ); + return true; +} + +bool QgsVectorTileLayer::writeSymbology( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, QgsMapLayer::StyleCategories categories ) const +{ + Q_UNUSED( errorMessage ) + QDomElement elem = node.toElement(); + + writeCommonStyle( elem, doc, context, categories ); + + if ( mRenderer ) + { + QDomElement elemRenderer = doc.createElement( QStringLiteral( "renderer" ) ); + elemRenderer.setAttribute( QStringLiteral( "type" ), mRenderer->type() ); + mRenderer->writeXml( elemRenderer, context ); + elem.appendChild( elemRenderer ); + } + return true; +} + +void QgsVectorTileLayer::setTransformContext( const QgsCoordinateTransformContext &transformContext ) +{ + Q_UNUSED( transformContext ) +} + +QByteArray QgsVectorTileLayer::getRawTile( QgsTileXYZ tileID ) +{ + QgsTileRange tileRange( tileID.column(), tileID.column(), tileID.row(), tileID.row() ); + QList rawTiles = QgsVectorTileLoader::blockingFetchTileRawData( mSourceType, mSourcePath, tileID.zoomLevel(), QPointF(), tileRange ); + if ( rawTiles.isEmpty() ) + return QByteArray(); + return rawTiles.first().data; +} + +void QgsVectorTileLayer::setRenderer( QgsVectorTileRenderer *r ) +{ + mRenderer.reset( r ); +} + +QgsVectorTileRenderer *QgsVectorTileLayer::renderer() const +{ + return mRenderer.get(); +} diff --git a/src/core/vectortile/qgsvectortilelayer.h b/src/core/vectortile/qgsvectortilelayer.h new file mode 100644 index 000000000000..82e6601e13be --- /dev/null +++ b/src/core/vectortile/qgsvectortilelayer.h @@ -0,0 +1,157 @@ +/*************************************************************************** + qgsvectortilelayer.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSVECTORTILELAYER_H +#define QGSVECTORTILELAYER_H + +#include "qgis_core.h" +#include "qgis_sip.h" + +#include "qgsmaplayer.h" + +class QgsVectorTileRenderer; + +class QgsTileXYZ; + +/** + * \ingroup core + * Implements a map layer that is dedicated to rendering of vector tiles. + * Vector tiles compared to "ordinary" vector layers are pre-processed data + * optimized for fast rendering. A dataset is provided with a series of zoom levels + * for different map scales. Each zoom level has a matrix of tiles that contain + * actual data. A single vector tile may be a a file stored on a local drive, + * requested over HTTP request or retrieved from a database. + * + * Content of a vector tile is divided into one or more named sub-layers. Each such + * sub-layer may contain many features which consist of geometry and attributes. + * Contrary to traditional vector layers, these sub-layers do not need to have a rigid + * schema where geometry type and attributes are the same for all features. A single + * sub-layer may have multiple geometry types in a single tile or have some attributes + * defined only at particular zoom levels. + * + * Vector tile layer currently does not use the concept of data providers that other + * layer types use. The process of rendering of vector tiles looks like this: + * + * +--------+ +------+ +---------+ + * | DATA | | RAW | | DECODED | + * | | --> LOADER --> | | --> DECODER --> | | --> RENDERER + * | SOURCE | | TILE | | TILE | + * +--------+ +------+ +---------+ + * + * Data source is a place from where tiles are fetched from (URL for HTTP access, local + * files, MBTiles file, GeoPackage file or others. Loader (QgsVectorTileLoader) class + * takes care of loading data from the data source. The "raw tile" data is just a blob + * (QByteArray) that is encoded in some way. There are multiple ways how vector tiles + * are encoded just like there are different formats how to store images. For example, + * tiles can be encoded using Mapbox Vector Tiles (MVT) format or in GeoJSON. Decoder + * (QgsVectorTileDecoder) takes care of decoding raw tile data into QgsFeature objects. + * A decoded tile is essentially an array of vector features for each sub-layer found + * in the tile - this is what vector tile renderer (QgsVectorTileRenderer) expects + * and does the map rendering. + * + * To construct a vector tile layer, it is best to use QgsDataSourceUri class and set + * the following parameters to get a valid encoded URI: + * - "type" - what kind of data source will be used + * - "url" - URL or path of the data source (specific to each data source type, see below) + * + * Currently supported data source types: + * - "xyz" - the "url" should be a template like http://example.com/{z}/{x}/{y}.pbf where + * {x},{y},{z} will be replaced by tile coordinates + * - "mbtiles" - tiles read from a MBTiles file (a SQLite database) + * + * Currently supported decoders: + * - MVT - following Mapbox Vector Tiles specification + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsVectorTileLayer : public QgsMapLayer +{ + Q_OBJECT + + public: + //! Constructs a new vector tile layer + explicit QgsVectorTileLayer( const QString &path = QString(), const QString &baseName = QString() ); + ~QgsVectorTileLayer() override; + + // implementation of virtual functions from QgsMapLayer + + QgsVectorTileLayer *clone() const override SIP_FACTORY; + + virtual QgsMapLayerRenderer *createMapRenderer( QgsRenderContext &rendererContext ) override SIP_FACTORY; + + virtual bool readXml( const QDomNode &layerNode, QgsReadWriteContext &context ) override; + + virtual bool writeXml( QDomNode &layerNode, QDomDocument &doc, const QgsReadWriteContext &context ) const override; + + virtual bool readSymbology( const QDomNode &node, QString &errorMessage, + QgsReadWriteContext &context, StyleCategories categories = AllStyleCategories ) override; + + virtual bool writeSymbology( QDomNode &node, QDomDocument &doc, QString &errorMessage, const QgsReadWriteContext &context, + StyleCategories categories = AllStyleCategories ) const override; + + virtual void setTransformContext( const QgsCoordinateTransformContext &transformContext ) override; + + // new methods + + //! Returns type of the data source + QString sourceType() const { return mSourceType; } + //! Returns URL/path of the data source (syntax different to each data source type) + QString sourcePath() const { return mSourcePath; } + + //! Returns minimum zoom level at which source has any valid tiles (negative = unconstrained) + int sourceMinZoom() const { return mSourceMinZoom; } + //! Returns maximum zoom level at which source has any valid tiles (negative = unconstrained) + int sourceMaxZoom() const { return mSourceMaxZoom; } + + /** + * Fetches raw tile data for the give tile coordinates. If failed to fetch tile data, + * it will return an empty byte array. + * + * \note This call may issue a network request (depending on the source type) and will block + * the caller until the request is finished. + */ + QByteArray getRawTile( QgsTileXYZ tileID ) SIP_SKIP; + + /** + * Sets renderer for the map layer. + * \note Takes ownership of the passed renderer + */ + void setRenderer( QgsVectorTileRenderer *r SIP_TRANSFER ); + //! Returns currently assigned renderer + QgsVectorTileRenderer *renderer() const; + + //! Sets whether to render also borders of tiles (useful for debugging) + void setTileBorderRenderingEnabled( bool enabled ) { mTileBorderRendering = enabled; } + //! Returns whether to render also borders of tiles (useful for debugging) + bool isTileBorderRenderingEnabled() const { return mTileBorderRendering; } + + private: + //! Type of the data source + QString mSourceType; + //! URL/Path of the data source + QString mSourcePath; + //! Minimum zoom level at which source has any valid tiles (negative = unconstrained) + int mSourceMinZoom = -1; + //! Maximum zoom level at which source has any valid tiles (negative = unconstrained) + int mSourceMaxZoom = -1; + + //! Renderer assigned to the layer to draw map + std::unique_ptr mRenderer; + //! Whether we draw borders of tiles + bool mTileBorderRendering = false; +}; + + +#endif // QGSVECTORTILELAYER_H diff --git a/src/core/vectortile/qgsvectortilelayerrenderer.cpp b/src/core/vectortile/qgsvectortilelayerrenderer.cpp new file mode 100644 index 000000000000..6cdc4f6741eb --- /dev/null +++ b/src/core/vectortile/qgsvectortilelayerrenderer.cpp @@ -0,0 +1,188 @@ +/*************************************************************************** + qgsvectortilelayerrenderer.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsvectortilelayerrenderer.h" + +#include + +#include "qgsexpressioncontextutils.h" +#include "qgsfeedback.h" +#include "qgslogger.h" + +#include "qgsvectortilemvtdecoder.h" +#include "qgsvectortilelayer.h" +#include "qgsvectortileloader.h" +#include "qgsvectortileutils.h" + + +QgsVectorTileLayerRenderer::QgsVectorTileLayerRenderer( QgsVectorTileLayer *layer, QgsRenderContext &context ) + : QgsMapLayerRenderer( layer->id(), &context ) + , mSourceType( layer->sourceType() ) + , mSourcePath( layer->sourcePath() ) + , mSourceMinZoom( layer->sourceMinZoom() ) + , mSourceMaxZoom( layer->sourceMaxZoom() ) + , mRenderer( layer->renderer()->clone() ) + , mDrawTileBoundaries( layer->isTileBorderRenderingEnabled() ) + , mFeedback( new QgsFeedback ) +{ +} + +bool QgsVectorTileLayerRenderer::render() +{ + QgsRenderContext &ctx = *renderContext(); + + if ( ctx.renderingStopped() ) + return false; + + QElapsedTimer tTotal; + tTotal.start(); + + QgsDebugMsgLevel( QStringLiteral( "Vector tiles rendering extent: " ) + ctx.extent().toString( -1 ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Vector tiles map scale 1 : %1" ).arg( ctx.rendererScale() ), 2 ); + + mTileZoom = QgsVectorTileUtils::scaleToZoomLevel( ctx.rendererScale(), mSourceMinZoom, mSourceMaxZoom ); + QgsDebugMsgLevel( QStringLiteral( "Vector tiles zoom level: %1" ).arg( mTileZoom ), 2 ); + + mTileMatrix = QgsTileMatrix::fromWebMercator( mTileZoom ); + + mTileRange = mTileMatrix.tileRangeFromExtent( ctx.extent() ); + QgsDebugMsgLevel( QStringLiteral( "Vector tiles range X: %1 - %2 Y: %3 - %4" ) + .arg( mTileRange.startColumn() ).arg( mTileRange.endColumn() ) + .arg( mTileRange.startRow() ).arg( mTileRange.endRow() ), 2 ); + + // view center is used to sort the order of tiles for fetching and rendering + QPointF viewCenter = mTileMatrix.mapToTileCoordinates( ctx.extent().center() ); + + if ( !mTileRange.isValid() ) + { + QgsDebugMsgLevel( QStringLiteral( "Vector tiles - outside of range" ), 2 ); + return true; // nothing to do + } + + bool isAsync = ( mSourceType == QStringLiteral( "xyz" ) ); + + std::unique_ptr asyncLoader; + QList rawTiles; + if ( !isAsync ) + { + QElapsedTimer tFetch; + tFetch.start(); + rawTiles = QgsVectorTileLoader::blockingFetchTileRawData( mSourceType, mSourcePath, mTileZoom, viewCenter, mTileRange ); + QgsDebugMsgLevel( QStringLiteral( "Tile fetching time: %1" ).arg( tFetch.elapsed() / 1000. ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Fetched tiles: %1" ).arg( rawTiles.count() ), 2 ); + } + else + { + asyncLoader.reset( new QgsVectorTileLoader( mSourcePath, mTileZoom, mTileRange, viewCenter, mFeedback.get() ) ); + QObject::connect( asyncLoader.get(), &QgsVectorTileLoader::tileRequestFinished, [this]( const QgsVectorTileRawData & rawTile ) + { + QgsDebugMsgLevel( QStringLiteral( "Got tile asynchronously: " ) + rawTile.id.toString(), 2 ); + if ( !rawTile.data.isEmpty() ) + decodeAndDrawTile( rawTile ); + } ); + } + + if ( ctx.renderingStopped() ) + return false; + + mRenderer->startRender( *renderContext(), mTileZoom, mTileRange ); + + QMap > requiredFields = mRenderer->usedAttributes( *renderContext() ); + + QMap perLayerFields; + for ( QString layerName : requiredFields.keys() ) + mPerLayerFields[layerName] = QgsVectorTileUtils::makeQgisFields( requiredFields[layerName] ); + + if ( !isAsync ) + { + for ( QgsVectorTileRawData &rawTile : rawTiles ) + { + if ( ctx.renderingStopped() ) + break; + + decodeAndDrawTile( rawTile ); + } + } + else + { + // Block until tiles are fetched and rendered. If the rendering gets canceled at some point, + // the async loader will catch the signal, abort requests and return from downloadBlocking() + asyncLoader->downloadBlocking(); + } + + mRenderer->stopRender( ctx ); + + ctx.painter()->setClipping( false ); + + QgsDebugMsgLevel( QStringLiteral( "Total time for decoding: %1" ).arg( mTotalDecodeTime / 1000. ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Drawing time: %1" ).arg( mTotalDrawTime / 1000. ), 2 ); + QgsDebugMsgLevel( QStringLiteral( "Total time: %1" ).arg( tTotal.elapsed() / 1000. ), 2 ); + + return !ctx.renderingStopped(); +} + +void QgsVectorTileLayerRenderer::decodeAndDrawTile( const QgsVectorTileRawData &rawTile ) +{ + QgsRenderContext &ctx = *renderContext(); + + QgsDebugMsgLevel( QStringLiteral( "Drawing tile " ) + rawTile.id.toString(), 2 ); + + QElapsedTimer tLoad; + tLoad.start(); + + // currently only MVT encoding supported + QgsVectorTileMVTDecoder decoder; + if ( !decoder.decode( rawTile.id, rawTile.data ) ) + { + QgsDebugMsgLevel( QStringLiteral( "Failed to parse raw tile data! " ) + rawTile.id.toString(), 2 ); + return; + } + + if ( ctx.renderingStopped() ) + return; + + QgsCoordinateTransform ct = ctx.coordinateTransform(); + + QgsVectorTileRendererData tile( rawTile.id ); + tile.setFeatures( decoder.layerFeatures( mPerLayerFields, ct ) ); + tile.setTilePolygon( QgsVectorTileUtils::tilePolygon( rawTile.id, ct, mTileMatrix, ctx.mapToPixel() ) ); + + mTotalDecodeTime += tLoad.elapsed(); + + // calculate tile polygon in screen coordinates + + if ( ctx.renderingStopped() ) + return; + + // set up clipping so that rendering does not go behind tile's extent + + ctx.painter()->setClipRegion( QRegion( tile.tilePolygon() ) ); + + QElapsedTimer tDraw; + tDraw.start(); + + mRenderer->renderTile( tile, ctx ); + mTotalDrawTime += tDraw.elapsed(); + + if ( mDrawTileBoundaries ) + { + ctx.painter()->setClipping( false ); + + QPen pen( Qt::red ); + pen.setWidth( 3 ); + ctx.painter()->setPen( pen ); + ctx.painter()->drawPolygon( tile.tilePolygon() ); + } +} diff --git a/src/core/vectortile/qgsvectortilelayerrenderer.h b/src/core/vectortile/qgsvectortilelayerrenderer.h new file mode 100644 index 000000000000..68bbb29d75e3 --- /dev/null +++ b/src/core/vectortile/qgsvectortilelayerrenderer.h @@ -0,0 +1,85 @@ +/*************************************************************************** + qgsvectortilelayerrenderer.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSVECTORTILELAYERRENDERER_H +#define QGSVECTORTILELAYERRENDERER_H + +#define SIP_NO_FILE + +#include "qgsmaplayerrenderer.h" + +class QgsVectorTileLayer; +class QgsVectorTileRawData; + +#include "qgsvectortilerenderer.h" + +/** + * \ingroup core + * This class provides map rendering functionality for vector tile layers. + * In render() function (assumed to be run in a worker thread) it will: + * 1. fetch vector tiles using QgsVectorTileLoader + * 2. decode raw tiles into QgsFeature objects using QgsVectorTileDecoder + * 3. render tiles using a class derived from QgsVectorTileRenderer + * + * \since QGIS 3.14 + */ +class QgsVectorTileLayerRenderer : public QgsMapLayerRenderer +{ + public: + //! Creates the renderer. Always called from main thread, should copy whatever necessary from the layer + QgsVectorTileLayerRenderer( QgsVectorTileLayer *layer, QgsRenderContext &context ); + + virtual bool render() override; + virtual QgsFeedback *feedback() const override { return mFeedback.get(); } + + private: + void decodeAndDrawTile( const QgsVectorTileRawData &rawTile ); + + // data coming from the vector tile layer + + //! Type of the source from which we will be loading tiles (e.g. "xyz" or "mbtiles") + QString mSourceType; + //! Path/URL of the source. Format depends on source type + QString mSourcePath; + //! Minimum zoom level at which source has any valid tiles (negative = unconstrained) + int mSourceMinZoom = -1; + //! Maximum zoom level at which source has any valid tiles (negative = unconstrained) + int mSourceMaxZoom = -1; + //! Tile renderer object to do rendering of individual tiles + std::unique_ptr mRenderer; + + //! Whether to draw boundaries of tiles (useful for debugging) + bool mDrawTileBoundaries = false; + + // temporary data used during rendering process + + //! Feedback object that may be used by the caller to cancel the rendering + std::unique_ptr mFeedback; + //! Zoom level at which we will be rendering + int mTileZoom = 0; + //! Definition of the tile matrix for our zoom level + QgsTileMatrix mTileMatrix; + //!< Block of tiles we will be rendering in that zoom level + QgsTileRange mTileRange; + //! Cached QgsFields object for each sub-layer that will be rendered + QMap mPerLayerFields; + //! Counter of total elapsed time to decode tiles (ms) + int mTotalDecodeTime = 0; + //! Counter of total elapsed time to render tiles (ms) + int mTotalDrawTime = 0; +}; + + +#endif // QGSVECTORTILELAYERRENDERER_H diff --git a/src/core/vectortile/qgsvectortileloader.cpp b/src/core/vectortile/qgsvectortileloader.cpp new file mode 100644 index 000000000000..bb951fce6232 --- /dev/null +++ b/src/core/vectortile/qgsvectortileloader.cpp @@ -0,0 +1,270 @@ +/*************************************************************************** + qgsvectortileloader.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsvectortileloader.h" + +#include + +#include + +#include "qgsblockingnetworkrequest.h" +#include "qgslogger.h" +#include "qgsmbtilesreader.h" +#include "qgsnetworkaccessmanager.h" +#include "qgsvectortileutils.h" + +QgsVectorTileLoader::QgsVectorTileLoader( const QString &uri, int zoomLevel, const QgsTileRange &range, const QPointF &viewCenter, QgsFeedback *feedback ) + : mEventLoop( new QEventLoop ) + , mFeedback( feedback ) +{ + if ( feedback ) + { + connect( feedback, &QgsFeedback::canceled, this, &QgsVectorTileLoader::canceled, Qt::QueuedConnection ); + + // rendering could have been canceled before we started to listen to canceled() signal + // so let's check before doing the download and maybe quit prematurely + if ( feedback->isCanceled() ) + return; + } + + QgsDebugMsgLevel( QStringLiteral( "Starting network loader" ), 2 ); + QVector tiles = QgsVectorTileUtils::tilesInRange( range, zoomLevel ); + QgsVectorTileUtils::sortTilesByDistanceFromCenter( tiles, viewCenter ); + for ( QgsTileXYZ id : qgis::as_const( tiles ) ) + { + loadFromNetworkAsync( id, uri ); + } +} + +QgsVectorTileLoader::~QgsVectorTileLoader() +{ + QgsDebugMsgLevel( QStringLiteral( "Terminating network loader" ), 2 ); + + if ( !mReplies.isEmpty() ) + { + // this can happen when the loader is terminated without getting requests finalized + // (e.g. downloadBlocking() was not called) + canceled(); + } +} + +void QgsVectorTileLoader::downloadBlocking() +{ + if ( mFeedback && mFeedback->isCanceled() ) + { + QgsDebugMsgLevel( QStringLiteral( "downloadBlocking - not staring event loop - canceled" ), 2 ); + return; // nothing to do + } + + QgsDebugMsgLevel( QStringLiteral( "Starting event loop with %1 requests" ).arg( mReplies.count() ), 2 ); + + mEventLoop->exec( QEventLoop::ExcludeUserInputEvents ); + + QgsDebugMsgLevel( QStringLiteral( "downloadBlocking finished" ), 2 ); + + Q_ASSERT( mReplies.isEmpty() ); +} + +void QgsVectorTileLoader::loadFromNetworkAsync( const QgsTileXYZ &id, const QString &requestUrl ) +{ + QString url = QgsVectorTileUtils::formatXYZUrlTemplate( requestUrl, id ); + QNetworkRequest request( url ); + QgsSetRequestInitiatorClass( request, QStringLiteral( "QgsVectorTileLoader" ) ); + QgsSetRequestInitiatorId( request, id.toString() ); + + request.setAttribute( static_cast( QNetworkRequest::User + 1 ), id.column() ); + request.setAttribute( static_cast( QNetworkRequest::User + 2 ), id.row() ); + request.setAttribute( static_cast( QNetworkRequest::User + 3 ), id.zoomLevel() ); + + request.setAttribute( QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::PreferCache ); + request.setAttribute( QNetworkRequest::CacheSaveControlAttribute, true ); + + QNetworkReply *reply = QgsNetworkAccessManager::instance()->get( request ); + connect( reply, &QNetworkReply::finished, this, &QgsVectorTileLoader::tileReplyFinished ); + + mReplies << reply; +} + +void QgsVectorTileLoader::tileReplyFinished() +{ + QNetworkReply *reply = qobject_cast( sender() ); + + int reqX = reply->request().attribute( static_cast( QNetworkRequest::User + 1 ) ).toInt(); + int reqY = reply->request().attribute( static_cast( QNetworkRequest::User + 2 ) ).toInt(); + int reqZ = reply->request().attribute( static_cast( QNetworkRequest::User + 3 ) ).toInt(); + QgsTileXYZ tileID( reqX, reqY, reqZ ); + + if ( reply->error() == QNetworkReply::NoError ) + { + // TODO: handle redirections? + + QgsDebugMsgLevel( QStringLiteral( "Tile download successful: " ) + tileID.toString(), 2 ); + QByteArray rawData = reply->readAll(); + mReplies.removeOne( reply ); + reply->deleteLater(); + + emit tileRequestFinished( QgsVectorTileRawData( tileID, rawData ) ); + } + else + { + QgsDebugMsg( QStringLiteral( "Tile download failed! " ) + reply->errorString() ); + mReplies.removeOne( reply ); + reply->deleteLater(); + + emit tileRequestFinished( QgsVectorTileRawData( tileID, QByteArray() ) ); + } + + if ( mReplies.isEmpty() ) + { + // exist the event loop + QMetaObject::invokeMethod( mEventLoop.get(), "quit", Qt::QueuedConnection ); + } +} + +void QgsVectorTileLoader::canceled() +{ + QgsDebugMsgLevel( QStringLiteral( "Canceling %1 pending requests" ).arg( mReplies.count() ), 2 ); + const QList replies = mReplies; + for ( QNetworkReply *reply : replies ) + { + reply->abort(); + } +} + +////// + +QList QgsVectorTileLoader::blockingFetchTileRawData( const QString &sourceType, const QString &sourcePath, int zoomLevel, const QPointF &viewCenter, const QgsTileRange &range ) +{ + QList rawTiles; + + QgsMBTilesReader mbReader( sourcePath ); + bool isUrl = ( sourceType == QStringLiteral( "xyz" ) ); + if ( !isUrl ) + { + bool res = mbReader.open(); + Q_ASSERT( res ); + } + + QVector tiles = QgsVectorTileUtils::tilesInRange( range, zoomLevel ); + QgsVectorTileUtils::sortTilesByDistanceFromCenter( tiles, viewCenter ); + for ( QgsTileXYZ id : qgis::as_const( tiles ) ) + { + QByteArray rawData = isUrl ? loadFromNetwork( id, sourcePath ) : loadFromMBTiles( id, mbReader ); + if ( !rawData.isEmpty() ) + { + rawTiles.append( QgsVectorTileRawData( id, rawData ) ); + } + } + return rawTiles; +} + +QByteArray QgsVectorTileLoader::loadFromNetwork( const QgsTileXYZ &id, const QString &requestUrl ) +{ + QString url = QgsVectorTileUtils::formatXYZUrlTemplate( requestUrl, id ); + QNetworkRequest nr; + nr.setUrl( QUrl( url ) ); + QgsBlockingNetworkRequest req; + QgsDebugMsgLevel( QStringLiteral( "Blocking request: " ) + url, 2 ); + QgsBlockingNetworkRequest::ErrorCode errCode = req.get( nr ); + if ( errCode != QgsBlockingNetworkRequest::NoError ) + { + QgsDebugMsg( QStringLiteral( "Request failed: " ) + url ); + return QByteArray(); + } + QgsNetworkReplyContent reply = req.reply(); + QgsDebugMsgLevel( QStringLiteral( "Request successful, content size %1" ).arg( reply.content().size() ), 2 ); + return reply.content(); +} + + +QByteArray QgsVectorTileLoader::loadFromMBTiles( const QgsTileXYZ &id, QgsMBTilesReader &mbTileReader ) +{ + // MBTiles uses TMS specs with Y starting at the bottom while XYZ uses Y starting at the top + int rowTMS = pow( 2, id.zoomLevel() ) - id.row() - 1; + QByteArray gzippedTileData = mbTileReader.tileData( id.zoomLevel(), id.column(), rowTMS ); + if ( gzippedTileData.isEmpty() ) + { + QgsDebugMsg( QStringLiteral( "Failed to get tile " ) + id.toString() ); + return QByteArray(); + } + + // TODO: check format is "pbf" + + QByteArray data; + if ( !decodeGzip( gzippedTileData, data ) ) + { + QgsDebugMsg( QStringLiteral( "Failed to decompress tile " ) + id.toString() ); + return QByteArray(); + } + + QgsDebugMsgLevel( QStringLiteral( "Tile blob size %1 -> uncompressed size %2" ).arg( gzippedTileData.size() ).arg( data.size() ), 2 ); + return data; +} + + +bool QgsVectorTileLoader::decodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut ) +{ + unsigned char *bytesInPtr = reinterpret_cast( const_cast( bytesIn.constData() ) ); + uint bytesInLeft = static_cast( bytesIn.count() ); + + const uint CHUNK = 16384; + unsigned char out[CHUNK]; + const int DEC_MAGIC_NUM_FOR_GZIP = 16; + + // allocate inflate state + z_stream strm; + strm.zalloc = Z_NULL; + strm.zfree = Z_NULL; + strm.opaque = Z_NULL; + strm.avail_in = 0; + strm.next_in = Z_NULL; + + int ret = inflateInit2( &strm, MAX_WBITS + DEC_MAGIC_NUM_FOR_GZIP ); + if ( ret != Z_OK ) + return false; + + while ( ret != Z_STREAM_END ) // done when inflate() says it's done + { + // prepare next chunk + uint bytesToProcess = std::min( CHUNK, bytesInLeft ); + strm.next_in = bytesInPtr; + strm.avail_in = bytesToProcess; + bytesInPtr += bytesToProcess; + bytesInLeft -= bytesToProcess; + + if ( bytesToProcess == 0 ) + break; // we end with an error - no more data but inflate() wants more data + + // run inflate() on input until output buffer not full + do + { + strm.avail_out = CHUNK; + strm.next_out = out; + ret = inflate( &strm, Z_NO_FLUSH ); + Q_ASSERT( ret != Z_STREAM_ERROR ); // state not clobbered + if ( ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR ) + { + inflateEnd( &strm ); + return false; + } + unsigned have = CHUNK - strm.avail_out; + bytesOut.append( QByteArray::fromRawData( reinterpret_cast( out ), static_cast( have ) ) ); + } + while ( strm.avail_out == 0 ); + } + + inflateEnd( &strm ); + return ret == Z_STREAM_END; +} diff --git a/src/core/vectortile/qgsvectortileloader.h b/src/core/vectortile/qgsvectortileloader.h new file mode 100644 index 000000000000..e4b0b78a330c --- /dev/null +++ b/src/core/vectortile/qgsvectortileloader.h @@ -0,0 +1,103 @@ +/*************************************************************************** + qgsvectortileloader.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSVECTORTILELOADER_H +#define QGSVECTORTILELOADER_H + +#define SIP_NO_FILE + +class QByteArray; + +#include "qgsvectortilerenderer.h" + +/** + * \ingroup core + * Keeps track of raw tile data that need to be decoded + * + * \since QGIS 3.14 + */ +class QgsVectorTileRawData +{ + public: + //! Constructs a raw tile object + QgsVectorTileRawData( QgsTileXYZ tileID = QgsTileXYZ(), const QByteArray &raw = QByteArray() ) + : id( tileID ), data( raw ) {} + + //! Tile position in tile matrix set + QgsTileXYZ id; + //! Raw tile data + QByteArray data; +}; + + +class QNetworkReply; +class QEventLoop; + +class QgsMBTilesReader; + +/** + * \ingroup core + * The loader class takes care of loading raw vector tile data from a tile source. + * + * \since QGIS 3.14 + */ +class QgsVectorTileLoader : public QObject +{ + Q_OBJECT + public: + + //! Returns raw tile data for the specified range of tiles. Blocks the caller until all tiles are fetched. + static QList blockingFetchTileRawData( const QString &sourceType, const QString &sourcePath, int zoomLevel, const QPointF &viewCenter, const QgsTileRange &range ); + + //! Returns raw tile data for a single tile, doing a HTTP request. Block the caller until tile data are downloaded. + static QByteArray loadFromNetwork( const QgsTileXYZ &id, const QString &requestUrl ); + //! Returns raw tile data for a single tile loaded from MBTiles file + static QByteArray loadFromMBTiles( const QgsTileXYZ &id, QgsMBTilesReader &mbTileReader ); + //! Decodes gzip byte stream, returns true on success + static bool decodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut ); + + // + // non-static stuff + // + + //! Constructs tile loader for doing asynchronous requests and starts network requests + QgsVectorTileLoader( const QString &uri, int zoomLevel, const QgsTileRange &range, const QPointF &viewCenter, QgsFeedback *feedback ); + ~QgsVectorTileLoader(); + + //! Blocks the caller until all asynchronous requests are finished (with a success or a failure) + void downloadBlocking(); + + private: + void loadFromNetworkAsync( const QgsTileXYZ &id, const QString &requestUrl ); + + private slots: + void tileReplyFinished(); + void canceled(); + + signals: + //! Emitted when a tile request has finished. If a tile request has failed, the returned raw tile byte array is empty. + void tileRequestFinished( const QgsVectorTileRawData &rawTile ); + + private: + //! Event loop used for blocking download + std::unique_ptr mEventLoop; + //! Feedback object that allows cancellation of pending requests + QgsFeedback *mFeedback; + //! Running tile requests + QList mReplies; + +}; + +#endif // QGSVECTORTILELOADER_H diff --git a/src/core/vectortile/qgsvectortilemvtdecoder.cpp b/src/core/vectortile/qgsvectortilemvtdecoder.cpp new file mode 100644 index 000000000000..5e261c3df572 --- /dev/null +++ b/src/core/vectortile/qgsvectortilemvtdecoder.cpp @@ -0,0 +1,338 @@ +/*************************************************************************** + qgsvectortilemvtdecoder.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include + +#include "qgsvectortilemvtdecoder.h" + +#include "qgsvectortilelayerrenderer.h" +#include "qgsvectortileutils.h" + +#include "qgslogger.h" +#include "qgsmultipoint.h" +#include "qgslinestring.h" +#include "qgsmultilinestring.h" +#include "qgsmultipolygon.h" +#include "qgspolygon.h" + + +inline bool _isExteriorRing( const QVector &pts ) +{ + // Exterior rings have POSITIVE area while interior rings have NEGATIVE area + // when calculated with https://en.wikipedia.org/wiki/Shoelace_formula + // The orientation of axes is that X grows to the right and Y grows to the bottom. + // the input data are expected to form a closed ring, i.e. first pt == last pt. + + double total = 0.0; + const QgsPoint *ptsPtr = pts.constData(); + int count = pts.count(); + for ( int i = 0; i < count - 1; i++ ) + { + double val = ( pts[i + 1].x() - ptsPtr[i].x() ) * ( ptsPtr[i + 1].y() + pts[i].y() ); + //double val = ptsPtr[i].x() * (-ptsPtr[i+1].y()) - ptsPtr[i+1].x() * (-ptsPtr[i].y()); // gives the same result + total += val; + } + return total >= 0; +} + + +bool QgsVectorTileMVTDecoder::decode( QgsTileXYZ tileID, const QByteArray &rawTileData ) +{ + if ( !tile.ParseFromArray( rawTileData.constData(), rawTileData.count() ) ) + return false; + + mTileID = tileID; + + mLayerNameToIndex.clear(); + for ( int layerNum = 0; layerNum < tile.layers_size(); layerNum++ ) + { + const ::vector_tile::Tile_Layer &layer = tile.layers( layerNum ); + QString layerName = layer.name().c_str(); + mLayerNameToIndex[layerName] = layerNum; + } + return true; +} + +QStringList QgsVectorTileMVTDecoder::layers() const +{ + QStringList layerNames; + for ( int layerNum = 0; layerNum < tile.layers_size(); layerNum++ ) + { + const ::vector_tile::Tile_Layer &layer = tile.layers( layerNum ); + QString layerName = layer.name().c_str(); + layerNames << layerName; + } + return layerNames; +} + +QStringList QgsVectorTileMVTDecoder::layerFieldNames( const QString &layerName ) const +{ + if ( !mLayerNameToIndex.contains( layerName ) ) + return QStringList(); + + const ::vector_tile::Tile_Layer &layer = tile.layers( mLayerNameToIndex[layerName] ); + QStringList fieldNames; + for ( int i = 0; i < layer.keys_size(); ++i ) + { + QString fieldName = layer.keys( i ).c_str(); + fieldNames << fieldName; + } + return fieldNames; +} + +QgsVectorTileFeatures QgsVectorTileMVTDecoder::layerFeatures( const QMap &perLayerFields, const QgsCoordinateTransform &ct ) const +{ + QgsVectorTileFeatures features; + + int numTiles = static_cast( pow( 2, mTileID.zoomLevel() ) ); // assuming we won't ever go over 30 zoom levels + double z0xMin = -20037508.3427892, z0yMin = -20037508.3427892; + double z0xMax = 20037508.3427892, z0yMax = 20037508.3427892; + double tileDX = ( z0xMax - z0xMin ) / numTiles; + double tileDY = ( z0yMax - z0yMin ) / numTiles; + double tileXMin = z0xMin + mTileID.column() * tileDX; + double tileYMax = z0yMax - mTileID.row() * tileDY; + + for ( int layerNum = 0; layerNum < tile.layers_size(); layerNum++ ) + { + const ::vector_tile::Tile_Layer &layer = tile.layers( layerNum ); + + QString layerName = layer.name().c_str(); + QVector layerFeatures; + QgsFields layerFields = perLayerFields[layerName]; + + // figure out how field indexes in MVT encoding map to field indexes in QgsFields (we may not use all available fields) + QHash tagKeyIndexToFieldIndex; + for ( int i = 0; i < layer.keys_size(); ++i ) + { + int fieldIndex = layerFields.indexOf( layer.keys( i ).c_str() ); + if ( fieldIndex != -1 ) + tagKeyIndexToFieldIndex.insert( i, fieldIndex ); + } + + // go through features of a layer + for ( int featureNum = 0; featureNum < layer.features_size(); featureNum++ ) + { + const ::vector_tile::Tile_Feature &feature = layer.features( featureNum ); + + QgsFeature f( layerFields, static_cast( feature.id() ) ); + + // + // parse attributes + // + + for ( int tagNum = 0; tagNum + 1 < feature.tags_size(); tagNum += 2 ) + { + int keyIndex = static_cast( feature.tags( tagNum ) ); + int fieldIndex = tagKeyIndexToFieldIndex.value( keyIndex, -1 ); + if ( fieldIndex == -1 ) + continue; + + int valueIndex = static_cast( feature.tags( tagNum + 1 ) ); + if ( valueIndex >= layer.values_size() ) + { + QgsDebugMsg( QStringLiteral( "Invalid value index for attribute" ) ); + continue; + } + const ::vector_tile::Tile_Value &value = layer.values( valueIndex ); + + if ( value.has_string_value() ) + f.setAttribute( fieldIndex, QString::fromStdString( value.string_value() ) ); + else if ( value.has_float_value() ) + f.setAttribute( fieldIndex, static_cast( value.float_value() ) ); + else if ( value.has_double_value() ) + f.setAttribute( fieldIndex, value.double_value() ); + else if ( value.has_int_value() ) + f.setAttribute( fieldIndex, static_cast( value.int_value() ) ); + else if ( value.has_uint_value() ) + f.setAttribute( fieldIndex, static_cast( value.uint_value() ) ); + else if ( value.has_sint_value() ) + f.setAttribute( fieldIndex, static_cast( value.sint_value() ) ); + else if ( value.has_bool_value() ) + f.setAttribute( fieldIndex, static_cast( value.bool_value() ) ); + else + { + QgsDebugMsg( QStringLiteral( "Unexpected attribute value" ) ); + } + } + + // + // parse geometry + // + + int extent = static_cast( layer.extent() ); + int cursorx = 0, cursory = 0; + + QVector outputPoints; // for point/multi-point + QVector outputLinestrings; // for linestring/multi-linestring + QVector outputPolygons; + QVector tmpPoints; + + for ( int i = 0; i < feature.geometry_size(); i ++ ) + { + unsigned g = feature.geometry( i ); + unsigned cmdId = g & 0x7; + unsigned cmdCount = g >> 3; + if ( cmdId == 1 ) // MoveTo + { + if ( i + static_cast( cmdCount ) * 2 >= feature.geometry_size() ) + { + QgsDebugMsg( QStringLiteral( "Malformed geometry: invalid cmdCount" ) ); + break; + } + for ( unsigned j = 0; j < cmdCount; j++ ) + { + unsigned v = feature.geometry( i + 1 ); + unsigned w = feature.geometry( i + 2 ); + int dx = ( ( v >> 1 ) ^ ( -( v & 1 ) ) ); + int dy = ( ( w >> 1 ) ^ ( -( w & 1 ) ) ); + cursorx += dx; + cursory += dy; + double px = tileXMin + tileDX * double( cursorx ) / double( extent ); + double py = tileYMax - tileDY * double( cursory ) / double( extent ); + + if ( feature.type() == vector_tile::Tile_GeomType_POINT ) + { + outputPoints.append( new QgsPoint( px, py ) ); + } + else if ( feature.type() == vector_tile::Tile_GeomType_LINESTRING ) + { + if ( tmpPoints.size() > 0 ) + { + outputLinestrings.append( new QgsLineString( tmpPoints ) ); + tmpPoints.clear(); + } + tmpPoints.append( QgsPoint( px, py ) ); + } + else if ( feature.type() == vector_tile::Tile_GeomType_POLYGON ) + { + tmpPoints.append( QgsPoint( px, py ) ); + } + i += 2; + } + } + else if ( cmdId == 2 ) // LineTo + { + if ( i + static_cast( cmdCount ) * 2 >= feature.geometry_size() ) + { + QgsDebugMsg( QStringLiteral( "Malformed geometry: invalid cmdCount" ) ); + break; + } + for ( unsigned j = 0; j < cmdCount; j++ ) + { + unsigned v = feature.geometry( i + 1 ); + unsigned w = feature.geometry( i + 2 ); + int dx = ( ( v >> 1 ) ^ ( -( v & 1 ) ) ); + int dy = ( ( w >> 1 ) ^ ( -( w & 1 ) ) ); + cursorx += dx; + cursory += dy; + double px = tileXMin + tileDX * double( cursorx ) / double( extent ); + double py = tileYMax - tileDY * double( cursory ) / double( extent ); + + tmpPoints.push_back( QgsPoint( px, py ) ); + i += 2; + } + } + else if ( cmdId == 7 ) // ClosePath + { + if ( feature.type() == vector_tile::Tile_GeomType_POLYGON ) + { + tmpPoints.append( tmpPoints.first() ); // close the ring + + if ( _isExteriorRing( tmpPoints ) ) + { + // start a new polygon + QgsPolygon *p = new QgsPolygon; + p->setExteriorRing( new QgsLineString( tmpPoints ) ); + outputPolygons.append( p ); + tmpPoints.clear(); + } + else + { + // interior ring (hole) + if ( outputPolygons.count() != 0 ) + { + outputPolygons[outputPolygons.count() - 1]->addInteriorRing( new QgsLineString( tmpPoints ) ); + } + else + { + QgsDebugMsg( QStringLiteral( "Malformed geometry: first ring of a polygon is interior ring" ) ); + } + tmpPoints.clear(); + } + } + + } + else + { + QgsDebugMsg( QStringLiteral( "Unexpected command ID: %1" ).arg( cmdId ) ); + } + } + + QString geomType; + if ( feature.type() == vector_tile::Tile_GeomType_POINT ) + { + geomType = QStringLiteral( "Point" ); + if ( outputPoints.count() == 1 ) + f.setGeometry( QgsGeometry( outputPoints[0] ) ); + else + { + QgsMultiPoint *mp = new QgsMultiPoint; + for ( int k = 0; k < outputPoints.count(); ++k ) + mp->addGeometry( outputPoints[k] ); + f.setGeometry( QgsGeometry( mp ) ); + } + } + else if ( feature.type() == vector_tile::Tile_GeomType_LINESTRING ) + { + geomType = QStringLiteral( "LineString" ); + + // finish the linestring we have started + outputLinestrings.append( new QgsLineString( tmpPoints ) ); + + if ( outputLinestrings.count() == 1 ) + f.setGeometry( QgsGeometry( outputLinestrings[0] ) ); + else + { + QgsMultiLineString *mls = new QgsMultiLineString; + for ( int k = 0; k < outputLinestrings.count(); ++k ) + mls->addGeometry( outputLinestrings[k] ); + f.setGeometry( QgsGeometry( mls ) ); + } + } + else if ( feature.type() == vector_tile::Tile_GeomType_POLYGON ) + { + geomType = QStringLiteral( "Polygon" ); + + if ( outputPolygons.count() == 1 ) + f.setGeometry( QgsGeometry( outputPolygons[0] ) ); + else + { + QgsMultiPolygon *mpl = new QgsMultiPolygon; + for ( int k = 0; k < outputPolygons.count(); ++k ) + mpl->addGeometry( outputPolygons[k] ); + f.setGeometry( QgsGeometry( mpl ) ); + } + } + + f.setAttribute( QStringLiteral( "_geom_type" ), geomType ); + f.geometry().transform( ct ); + + layerFeatures.append( f ); + } + + features[layerName] = layerFeatures; + } + return features; +} diff --git a/src/core/vectortile/qgsvectortilemvtdecoder.h b/src/core/vectortile/qgsvectortilemvtdecoder.h new file mode 100644 index 000000000000..aff57065d6ef --- /dev/null +++ b/src/core/vectortile/qgsvectortilemvtdecoder.h @@ -0,0 +1,58 @@ +/*************************************************************************** + qgsvectortilemvtdecoder.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSVECTORTILEMVTDECODER_H +#define QGSVECTORTILEMVTDECODER_H + +#define SIP_NO_FILE + +class QgsFeature; + +#include +#include + +#include "vector_tile.pb.h" + +#include "qgsvectortilerenderer.h" + +/** + * \ingroup core + * This class is responsible for decoding raw tile data written with Mapbox Vector Tiles encoding. + * + * \since QGIS 3.14 + */ +class QgsVectorTileMVTDecoder +{ + public: + + //! Tries to decode raw tile data, returns true on success + bool decode( QgsTileXYZ tileID, const QByteArray &rawTileData ); + + //! Returns a list of sub-layer names in a tile. It can only be called after a successful decode() + QStringList layers() const; + + //! Returns a list of all field names in a tile. It can only be called after a successful decode() + QStringList layerFieldNames( const QString &layerName ) const; + + //! Returns decoded features grouped by sub-layers. It can only be called after a successful decode() + QgsVectorTileFeatures layerFeatures( const QMap &perLayerFields, const QgsCoordinateTransform &ct ) const; + + private: + vector_tile::Tile tile; + QgsTileXYZ mTileID; + QMap mLayerNameToIndex; +}; + +#endif // QGSVECTORTILEMVTDECODER_H diff --git a/src/core/vectortile/qgsvectortilerenderer.h b/src/core/vectortile/qgsvectortilerenderer.h new file mode 100644 index 000000000000..2b42704a938e --- /dev/null +++ b/src/core/vectortile/qgsvectortilerenderer.h @@ -0,0 +1,125 @@ +/*************************************************************************** + qgsvectortilerenderer.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSVECTORTILERENDERER_H +#define QGSVECTORTILERENDERER_H + +#include "qgis_core.h" + +#include "qgsfeature.h" + +#include "qgstiles.h" + +class QgsRenderContext; + +//! Features of a vector tile, grouped by sub-layer names (key of the map) +typedef QMap > QgsVectorTileFeatures SIP_SKIP; + +/** + * \ingroup core + * Contains decoded features of a single vector tile and any other data necessary + * for rendering of it. + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsVectorTileRendererData +{ + public: + //! Constructs the object + explicit QgsVectorTileRendererData( QgsTileXYZ id ): mId( id ) {} + + //! Returns coordinates of the tile + QgsTileXYZ id() const { return mId; } + + //! Sets polygon of the tile + void setTilePolygon( QPolygon polygon ) { mTilePolygon = polygon; } + //! Returns polygon (made out of four corners of the tile) in screen coordinates calculated from render context + QPolygon tilePolygon() const { return mTilePolygon; } + + //! Sets features of the tile + void setFeatures( const QgsVectorTileFeatures &features ) SIP_SKIP { mFeatures = features; } + //! Returns features of the tile grouped by sub-layer names + QgsVectorTileFeatures features() const SIP_SKIP { return mFeatures; } + //! Returns list of layer names present in the tile + QStringList layers() const { return mFeatures.keys(); } + //! Returns list of all features within a single sub-layer + QVector layerFeatures( const QString &layerName ) const { return mFeatures[layerName]; } + + private: + //! Position of the tile in the tile matrix set + QgsTileXYZ mId; + //! Features of the tile grouped into sub-layers + QgsVectorTileFeatures mFeatures; + //! Polygon (made out of four corners of the tile) in screen coordinates calculated from render context + QPolygon mTilePolygon; +}; + +/** + * \ingroup core + * Abstract base class for all vector tile renderer implementations. + * + * For rendering it is expected that client code calls: + * 1. startRender() to prepare renderer + * 2. renderTile() for each tile + * 3. stopRender() to clean up renderer and free resources + * + * \since QGIS 3.14 + */ +class CORE_EXPORT QgsVectorTileRenderer +{ + +#ifdef SIP_RUN + SIP_CONVERT_TO_SUBCLASS_CODE + + const QString type = sipCpp->type(); + + if ( type == QStringLiteral( "basic" ) ) + sipType = sipType_QgsVectorTileBasicRenderer; + else + sipType = 0; + SIP_END +#endif + + public: + virtual ~QgsVectorTileRenderer() = default; + + //! Returns unique type name of the renderer implementation + virtual QString type() const = 0; + + //! Returns a clone of the renderer + virtual QgsVectorTileRenderer *clone() const = 0 SIP_FACTORY; + + //! Initializes rendering. It should be paired with a stopRender() call. + virtual void startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ) = 0; + + //! Returns field names of sub-layers that will be used for rendering. Must be called between startRender/stopRender. + virtual QMap > usedAttributes( const QgsRenderContext & ) SIP_SKIP { return QMap >(); } + + //! Finishes rendering and cleans up any resources + virtual void stopRender( QgsRenderContext &context ) = 0; + + //! Renders given vector tile. Must be called between startRender/stopRender. + virtual void renderTile( const QgsVectorTileRendererData &tile, QgsRenderContext &context ) = 0; + + //! Writes renderer's properties to given XML element + virtual void writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const = 0; + //! Reads renderer's properties from given XML element + virtual void readXml( const QDomElement &elem, const QgsReadWriteContext &context ) = 0; + //! Resolves references to other objects - second phase of loading - after readXml() + virtual void resolveReferences( const QgsProject &project ) { Q_UNUSED( project ) } + +}; + +#endif // QGSVECTORTILERENDERER_H diff --git a/src/core/vectortile/qgsvectortileutils.cpp b/src/core/vectortile/qgsvectortileutils.cpp new file mode 100644 index 000000000000..5b4022a95ccc --- /dev/null +++ b/src/core/vectortile/qgsvectortileutils.cpp @@ -0,0 +1,168 @@ +/*************************************************************************** + qgsvectortileutils.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsvectortileutils.h" + +#include + +#include + +#include "qgscoordinatetransform.h" +#include "qgsgeometrycollection.h" +#include "qgsfields.h" +#include "qgslogger.h" +#include "qgsmaptopixel.h" +#include "qgsrectangle.h" +#include "qgsvectorlayer.h" + +#include "qgsvectortilemvtdecoder.h" +#include "qgsvectortilelayer.h" +#include "qgsvectortilerenderer.h" + + + +QPolygon QgsVectorTileUtils::tilePolygon( QgsTileXYZ id, const QgsCoordinateTransform &ct, const QgsTileMatrix &tm, const QgsMapToPixel &mtp ) +{ + QgsRectangle r = tm.tileExtent( id ); + QgsPointXY p00a = mtp.transform( ct.transform( r.xMinimum(), r.yMinimum() ) ); + QgsPointXY p11a = mtp.transform( ct.transform( r.xMaximum(), r.yMaximum() ) ); + QgsPointXY p01a = mtp.transform( ct.transform( r.xMinimum(), r.yMaximum() ) ); + QgsPointXY p10a = mtp.transform( ct.transform( r.xMaximum(), r.yMinimum() ) ); + QPolygon path; + path << p00a.toQPointF().toPoint(); + path << p01a.toQPointF().toPoint(); + path << p11a.toQPointF().toPoint(); + path << p10a.toQPointF().toPoint(); + return path; +} + +QgsFields QgsVectorTileUtils::makeQgisFields( QSet flds ) +{ + QgsFields fields; + for ( QString fieldName : flds ) + { + fields.append( QgsField( fieldName, QVariant::String ) ); + } + return fields; +} + + +int QgsVectorTileUtils::scaleToZoomLevel( double mapScale, int sourceMinZoom, int sourceMaxZoom ) +{ + double s0 = 559082264.0287178; // scale denominator at zoom level 0 of GoogleCRS84Quad + double tileZoom2 = log( s0 / mapScale ) / log( 2 ); + tileZoom2 -= 1; // TODO: it seems that map scale is double (is that because of high-dpi screen?) + int tileZoom = static_cast( round( tileZoom2 ) ); + + if ( tileZoom < sourceMinZoom ) + tileZoom = sourceMinZoom; + if ( tileZoom > sourceMaxZoom ) + tileZoom = sourceMaxZoom; + + return tileZoom; +} + +QgsVectorLayer *QgsVectorTileUtils::makeVectorLayerForTile( QgsVectorTileLayer *mvt, QgsTileXYZ tileID, const QString &layerName ) +{ + QgsVectorTileMVTDecoder decoder; + decoder.decode( tileID, mvt->getRawTile( tileID ) ); + QSet fieldNames = QSet::fromList( decoder.layerFieldNames( layerName ) ); + fieldNames << QStringLiteral( "_geom_type" ); + QMap perLayerFields; + QgsFields fields = QgsVectorTileUtils::makeQgisFields( fieldNames ); + perLayerFields[layerName] = fields; + QgsVectorTileFeatures data = decoder.layerFeatures( perLayerFields, QgsCoordinateTransform() ); + QgsFeatureList featuresList = data[layerName].toList(); + + // turn all geometries to geom. collections (otherwise they won't be accepted by memory provider) + for ( int i = 0; i < featuresList.count(); ++i ) + { + QgsGeometry g = featuresList[i].geometry(); + QgsGeometryCollection *gc = new QgsGeometryCollection; + const QgsAbstractGeometry *gg = g.constGet(); + if ( const QgsGeometryCollection *ggc = qgsgeometry_cast( gg ) ) + { + for ( int k = 0; k < ggc->numGeometries(); ++k ) + gc->addGeometry( ggc->geometryN( k )->clone() ); + } + else + gc->addGeometry( gg->clone() ); + featuresList[i].setGeometry( QgsGeometry( gc ) ); + } + + QgsVectorLayer *vl = new QgsVectorLayer( QStringLiteral( "GeometryCollection" ), layerName, QStringLiteral( "memory" ) ); + vl->dataProvider()->addAttributes( fields.toList() ); + vl->updateFields(); + bool res = vl->dataProvider()->addFeatures( featuresList ); + Q_ASSERT( res ); + Q_ASSERT( featuresList.count() == vl->featureCount() ); + vl->updateExtents(); + QgsDebugMsgLevel( QStringLiteral( "Layer %1 features %2" ).arg( layerName ).arg( vl->featureCount() ), 2 ); + return vl; +} + + +QString QgsVectorTileUtils::formatXYZUrlTemplate( const QString &url, QgsTileXYZ tile ) +{ + QString turl( url ); + + turl.replace( QLatin1String( "{x}" ), QString::number( tile.column() ), Qt::CaseInsensitive ); + // TODO: inverted Y axis +// if ( turl.contains( QLatin1String( "{-y}" ) ) ) +// { +// turl.replace( QLatin1String( "{-y}" ), QString::number( tm.matrixHeight - tile.tileRow - 1 ), Qt::CaseInsensitive ); +// } +// else + { + turl.replace( QLatin1String( "{y}" ), QString::number( tile.row() ), Qt::CaseInsensitive ); + } + turl.replace( QLatin1String( "{z}" ), QString::number( tile.zoomLevel() ), Qt::CaseInsensitive ); + return turl; +} + +//! a helper class for ordering tile requests according to the distance from view center +struct LessThanTileRequest +{ + QPointF center; //!< Center in tile matrix (!) coordinates + bool operator()( const QgsTileXYZ &req1, const QgsTileXYZ &req2 ) + { + QPointF p1( req1.column() + 0.5, req1.row() + 0.5 ); + QPointF p2( req2.column() + 0.5, req2.row() + 0.5 ); + // using chessboard distance (loading order more natural than euclidean/manhattan distance) + double d1 = std::max( std::fabs( center.x() - p1.x() ), std::fabs( center.y() - p1.y() ) ); + double d2 = std::max( std::fabs( center.x() - p2.x() ), std::fabs( center.y() - p2.y() ) ); + return d1 < d2; + } +}; + +QVector QgsVectorTileUtils::tilesInRange( const QgsTileRange &range, int zoomLevel ) +{ + QVector tiles; + for ( int tileRow = range.startRow(); tileRow <= range.endRow(); ++tileRow ) + { + for ( int tileColumn = range.startColumn(); tileColumn <= range.endColumn(); ++tileColumn ) + { + tiles.append( QgsTileXYZ( tileColumn, tileRow, zoomLevel ) ); + } + } + return tiles; +} + +void QgsVectorTileUtils::sortTilesByDistanceFromCenter( QVector &tiles, const QPointF ¢er ) +{ + LessThanTileRequest cmp; + cmp.center = center; + std::sort( tiles.begin(), tiles.end(), cmp ); +} diff --git a/src/core/vectortile/qgsvectortileutils.h b/src/core/vectortile/qgsvectortileutils.h new file mode 100644 index 000000000000..4ceb86dbbdfa --- /dev/null +++ b/src/core/vectortile/qgsvectortileutils.h @@ -0,0 +1,64 @@ +/*************************************************************************** + qgsvectortileutils.h + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSVECTORTILEUTILS_H +#define QGSVECTORTILEUTILS_H + +#define SIP_NO_FILE + +#include + +class QPointF; +class QPolygon; + +class QgsCoordinateTransform; +class QgsFields; +class QgsMapToPixel; +class QgsRectangle; +class QgsVectorLayer; + +class QgsTileMatrix; +class QgsTileRange; +class QgsTileXYZ; +class QgsVectorTileLayer; + +/** + * \ingroup core + * Random utility functions for working with vector tiles + * + * \since QGIS 3.14 + */ +class QgsVectorTileUtils +{ + public: + + //! Returns a list of tiles in the given tile range + static QVector tilesInRange( const QgsTileRange &range, int zoomLevel ); + //! Orders tile requests according to the distance from view center (given in tile matrix coords) + static void sortTilesByDistanceFromCenter( QVector &tiles, const QPointF ¢er ); + + //! Returns polygon (made by four corners of the tile) in screen coordinates + static QPolygon tilePolygon( QgsTileXYZ id, const QgsCoordinateTransform &ct, const QgsTileMatrix &tm, const QgsMapToPixel &mtp ); + //! Returns QgsFields instance based on the set of field names + static QgsFields makeQgisFields( QSet flds ); + //! Finds best fitting zoom level (assuming GoogleCRS84Quad tile matrix set) given map scale denominator and allowed zoom level range + static int scaleToZoomLevel( double mapScale, int sourceMinZoom, int sourceMaxZoom ); + //! Returns a temporary vector layer for given sub-layer of tile in vector tile layer + static QgsVectorLayer *makeVectorLayerForTile( QgsVectorTileLayer *mvt, QgsTileXYZ tileID, const QString &layerName ); + //! Returns formatted tile URL string replacing {x}, {y}, {z} placeholders + static QString formatXYZUrlTemplate( const QString &url, QgsTileXYZ tile ); +}; + +#endif // QGSVECTORTILEUTILS_H diff --git a/src/core/vectortile/vector_tile.proto b/src/core/vectortile/vector_tile.proto new file mode 100644 index 000000000000..f9aa8023d786 --- /dev/null +++ b/src/core/vectortile/vector_tile.proto @@ -0,0 +1,82 @@ +syntax = "proto2"; // needed by newer protoc compilers +// The rest of the file is a verbatim copy of MVT 2.1 proto file: +// https://github.com/mapbox/vector-tile-spec/blob/master/2.1/vector_tile.proto + +package vector_tile; + +option optimize_for = LITE_RUNTIME; + +message Tile { + + // GeomType is described in section 4.3.4 of the specification + enum GeomType { + UNKNOWN = 0; + POINT = 1; + LINESTRING = 2; + POLYGON = 3; + } + + // Variant type encoding + // The use of values is described in section 4.1 of the specification + message Value { + // Exactly one of these values must be present in a valid message + optional string string_value = 1; + optional float float_value = 2; + optional double double_value = 3; + optional int64 int_value = 4; + optional uint64 uint_value = 5; + optional sint64 sint_value = 6; + optional bool bool_value = 7; + + extensions 8 to max; + } + + // Features are described in section 4.2 of the specification + message Feature { + optional uint64 id = 1 [ default = 0 ]; + + // Tags of this feature are encoded as repeated pairs of + // integers. + // A detailed description of tags is located in sections + // 4.2 and 4.4 of the specification + repeated uint32 tags = 2 [ packed = true ]; + + // The type of geometry stored in this feature. + optional GeomType type = 3 [ default = UNKNOWN ]; + + // Contains a stream of commands and parameters (vertices). + // A detailed description on geometry encoding is located in + // section 4.3 of the specification. + repeated uint32 geometry = 4 [ packed = true ]; + } + + // Layers are described in section 4.1 of the specification + message Layer { + // Any compliant implementation must first read the version + // number encoded in this message and choose the correct + // implementation for this version number before proceeding to + // decode other parts of this message. + required uint32 version = 15 [ default = 1 ]; + + required string name = 1; + + // The actual features in this tile. + repeated Feature features = 2; + + // Dictionary encoding for keys + repeated string keys = 3; + + // Dictionary encoding for values + repeated Value values = 4; + + // Although this is an "optional" field it is required by the specification. + // See https://github.com/mapbox/vector-tile-spec/issues/47 + optional uint32 extent = 5 [ default = 4096 ]; + + extensions 16 to max; + } + + repeated Layer layers = 3; + + extensions 16 to 8191; +} diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 3e1efdd206b1..74995087087b 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -1197,6 +1197,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core/metadata ${CMAKE_SOURCE_DIR}/src/core/expression ${CMAKE_SOURCE_DIR}/src/core/validity + ${CMAKE_SOURCE_DIR}/src/core/vectortile ${CMAKE_SOURCE_DIR}/src/native ${CMAKE_SOURCE_DIR}/external ${CMAKE_SOURCE_DIR}/external/nlohmann diff --git a/src/gui/layertree/qgslayertreeembeddedwidgetsimpl.cpp b/src/gui/layertree/qgslayertreeembeddedwidgetsimpl.cpp index f4c0f754fde6..08b73551505a 100644 --- a/src/gui/layertree/qgslayertreeembeddedwidgetsimpl.cpp +++ b/src/gui/layertree/qgslayertreeembeddedwidgetsimpl.cpp @@ -73,6 +73,7 @@ QgsLayerTreeOpacityWidget::QgsLayerTreeOpacityWidget( QgsMapLayer *layer ) case QgsMapLayerType::PluginLayer: case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: break; } @@ -112,6 +113,7 @@ void QgsLayerTreeOpacityWidget::updateOpacityFromSlider() case QgsMapLayerType::PluginLayer: case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: break; } @@ -152,6 +154,7 @@ bool QgsLayerTreeOpacityWidget::Provider::supportsLayer( QgsMapLayer *layer ) return true; case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: return false; } diff --git a/src/gui/qgsbrowserdockwidget_p.cpp b/src/gui/qgsbrowserdockwidget_p.cpp index 2fd1931766ad..147f008232d3 100644 --- a/src/gui/qgsbrowserdockwidget_p.cpp +++ b/src/gui/qgsbrowserdockwidget_p.cpp @@ -44,6 +44,7 @@ #include "qgsnative.h" #include "qgsmaptoolpan.h" #include "qgsvectorlayercache.h" +#include "qgsvectortilelayer.h" #include "qgsattributetablemodel.h" #include "qgsattributetablefiltermodel.h" #include "qgsapplication.h" @@ -208,6 +209,13 @@ void QgsBrowserLayerProperties::setItem( QgsDataItem *item ) break; } + case QgsMapLayerType::VectorTileLayer: + { + QgsDebugMsgLevel( QStringLiteral( "creating vector tile layer" ), 2 ); + mLayer = qgis::make_unique< QgsVectorTileLayer >( layerItem->uri(), layerItem->name() ); + break; + } + case QgsMapLayerType::PluginLayer: { // TODO: support display of properties for plugin layers diff --git a/src/gui/qgsidentifymenu.cpp b/src/gui/qgsidentifymenu.cpp index cd973bd07546..f387c00d8278 100644 --- a/src/gui/qgsidentifymenu.cpp +++ b/src/gui/qgsidentifymenu.cpp @@ -126,6 +126,10 @@ QList QgsIdentifyMenu::exec( const QList &layers ) } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } @@ -128,6 +129,7 @@ QgsLayerRestorer::~QgsLayerRestorer() } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } diff --git a/src/server/services/wms/qgswmsdescribelayer.cpp b/src/server/services/wms/qgswmsdescribelayer.cpp index eef78646bd3a..eb6944fbebb4 100644 --- a/src/server/services/wms/qgswmsdescribelayer.cpp +++ b/src/server/services/wms/qgswmsdescribelayer.cpp @@ -191,6 +191,7 @@ namespace QgsWms } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } diff --git a/src/server/services/wms/qgswmsgetcapabilities.cpp b/src/server/services/wms/qgswmsgetcapabilities.cpp index 9f365e6b7331..46ad89958173 100644 --- a/src/server/services/wms/qgswmsgetcapabilities.cpp +++ b/src/server/services/wms/qgswmsgetcapabilities.cpp @@ -1956,6 +1956,7 @@ namespace QgsWms } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } diff --git a/src/server/services/wms/qgswmsrenderer.cpp b/src/server/services/wms/qgswmsrenderer.cpp index 1d271edc39ea..b1446e6162a1 100644 --- a/src/server/services/wms/qgswmsrenderer.cpp +++ b/src/server/services/wms/qgswmsrenderer.cpp @@ -2725,6 +2725,7 @@ namespace QgsWms } case QgsMapLayerType::MeshLayer: + case QgsMapLayerType::VectorTileLayer: case QgsMapLayerType::PluginLayer: break; } diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index be74141a7b23..d717b457a47e 100644 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -31,6 +31,7 @@ INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/core/classification ${CMAKE_SOURCE_DIR}/src/core/mesh + ${CMAKE_SOURCE_DIR}/src/core/vectortile ${CMAKE_SOURCE_DIR}/src/test ${CMAKE_BINARY_DIR}/src/core @@ -237,6 +238,7 @@ SET(TESTS testqgsvectorlayerjoinbuffer.cpp testqgsvectorlayer.cpp testqgsvectorlayerutils.cpp + testqgsvectortilelayer.cpp testqgsziputils.cpp testziplayer.cpp testqgslayerdefinition.cpp diff --git a/tests/src/core/testqgsvectortilelayer.cpp b/tests/src/core/testqgsvectortilelayer.cpp new file mode 100644 index 000000000000..00ee27d5b731 --- /dev/null +++ b/tests/src/core/testqgsvectortilelayer.cpp @@ -0,0 +1,138 @@ +/*************************************************************************** + testqgsvectortilelayer.cpp + -------------------------------------- + Date : March 2020 + Copyright : (C) 2020 by Martin Dobias + Email : wonder dot sk at gmail dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstest.h" +#include +#include + +//qgis includes... +#include "qgsapplication.h" +#include "qgsproject.h" +#include "qgsrenderchecker.h" +#include "qgstiles.h" +#include "qgsvectortilebasicrenderer.h" +#include "qgsvectortilelayer.h" + +/** + * \ingroup UnitTests + * This is a unit test for a vector tile layer + */ +class TestQgsVectorTileLayer : public QObject +{ + Q_OBJECT + + public: + TestQgsVectorTileLayer() = default; + + private: + QString mDataDir; + QgsVectorTileLayer *mLayer = nullptr; + QString mReport; + QgsMapSettings *mMapSettings = nullptr; + + bool imageCheck( const QString &testType, QgsVectorTileLayer *layer, QgsRectangle extent ); + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init() {} // will be called before each testfunction is executed. + void cleanup() {} // will be called after every testfunction. + + void test_basic(); + void test_render(); +}; + + +void TestQgsVectorTileLayer::initTestCase() +{ + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + QgsApplication::showSettings(); + mDataDir = QString( TEST_DATA_DIR ); //defined in CmakeLists.txt + mDataDir += "/vector_tile"; + + QgsDataSourceUri ds; + ds.setParam( "type", "xyz" ); + ds.setParam( "url", QString( "file://%1/{z}-{x}-{y}.pbf" ).arg( mDataDir ) ); + ds.setParam( "zmax", "1" ); + mLayer = new QgsVectorTileLayer( ds.encodedUri(), "Vector Tiles Test" ); + QVERIFY( mLayer->isValid() ); + + QgsProject::instance()->addMapLayer( mLayer ); + + mMapSettings = new QgsMapSettings(); + mMapSettings->setLayers( QList() << mLayer ); + + // let's have some standard style config for the layer + QColor polygonFillColor = Qt::blue; + QColor polygonStrokeColor = polygonFillColor; + polygonFillColor.setAlpha( 100 ); + double polygonStrokeWidth = DEFAULT_LINE_WIDTH * 2; + QColor lineStrokeColor = Qt::blue; + double lineStrokeWidth = DEFAULT_LINE_WIDTH * 2; + QColor pointFillColor = Qt::red; + QColor pointStrokeColor = pointFillColor; + pointFillColor.setAlpha( 100 ); + double pointSize = DEFAULT_POINT_SIZE; + + QgsVectorTileBasicRenderer *rend = new QgsVectorTileBasicRenderer; + rend->setStyles( QgsVectorTileBasicRenderer::simpleStyle( + polygonFillColor, polygonStrokeColor, polygonStrokeWidth, + lineStrokeColor, lineStrokeWidth, + pointFillColor, pointStrokeColor, pointSize ) ); + mLayer->setRenderer( rend ); // takes ownership +} + +void TestQgsVectorTileLayer::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsVectorTileLayer::test_basic() +{ + // tile fetch test + QByteArray tile0rawData = mLayer->getRawTile( QgsTileXYZ( 0, 0, 0 ) ); + QCOMPARE( tile0rawData.length(), 64822 ); + + QByteArray invalidTileRawData = mLayer->getRawTile( QgsTileXYZ( 0, 0, 99 ) ); + QCOMPARE( invalidTileRawData.length(), 0 ); +} + + +bool TestQgsVectorTileLayer::imageCheck( const QString &testType, QgsVectorTileLayer *layer, QgsRectangle extent ) +{ + mReport += "

" + testType + "

\n"; + mMapSettings->setExtent( extent ); + mMapSettings->setDestinationCrs( layer->crs() ); + mMapSettings->setOutputDpi( 96 ); + QgsRenderChecker myChecker; + myChecker.setControlPathPrefix( QStringLiteral( "vector_tile" ) ); + myChecker.setControlName( "expected_" + testType ); + myChecker.setMapSettings( *mMapSettings ); + myChecker.setColorTolerance( 15 ); + bool myResultFlag = myChecker.runTest( testType, 0 ); + mReport += myChecker.report(); + return myResultFlag; +} + +void TestQgsVectorTileLayer::test_render() +{ + QVERIFY( imageCheck( "render_test_basic", mLayer, mLayer->extent() ) ); +} + + +QGSTEST_MAIN( TestQgsVectorTileLayer ) +#include "testqgsvectortilelayer.moc" diff --git a/tests/testdata/control_images/vector_tile/expected_render_test_basic/expected_render_test_basic.png b/tests/testdata/control_images/vector_tile/expected_render_test_basic/expected_render_test_basic.png new file mode 100644 index 000000000000..13e0421ed2b0 Binary files /dev/null and b/tests/testdata/control_images/vector_tile/expected_render_test_basic/expected_render_test_basic.png differ diff --git a/tests/testdata/vector_tile/0-0-0.pbf b/tests/testdata/vector_tile/0-0-0.pbf new file mode 100644 index 000000000000..3a2e0450500e Binary files /dev/null and b/tests/testdata/vector_tile/0-0-0.pbf differ diff --git a/tests/testdata/vector_tile/1-0-0.pbf b/tests/testdata/vector_tile/1-0-0.pbf new file mode 100644 index 000000000000..876a0b7ddf59 Binary files /dev/null and b/tests/testdata/vector_tile/1-0-0.pbf differ diff --git a/tests/testdata/vector_tile/1-0-1.pbf b/tests/testdata/vector_tile/1-0-1.pbf new file mode 100644 index 000000000000..3c4f544d9874 Binary files /dev/null and b/tests/testdata/vector_tile/1-0-1.pbf differ diff --git a/tests/testdata/vector_tile/1-1-0.pbf b/tests/testdata/vector_tile/1-1-0.pbf new file mode 100644 index 000000000000..cfcc8aedc45a Binary files /dev/null and b/tests/testdata/vector_tile/1-1-0.pbf differ diff --git a/tests/testdata/vector_tile/1-1-1.pbf b/tests/testdata/vector_tile/1-1-1.pbf new file mode 100644 index 000000000000..896ef86722a6 Binary files /dev/null and b/tests/testdata/vector_tile/1-1-1.pbf differ diff --git a/tests/testdata/vector_tile/README.md b/tests/testdata/vector_tile/README.md new file mode 100644 index 000000000000..81c38e58dcbe --- /dev/null +++ b/tests/testdata/vector_tile/README.md @@ -0,0 +1,15 @@ + +# Vector Tiles Test Data + +Downloaded from https://api.maptiler.com/tiles/v3/{z}/{x}/{y}.pbf?key=XXX +(where XXX stands for a key that can be obtained when registered at MapTiler: https://www.maptiler.com/) + +## License + +These tiles are coming from OpenMapTiles project, under the "Free data" terms of use: https://openmaptiles.com/terms/ + +The FREE tiles are legally usable for: + +- open-source and open-data community project websites +- non-commercial personal projects +- evaluation and education purposes