mirror of
https://github.com/qgis/QGIS.git
synced 2025-03-01 00:46:20 -05:00
Add writing of vector tiles to MBTiles container
This commit is contained in:
parent
5e70067cdf
commit
6f746b557e
@ -111,6 +111,16 @@ Please note that we follow the XYZ convention of X/Y axes, i.e. top-left tile ha
|
||||
static QgsTileMatrix fromWebMercator( int mZoomLevel );
|
||||
%Docstring
|
||||
Returns a tile matrix for the usual web mercator
|
||||
%End
|
||||
|
||||
int matrixWidth() const;
|
||||
%Docstring
|
||||
Returns number of columns of the tile matrix
|
||||
%End
|
||||
|
||||
int matrixHeight() const;
|
||||
%Docstring
|
||||
Returns number of rows of the tile matrix
|
||||
%End
|
||||
|
||||
QgsRectangle tileExtent( QgsTileXYZ id ) const;
|
||||
|
@ -26,8 +26,8 @@ the "url" key is normally the path. Currently supported types:
|
||||
- "xyz" - tile data written as local files, using a template where {x},{y},{z}
|
||||
are replaced by the actual tile column, row and zoom level numbers, e.g.:
|
||||
file:///home/qgis/tiles/{z}/{x}/{y}.pbf
|
||||
|
||||
(More types such as "mbtiles" or "gpkg" may be added later.)
|
||||
- "mbtiles" - tile data written to a new MBTiles file, the "url" key should
|
||||
be ordinary file system path, e.g.: /home/qgis/output.mbtiles
|
||||
|
||||
Currently the writer only support MVT encoding of data.
|
||||
|
||||
|
@ -18,8 +18,11 @@
|
||||
#include "qgslogger.h"
|
||||
#include "qgsrectangle.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QImage>
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
|
||||
QgsMBTilesReader::QgsMBTilesReader( const QString &filename )
|
||||
: mFilename( filename )
|
||||
@ -46,6 +49,37 @@ bool QgsMBTilesReader::isOpen() const
|
||||
return bool( mDatabase );
|
||||
}
|
||||
|
||||
bool QgsMBTilesReader::create()
|
||||
{
|
||||
if ( mDatabase )
|
||||
return false;
|
||||
|
||||
if ( QFile::exists( mFilename ) )
|
||||
return false;
|
||||
|
||||
sqlite3_database_unique_ptr database;
|
||||
int result = mDatabase.open_v2( mFilename, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr );
|
||||
if ( result != SQLITE_OK )
|
||||
{
|
||||
QgsDebugMsg( QStringLiteral( "Can't create MBTiles database: %1" ).arg( database.errorMessage() ) );
|
||||
return false;
|
||||
}
|
||||
|
||||
QString sql = \
|
||||
"CREATE TABLE metadata (name text, value text);" \
|
||||
"CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob);" \
|
||||
"CREATE UNIQUE INDEX tile_index on tiles (zoom_level, tile_column, tile_row);";
|
||||
QString errorMessage;
|
||||
result = mDatabase.exec( sql, errorMessage );
|
||||
if ( result != SQLITE_OK )
|
||||
{
|
||||
QgsDebugMsg( QStringLiteral( "Failed to initialize MBTiles database: " ) + errorMessage );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
QString QgsMBTilesReader::metadataValue( const QString &key )
|
||||
{
|
||||
if ( !mDatabase )
|
||||
@ -72,6 +106,30 @@ QString QgsMBTilesReader::metadataValue( const QString &key )
|
||||
return preparedStatement.columnAsText( 0 );
|
||||
}
|
||||
|
||||
void QgsMBTilesReader::setMetadataValue( const QString &key, const QString &value )
|
||||
{
|
||||
if ( !mDatabase )
|
||||
{
|
||||
QgsDebugMsg( QStringLiteral( "MBTiles database not open: " ) + mFilename );
|
||||
return;
|
||||
}
|
||||
|
||||
int result;
|
||||
QString sql = QStringLiteral( "insert into metadata values (%1, %2)" ).arg( QgsSqliteUtils::quotedValue( key ), QgsSqliteUtils::quotedValue( value ) );
|
||||
sqlite3_statement_unique_ptr preparedStatement = mDatabase.prepare( sql, result );
|
||||
if ( result != SQLITE_OK )
|
||||
{
|
||||
QgsDebugMsg( QStringLiteral( "MBTile failed to prepare statement: " ) + sql );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( preparedStatement.step() != SQLITE_DONE )
|
||||
{
|
||||
QgsDebugMsg( QStringLiteral( "MBTile metadata value failed to be set: " ) + key );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QgsRectangle QgsMBTilesReader::extent()
|
||||
{
|
||||
QString boundsStr = metadataValue( "bounds" );
|
||||
@ -122,3 +180,127 @@ QImage QgsMBTilesReader::tileDataAsImage( int z, int x, int y )
|
||||
}
|
||||
return tileImage;
|
||||
}
|
||||
|
||||
void QgsMBTilesReader::setTileData( int z, int x, int y, const QByteArray &data )
|
||||
{
|
||||
if ( !mDatabase )
|
||||
{
|
||||
QgsDebugMsg( QStringLiteral( "MBTiles database not open: " ) + mFilename );
|
||||
return;
|
||||
}
|
||||
|
||||
int result;
|
||||
QString sql = QStringLiteral( "insert into tiles values (%1, %2, %3, ?)" ).arg( z ).arg( x ).arg( y );
|
||||
sqlite3_statement_unique_ptr preparedStatement = mDatabase.prepare( sql, result );
|
||||
if ( result != SQLITE_OK )
|
||||
{
|
||||
QgsDebugMsg( QStringLiteral( "MBTile failed to prepare statement: " ) + sql );
|
||||
return;
|
||||
}
|
||||
|
||||
sqlite3_bind_blob( preparedStatement.get(), 1, data.constData(), data.size(), SQLITE_TRANSIENT );
|
||||
|
||||
if ( preparedStatement.step() != SQLITE_DONE )
|
||||
{
|
||||
QgsDebugMsg( QStringLiteral( "MBTile tile failed to be set: %1,%2,%3" ).arg( z ).arg( x ).arg( y ) );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
bool QgsMBTilesReader::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;
|
||||
}
|
||||
|
||||
|
||||
bool QgsMBTilesReader::encodeGzip( 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 deflate state
|
||||
z_stream strm;
|
||||
strm.zalloc = Z_NULL;
|
||||
strm.zfree = Z_NULL;
|
||||
strm.opaque = Z_NULL;
|
||||
|
||||
int ret = deflateInit2( &strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS + DEC_MAGIC_NUM_FOR_GZIP, 8, Z_DEFAULT_STRATEGY );
|
||||
if ( ret != Z_OK )
|
||||
return false;
|
||||
|
||||
strm.avail_in = bytesInLeft;
|
||||
strm.next_in = bytesInPtr;
|
||||
|
||||
// run deflate() on input until output buffer not full, finish
|
||||
// compression if all of source has been read in
|
||||
do
|
||||
{
|
||||
strm.avail_out = CHUNK;
|
||||
strm.next_out = out;
|
||||
ret = deflate( &strm, Z_FINISH ); // no bad return value
|
||||
Q_ASSERT( ret != Z_STREAM_ERROR ); // state not clobbered
|
||||
|
||||
unsigned have = CHUNK - strm.avail_out;
|
||||
bytesOut.append( QByteArray::fromRawData( reinterpret_cast<const char *>( out ), static_cast<int>( have ) ) );
|
||||
}
|
||||
while ( strm.avail_out == 0 );
|
||||
Q_ASSERT( ret == Z_STREAM_END ); // stream will be complete
|
||||
|
||||
// clean up and return
|
||||
deflateEnd( &strm );
|
||||
return true;
|
||||
}
|
||||
|
@ -44,9 +44,22 @@ class CORE_EXPORT QgsMBTilesReader
|
||||
//! Returns whether the MBTiles file is currently opened
|
||||
bool isOpen() const;
|
||||
|
||||
/**
|
||||
* Creates a new MBTiles file and initializes it with metadata and tiles tables.
|
||||
* It is up to the caller to set appropriate metadata entries and add tiles afterwards.
|
||||
* Returns true on success. If the file exists already, returns false.
|
||||
*/
|
||||
bool create();
|
||||
|
||||
//! Requests metadata value for the given key
|
||||
QString metadataValue( const QString &key );
|
||||
|
||||
/**
|
||||
* Sets metadata value for the given key. Does not overwrite existing entries.
|
||||
* \note the database has to be opened in read-write mode (currently only when opened with create()
|
||||
*/
|
||||
void setMetadataValue( const QString &key, const QString &value );
|
||||
|
||||
//! Returns bounding box from metadata, given in WGS 84 (if available)
|
||||
QgsRectangle extent();
|
||||
|
||||
@ -56,6 +69,17 @@ class CORE_EXPORT QgsMBTilesReader
|
||||
//! Returns tile decoded as a raster image (if stored in a known format like JPG or PNG)
|
||||
QImage tileDataAsImage( int z, int x, int y );
|
||||
|
||||
/**
|
||||
* Adds tile data for the given tile coordinates. Does not overwrite existing entries.
|
||||
* \note the database has to be opened in read-write mode (currently only when opened with create()
|
||||
*/
|
||||
void setTileData( int z, int x, int y, const QByteArray &data );
|
||||
|
||||
//! Decodes gzip byte stream, returns true on success. Useful for reading vector tiles.
|
||||
static bool decodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut );
|
||||
//! Encodes gzip byte stream, returns true on success. Useful for writing vector tiles.
|
||||
static bool encodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut );
|
||||
|
||||
private:
|
||||
QString mFilename;
|
||||
sqlite3_database_unique_ptr mDatabase;
|
||||
|
@ -106,6 +106,12 @@ class CORE_EXPORT QgsTileMatrix
|
||||
//! Returns a tile matrix for the usual web mercator
|
||||
static QgsTileMatrix fromWebMercator( int mZoomLevel );
|
||||
|
||||
//! Returns number of columns of the tile matrix
|
||||
int matrixWidth() const { return mMatrixWidth; }
|
||||
|
||||
//! Returns number of rows of the tile matrix
|
||||
int matrixHeight() const { return mMatrixHeight; }
|
||||
|
||||
//! Returns extent of the given tile in this matrix
|
||||
QgsRectangle tileExtent( QgsTileXYZ id ) const;
|
||||
|
||||
|
@ -68,6 +68,13 @@ bool QgsVectorTileLayer::loadDataSource()
|
||||
return false;
|
||||
}
|
||||
|
||||
QString format = reader.metadataValue( QStringLiteral( "format" ) );
|
||||
if ( format != QStringLiteral( "pbf" ) )
|
||||
{
|
||||
QgsDebugMsg( QStringLiteral( "Cannot open MBTiles for vector tiles. Format = " ) + format );
|
||||
return false;
|
||||
}
|
||||
|
||||
QgsDebugMsgLevel( QStringLiteral( "name: " ) + reader.metadataValue( QStringLiteral( "name" ) ), 2 );
|
||||
bool minZoomOk, maxZoomOk;
|
||||
int minZoom = reader.metadataValue( QStringLiteral( "minzoom" ) ).toInt( &minZoomOk );
|
||||
|
@ -17,8 +17,6 @@
|
||||
|
||||
#include <QEventLoop>
|
||||
|
||||
#include <zlib.h>
|
||||
|
||||
#include "qgsblockingnetworkrequest.h"
|
||||
#include "qgslogger.h"
|
||||
#include "qgsmbtilesreader.h"
|
||||
@ -200,10 +198,8 @@ QByteArray QgsVectorTileLoader::loadFromMBTiles( const QgsTileXYZ &id, QgsMBTile
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
// TODO: check format is "pbf"
|
||||
|
||||
QByteArray data;
|
||||
if ( !decodeGzip( gzippedTileData, data ) )
|
||||
if ( !QgsMBTilesReader::decodeGzip( gzippedTileData, data ) )
|
||||
{
|
||||
QgsDebugMsg( QStringLiteral( "Failed to decompress tile " ) + id.toString() );
|
||||
return QByteArray();
|
||||
@ -212,59 +208,3 @@ QByteArray QgsVectorTileLoader::loadFromMBTiles( const QgsTileXYZ &id, QgsMBTile
|
||||
QgsDebugMsgLevel( QStringLiteral( "Tile blob size %1 -> uncompressed size %2" ).arg( gzippedTileData.size() ).arg( data.size() ), 2 );
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
bool QgsVectorTileLoader::decodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut )
|
||||
{
|
||||
unsigned char *bytesInPtr = reinterpret_cast<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;
|
||||
}
|
||||
|
@ -65,8 +65,6 @@ class QgsVectorTileLoader : public QObject
|
||||
static QByteArray loadFromNetwork( const QgsTileXYZ &id, const QString &requestUrl );
|
||||
//! Returns raw tile data for a single tile loaded from MBTiles file
|
||||
static QByteArray loadFromMBTiles( const QgsTileXYZ &id, QgsMBTilesReader &mbTileReader );
|
||||
//! Decodes gzip byte stream, returns true on success
|
||||
static bool decodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut );
|
||||
|
||||
//
|
||||
// non-static stuff
|
||||
|
@ -17,13 +17,17 @@
|
||||
|
||||
#include "qgsdatasourceuri.h"
|
||||
#include "qgsfeedback.h"
|
||||
#include "qgsmbtilesreader.h"
|
||||
#include "qgstiles.h"
|
||||
#include "qgsvectorlayer.h"
|
||||
#include "qgsvectortilemvtencoder.h"
|
||||
#include "qgsvectortileutils.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QUrl>
|
||||
|
||||
|
||||
@ -46,20 +50,28 @@ bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<QgsMBTilesReader> mbtiles;
|
||||
|
||||
QgsDataSourceUri dsUri;
|
||||
dsUri.setEncodedUri( mDestinationUri );
|
||||
|
||||
QString sourceType = dsUri.param( QStringLiteral( "type" ) );
|
||||
QString sourcePath = dsUri.param( QStringLiteral( "url" ) );
|
||||
if ( sourceType != QStringLiteral( "xyz" ) )
|
||||
if ( sourceType == QStringLiteral( "xyz" ) )
|
||||
{
|
||||
// remove the initial file:// scheme
|
||||
sourcePath = QUrl( sourcePath ).toLocalFile();
|
||||
}
|
||||
else if ( sourceType == QStringLiteral( "mbtiles" ) )
|
||||
{
|
||||
mbtiles.reset( new QgsMBTilesReader( sourcePath ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
mErrorMessage = tr( "Unsupported source type for writing: " ) + sourceType;
|
||||
return false;
|
||||
}
|
||||
|
||||
// remove the initial file:// scheme
|
||||
sourcePath = QUrl( sourcePath ).toLocalFile();
|
||||
|
||||
// figure out how many tiles we will need to do
|
||||
int tilesToCreate = 0;
|
||||
for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
|
||||
@ -77,6 +89,28 @@ bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( mbtiles )
|
||||
{
|
||||
if ( !mbtiles->create() )
|
||||
{
|
||||
mErrorMessage = tr( "Failed to create MBTiles file: " ) + sourcePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
// required metadata
|
||||
mbtiles->setMetadataValue( "name", "???" ); // TODO: custom name?
|
||||
mbtiles->setMetadataValue( "format", "pbf" );
|
||||
|
||||
// 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]
|
||||
|
||||
// required metadata for vector tiles: "json" with schema of layers
|
||||
mbtiles->setMetadataValue( "json", mbtilesJsonSchema() );
|
||||
}
|
||||
|
||||
int tilesCreated = 0;
|
||||
for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
|
||||
{
|
||||
@ -115,32 +149,85 @@ bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
|
||||
continue;
|
||||
}
|
||||
|
||||
QString filePath = QgsVectorTileUtils::formatXYZUrlTemplate( sourcePath, tileID );
|
||||
|
||||
// make dirs if needed
|
||||
QFileInfo fi( filePath );
|
||||
QDir fileDir = fi.dir();
|
||||
if ( !fileDir.exists() )
|
||||
if ( sourceType == QStringLiteral( "xyz" ) )
|
||||
{
|
||||
if ( !fileDir.mkpath( "." ) )
|
||||
{
|
||||
mErrorMessage = tr( "Cannot create directory " ) + fileDir.path();
|
||||
return false;
|
||||
}
|
||||
if ( !writeTileFileXYZ( sourcePath, tileID, tileData ) )
|
||||
return false; // error message already set
|
||||
}
|
||||
|
||||
QFile f( filePath );
|
||||
if ( !f.open( QIODevice::WriteOnly ) )
|
||||
else // mbtiles
|
||||
{
|
||||
mErrorMessage = tr( "Cannot open file for writing " ) + filePath;
|
||||
return false;
|
||||
QByteArray gzipTileData;
|
||||
QgsMBTilesReader::encodeGzip( tileData, gzipTileData );
|
||||
int rowTMS = pow( 2, tileID.zoomLevel() ) - tileID.row() - 1;
|
||||
mbtiles->setTileData( tileID.zoomLevel(), tileID.column(), rowTMS, gzipTileData );
|
||||
}
|
||||
|
||||
f.write( tileData );
|
||||
f.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QgsVectorTileWriter::writeTileFileXYZ( const QString &sourcePath, QgsTileXYZ tileID, const QByteArray &tileData )
|
||||
{
|
||||
QString filePath = QgsVectorTileUtils::formatXYZUrlTemplate( sourcePath, tileID );
|
||||
|
||||
// make dirs if needed
|
||||
QFileInfo fi( filePath );
|
||||
QDir fileDir = fi.dir();
|
||||
if ( !fileDir.exists() )
|
||||
{
|
||||
if ( !fileDir.mkpath( "." ) )
|
||||
{
|
||||
mErrorMessage = tr( "Cannot create directory " ) + fileDir.path();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QFile f( filePath );
|
||||
if ( !f.open( QIODevice::WriteOnly ) )
|
||||
{
|
||||
mErrorMessage = tr( "Cannot open file for writing " ) + filePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
f.write( tileData );
|
||||
f.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString QgsVectorTileWriter::mbtilesJsonSchema()
|
||||
{
|
||||
QJsonArray arrayLayers;
|
||||
for ( const Layer &layer : qgis::as_const( mLayers ) )
|
||||
{
|
||||
QgsVectorLayer *vl = layer.layer();
|
||||
const QgsFields fields = vl->fields();
|
||||
|
||||
QJsonObject fieldsObj;
|
||||
for ( const QgsField &field : fields )
|
||||
{
|
||||
QString fieldTypeStr;
|
||||
if ( field.type() == QVariant::Bool )
|
||||
fieldTypeStr = "Boolean";
|
||||
else if ( field.type() == QVariant::Int || field.type() == QVariant::Double )
|
||||
fieldTypeStr = "Number";
|
||||
else
|
||||
fieldTypeStr = "String";
|
||||
|
||||
fieldsObj["field"] = fieldTypeStr;
|
||||
}
|
||||
|
||||
QJsonObject layerObj;
|
||||
layerObj["id"] = vl->name();
|
||||
layerObj["fields"] = fieldsObj;
|
||||
arrayLayers.append( layerObj );
|
||||
}
|
||||
|
||||
QJsonObject rootObj;
|
||||
rootObj["vector_layers"] = arrayLayers;
|
||||
|
||||
QJsonDocument doc;
|
||||
doc.setObject( rootObj );
|
||||
return doc.toJson();
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
#include "qgsrectangle.h"
|
||||
|
||||
class QgsFeedback;
|
||||
class QgsTileXYZ;
|
||||
class QgsVectorLayer;
|
||||
|
||||
|
||||
@ -38,8 +39,8 @@ class QgsVectorLayer;
|
||||
* - "xyz" - tile data written as local files, using a template where {x},{y},{z}
|
||||
* are replaced by the actual tile column, row and zoom level numbers, e.g.:
|
||||
* file:///home/qgis/tiles/{z}/{x}/{y}.pbf
|
||||
*
|
||||
* (More types such as "mbtiles" or "gpkg" may be added later.)
|
||||
* - "mbtiles" - tile data written to a new MBTiles file, the "url" key should
|
||||
* be ordinary file system path, e.g.: /home/qgis/output.mbtiles
|
||||
*
|
||||
* Currently the writer only support MVT encoding of data.
|
||||
*
|
||||
@ -110,6 +111,10 @@ class CORE_EXPORT QgsVectorTileWriter
|
||||
*/
|
||||
QString errorMessage() const { return mErrorMessage; }
|
||||
|
||||
private:
|
||||
bool writeTileFileXYZ( const QString &sourcePath, QgsTileXYZ tileID, const QByteArray &tileData );
|
||||
QString mbtilesJsonSchema();
|
||||
|
||||
private:
|
||||
QgsRectangle mExtent;
|
||||
int mMinZoom = 0;
|
||||
|
@ -50,6 +50,7 @@ class TestQgsVectorTileWriter : public QObject
|
||||
void cleanup() {} // will be called after every testfunction.
|
||||
|
||||
void test_basic();
|
||||
void test_mbtiles();
|
||||
};
|
||||
|
||||
|
||||
@ -141,5 +142,74 @@ void TestQgsVectorTileWriter::test_basic()
|
||||
}
|
||||
|
||||
|
||||
void TestQgsVectorTileWriter::test_mbtiles()
|
||||
{
|
||||
QString fileName = QDir::tempPath() + "/test_qgsvectortilewriter.mbtiles";
|
||||
if ( QFile::exists( fileName ) )
|
||||
QFile::remove( fileName );
|
||||
|
||||
QgsDataSourceUri ds;
|
||||
ds.setParam( "type", "mbtiles" );
|
||||
ds.setParam( "url", fileName );
|
||||
|
||||
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 );
|
||||
|
||||
bool res = writer.writeTiles();
|
||||
QVERIFY( res );
|
||||
QVERIFY( writer.errorMessage().isEmpty() );
|
||||
|
||||
delete vlPoints;
|
||||
delete vlLines;
|
||||
delete vlPolys;
|
||||
|
||||
// do some checks on the output
|
||||
|
||||
QgsVectorTileLayer *vtLayer = new QgsVectorTileLayer( ds.encodedUri(), "output" );
|
||||
|
||||
QByteArray tile0 = vtLayer->getRawTile( QgsTileXYZ( 0, 0, 0 ) );
|
||||
QgsVectorTileMVTDecoder decoder;
|
||||
bool resDecode0 = decoder.decode( QgsTileXYZ( 0, 0, 0 ), tile0 );
|
||||
QVERIFY( resDecode0 );
|
||||
QStringList layerNames = decoder.layers();
|
||||
QCOMPARE( layerNames, QStringList() << "points" << "lines" << "polys" );
|
||||
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 );
|
||||
QString attrNamePolys0_0 = attrsPolys0_0[0].toString();
|
||||
QVERIFY( attrNamePolys0_0 == "Dam" || attrNamePolys0_0 == "Lake" );
|
||||
|
||||
delete vtLayer;
|
||||
}
|
||||
|
||||
|
||||
QGSTEST_MAIN( TestQgsVectorTileWriter )
|
||||
#include "testqgsvectortilewriter.moc"
|
||||
|
Loading…
x
Reference in New Issue
Block a user