QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgsvectortilewriter.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsvectortilewriter.cpp
3 --------------------------------------
4 Date : April 2020
5 Copyright : (C) 2020 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16#include "qgsvectortilewriter.h"
17
18#include "qgsdatasourceuri.h"
19#include "qgsfeedback.h"
20#include "qgsjsonutils.h"
21#include "qgslogger.h"
22#include "qgsmbtiles.h"
23#include "qgstiles.h"
24#include "qgsvectorlayer.h"
26#include "qgsvectortileutils.h"
27#include "qgsziputils.h"
28
29#include <nlohmann/json.hpp>
30
31#include <QDir>
32#include <QFile>
33#include <QFileInfo>
34#include <QUrl>
35
36
38{
40}
41
42
44{
45 if ( tileMatrix.isRootTileMatrix() )
46 {
47 mRootTileMatrix = tileMatrix;
48 return true;
49 }
50 return false;
51}
52
53
55{
56 if ( mMinZoom < 0 )
57 {
58 mErrorMessage = tr( "Invalid min. zoom level" );
59 return false;
60 }
61 if ( mMaxZoom > 24 )
62 {
63 mErrorMessage = tr( "Invalid max. zoom level" );
64 return false;
65 }
66
67 std::unique_ptr<QgsMbTiles> mbtiles;
68
69 QgsDataSourceUri dsUri;
70 dsUri.setEncodedUri( mDestinationUri );
71
72 QString sourceType = dsUri.param( QStringLiteral( "type" ) );
73 QString sourcePath = dsUri.param( QStringLiteral( "url" ) );
74 if ( sourceType == QLatin1String( "xyz" ) )
75 {
76 // remove the initial file:// scheme
77 sourcePath = QUrl( sourcePath ).toLocalFile();
78
79 if ( !QgsVectorTileUtils::checkXYZUrlTemplate( sourcePath ) )
80 {
81 mErrorMessage = tr( "Invalid template for XYZ: " ) + sourcePath;
82 return false;
83 }
84 }
85 else if ( sourceType == QLatin1String( "mbtiles" ) )
86 {
87 mbtiles.reset( new QgsMbTiles( sourcePath ) );
88 }
89 else
90 {
91 mErrorMessage = tr( "Unsupported source type for writing: " ) + sourceType;
92 return false;
93 }
94
95 QgsRectangle outputExtent = mExtent;
96 if ( outputExtent.isEmpty() )
97 {
98 outputExtent = fullExtent();
99 if ( outputExtent.isEmpty() )
100 {
101 mErrorMessage = tr( "Failed to calculate output extent" );
102 return false;
103 }
104 }
105
106 // figure out how many tiles we will need to do
107 int tilesToCreate = 0;
108 for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
109 {
110 const QgsTileMatrix tileMatrix = QgsTileMatrix::fromTileMatrix( zoomLevel, mRootTileMatrix );
111
112 QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
113 tilesToCreate += ( tileRange.endRow() - tileRange.startRow() + 1 ) *
114 ( tileRange.endColumn() - tileRange.startColumn() + 1 );
115 }
116
117 if ( tilesToCreate == 0 )
118 {
119 mErrorMessage = tr( "No tiles to generate" );
120 return false;
121 }
122
123 if ( mbtiles )
124 {
125 if ( !mbtiles->create() )
126 {
127 mErrorMessage = tr( "Failed to create MBTiles file: " ) + sourcePath;
128 return false;
129 }
130
131 // required metadata
132 mbtiles->setMetadataValue( "format", "pbf" );
133 mbtiles->setMetadataValue( "json", mbtilesJsonSchema() );
134
135 // metadata specified by the client
136 const QStringList metaKeys = mMetadata.keys();
137 for ( const QString &key : metaKeys )
138 {
139 mbtiles->setMetadataValue( key, mMetadata[key].toString() );
140 }
141
142 // default metadata that we always write (if not written by the client)
143 if ( !mMetadata.contains( "name" ) )
144 mbtiles->setMetadataValue( "name", "unnamed" ); // required by the spec
145 if ( !mMetadata.contains( "minzoom" ) )
146 mbtiles->setMetadataValue( "minzoom", QString::number( mMinZoom ) );
147 if ( !mMetadata.contains( "maxzoom" ) )
148 mbtiles->setMetadataValue( "maxzoom", QString::number( mMaxZoom ) );
149 if ( !mMetadata.contains( "bounds" ) )
150 {
151 try
152 {
153 QgsCoordinateTransform ct( mRootTileMatrix.crs(), QgsCoordinateReferenceSystem( "EPSG:4326" ), mTransformContext );
155 QgsRectangle wgsExtent = ct.transform( outputExtent );
156 QString boundsStr = QString( "%1,%2,%3,%4" )
157 .arg( wgsExtent.xMinimum() ).arg( wgsExtent.yMinimum() )
158 .arg( wgsExtent.xMaximum() ).arg( wgsExtent.yMaximum() );
159 mbtiles->setMetadataValue( "bounds", boundsStr );
160 }
161 catch ( const QgsCsException & )
162 {
163 // bounds won't be written (not a problem - it is an optional value)
164 }
165 }
166 if ( !mMetadata.contains( "crs" ) )
167 mbtiles->setMetadataValue( "crs", mRootTileMatrix.crs().authid() );
168 }
169
170 int tilesCreated = 0;
171 for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
172 {
173 const QgsTileMatrix tileMatrix = QgsTileMatrix::fromTileMatrix( zoomLevel, mRootTileMatrix );
174
175 QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
176 for ( int row = tileRange.startRow(); row <= tileRange.endRow(); ++row )
177 {
178 for ( int col = tileRange.startColumn(); col <= tileRange.endColumn(); ++col )
179 {
180 QgsTileXYZ tileID( col, row, zoomLevel );
181 QgsVectorTileMVTEncoder encoder( tileID );
182 encoder.setTransformContext( mTransformContext );
183
184 for ( const Layer &layer : std::as_const( mLayers ) )
185 {
186 if ( ( layer.minZoom() >= 0 && zoomLevel < layer.minZoom() ) ||
187 ( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) )
188 continue;
189
190 encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() );
191 }
192
193 if ( feedback && feedback->isCanceled() )
194 {
195 mErrorMessage = tr( "Operation has been canceled" );
196 return false;
197 }
198
199 QByteArray tileData = encoder.encode();
200
201 ++tilesCreated;
202 if ( feedback )
203 {
204 feedback->setProgress( static_cast<double>( tilesCreated ) / tilesToCreate * 100 );
205 }
206
207 if ( tileData.isEmpty() )
208 {
209 // skipping empty tile - no need to write it
210 continue;
211 }
212
213 if ( sourceType == QLatin1String( "xyz" ) )
214 {
215 if ( !writeTileFileXYZ( sourcePath, tileID, tileMatrix, tileData ) )
216 return false; // error message already set
217 }
218 else // mbtiles
219 {
220 QByteArray gzipTileData;
221 QgsZipUtils::encodeGzip( tileData, gzipTileData );
222 int rowTMS = pow( 2, tileID.zoomLevel() ) - tileID.row() - 1;
223 mbtiles->setTileData( tileID.zoomLevel(), tileID.column(), rowTMS, gzipTileData );
224 }
225 }
226 }
227 }
228
229 return true;
230}
231
233{
234 QgsRectangle extent;
235
236 for ( const Layer &layer : mLayers )
237 {
238 QgsVectorLayer *vl = layer.layer();
239 QgsCoordinateTransform ct( vl->crs(), mRootTileMatrix.crs(), mTransformContext );
241 try
242 {
244 extent.combineExtentWith( r );
245 }
246 catch ( const QgsCsException & )
247 {
248 QgsDebugError( "Failed to reproject layer extent to destination CRS" );
249 }
250 }
251 return extent;
252}
253
254bool QgsVectorTileWriter::writeTileFileXYZ( const QString &sourcePath, QgsTileXYZ tileID, const QgsTileMatrix &tileMatrix, const QByteArray &tileData )
255{
256 QString filePath = QgsVectorTileUtils::formatXYZUrlTemplate( sourcePath, tileID, tileMatrix );
257
258 // make dirs if needed
259 QFileInfo fi( filePath );
260 QDir fileDir = fi.dir();
261 if ( !fileDir.exists() )
262 {
263 if ( !fileDir.mkpath( "." ) )
264 {
265 mErrorMessage = tr( "Cannot create directory " ) + fileDir.path();
266 return false;
267 }
268 }
269
270 QFile f( filePath );
271 if ( !f.open( QIODevice::WriteOnly ) )
272 {
273 mErrorMessage = tr( "Cannot open file for writing " ) + filePath;
274 return false;
275 }
276
277 f.write( tileData );
278 f.close();
279 return true;
280}
281
282
283QString QgsVectorTileWriter::mbtilesJsonSchema()
284{
285 QVariantList arrayLayers;
286 for ( const Layer &layer : std::as_const( mLayers ) )
287 {
288 QgsVectorLayer *vl = layer.layer();
289 const QgsFields fields = vl->fields();
290
291 QVariantMap fieldsObj;
292 for ( const QgsField &field : fields )
293 {
294 QString fieldTypeStr;
295 if ( field.type() == QVariant::Bool )
296 fieldTypeStr = QStringLiteral( "Boolean" );
297 else if ( field.type() == QVariant::Int || field.type() == QVariant::Double )
298 fieldTypeStr = QStringLiteral( "Number" );
299 else
300 fieldTypeStr = QStringLiteral( "String" );
301
302 fieldsObj[field.name()] = fieldTypeStr;
303 }
304
305 QVariantMap layerObj;
306 layerObj["id"] = vl->name();
307 layerObj["fields"] = fieldsObj;
308 arrayLayers.append( layerObj );
309 }
310
311 QVariantMap rootObj;
312 rootObj["vector_layers"] = arrayLayers;
313 return QString::fromStdString( QgsJsonUtils::jsonFromVariant( rootObj ).dump() );
314}
315
316
317QByteArray QgsVectorTileWriter::writeSingleTile( QgsTileXYZ tileID, QgsFeedback *feedback, int buffer, int resolution ) const
318{
319 int zoomLevel = tileID.zoomLevel();
320
321 QgsVectorTileMVTEncoder encoder( tileID );
322 encoder.setTileBuffer( buffer );
323 encoder.setResolution( resolution );
324 encoder.setTransformContext( mTransformContext );
325
326 for ( const QgsVectorTileWriter::Layer &layer : std::as_const( mLayers ) )
327 {
328 if ( ( layer.minZoom() >= 0 && zoomLevel < layer.minZoom() ) ||
329 ( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) )
330 continue;
331
332 encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() );
333 }
334
335 return encoder.encode();
336}
This class represents a coordinate reference system (CRS).
Class for doing transforms between two map coordinate systems.
void setBallparkTransformsAreAppropriate(bool appropriate)
Sets whether approximate "ballpark" results are appropriate for this coordinate transform.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const
Transform the point from the source CRS to the destination CRS.
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool handle180Crossover=false) const
Transforms a rectangle from the source CRS to the destination CRS.
Custom exception class for Coordinate Reference System related exceptions.
Definition: qgsexception.h:67
Class for storing the component parts of a RDBMS data source URI (e.g.
void setEncodedUri(const QByteArray &uri)
Sets the complete encoded uri.
QString param(const QString &key) const
Returns a generic parameter value corresponding to the specified key.
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition: qgsfeedback.h:44
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:53
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition: qgsfeedback.h:61
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:53
Container of fields for a vector layer.
Definition: qgsfields.h:45
static json jsonFromVariant(const QVariant &v)
Converts a QVariant v to a json object.
QString name
Definition: qgsmaplayer.h:78
QgsCoordinateReferenceSystem crs
Definition: qgsmaplayer.h:81
Utility class for reading and writing MBTiles files (which are SQLite3 databases).
Definition: qgsmbtiles.h:39
A rectangle specified with double values.
Definition: qgsrectangle.h:42
double xMinimum() const
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:201
double yMinimum() const
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:211
double xMaximum() const
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:196
double yMaximum() const
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:206
void combineExtentWith(const QgsRectangle &rect)
Expands the rectangle so that it covers both the original rectangle and the given rectangle.
Definition: qgsrectangle.h:413
bool isEmpty() const
Returns true if the rectangle has no area.
Definition: qgsrectangle.h:492
Defines a matrix of tiles for a single zoom level: it is defined by its size (width *.
Definition: qgstiles.h:134
QgsTileRange tileRangeFromExtent(const QgsRectangle &mExtent) const
Returns tile range that fully covers the given extent.
Definition: qgstiles.cpp:97
static QgsTileMatrix fromWebMercator(int zoomLevel)
Returns a tile matrix for the usual web mercator.
Definition: qgstiles.cpp:23
QgsCoordinateReferenceSystem crs() const
Returns the crs of the tile matrix.
Definition: qgstiles.h:157
bool isRootTileMatrix() const
Returns the root status of the tile matrix (zoom level == 0)
Definition: qgstiles.h:219
static QgsTileMatrix fromTileMatrix(int zoomLevel, const QgsTileMatrix &tileMatrix)
Returns a tile matrix based on another one.
Definition: qgstiles.cpp:61
Range of tiles in a tile matrix to be rendered.
Definition: qgstiles.h:97
int endColumn() const
Returns index of the last column in the range.
Definition: qgstiles.h:109
int endRow() const
Returns index of the last row in the range.
Definition: qgstiles.h:113
int startRow() const
Returns index of the first row in the range.
Definition: qgstiles.h:111
int startColumn() const
Returns index of the first column in the range.
Definition: qgstiles.h:107
Stores coordinates of a tile in a tile matrix set.
Definition: qgstiles.h:38
int zoomLevel() const
Returns tile's zoom level (Z)
Definition: qgstiles.h:51
int column() const
Returns tile's column index (X)
Definition: qgstiles.h:47
int row() const
Returns tile's row index (Y)
Definition: qgstiles.h:49
Represents a vector layer which manages a vector based data sets.
QgsFields fields() const FINAL
Returns the list of fields of this layer.
QgsRectangle extent() const FINAL
Returns the extent of the layer.
Handles conversion of vector features to Mapbox vector tiles encoding.
void addLayer(QgsVectorLayer *layer, QgsFeedback *feedback=nullptr, QString filterExpression=QString(), QString layerName=QString())
Fetches data from vector layer for the given tile, does reprojection and clipping.
void setTileBuffer(int buffer)
Sets size of the buffer zone around tile edges in integer tile coordinates.
void setResolution(int extent)
Sets the resolution of coordinates of geometries within the tile.
QByteArray encode() const
Encodes MVT using data stored previously with addLayer() calls.
void setTransformContext(const QgsCoordinateTransformContext &transformContext)
Sets coordinate transform context for transforms between layers and tile matrix CRS.
static bool checkXYZUrlTemplate(const QString &url)
Checks whether the URL template string is correct (contains {x}, {y} / {-y}, {z} placeholders)
static QString formatXYZUrlTemplate(const QString &url, QgsTileXYZ tile, const QgsTileMatrix &tileMatrix)
Returns formatted tile URL string replacing {x}, {y}, {z} placeholders (or {-y} instead of {y} for TM...
Configuration of a single input vector layer to be included in the output.
bool setRootTileMatrix(const QgsTileMatrix &tileMatrix)
Sets zoom level 0 tile matrix.
QgsRectangle fullExtent() const
Returns calculated extent that combines extent of all input layers.
QByteArray writeSingleTile(QgsTileXYZ tileID, QgsFeedback *feedback=nullptr, int buffer=256, int resolution=4096) const
Encodes single MVT tile.
bool writeTiles(QgsFeedback *feedback=nullptr)
Writes vector tiles according to the configuration.
CORE_EXPORT bool encodeGzip(const QByteArray &bytesIn, QByteArray &bytesOut)
Encodes gzip byte stream, returns true on success.
#define QgsDebugError(str)
Definition: qgslogger.h:38