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 @@
+
+
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