QGIS API Documentation 3.99.0-Master (d270888f95f)
Loading...
Searching...
No Matches
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 <memory>
19#include <nlohmann/json.hpp>
20
21#include "qgsdatasourceuri.h"
22#include "qgsfeedback.h"
23#include "qgsjsonutils.h"
24#include "qgslogger.h"
25#include "qgsmbtiles.h"
26#include "qgstiles.h"
27#include "qgsvectorlayer.h"
29#include "qgsvectortileutils.h"
30#include "qgsziputils.h"
31
32#include <QDir>
33#include <QFile>
34#include <QFileInfo>
35#include <QString>
36#include <QUrl>
37
38using namespace Qt::StringLiterals;
39
44
45
47{
48 if ( tileMatrix.isRootTileMatrix() )
49 {
50 mRootTileMatrix = tileMatrix;
51 return true;
52 }
53 return false;
54}
55
56
58{
59 if ( mMinZoom < 0 )
60 {
61 mErrorMessage = tr( "Invalid min. zoom level" );
62 return false;
63 }
64 if ( mMaxZoom > 24 )
65 {
66 mErrorMessage = tr( "Invalid max. zoom level" );
67 return false;
68 }
69
70 std::unique_ptr<QgsMbTiles> mbtiles;
71
72 QgsDataSourceUri dsUri;
73 dsUri.setEncodedUri( mDestinationUri );
74
75 QString sourceType = dsUri.param( u"type"_s );
76 QString sourcePath = dsUri.param( u"url"_s );
77 if ( sourceType == "xyz"_L1 )
78 {
79 // remove the initial file:// scheme
80 sourcePath = QUrl( sourcePath ).toLocalFile();
81
82 if ( !QgsVectorTileUtils::checkXYZUrlTemplate( sourcePath ) )
83 {
84 mErrorMessage = tr( "Invalid template for XYZ: " ) + sourcePath;
85 return false;
86 }
87 }
88 else if ( sourceType == "mbtiles"_L1 )
89 {
90 mbtiles = std::make_unique<QgsMbTiles>( sourcePath );
91 }
92 else
93 {
94 mErrorMessage = tr( "Unsupported source type for writing: " ) + sourceType;
95 return false;
96 }
97
98 QgsRectangle outputExtent = mExtent;
99 if ( outputExtent.isEmpty() )
100 {
101 outputExtent = fullExtent();
102 if ( outputExtent.isEmpty() )
103 {
104 mErrorMessage = tr( "Failed to calculate output extent" );
105 return false;
106 }
107 }
108
109 // figure out how many tiles we will need to do
110 int tilesToCreate = 0;
111 for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
112 {
113 const QgsTileMatrix tileMatrix = QgsTileMatrix::fromTileMatrix( zoomLevel, mRootTileMatrix );
114
115 QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
116 tilesToCreate += ( tileRange.endRow() - tileRange.startRow() + 1 ) *
117 ( tileRange.endColumn() - tileRange.startColumn() + 1 );
118 }
119
120 if ( tilesToCreate == 0 )
121 {
122 mErrorMessage = tr( "No tiles to generate" );
123 return false;
124 }
125
126 if ( mbtiles )
127 {
128 if ( !mbtiles->create() )
129 {
130 mErrorMessage = tr( "Failed to create MBTiles file: " ) + sourcePath;
131 return false;
132 }
133
134 // required metadata
135 mbtiles->setMetadataValue( "format", "pbf" );
136 mbtiles->setMetadataValue( "json", mbtilesJsonSchema() );
137
138 // metadata specified by the client
139 const QStringList metaKeys = mMetadata.keys();
140 for ( const QString &key : metaKeys )
141 {
142 mbtiles->setMetadataValue( key, mMetadata[key].toString() );
143 }
144
145 // default metadata that we always write (if not written by the client)
146 if ( !mMetadata.contains( "name" ) )
147 mbtiles->setMetadataValue( "name", "unnamed" ); // required by the spec
148 if ( !mMetadata.contains( "minzoom" ) )
149 mbtiles->setMetadataValue( "minzoom", QString::number( mMinZoom ) );
150 if ( !mMetadata.contains( "maxzoom" ) )
151 mbtiles->setMetadataValue( "maxzoom", QString::number( mMaxZoom ) );
152 if ( !mMetadata.contains( "bounds" ) )
153 {
154 try
155 {
156 QgsCoordinateTransform ct( mRootTileMatrix.crs(), QgsCoordinateReferenceSystem( "EPSG:4326" ), mTransformContext );
158 QgsRectangle wgsExtent = ct.transform( outputExtent );
159 QString boundsStr = QString( "%1,%2,%3,%4" )
160 .arg( wgsExtent.xMinimum() ).arg( wgsExtent.yMinimum() )
161 .arg( wgsExtent.xMaximum() ).arg( wgsExtent.yMaximum() );
162 mbtiles->setMetadataValue( "bounds", boundsStr );
163 }
164 catch ( const QgsCsException & )
165 {
166 // bounds won't be written (not a problem - it is an optional value)
167 }
168 }
169 if ( !mMetadata.contains( "crs" ) )
170 mbtiles->setMetadataValue( "crs", mRootTileMatrix.crs().authid() );
171 }
172
173 int tilesCreated = 0;
174 for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
175 {
176 const QgsTileMatrix tileMatrix = QgsTileMatrix::fromTileMatrix( zoomLevel, mRootTileMatrix );
177
178 QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
179 for ( int row = tileRange.startRow(); row <= tileRange.endRow(); ++row )
180 {
181 for ( int col = tileRange.startColumn(); col <= tileRange.endColumn(); ++col )
182 {
183 QgsTileXYZ tileID( col, row, zoomLevel );
184 QgsVectorTileMVTEncoder encoder( tileID );
185 encoder.setTransformContext( mTransformContext );
186
187 for ( const Layer &layer : std::as_const( mLayers ) )
188 {
189 if ( ( layer.minZoom() >= 0 && zoomLevel < layer.minZoom() ) ||
190 ( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) )
191 continue;
192
193 encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() );
194 }
195
196 if ( feedback && feedback->isCanceled() )
197 {
198 mErrorMessage = tr( "Operation has been canceled" );
199 return false;
200 }
201
202 QByteArray tileData = encoder.encode();
203
204 ++tilesCreated;
205 if ( feedback )
206 {
207 feedback->setProgress( static_cast<double>( tilesCreated ) / tilesToCreate * 100 );
208 }
209
210 if ( tileData.isEmpty() )
211 {
212 // skipping empty tile - no need to write it
213 continue;
214 }
215
216 if ( sourceType == "xyz"_L1 )
217 {
218 if ( !writeTileFileXYZ( sourcePath, tileID, tileMatrix, tileData ) )
219 return false; // error message already set
220 }
221 else // mbtiles
222 {
223 QByteArray gzipTileData;
224 QgsZipUtils::encodeGzip( tileData, gzipTileData );
225 int rowTMS = pow( 2, tileID.zoomLevel() ) - tileID.row() - 1;
226 mbtiles->setTileData( tileID.zoomLevel(), tileID.column(), rowTMS, gzipTileData );
227 }
228 }
229 }
230 }
231
232 return true;
233}
234
236{
237 QgsRectangle extent;
238
239 for ( const Layer &layer : mLayers )
240 {
241 QgsVectorLayer *vl = layer.layer();
242 QgsCoordinateTransform ct( vl->crs(), mRootTileMatrix.crs(), mTransformContext );
244 try
245 {
247 extent.combineExtentWith( r );
248 }
249 catch ( const QgsCsException & )
250 {
251 QgsDebugError( "Failed to reproject layer extent to destination CRS" );
252 }
253 }
254 return extent;
255}
256
257bool QgsVectorTileWriter::writeTileFileXYZ( const QString &sourcePath, QgsTileXYZ tileID, const QgsTileMatrix &tileMatrix, const QByteArray &tileData )
258{
259 QString filePath = QgsVectorTileUtils::formatXYZUrlTemplate( sourcePath, tileID, tileMatrix );
260
261 // make dirs if needed
262 QFileInfo fi( filePath );
263 QDir fileDir = fi.dir();
264 if ( !fileDir.exists() )
265 {
266 if ( !fileDir.mkpath( "." ) )
267 {
268 mErrorMessage = tr( "Cannot create directory " ) + fileDir.path();
269 return false;
270 }
271 }
272
273 QFile f( filePath );
274 if ( !f.open( QIODevice::WriteOnly ) )
275 {
276 mErrorMessage = tr( "Cannot open file for writing " ) + filePath;
277 return false;
278 }
279
280 f.write( tileData );
281 f.close();
282 return true;
283}
284
285
286QString QgsVectorTileWriter::mbtilesJsonSchema() const
287{
288 QVariantList arrayLayers;
289 for ( const Layer &layer : std::as_const( mLayers ) )
290 {
291 QgsVectorLayer *vl = layer.layer();
292 const QgsFields fields = vl->fields();
293
294 QVariantMap fieldsObj;
295 for ( const QgsField &field : fields )
296 {
297 QString fieldTypeStr;
298 if ( field.type() == QMetaType::Type::Bool )
299 fieldTypeStr = u"Boolean"_s;
300 else if ( field.type() == QMetaType::Type::Int || field.type() == QMetaType::Type::Double )
301 fieldTypeStr = u"Number"_s;
302 else
303 fieldTypeStr = u"String"_s;
304
305 fieldsObj[field.name()] = fieldTypeStr;
306 }
307
308 QVariantMap layerObj;
309 layerObj["id"] = vl->name();
310 layerObj["fields"] = fieldsObj;
311 arrayLayers.append( layerObj );
312 }
313
314 QVariantMap rootObj;
315 rootObj["vector_layers"] = arrayLayers;
316 return QString::fromStdString( QgsJsonUtils::jsonFromVariant( rootObj ).dump() );
317}
318
319
320QByteArray QgsVectorTileWriter::writeSingleTile( QgsTileXYZ tileID, QgsFeedback *feedback, int buffer, int resolution ) const
321{
322 int zoomLevel = tileID.zoomLevel();
323
324 QgsVectorTileMVTEncoder encoder( tileID );
325 encoder.setTileBuffer( buffer );
326 encoder.setResolution( resolution );
327 encoder.setTransformContext( mTransformContext );
328
329 for ( const QgsVectorTileWriter::Layer &layer : std::as_const( mLayers ) )
330 {
331 if ( ( layer.minZoom() >= 0 && zoomLevel < layer.minZoom() ) ||
332 ( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) )
333 continue;
334
335 encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() );
336 }
337
338 return encoder.encode();
339}
Represents a coordinate reference system (CRS).
Handles coordinate transforms between two 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.
Stores the component parts of a 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:55
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:63
static json jsonFromVariant(const QVariant &v)
Converts a QVariant v to a json object.
QString name
Definition qgsmaplayer.h:87
QgsCoordinateReferenceSystem crs
Definition qgsmaplayer.h:90
A rectangle specified with double values.
double xMinimum
double yMinimum
double xMaximum
double yMaximum
void combineExtentWith(const QgsRectangle &rect)
Expands the rectangle so that it covers both the original rectangle and the given rectangle.
Defines a matrix of tiles for a single zoom level: it is defined by its size (width *.
Definition qgstiles.h:162
QgsTileRange tileRangeFromExtent(const QgsRectangle &mExtent) const
Returns tile range that fully covers the given extent.
Definition qgstiles.cpp:101
static QgsTileMatrix fromWebMercator(int zoomLevel)
Returns a tile matrix for the usual web mercator.
Definition qgstiles.cpp:27
bool isRootTileMatrix() const
Returns the root status of the tile matrix (zoom level == 0).
Definition qgstiles.h:247
static QgsTileMatrix fromTileMatrix(int zoomLevel, const QgsTileMatrix &tileMatrix)
Returns a tile matrix based on another one.
Definition qgstiles.cpp:65
A range of tiles in a tile matrix.
Definition qgstiles.h:118
int endColumn() const
Returns index of the last column in the range.
Definition qgstiles.h:130
int endRow() const
Returns index of the last row in the range.
Definition qgstiles.h:134
int startRow() const
Returns index of the first row in the range.
Definition qgstiles.h:132
int startColumn() const
Returns index of the first column in the range.
Definition qgstiles.h:128
Stores coordinates of a tile in a tile matrix set.
Definition qgstiles.h:43
int zoomLevel() const
Returns tile's zoom level (Z).
Definition qgstiles.h:56
int column() const
Returns tile's column index (X).
Definition qgstiles.h:52
int row() const
Returns tile's row index (Y).
Definition qgstiles.h:54
Represents a vector layer which manages a vector based dataset.
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}...
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.
static bool encodeGzip(const QByteArray &bytesIn, QByteArray &bytesOut)
Encodes gzip byte stream, returns true on success.
#define QgsDebugError(str)
Definition qgslogger.h:59