Initial work on vector tile layer support

This commit is contained in:
Martin Dobias 2020-03-25 13:27:11 +01:00
parent d10267f3aa
commit 8105ad1fe5
23 changed files with 5894 additions and 3 deletions

View File

@ -331,6 +331,11 @@ 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}")
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

View File

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

View File

@ -534,3 +534,4 @@
%Include auto_generated/gps/qgsgpsconnectionregistry.sip
%Include auto_generated/symbology/qgsmasksymbollayer.sip
%Include auto_generated/qgsuserprofile.sip
%Include auto_generated/vectortile/qgsvectortilelayer.sip

View File

@ -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/*|python/ext-libs/*|ui_*.py|*.astyle|tests/testdata/*|editors/*|src/core/vectortile/*.pb.*)
echo -ne "$f skipped $elcr"
continue
;;

View File

@ -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,14 @@ 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
vectortile/vector_tile.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/qgsexpression_texts.cpp
qgsuserprofile.cpp
@ -833,6 +843,7 @@ SET(QGIS_CORE_HDRS
qgsmapunitscale.h
qgsmargins.h
qgsmaskidprovider.h
qgsmbtilesreader.h
qgsmessagelog.h
qgsmessageoutput.h
qgsmimedatautils.h
@ -924,6 +935,7 @@ SET(QGIS_CORE_HDRS
qgstextrenderer.h
qgsthreadingutils.h
qgstilecache.h
qgstiles.h
qgstolerance.h
qgstracer.h
qgstrackedvectorlayertools.h
@ -1319,6 +1331,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
@ -1408,6 +1428,7 @@ INCLUDE_DIRECTORIES(
symbology
mesh
validity
vectortile
${CMAKE_SOURCE_DIR}/external
${CMAKE_SOURCE_DIR}/external/nlohmann
${CMAKE_SOURCE_DIR}/external/kdbush/include
@ -1429,6 +1450,8 @@ INCLUDE_DIRECTORIES(SYSTEM
${QCA_INCLUDE_DIR}
${QTKEYCHAIN_INCLUDE_DIR}
${Qt5SerialPort_INCLUDE_DIRS}
${Protobuf_INCLUDE_DIRS}
${ZLIB_INCLUDE_DIRS}
)
@ -1563,6 +1586,8 @@ TARGET_LINK_LIBRARIES(qgis_core
${SQLITE3_LIBRARY}
${SPATIALITE_LIBRARY}
${LIBZIP_LIBRARY}
${Protobuf_LIBRARIES}
${ZLIB_LIBRARIES}
)
IF (FORCE_STATIC_PROVIDERS)

View File

@ -16,6 +16,7 @@
#ifndef QGSMBTILESREADER_H
#define QGSMBTILESREADER_H
#include "qgis_core.h"
#include "sqlite3.h"
#include "qgssqliteutils.h"
@ -23,7 +24,7 @@
class QImage;
class QgsRectangle;
class QgsMBTilesReader
class CORE_EXPORT QgsMBTilesReader
{
public:
explicit QgsMBTilesReader( const QString &filename );

View File

@ -0,0 +1,259 @@
/***************************************************************************
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 "qgslinesymbollayer.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;
}
void QgsVectorTileBasicRendererStyle::setSymbol( QgsSymbol *sym )
{
mSymbol.reset( sym );
}
void QgsVectorTileBasicRendererStyle::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const
{
elem.setAttribute( "name", mStyleName );
elem.setAttribute( "layer", mLayerName );
elem.setAttribute( "geometry", mGeometryType );
elem.setAttribute( "enabled", mEnabled ? "1" : "0" );
elem.setAttribute( "expression", mExpression );
elem.setAttribute( "min-zoom", mMinZoomLevel );
elem.setAttribute( "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( "name" );
mLayerName = elem.attribute( "layer" );
mGeometryType = static_cast<QgsWkbTypes::GeometryType>( elem.attribute( "geometry" ).toInt() );
mEnabled = elem.attribute( "enabled" ).toInt();
mExpression = elem.attribute( "expression" );
mMinZoomLevel = elem.attribute( "min-zoom" ).toInt();
mMaxZoomLevel = elem.attribute( "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()
{
setDefaultStyle();
}
QString QgsVectorTileBasicRenderer::type() const
{
return "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<QString, QSet<QString> > 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" ) );
scope->setFields( fields );
context.expressionContext().appendScope( 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 );
delete context.expressionContext().popScope();
}
}
void QgsVectorTileBasicRenderer::writeXml( QDomElement &elem, const QgsReadWriteContext &context ) const
{
QDomDocument doc = elem.ownerDocument();
QDomElement elemStyles = doc.createElement( "styles" );
for ( const QgsVectorTileBasicRendererStyle &layerStyle : mStyles )
{
QDomElement elemStyle = doc.createElement( "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( "styles" );
QDomElement elemStyle = elemStyles.firstChildElement( "style" );
while ( !elemStyle.isNull() )
{
QgsVectorTileBasicRendererStyle layerStyle;
layerStyle.readXml( elemStyle, context );
mStyles.append( layerStyle );
}
}
void QgsVectorTileBasicRenderer::setStyles( const QList<QgsVectorTileBasicRendererStyle> &styles )
{
mStyles = styles;
}
QList<QgsVectorTileBasicRendererStyle> QgsVectorTileBasicRenderer::styles() const
{
return mStyles;
}
void QgsVectorTileBasicRenderer::setDefaultStyle()
{
QColor color = Qt::blue;
QColor polygonColor = color;
polygonColor.setAlpha( 100 );
QColor pointColor = Qt::red;
QgsFillSymbol *polygonSymbol = static_cast<QgsFillSymbol *>( QgsLineSymbol::defaultSymbol( QgsWkbTypes::PolygonGeometry ) );
polygonSymbol->setColor( polygonColor );
QgsLineSymbol *lineSymbol = static_cast<QgsLineSymbol *>( QgsLineSymbol::defaultSymbol( QgsWkbTypes::LineGeometry ) );
lineSymbol->setColor( color );
QgsMarkerSymbol *pointSymbol = static_cast<QgsMarkerSymbol *>( QgsLineSymbol::defaultSymbol( QgsWkbTypes::PointGeometry ) );
pointSymbol->setColor( pointColor );
QgsVectorTileBasicRendererStyle st1( "polygons", QString(), QgsWkbTypes::PolygonGeometry );
st1.setSymbol( polygonSymbol );
QgsVectorTileBasicRendererStyle st2( "lines", QString(), QgsWkbTypes::LineGeometry );
st2.setSymbol( lineSymbol );
QgsVectorTileBasicRendererStyle st3( "points", QString(), QgsWkbTypes::PointGeometry );
st3.setSymbol( pointSymbol );
QList<QgsVectorTileBasicRendererStyle> lst;
lst << st1 << st2 << st3;
setStyles( lst );
}

View File

@ -0,0 +1,156 @@
/***************************************************************************
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 "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
*/
struct QgsVectorTileBasicRendererStyle
{
public:
//! Constructs a style object
QgsVectorTileBasicRendererStyle( const QString &stName = QString(), const QString &laName = QString(), QgsWkbTypes::GeometryType geomType = QgsWkbTypes::UnknownGeometry );
QgsVectorTileBasicRendererStyle( const QgsVectorTileBasicRendererStyle &other );
QgsVectorTileBasicRendererStyle &operator=( const QgsVectorTileBasicRendererStyle &other );
//! 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 );
//! 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<QgsSymbol> 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 QgsVectorTileBasicRenderer : public QgsVectorTileRenderer
{
public:
//! Constructs renderer with some default styles
QgsVectorTileBasicRenderer();
QString type() const override;
QgsVectorTileBasicRenderer *clone() const override;
void startRender( QgsRenderContext &context, int tileZoom, const QgsTileRange &tileRange ) override;
QMap<QString, QSet<QString> > usedAttributes( const QgsRenderContext & ) override;
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<QgsVectorTileBasicRendererStyle> &styles );
//! Returns list of styles of the renderer
QList<QgsVectorTileBasicRendererStyle> styles() const;
private:
void setDefaultStyle();
private:
//! List of rendering styles
QList<QgsVectorTileBasicRendererStyle> mStyles;
// temporary bits
//! Names of required fields for each sub-layer (only valid between startRender/stopRender calls)
QMap<QString, QSet<QString> > mRequiredFields;
};
#endif // QGSVECTORTILEBASICRENDERER_H

View File

@ -0,0 +1,176 @@
/***************************************************************************
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 "qgsvectortilelayerrenderer.h"
#include "qgsmbtilesreader.h"
#include "qgsvectortilebasicrenderer.h"
#include "qgsvectortileloader.h"
#include "qgsdatasourceuri.h"
QgsVectorTileLayer::QgsVectorTileLayer( const QString &uri, const QString &baseName )
: QgsPluginLayer( "vector-tile", baseName )
{
mDataSource = uri;
QgsDataSourceUri dsUri;
dsUri.setEncodedUri( uri );
mSourceType = dsUri.param( "type" );
mSourcePath = dsUri.param( "url" );
if ( mSourceType == "xyz" )
{
// online tiles
mSourceMinZoom = 0;
mSourceMaxZoom = 14;
setExtent( QgsRectangle( -20037508.3427892, -20037508.3427892, 20037508.3427892, 20037508.3427892 ) );
}
else if ( mSourceType == "mbtiles" )
{
QgsMBTilesReader reader( mSourcePath );
if ( !reader.open() )
{
qDebug() << "failed to open MBTiles file:" << mSourcePath;
return;
}
qDebug() << "name:" << reader.metadataValue( "name" );
bool minZoomOk, maxZoomOk;
int minZoom = reader.metadataValue( "minzoom" ).toInt( &minZoomOk );
int maxZoom = reader.metadataValue( "maxzoom" ).toInt( &maxZoomOk );
if ( minZoomOk )
mSourceMinZoom = minZoom;
if ( maxZoomOk )
mSourceMaxZoom = maxZoom;
qDebug() << "zoom range:" << mSourceMinZoom << mSourceMaxZoom;
QgsRectangle r = reader.extent();
// TODO: reproject to EPSG:3857
setExtent( r );
}
else
{
// TODO: report error - unknown type
return;
}
setCrs( QgsCoordinateReferenceSystem( "EPSG:3857" ) );
setValid( true );
// set a default renderer
setRenderer( new QgsVectorTileBasicRenderer );
}
QgsVectorTileLayer::~QgsVectorTileLayer() = default;
QgsPluginLayer *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( "type", "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( "renderer" );
if ( elemRenderer.isNull() )
{
errorMessage = "Missing <renderer> tag";
return false;
}
QString rendererType = elemRenderer.attribute( "type" );
QgsVectorTileRenderer *r = nullptr;
if ( rendererType == "basic" )
r = new QgsVectorTileBasicRenderer;
//else if ( rendererType == "mapbox-gl" )
// r = new MapboxGLStyleRenderer;
else
{
errorMessage = "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( "renderer" );
elemRenderer.setAttribute( "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<QgsVectorTileRawData> 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();
}

View File

@ -0,0 +1,148 @@
/***************************************************************************
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 "qgspluginlayer.h"
class QgsVectorTileRenderer;
struct 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 QgsPluginLayer
{
public:
//! Constructs a new vector tile layer
explicit QgsVectorTileLayer( const QString &path = QString(), const QString &baseName = QString() );
~QgsVectorTileLayer();
// implementation of virtual functions from QgsMapLayer
QgsPluginLayer *clone() const override;
virtual QgsMapLayerRenderer *createMapRenderer( QgsRenderContext &rendererContext ) override;
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_SKIP;
//! Returns currently assigned renderer
QgsVectorTileRenderer *renderer() const SIP_SKIP;
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<QgsVectorTileRenderer> mRenderer;
};
#endif // QGSVECTORTILELAYER_H

View File

@ -0,0 +1,184 @@
/***************************************************************************
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 <QElapsedTimer>
#include "qgsexpressioncontextutils.h"
#include "qgsfeedback.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() )
, mFeedback( new QgsFeedback )
{
}
bool QgsVectorTileLayerRenderer::render()
{
QgsRenderContext &ctx = *renderContext();
if ( ctx.renderingStopped() )
return false;
QElapsedTimer tTotal;
tTotal.start();
qDebug() << "MVT rend" << ctx.extent().toString( -1 );
mTileZoom = QgsVectorTileUtils::scaleToZoomLevel( ctx.rendererScale(), mSourceMinZoom, mSourceMaxZoom );
qDebug() << "MVT zoom level" << mTileZoom;
mTileMatrix = QgsTileMatrix::fromWebMercator( mTileZoom );
mTileRange = mTileMatrix.tileRangeFromExtent( ctx.extent() );
qDebug() << "MVT tile range" << mTileRange.startColumn() << mTileRange.endColumn() << " | " << mTileRange.startRow() << mTileRange.endRow();
// view center is used to sort the order of tiles for fetching and rendering
QPointF viewCenter = mTileMatrix.mapToTileCoordinates( ctx.extent().center() );
if ( !mTileRange.isValid() )
{
qDebug() << "outside of range";
return true; // nothing to do
}
bool isAsync = ( mSourceType == "xyz" );
std::unique_ptr<QgsVectorTileLoader> asyncLoader;
QList<QgsVectorTileRawData> rawTiles;
if ( !isAsync )
{
QElapsedTimer tFetch;
tFetch.start();
rawTiles = QgsVectorTileLoader::blockingFetchTileRawData( mSourceType, mSourcePath, mTileZoom, viewCenter, mTileRange );
qDebug() << "FETCH TIME" << tFetch.elapsed() / 1000.;
qDebug() << "fetched tiles:" << rawTiles.count();
}
else
{
asyncLoader.reset( new QgsVectorTileLoader( mSourcePath, mTileZoom, mTileRange, viewCenter, mFeedback.get() ) );
QObject::connect( asyncLoader.get(), &QgsVectorTileLoader::tileRequestFinished, [this]( const QgsVectorTileRawData & rawTile )
{
qDebug() << "got async tile" << rawTile.id.column() << rawTile.id.row() << rawTile.id.zoomLevel();
if ( !rawTile.data.isEmpty() )
decodeAndDrawTile( rawTile );
} );
}
if ( ctx.renderingStopped() )
return false;
mRenderer->startRender( *renderContext(), mTileZoom, mTileRange );
QMap<QString, QSet<QString> > requiredFields = mRenderer->usedAttributes( *renderContext() );
QMap<QString, QgsFields> 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 cancelled 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 );
qDebug() << "DECODE TIME" << mTotalDecodeTime / 1000.;
qDebug() << "DRAW TIME" << mTotalDrawTime / 1000.;
qDebug() << "TOTAL TIME" << tTotal.elapsed() / 1000.;
return !ctx.renderingStopped();
}
void QgsVectorTileLayerRenderer::decodeAndDrawTile( const QgsVectorTileRawData &rawTile )
{
QgsRenderContext &ctx = *renderContext();
qDebug() << "decoding tile " << rawTile.id.zoomLevel() << rawTile.id.column() << rawTile.id.row();
QElapsedTimer tLoad;
tLoad.start();
// currently only MVT encoding supported
QgsVectorTileMVTDecoder decoder;
if ( !decoder.decode( rawTile.id, rawTile.data ) )
{
qDebug() << "Failed to parse raw tile data!";
return;
}
if ( ctx.renderingStopped() )
return;
QgsVectorTileRendererData tile;
tile.id = rawTile.id;
tile.features = decoder.layerFeatures( mPerLayerFields );
mTotalDecodeTime += tLoad.elapsed();
// calculate tile polygon in screen coordinates
tile.tilePolygon = QgsVectorTileUtils::tilePolygon( rawTile.id, mTileMatrix, ctx.mapToPixel() );
if ( ctx.renderingStopped() )
return;
// set up clipping so that rendering does not go behind tile's extent
ctx.painter()->setClipRegion( QRegion( tile.tilePolygon ) );
qDebug() << "drawing tile" << tile.id.zoomLevel() << tile.id.column() << tile.id.row();
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 );
}
}

View File

@ -0,0 +1,83 @@
/***************************************************************************
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
#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<QgsVectorTileRenderer> mRenderer;
//! Whether to draw boundaries of tiles (useful for debugging)
bool mDrawTileBoundaries = true;
// temporary data used during rendering process
//! Feedback object that may be used by the caller to cancel the rendering
std::unique_ptr<QgsFeedback> 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<QString, QgsFields> 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

View File

@ -0,0 +1,265 @@
/***************************************************************************
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 <QtDebug>
#include <QEventLoop>
#include <zlib.h>
#include "qgsblockingnetworkrequest.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;
}
qDebug() << "starting loader";
QVector<QgsTileXYZ> tiles = QgsVectorTileUtils::tilesInRange( range, zoomLevel );
QgsVectorTileUtils::sortTilesByDistanceFromCenter( tiles, viewCenter );
for ( QgsTileXYZ id : qgis::as_const( tiles ) )
{
loadFromNetworkAsync( id, uri );
}
}
QgsVectorTileLoader::~QgsVectorTileLoader()
{
qDebug() << "terminating loader";
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()
{
qDebug() << "starting event loop" << mReplies.count() << "requests";
if ( mFeedback && mFeedback->isCanceled() )
{
qDebug() << "actually not - we were cancelled";
return; // nothing to do
}
mEventLoop->exec( QEventLoop::ExcludeUserInputEvents );
qDebug() << "download blocking finished";
Q_ASSERT( mReplies.isEmpty() );
}
void QgsVectorTileLoader::loadFromNetworkAsync( const QgsTileXYZ &id, const QString &requestUrl )
{
QString url = QgsVectorTileUtils::formatXYZUrlTemplate( requestUrl, id );
QNetworkRequest request( url );
// TODO: some extra headers? QgsSetRequestInitiatorClass / auth / "Accept" header
request.setAttribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 1 ), id.column() );
request.setAttribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 2 ), id.row() );
request.setAttribute( static_cast<QNetworkRequest::Attribute>( 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<QNetworkReply *>( sender() );
int reqX = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 1 ) ).toInt();
int reqY = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 2 ) ).toInt();
int reqZ = reply->request().attribute( static_cast<QNetworkRequest::Attribute>( QNetworkRequest::User + 3 ) ).toInt();
QgsTileXYZ tileID( reqX, reqY, reqZ );
if ( reply->error() == QNetworkReply::NoError )
{
// TODO: handle redirections?
qDebug() << "tile reply - all good!";
QByteArray rawData = reply->readAll();
mReplies.removeOne( reply );
reply->deleteLater();
emit tileRequestFinished( QgsVectorTileRawData( tileID, rawData ) );
}
else
{
qDebug() << "tile reply - error! " << 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()
{
qDebug() << "cancelling pending requests";
const QList<QNetworkReply *> replies = mReplies;
for ( QNetworkReply *reply : replies )
{
qDebug() << "aborting request";
reply->abort();
}
}
//////
QList<QgsVectorTileRawData> QgsVectorTileLoader::blockingFetchTileRawData( const QString &sourceType, const QString &sourcePath, int zoomLevel, const QPointF &viewCenter, const QgsTileRange &range )
{
QList<QgsVectorTileRawData> rawTiles;
QgsMBTilesReader mbReader( sourcePath );
bool isUrl = ( sourceType == "xyz" );
if ( !isUrl )
{
bool res = mbReader.open();
Q_ASSERT( res );
}
QVector<QgsTileXYZ> 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;
qDebug() << "requestiong" << url;
QgsBlockingNetworkRequest::ErrorCode errCode = req.get( nr );
qDebug() << "get" << errCode;
QgsNetworkReplyContent reply = req.reply();
qDebug() << "content size" << reply.content().size();
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() )
{
qDebug() << "Failed to get tile" << id.zoomLevel() << id.column() << id.row();
return QByteArray();
}
// TODO: check format is "pbf"
QByteArray data;
if ( !decodeGzip( gzippedTileData, data ) )
{
qDebug() << "failed to decompress tile" << id.zoomLevel() << id.column() << id.row();
return QByteArray();
}
qDebug() << "tile blob size" << gzippedTileData.size() << " -> uncompressed size" << data.size();
return data;
}
bool QgsVectorTileLoader::decodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut )
{
unsigned char *bytesInPtr = reinterpret_cast<unsigned char *>( const_cast<char *>( bytesIn.constData() ) );
uint bytesInLeft = static_cast<uint>( 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<const char *>( out ), static_cast<int>( have ) ) );
}
while ( strm.avail_out == 0 );
}
inflateEnd( &strm );
return ret == Z_STREAM_END;
}

View File

@ -0,0 +1,100 @@
/***************************************************************************
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
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:
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<QgsVectorTileRawData> 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 signle 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<QEventLoop> mEventLoop;
//! Feedback object that allows cancellation of pending requests
QgsFeedback *mFeedback;
//! Running tile requests
QList<QNetworkReply *> mReplies;
};
#endif // QGSVECTORTILELOADER_H

View File

@ -0,0 +1,322 @@
/***************************************************************************
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 <string>
#include <QtDebug>
#include "qgsvectortilemvtdecoder.h"
#include "qgsvectortilelayerrenderer.h"
#include "qgsvectortileutils.h"
#include "qgsmultipoint.h"
#include "qgslinestring.h"
#include "qgsmultilinestring.h"
#include "qgsmultipolygon.h"
#include "qgspolygon.h"
inline bool _isExteriorRing( const QVector<QgsPoint> &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<QString, QgsFields> &perLayerFields ) const
{
QgsVectorTileFeatures features;
int numTiles = static_cast<int>( 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<QgsFeature> 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<int, int> 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<QgsFeatureId>( feature.id() ) );
// initialize all fields to empty string.
// TODO: this should not be necessary, but some rules don't like NULL
for ( int i = 0; i < layerFields.count(); ++i )
f.setAttribute( i, QStringLiteral( "" ) );
//
// parse attributes
//
for ( int tagNum = 0; tagNum < feature.tags_size(); tagNum += 2 )
{
int keyIndex = static_cast<int>( feature.tags( tagNum ) );
int fieldIndex = tagKeyIndexToFieldIndex.value( keyIndex, -1 );
if ( fieldIndex == -1 )
continue;
int valueIndex = static_cast<int>( feature.tags( tagNum + 1 ) );
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<double>( 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<int>( value.int_value() ) );
else if ( value.has_uint_value() )
f.setAttribute( fieldIndex, static_cast<int>( value.uint_value() ) );
else if ( value.has_sint_value() )
f.setAttribute( fieldIndex, static_cast<int>( value.sint_value() ) );
else if ( value.has_bool_value() )
f.setAttribute( fieldIndex, static_cast<int>( value.bool_value() ) ); // or keep it bool? (do we have good support for that?)
else
{
// TODO: report - should not happen
Q_ASSERT( false );
}
}
//
// parse geometry
//
int extent = static_cast<int>( layer.extent() );
int cursorx = 0, cursory = 0;
QVector<QgsPoint *> outputPoints; // for point/multi-point
QVector<QgsLineString *> outputLinestrings; // for linestring/multi-linestring
QVector<QgsPolygon *> outputPolygons;
QVector<QgsPoint> 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
{
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
{
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)
Q_ASSERT( outputPolygons.count() != 0 ); // TODO: better error handling
outputPolygons[outputPolygons.count() - 1]->addInteriorRing( new QgsLineString( tmpPoints ) );
tmpPoints.clear();
}
}
}
else
{
qDebug() << "huh?"; // TODO: handle properly
}
}
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( "ty_pe", geomType );
layerFeatures.append( f );
}
features[layerName] = layerFeatures;
}
return features;
}

View File

@ -0,0 +1,56 @@
/***************************************************************************
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
class QgsFeature;
#include <QStringList>
#include <QMap>
#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<QString, QgsFields> &perLayerFields ) const;
private:
vector_tile::Tile tile;
QgsTileXYZ mTileID;
QMap<QString, int> mLayerNameToIndex;
};
#endif // QGSVECTORTILEMVTDECODER_H

View File

@ -0,0 +1,90 @@
/***************************************************************************
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<QString, QVector<QgsFeature> > QgsVectorTileFeatures;
/**
* \ingroup core
* Contains decoded features of a single vector tile and any other data necessary
* for rendering of it.
*
* \since QGIS 3.14
*/
struct QgsVectorTileRendererData
{
//! Position of the tile in the tile matrix set
QgsTileXYZ id;
//! Features of the tile grouped into sub-layers
QgsVectorTileFeatures features;
//! Polygon (made out of four corners of the tile) in screen coordinates calculated from render context
QPolygon tilePolygon;
};
/**
* \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
{
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;
//! 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<QString, QSet<QString> > usedAttributes( const QgsRenderContext & ) = 0;
//! 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

View File

@ -0,0 +1,171 @@
/***************************************************************************
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 <math.h>
#include <QPolygon>
#include <QtDebug>
#include "qgscoordinatetransform.h"
#include "qgsgeometrycollection.h"
#include "qgsfields.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 QgsTileMatrix &tm, const QgsMapToPixel &mtp )
{
QgsRectangle r = tm.tileExtent( id );
QgsPointXY p00a = mtp.transform( r.xMinimum(), r.yMinimum() );
QgsPointXY p11a = mtp.transform( r.xMaximum(), r.yMaximum() );
QgsPointXY p01a = mtp.transform( r.xMinimum(), r.yMaximum() );
QgsPointXY p10a = mtp.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<QString> flds )
{
QgsFields fields;
for ( QString fieldName : flds )
{
fields.append( QgsField( fieldName, QVariant::String ) );
}
return fields;
}
int QgsVectorTileUtils::scaleToZoomLevel( double mapScale, int sourceMinZoom, int sourceMaxZoom )
{
qDebug() << "MVT map scale 1 :" << mapScale;
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<int>( 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 ) );
qDebug() << decoder.layers();
QSet<QString> fieldNames = QSet<QString>::fromList( decoder.layerFieldNames( layerName ) );
fieldNames << "ty_pe"; // geom. type
QMap<QString, QgsFields> perLayerFields;
QgsFields fields = QgsVectorTileUtils::makeQgisFields( fieldNames );
perLayerFields[layerName] = fields;
QgsVectorTileFeatures data = decoder.layerFeatures( perLayerFields );
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<const QgsGeometryCollection *>( 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( "GeometryCollection", layerName, "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();
qDebug() << "layer" << layerName << "features" << vl->featureCount();
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<QgsTileXYZ> QgsVectorTileUtils::tilesInRange( const QgsTileRange &range, int zoomLevel )
{
QVector<QgsTileXYZ> 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<QgsTileXYZ> &tiles, const QPointF &center )
{
LessThanTileRequest cmp;
cmp.center = center;
std::sort( tiles.begin(), tiles.end(), cmp );
}

View File

@ -0,0 +1,62 @@
/***************************************************************************
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
#include <QSet>
class QPointF;
class QPolygon;
class QgsCoordinateTransform;
class QgsFields;
class QgsMapToPixel;
class QgsRectangle;
class QgsVectorLayer;
class QgsTileMatrix;
class QgsTileRange;
struct 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<QgsTileXYZ> 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<QgsTileXYZ> &tiles, const QPointF &center );
//! Returns polygon (made by four corners of the tile) in screen coordinates
static QPolygon tilePolygon( QgsTileXYZ id, const QgsTileMatrix &tm, const QgsMapToPixel &mtp );
//! Returns QgsFields instance based on the set of field names
static QgsFields makeQgisFields( QSet<QString> 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
SET (WMS_SRCS
qgsmbtilesreader.cpp
qgswmscapabilities.cpp
qgswmsprovider.cpp
qgswmsconnection.cpp