Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Encode and write vector tiles in different CRS than EPSG:3857
The Mapbox Vector Tile specification provides these definition for projection and bounds.

A Vector Tile represents data based on a square extent within a projection. A Vector Tile SHOULD NOT contain information about its bounds and projection. The file format assumes that the decoder knows the bounds and projection of a Vector Tile before decoding it.

Web Mercator is the projection of reference, and the Google tile scheme is the tile extent convention of reference. Together, they provide a 1-to-1 relationship between a specific geographical area, at a specific level of detail, and a path such as https://example.com/17/65535/43602.mvt.

Vector Tiles MAY be used to represent data with any projection and tile extent scheme.

It is possible to encode and write vector tiles in different CRS than EPSG:3857.

The implementation used the CRS bounds to defined the tile 0 top left coordinates and the scale denominator for 0 zoom level.
  • Loading branch information
rldhont committed Sep 10, 2021
1 parent 95328ee commit da42404
Show file tree
Hide file tree
Showing 6 changed files with 212 additions and 10 deletions.
Expand Up @@ -128,7 +128,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.
Sets extent of vector tile output.
If unset, it will use the full extent of all input layers combined
%End

Expand All @@ -154,6 +154,11 @@ Sets that will be written to the output dataset. See class description for more
void setTransformContext( const QgsCoordinateTransformContext &transformContext );
%Docstring
Sets coordinate transform context for transforms between layers and tile matrix CRS
%End

bool setRootTileMatrix( const QgsTileMatrix &tileMatrix );
%Docstring
Sets zoom level 0 tile matrix
%End

bool writeTiles( QgsFeedback *feedback = 0 );
Expand Down
10 changes: 9 additions & 1 deletion src/core/vectortile/qgsvectortilemvtencoder.cpp
Expand Up @@ -145,14 +145,22 @@ QgsVectorTileMVTEncoder::QgsVectorTileMVTEncoder( QgsTileXYZ tileID )
{
const QgsTileMatrix tm = QgsTileMatrix::fromWebMercator( mTileID.zoomLevel() );
mTileExtent = tm.tileExtent( mTileID );
mCrs = tm.crs();
}

QgsVectorTileMVTEncoder::QgsVectorTileMVTEncoder( QgsTileXYZ tileID, const QgsTileMatrix &tileMatrix )
: mTileID( tileID )
{
mTileExtent = tileMatrix.tileExtent( mTileID );
mCrs = tileMatrix.crs();
}

void QgsVectorTileMVTEncoder::addLayer( QgsVectorLayer *layer, QgsFeedback *feedback, QString filterExpression, QString layerName )
{
if ( feedback && feedback->isCanceled() )
return;

const QgsCoordinateTransform ct( layer->crs(), QgsCoordinateReferenceSystem( "EPSG:3857" ), mTransformContext );
const QgsCoordinateTransform ct( layer->crs(), mCrs, mTransformContext );

QgsRectangle layerTileExtent = mTileExtent;
try
Expand Down
6 changes: 5 additions & 1 deletion src/core/vectortile/qgsvectortilemvtencoder.h
Expand Up @@ -40,9 +40,12 @@
class CORE_EXPORT QgsVectorTileMVTEncoder
{
public:
//! Creates MVT encoder for the given tile coordinates
//! Creates MVT encoder for the given tile coordinates for Web Mercator
explicit QgsVectorTileMVTEncoder( QgsTileXYZ tileID );

//! Creates MVT encoder for the given tile coordinates and tile matrix
explicit QgsVectorTileMVTEncoder( QgsTileXYZ tileID, const QgsTileMatrix &tileMatrix );

//! Returns resolution of coordinates of geometries within the tile. The default is 4096.
int resolution() const { return mResolution; }
//! Sets the resolution of coordinates of geometries within the tile
Expand Down Expand Up @@ -76,6 +79,7 @@ class CORE_EXPORT QgsVectorTileMVTEncoder
QgsCoordinateTransformContext mTransformContext;

QgsRectangle mTileExtent;
QgsCoordinateReferenceSystem mCrs;

QgsVectorTileFeatures mFeatures;

Expand Down
24 changes: 18 additions & 6 deletions src/core/vectortile/qgsvectortilewriter.cpp
Expand Up @@ -35,6 +35,18 @@

QgsVectorTileWriter::QgsVectorTileWriter()
{
setRootTileMatrix( QgsTileMatrix::fromWebMercator( 0 ) );
}


bool QgsVectorTileWriter::setRootTileMatrix( const QgsTileMatrix &tileMatrix )
{
if ( tileMatrix.isRootTileMatrix() )
{
mRootTileMatrix = tileMatrix;
return true;
}
return false;
}


Expand Down Expand Up @@ -94,7 +106,7 @@ bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
int tilesToCreate = 0;
for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
{
QgsTileMatrix tileMatrix = QgsTileMatrix::fromWebMercator( zoomLevel );
QgsTileMatrix tileMatrix = QgsTileMatrix::fromTileMatrix( zoomLevel, mRootTileMatrix );

QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
tilesToCreate += ( tileRange.endRow() - tileRange.startRow() + 1 ) *
Expand Down Expand Up @@ -137,7 +149,7 @@ bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
{
try
{
QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( "EPSG:3857" ), QgsCoordinateReferenceSystem( "EPSG:4326" ), mTransformContext );
QgsCoordinateTransform ct( mRootTileMatrix.crs(), QgsCoordinateReferenceSystem( "EPSG:4326" ), mTransformContext );
QgsRectangle wgsExtent = ct.transform( outputExtent );
QString boundsStr = QString( "%1,%2,%3,%4" )
.arg( wgsExtent.xMinimum() ).arg( wgsExtent.yMinimum() )
Expand All @@ -149,12 +161,14 @@ bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
// bounds won't be written (not a problem - it is an optional value)
}
}
if ( !mMetadata.contains( "crs" ) )
mbtiles->setMetadataValue( "crs", mRootTileMatrix.crs().authid() );
}

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

QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
for ( int row = tileRange.startRow(); row <= tileRange.endRow(); ++row )
Expand Down Expand Up @@ -216,12 +230,11 @@ bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
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 );
QgsCoordinateTransform ct( vl->crs(), mRootTileMatrix.crs(), mTransformContext );
try
{
QgsRectangle r = ct.transformBoundingBox( vl->extent() );
Expand Down Expand Up @@ -318,4 +331,3 @@ QByteArray QgsVectorTileWriter::writeSingleTile( QgsTileXYZ tileID, QgsFeedback

return encoder.encode();
}

10 changes: 9 additions & 1 deletion src/core/vectortile/qgsvectortilewriter.h
Expand Up @@ -17,8 +17,10 @@
#define QGSVECTORTILEWRITER_H

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

class QgsFeedback;
class QgsTileMatrix;
Expand Down Expand Up @@ -128,7 +130,7 @@ class CORE_EXPORT QgsVectorTileWriter
void setDestinationUri( const QString &uri ) { mDestinationUri = uri; }

/**
* Sets extent of vector tile output. Currently always in EPSG:3857.
* Sets extent of vector tile output.
* If unset, it will use the full extent of all input layers combined
*/
void setExtent( const QgsRectangle &extent ) { mExtent = extent; }
Expand All @@ -147,6 +149,11 @@ class CORE_EXPORT QgsVectorTileWriter
//! Sets coordinate transform context for transforms between layers and tile matrix CRS
void setTransformContext( const QgsCoordinateTransformContext &transformContext ) { mTransformContext = transformContext; }

/**
* Sets zoom level 0 tile matrix
*/
bool setRootTileMatrix( const QgsTileMatrix &tileMatrix );

/**
* Writes vector tiles according to the configuration.
* Returns TRUE on success (upon failure one can get error cause using errorMessage())
Expand Down Expand Up @@ -184,6 +191,7 @@ class CORE_EXPORT QgsVectorTileWriter
QString mbtilesJsonSchema();

private:
QgsTileMatrix mRootTileMatrix;
QgsRectangle mExtent;
int mMinZoom = 0;
int mMaxZoom = 4;
Expand Down
165 changes: 165 additions & 0 deletions tests/src/core/testqgsvectortilewriter.cpp
Expand Up @@ -54,6 +54,8 @@ class TestQgsVectorTileWriter : public QObject
void test_mbtiles();
void test_mbtiles_metadata();
void test_filtering();
void test_z0TileMatrix3857();
void test_z0TileMatrix2154();
};


Expand Down Expand Up @@ -316,5 +318,168 @@ void TestQgsVectorTileWriter::test_filtering()
}


void TestQgsVectorTileWriter::test_z0TileMatrix3857()
{
QTemporaryDir dir;
dir.setAutoRemove( false ); // so that we can inspect the results later
const QString tmpDir = dir.path();

QgsDataSourceUri ds;
ds.setParam( "type", "xyz" );
ds.setParam( "url", QUrl::fromLocalFile( tmpDir ).toString() + "/custom3857-{z}-{x}-{y}.pbf" );

QgsVectorLayer *vlPoints = new QgsVectorLayer( mDataDir + "/points.shp", "points", "ogr" );
QgsVectorLayer *vlLines = new QgsVectorLayer( mDataDir + "/lines.shp", "lines", "ogr" );
QgsVectorLayer *vlPolys = new QgsVectorLayer( mDataDir + "/polys.shp", "polys", "ogr" );

QList<QgsVectorTileWriter::Layer> layers;
layers << QgsVectorTileWriter::Layer( vlPoints );
layers << QgsVectorTileWriter::Layer( vlLines );
layers << QgsVectorTileWriter::Layer( vlPolys );

QgsVectorTileWriter writer;
writer.setDestinationUri( ds.encodedUri() );
writer.setMaxZoom( 3 );
writer.setLayers( layers );

QgsTileMatrix tm0 = QgsTileMatrix::fromCustomDef( 0, QgsCoordinateReferenceSystem( "EPSG:3857" ), QgsPointXY( -20037508.3427892, 20037508.3427892 ), 40075016.6855784 );
writer.setRootTileMatrix( tm0 );
writer.setExtent( tm0.extent() );

const bool res = writer.writeTiles();
QVERIFY( res );
QVERIFY( writer.errorMessage().isEmpty() );

delete vlPoints;
delete vlLines;
delete vlPolys;

// check on the file level
const QDir dirInfo( tmpDir );
const QStringList dirFiles = dirInfo.entryList( QStringList( "*.pbf" ) );
QCOMPARE( dirFiles.count(), 8 ); // 1 tile at z0, 1 tile at z1, 2 tiles at z2, 4 tiles at z3
QVERIFY( dirFiles.contains( "custom3857-0-0-0.pbf" ) );

QgsVectorTileLayer *vtLayer = new QgsVectorTileLayer( ds.encodedUri(), "output" );

const QByteArray tile0 = vtLayer->getRawTile( QgsTileXYZ( 0, 0, 0 ) );
QgsVectorTileMVTDecoder decoder;
const bool resDecode0 = decoder.decode( QgsTileXYZ( 0, 0, 0 ), tile0 );
QVERIFY( resDecode0 );
const QStringList layerNames = decoder.layers();
QCOMPARE( layerNames, QStringList() << "points" << "lines" << "polys" );
const QStringList fieldNamesLines = decoder.layerFieldNames( "lines" );
QCOMPARE( fieldNamesLines, QStringList() << "Name" << "Value" );

QgsFields fieldsPolys;
fieldsPolys.append( QgsField( "Name", QVariant::String ) );
QMap<QString, QgsFields> perLayerFields;
perLayerFields["polys"] = fieldsPolys;
perLayerFields["lines"] = QgsFields();
perLayerFields["points"] = QgsFields();
QgsVectorTileFeatures features0 = decoder.layerFeatures( perLayerFields, QgsCoordinateTransform() );
QCOMPARE( features0["points"].count(), 17 );
QCOMPARE( features0["lines"].count(), 6 );
QCOMPARE( features0["polys"].count(), 10 );

QCOMPARE( features0["points"][0].geometry().wkbType(), QgsWkbTypes::Point );
QCOMPARE( features0["lines"][0].geometry().wkbType(), QgsWkbTypes::LineString );
QCOMPARE( features0["polys"][0].geometry().wkbType(), QgsWkbTypes::MultiPolygon ); // source geoms in shp are multipolygons

QgsAttributes attrsPolys0_0 = features0["polys"][0].attributes();
QCOMPARE( attrsPolys0_0.count(), 1 );
const QString attrNamePolys0_0 = attrsPolys0_0[0].toString();
QVERIFY( attrNamePolys0_0 == "Dam" || attrNamePolys0_0 == "Lake" );

delete vtLayer;
}


void TestQgsVectorTileWriter::test_z0TileMatrix2154()
{
QTemporaryDir dir;
dir.setAutoRemove( false ); // so that we can inspect the results later
const QString tmpDir = dir.path();

QgsDataSourceUri ds;
ds.setParam( "type", "xyz" );
ds.setParam( "url", QUrl::fromLocalFile( tmpDir ).toString() + "/custom2154-{z}-{x}-{y}.pbf" );

QgsVectorLayer *vlPoints = new QgsVectorLayer( mDataDir + "/points.shp", "points", "ogr" );
QgsVectorLayer *vlLines = new QgsVectorLayer( mDataDir + "/lines.shp", "lines", "ogr" );
QgsVectorLayer *vlPolys = new QgsVectorLayer( mDataDir + "/polys.shp", "polys", "ogr" );

QList<QgsVectorTileWriter::Layer> layers;
layers << QgsVectorTileWriter::Layer( vlPoints );
layers << QgsVectorTileWriter::Layer( vlLines );
layers << QgsVectorTileWriter::Layer( vlPolys );

QgsVectorTileWriter writer;
writer.setDestinationUri( ds.encodedUri() );
writer.setMaxZoom( 3 );
writer.setLayers( layers );

const QgsCoordinateReferenceSystem crs( "EPSG:2154" );
const QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( "EPSG:4326" ), crs, QgsCoordinateTransformContext() );
QgsRectangle r = ct.transformBoundingBox( crs.bounds() );
double z0Dimension = r.width();
if ( r.height() > z0Dimension )
{
z0Dimension = r.height();
}
QgsTileMatrix tm0 = QgsTileMatrix::fromCustomDef( 0, crs, QgsPointXY( r.xMinimum(), r.yMaximum() ), z0Dimension );

writer.setRootTileMatrix( tm0 );
writer.setExtent( r );

const bool res = writer.writeTiles();
QVERIFY( res );
QVERIFY( writer.errorMessage().isEmpty() );

delete vlPoints;
delete vlLines;
delete vlPolys;

// check on the file level
const QDir dirInfo( tmpDir );
const QStringList dirFiles = dirInfo.entryList( QStringList( "*.pbf" ) );
QCOMPARE( dirFiles.count(), 8 ); // 1 tile at z0, 1 tile at z1, 2 tiles at z2, 4 tiles at z3
QVERIFY( dirFiles.contains( "custom2154-0-0-0.pbf" ) );

QgsVectorTileLayer *vtLayer = new QgsVectorTileLayer( ds.encodedUri(), "output" );

const QByteArray tile0 = vtLayer->getRawTile( QgsTileXYZ( 0, 0, 0 ) );
QgsVectorTileMVTDecoder decoder;
const bool resDecode0 = decoder.decode( QgsTileXYZ( 0, 0, 0 ), tile0 );
QVERIFY( resDecode0 );
const QStringList layerNames = decoder.layers();
QCOMPARE( layerNames, QStringList() << "points" << "lines" << "polys" );
const QStringList fieldNamesLines = decoder.layerFieldNames( "lines" );
QCOMPARE( fieldNamesLines, QStringList() << "Name" << "Value" );

QgsFields fieldsPolys;
fieldsPolys.append( QgsField( "Name", QVariant::String ) );
QMap<QString, QgsFields> perLayerFields;
perLayerFields["polys"] = fieldsPolys;
perLayerFields["lines"] = QgsFields();
perLayerFields["points"] = QgsFields();
QgsVectorTileFeatures features0 = decoder.layerFeatures( perLayerFields, QgsCoordinateTransform() );
QCOMPARE( features0["points"].count(), 17 );
QCOMPARE( features0["lines"].count(), 6 );
QCOMPARE( features0["polys"].count(), 10 );

QCOMPARE( features0["points"][0].geometry().wkbType(), QgsWkbTypes::Point );
QCOMPARE( features0["lines"][0].geometry().wkbType(), QgsWkbTypes::LineString );
QCOMPARE( features0["polys"][0].geometry().wkbType(), QgsWkbTypes::MultiPolygon ); // source geoms in shp are multipolygons

QgsAttributes attrsPolys0_0 = features0["polys"][0].attributes();
QCOMPARE( attrsPolys0_0.count(), 1 );
const QString attrNamePolys0_0 = attrsPolys0_0[0].toString();
QVERIFY( attrNamePolys0_0 == "Dam" || attrNamePolys0_0 == "Lake" );

delete vtLayer;
}


QGSTEST_MAIN( TestQgsVectorTileWriter )
#include "testqgsvectortilewriter.moc"

0 comments on commit da42404

Please sign in to comment.