QGIS API Documentation 4.0.0-Norrköping (1ddcee3d0e4)
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 ) * ( tileRange.endColumn() - tileRange.startColumn() + 1 );
117 }
118
119 if ( tilesToCreate == 0 )
120 {
121 mErrorMessage = tr( "No tiles to generate" );
122 return false;
123 }
124
125 if ( mbtiles )
126 {
127 if ( !mbtiles->create() )
128 {
129 mErrorMessage = tr( "Failed to create MBTiles file: " ) + sourcePath;
130 return false;
131 }
132
133 // required metadata
134 mbtiles->setMetadataValue( "format", "pbf" );
135 mbtiles->setMetadataValue( "json", mbtilesJsonSchema() );
136
137 // metadata specified by the client
138 const QStringList metaKeys = mMetadata.keys();
139 for ( const QString &key : metaKeys )
140 {
141 mbtiles->setMetadataValue( key, mMetadata[key].toString() );
142 }
143
144 // default metadata that we always write (if not written by the client)
145 if ( !mMetadata.contains( "name" ) )
146 mbtiles->setMetadataValue( "name", "unnamed" ); // required by the spec
147 if ( !mMetadata.contains( "minzoom" ) )
148 mbtiles->setMetadataValue( "minzoom", QString::number( mMinZoom ) );
149 if ( !mMetadata.contains( "maxzoom" ) )
150 mbtiles->setMetadataValue( "maxzoom", QString::number( mMaxZoom ) );
151 if ( !mMetadata.contains( "bounds" ) )
152 {
153 try
154 {
155 QgsCoordinateTransform ct( mRootTileMatrix.crs(), QgsCoordinateReferenceSystem( "EPSG:4326" ), mTransformContext );
157 QgsRectangle wgsExtent = ct.transform( outputExtent );
158 QString boundsStr = QString( "%1,%2,%3,%4" ).arg( wgsExtent.xMinimum() ).arg( wgsExtent.yMinimum() ).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() ) || ( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) )
187 continue;
188
189 encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() );
190 }
191
192 if ( feedback && feedback->isCanceled() )
193 {
194 mErrorMessage = tr( "Operation has been canceled" );
195 return false;
196 }
197
198 QByteArray tileData = encoder.encode();
199
200 ++tilesCreated;
201 if ( feedback )
202 {
203 feedback->setProgress( static_cast<double>( tilesCreated ) / tilesToCreate * 100 );
204 }
205
206 if ( tileData.isEmpty() )
207 {
208 // skipping empty tile - no need to write it
209 continue;
210 }
211
212 if ( sourceType == "xyz"_L1 )
213 {
214 if ( !writeTileFileXYZ( sourcePath, tileID, tileMatrix, tileData ) )
215 return false; // error message already set
216 }
217 else // mbtiles
218 {
219 QByteArray gzipTileData;
220 QgsZipUtils::encodeGzip( tileData, gzipTileData );
221 int rowTMS = pow( 2, tileID.zoomLevel() ) - tileID.row() - 1;
222 mbtiles->setTileData( tileID.zoomLevel(), tileID.column(), rowTMS, gzipTileData );
223 }
224 }
225 }
226 }
227
228 return true;
229}
230
232{
233 QgsRectangle extent;
234
235 for ( const Layer &layer : mLayers )
236 {
237 QgsVectorLayer *vl = layer.layer();
238 QgsCoordinateTransform ct( vl->crs(), mRootTileMatrix.crs(), mTransformContext );
240 try
241 {
243 extent.combineExtentWith( r );
244 }
245 catch ( const QgsCsException & )
246 {
247 QgsDebugError( "Failed to reproject layer extent to destination CRS" );
248 }
249 }
250 return extent;
251}
252
253bool QgsVectorTileWriter::writeTileFileXYZ( const QString &sourcePath, QgsTileXYZ tileID, const QgsTileMatrix &tileMatrix, const QByteArray &tileData )
254{
255 QString filePath = QgsVectorTileUtils::formatXYZUrlTemplate( sourcePath, tileID, tileMatrix );
256
257 // make dirs if needed
258 QFileInfo fi( filePath );
259 QDir fileDir = fi.dir();
260 if ( !fileDir.exists() )
261 {
262 if ( !fileDir.mkpath( "." ) )
263 {
264 mErrorMessage = tr( "Cannot create directory " ) + fileDir.path();
265 return false;
266 }
267 }
268
269 QFile f( filePath );
270 if ( !f.open( QIODevice::WriteOnly ) )
271 {
272 mErrorMessage = tr( "Cannot open file for writing " ) + filePath;
273 return false;
274 }
275
276 f.write( tileData );
277 f.close();
278 return true;
279}
280
281
282QString QgsVectorTileWriter::mbtilesJsonSchema() const
283{
284 QVariantList arrayLayers;
285 for ( const Layer &layer : std::as_const( mLayers ) )
286 {
287 QgsVectorLayer *vl = layer.layer();
288 const QgsFields fields = vl->fields();
289
290 QVariantMap fieldsObj;
291 for ( const QgsField &field : fields )
292 {
293 QString fieldTypeStr;
294 if ( field.type() == QMetaType::Type::Bool )
295 fieldTypeStr = u"Boolean"_s;
296 else if ( field.type() == QMetaType::Type::Int || field.type() == QMetaType::Type::Double )
297 fieldTypeStr = u"Number"_s;
298 else
299 fieldTypeStr = u"String"_s;
300
301 fieldsObj[field.name()] = fieldTypeStr;
302 }
303
304 QVariantMap layerObj;
305 layerObj["id"] = vl->name();
306 layerObj["fields"] = fieldsObj;
307 arrayLayers.append( layerObj );
308 }
309
310 QVariantMap rootObj;
311 rootObj["vector_layers"] = arrayLayers;
312 return QString::fromStdString( QgsJsonUtils::jsonFromVariant( rootObj ).dump() );
313}
314
315
316QByteArray QgsVectorTileWriter::writeSingleTile( QgsTileXYZ tileID, QgsFeedback *feedback, int buffer, int resolution ) const
317{
318 int zoomLevel = tileID.zoomLevel();
319
320 QgsVectorTileMVTEncoder encoder( tileID );
321 encoder.setTileBuffer( buffer );
322 encoder.setResolution( resolution );
323 encoder.setTransformContext( mTransformContext );
324
325 for ( const QgsVectorTileWriter::Layer &layer : std::as_const( mLayers ) )
326 {
327 if ( ( layer.minZoom() >= 0 && zoomLevel < layer.minZoom() ) || ( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) )
328 continue;
329
330 encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() );
331 }
332
333 return encoder.encode();
334}
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:56
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition qgsfeedback.h:65
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:171
QgsTileRange tileRangeFromExtent(const QgsRectangle &mExtent) const
Returns tile range that fully covers the given extent.
Definition qgstiles.cpp:100
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:253
static QgsTileMatrix fromTileMatrix(int zoomLevel, const QgsTileMatrix &tileMatrix)
Returns a tile matrix based on another one.
Definition qgstiles.cpp:64
A range of tiles in a tile matrix.
Definition qgstiles.h:123
int endColumn() const
Returns index of the last column in the range.
Definition qgstiles.h:139
int endRow() const
Returns index of the last row in the range.
Definition qgstiles.h:143
int startRow() const
Returns index of the first row in the range.
Definition qgstiles.h:141
int startColumn() const
Returns index of the first column in the range.
Definition qgstiles.h:137
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:57
int column() const
Returns tile's column index (X).
Definition qgstiles.h:53
int row() const
Returns tile's row index (Y).
Definition qgstiles.h:55
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