Skip to content

Commit

Permalink
Improvements to the vector tile writer
Browse files Browse the repository at this point in the history
- filtering of input layers by expressions and min/max zoom level
- custom layer names in the output
- writing of custom metadata for MBTiles output
- auto-calculate output extent (instead of defaulting to the whole world's extent)
- passing transform context to the encoder
  • Loading branch information
wonder-sk committed Apr 27, 2020
1 parent 53ba864 commit c2a7f25
Show file tree
Hide file tree
Showing 4 changed files with 303 additions and 16 deletions.
71 changes: 70 additions & 1 deletion python/core/auto_generated/vectortile/qgsvectortilewriter.sip.in
Expand Up @@ -31,6 +31,24 @@ be ordinary file system path, e.g.: /home/qgis/output.mbtiles

Currently the writer only support MVT encoding of data.

Metadata support: it is possible to pass a QVariantMap with metadata. This
is backend dependent. Currently only "mbtiles" source type supports writing
of metadata. The key-value pairs will be written to the "metadata" table
in the MBTiles file. Some useful metadata keys listed here, but see MBTiles spec
for more details:
- "name" - human-readable name of the tileset
- "bounds" - extent in WGS 84: "minlon,minlat,maxlon,maxlat"
- "center" - default view of the map: "longitude,latitude,zoomlevel"
- "minzoom" - lowest zoom level
- "maxzoom" - highest zoom level
- "attribution" - string that explains the sources of data
- "description" - descriptions of the content
- "type" - whether this is an overlay or a basemap
- "version" - version of the tileset
Vector tile writer always writes "format" and "json" metadata. If not specified
in metadata by the client, tile writer also writes "name", "bounds", "minzoom"
and "maxzoom".

.. versionadded:: 3.14
%End

Expand Down Expand Up @@ -60,6 +78,42 @@ Constructs an entry for a vector layer
QgsVectorLayer *layer() const;
%Docstring
Returns vector layer of this entry
%End

QString filterExpression() const;
%Docstring
Returns filter expression. If not empty, only features matching the expression will be used
%End
void setFilterExpression( const QString &expr );
%Docstring
Sets filter expression. If not empty, only features matching the expression will be used
%End

QString layerName() const;
%Docstring
Returns layer name in the output. If not set, layer()->name() will be used.
%End
void setLayerName( const QString &name );
%Docstring
Sets layer name in the output. If not set, layer()->name() will be used.
%End

int minZoom() const;
%Docstring
Returns minimum zoom level at which this layer will be used. Negative value means no min. zoom level
%End
void setMinZoom( int minzoom );
%Docstring
Sets minimum zoom level at which this layer will be used. Negative value means no min. zoom level
%End

int maxZoom() const;
%Docstring
Returns maximum zoom level at which this layer will be used. Negative value means no max. zoom level
%End
void setMaxZoom( int maxzoom );
%Docstring
Sets maximum zoom level at which this layer will be used. Negative value means no max. zoom level
%End

};
Expand All @@ -73,7 +127,7 @@ See the class description about the syntax of destination URIs.
void setExtent( const QgsRectangle &extent );
%Docstring
Sets extent of vector tile output. Currently always in EPSG:3857.
If unset, it will use the standard range of the Web Mercator system.
If unset, it will use the full extent of all input layers combined
%End

void setMinZoom( int minZoom );
Expand All @@ -88,6 +142,16 @@ Sets the maximum zoom level of tiles. Allowed values are in interval [0,24]
void setLayers( const QList<QgsVectorTileWriter::Layer> &layers );
%Docstring
Sets vector layers and their configuration for output of vector tiles
%End

void setMetadata( const QVariantMap &metadata );
%Docstring
Sets that will be written to the output dataset. See class description for more on metadata support
%End

void setTransformContext( const QgsCoordinateTransformContext &transformContext );
%Docstring
Sets coordinate transform context for transforms between layers and tile matrix CRS
%End

bool writeTiles( QgsFeedback *feedback = 0 );
Expand All @@ -103,6 +167,11 @@ provide cancellation functionality.
%Docstring
Returns error message related to the previous call to writeTiles(). Will return
an empty string if writing was successful.
%End

QgsRectangle fullExtent() const;
%Docstring
Returns calculated extent that combines extent of all input layers
%End

};
Expand Down
84 changes: 72 additions & 12 deletions src/core/vectortile/qgsvectortilewriter.cpp
Expand Up @@ -18,6 +18,7 @@
#include "qgsdatasourceuri.h"
#include "qgsfeedback.h"
#include "qgsjsonutils.h"
#include "qgslogger.h"
#include "qgsmbtiles.h"
#include "qgstiles.h"
#include "qgsvectorlayer.h"
Expand All @@ -34,7 +35,6 @@

QgsVectorTileWriter::QgsVectorTileWriter()
{
mExtent = QgsTileMatrix::fromWebMercator( 0 ).tileExtent( QgsTileXYZ( 0, 0, 0 ) );
}


Expand Down Expand Up @@ -73,13 +73,24 @@ bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
return false;
}

QgsRectangle outputExtent = mExtent;
if ( outputExtent.isEmpty() )
{
outputExtent = fullExtent();
if ( outputExtent.isEmpty() )
{
mErrorMessage = tr( "Failed to calculate output extent" );
return false;
}
}

// figure out how many tiles we will need to do
int tilesToCreate = 0;
for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
{
QgsTileMatrix tileMatrix = QgsTileMatrix::fromWebMercator( zoomLevel );

QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( mExtent );
QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
tilesToCreate += ( tileRange.endRow() - tileRange.startRow() + 1 ) *
( tileRange.endColumn() - tileRange.startColumn() + 1 );
}
Expand All @@ -99,35 +110,62 @@ bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
}

// required metadata
mbtiles->setMetadataValue( "name", "???" ); // TODO: custom name?
mbtiles->setMetadataValue( "format", "pbf" );
mbtiles->setMetadataValue( "json", mbtilesJsonSchema() );

// optional metadata
mbtiles->setMetadataValue( "bounds", "-180.0,-85,180,85" );
mbtiles->setMetadataValue( "minzoom", QString::number( mMinZoom ) );
mbtiles->setMetadataValue( "maxzoom", QString::number( mMaxZoom ) );
// TODO: "center"? initial view with [lon,lat,zoom]
// metadata specified by the client
const QStringList metaKeys = mMetadata.keys();
for ( const QString &key : metaKeys )
{
mbtiles->setMetadataValue( key, mMetadata[key].toString() );
}

// required metadata for vector tiles: "json" with schema of layers
mbtiles->setMetadataValue( "json", mbtilesJsonSchema() );
// default metadata that we always write (if not written by the client)
if ( !mMetadata.contains( "name" ) )
mbtiles->setMetadataValue( "name", "unnamed" ); // required by the spec
if ( !mMetadata.contains( "minzoom" ) )
mbtiles->setMetadataValue( "minzoom", QString::number( mMinZoom ) );
if ( !mMetadata.contains( "maxzoom" ) )
mbtiles->setMetadataValue( "maxzoom", QString::number( mMaxZoom ) );
if ( !mMetadata.contains( "bounds" ) )
{
QString boundsStr = "-180,-85,180,85"; // fallback value - whole world except for poles
try
{
QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( "EPSG:3857" ), QgsCoordinateReferenceSystem( "EPSG:4326" ), mTransformContext );
QgsRectangle wgsExtent = ct.transform( outputExtent );
boundsStr = QString( "%1,%2,%3,%4" )
.arg( wgsExtent.xMinimum() ).arg( wgsExtent.yMinimum() )
.arg( wgsExtent.xMaximum() ).arg( wgsExtent.yMaximum() );
}
catch ( const QgsCsException & )
{
}
mbtiles->setMetadataValue( "bounds", boundsStr );
}
}

int tilesCreated = 0;
for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
{
QgsTileMatrix tileMatrix = QgsTileMatrix::fromWebMercator( zoomLevel );

QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( mExtent );
QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
for ( int row = tileRange.startRow(); row <= tileRange.endRow(); ++row )
{
for ( int col = tileRange.startColumn(); col <= tileRange.endColumn(); ++col )
{
QgsTileXYZ tileID( col, row, zoomLevel );
QgsVectorTileMVTEncoder encoder( tileID );
encoder.setTransformContext( mTransformContext );

for ( const Layer &layer : qgis::as_const( mLayers ) )
{
encoder.addLayer( layer.layer(), feedback );
if ( ( layer.minZoom() >= 0 && zoomLevel < layer.minZoom() ) ||
( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) )
continue;

encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() );
}

if ( feedback && feedback->isCanceled() )
Expand Down Expand Up @@ -169,6 +207,28 @@ bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
return true;
}

QgsRectangle QgsVectorTileWriter::fullExtent() const
{
QgsRectangle extent;
QgsCoordinateReferenceSystem destCrs( "EPSG:3857" );

for ( const Layer &layer : mLayers )
{
QgsVectorLayer *vl = layer.layer();
QgsCoordinateTransform ct( vl->crs(), destCrs, mTransformContext );
try
{
QgsRectangle r = ct.transformBoundingBox( vl->extent() );
extent.combineExtentWith( r );
}
catch ( const QgsCsException & )
{
QgsDebugMsg( "Failed to reproject layer extent to destination CRS" );
}
}
return extent;
}

bool QgsVectorTileWriter::writeTileFileXYZ( const QString &sourcePath, QgsTileXYZ tileID, const QByteArray &tileData )
{
QString filePath = QgsVectorTileUtils::formatXYZUrlTemplate( sourcePath, tileID );
Expand Down
59 changes: 56 additions & 3 deletions src/core/vectortile/qgsvectortilewriter.h
Expand Up @@ -18,6 +18,7 @@

#include <QCoreApplication>
#include "qgsrectangle.h"
#include "qgscoordinatetransformcontext.h"

class QgsFeedback;
class QgsTileXYZ;
Expand All @@ -44,6 +45,25 @@ class QgsVectorLayer;
*
* Currently the writer only support MVT encoding of data.
*
* Metadata support: it is possible to pass a QVariantMap with metadata. This
* is backend dependent. Currently only "mbtiles" source type supports writing
* of metadata. The key-value pairs will be written to the "metadata" table
* in the MBTiles file. Some useful metadata keys listed here, but see MBTiles spec
* for more details:
* - "name" - human-readable name of the tileset
* - "bounds" - extent in WGS 84: "minlon,minlat,maxlon,maxlat"
* - "center" - default view of the map: "longitude,latitude,zoomlevel"
* - "minzoom" - lowest zoom level
* - "maxzoom" - highest zoom level
* - "attribution" - string that explains the sources of data
* - "description" - descriptions of the content
* - "type" - whether this is an overlay or a basemap
* - "version" - version of the tileset
* Vector tile writer always writes "format" and "json" metadata. If not specified
* in metadata by the client, tile writer also writes "name", "bounds", "minzoom"
* and "maxzoom".
*
*
* \since QGIS 3.14
*/
class CORE_EXPORT QgsVectorTileWriter
Expand All @@ -70,10 +90,32 @@ class CORE_EXPORT QgsVectorTileWriter
//! Returns vector layer of this entry
QgsVectorLayer *layer() const { return mLayer; }

//! Returns filter expression. If not empty, only features matching the expression will be used
QString filterExpression() const { return mFilterExpression; }
//! Sets filter expression. If not empty, only features matching the expression will be used
void setFilterExpression( const QString &expr ) { mFilterExpression = expr; }

//! Returns layer name in the output. If not set, layer()->name() will be used.
QString layerName() const { return mLayerName; }
//! Sets layer name in the output. If not set, layer()->name() will be used.
void setLayerName( const QString &name ) { mLayerName = name; }

//! Returns minimum zoom level at which this layer will be used. Negative value means no min. zoom level
int minZoom() const { return mMinZoom; }
//! Sets minimum zoom level at which this layer will be used. Negative value means no min. zoom level
void setMinZoom( int minzoom ) { mMinZoom = minzoom; }

//! Returns maximum zoom level at which this layer will be used. Negative value means no max. zoom level
int maxZoom() const { return mMaxZoom; }
//! Sets maximum zoom level at which this layer will be used. Negative value means no max. zoom level
void setMaxZoom( int maxzoom ) { mMaxZoom = maxzoom; }

private:
QgsVectorLayer *mLayer;
//QString mFilterExpression;
//QString mLayerName;
QString mFilterExpression;
QString mLayerName;
int mMinZoom = -1;
int mMaxZoom = -1;
};

/**
Expand All @@ -84,7 +126,7 @@ class CORE_EXPORT QgsVectorTileWriter

/**
* Sets extent of vector tile output. Currently always in EPSG:3857.
* If unset, it will use the standard range of the Web Mercator system.
* If unset, it will use the full extent of all input layers combined
*/
void setExtent( const QgsRectangle &extent ) { mExtent = extent; }

Expand All @@ -96,6 +138,12 @@ class CORE_EXPORT QgsVectorTileWriter
//! Sets vector layers and their configuration for output of vector tiles
void setLayers( const QList<QgsVectorTileWriter::Layer> &layers ) { mLayers = layers; }

//! Sets that will be written to the output dataset. See class description for more on metadata support
void setMetadata( const QVariantMap &metadata ) { mMetadata = metadata; }

//! Sets coordinate transform context for transforms between layers and tile matrix CRS
void setTransformContext( const QgsCoordinateTransformContext &transformContext ) { mTransformContext = transformContext; }

/**
* Writes vector tiles according to the configuration.
* Returns true on success (upon failure one can get error cause using errorMessage())
Expand All @@ -111,6 +159,9 @@ class CORE_EXPORT QgsVectorTileWriter
*/
QString errorMessage() const { return mErrorMessage; }

//! Returns calculated extent that combines extent of all input layers
QgsRectangle fullExtent() const;

private:
bool writeTileFileXYZ( const QString &sourcePath, QgsTileXYZ tileID, const QByteArray &tileData );
QString mbtilesJsonSchema();
Expand All @@ -121,6 +172,8 @@ class CORE_EXPORT QgsVectorTileWriter
int mMaxZoom = 4;
QList<Layer> mLayers;
QString mDestinationUri;
QVariantMap mMetadata;
QgsCoordinateTransformContext mTransformContext;

QString mErrorMessage;
};
Expand Down

0 comments on commit c2a7f25

Please sign in to comment.