QGIS API Documentation  3.24.2-Tisler (13c1a02865)
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 
28 #include <nlohmann/json.hpp>
29 
30 #include <QDir>
31 #include <QFile>
32 #include <QFileInfo>
33 #include <QUrl>
34 
35 
37 {
39 }
40 
41 
43 {
44  if ( tileMatrix.isRootTileMatrix() )
45  {
46  mRootTileMatrix = tileMatrix;
47  return true;
48  }
49  return false;
50 }
51 
52 
54 {
55  if ( mMinZoom < 0 )
56  {
57  mErrorMessage = tr( "Invalid min. zoom level" );
58  return false;
59  }
60  if ( mMaxZoom > 24 )
61  {
62  mErrorMessage = tr( "Invalid max. zoom level" );
63  return false;
64  }
65 
66  std::unique_ptr<QgsMbTiles> mbtiles;
67 
68  QgsDataSourceUri dsUri;
69  dsUri.setEncodedUri( mDestinationUri );
70 
71  QString sourceType = dsUri.param( QStringLiteral( "type" ) );
72  QString sourcePath = dsUri.param( QStringLiteral( "url" ) );
73  if ( sourceType == QLatin1String( "xyz" ) )
74  {
75  // remove the initial file:// scheme
76  sourcePath = QUrl( sourcePath ).toLocalFile();
77 
78  if ( !QgsVectorTileUtils::checkXYZUrlTemplate( sourcePath ) )
79  {
80  mErrorMessage = tr( "Invalid template for XYZ: " ) + sourcePath;
81  return false;
82  }
83  }
84  else if ( sourceType == QLatin1String( "mbtiles" ) )
85  {
86  mbtiles.reset( new QgsMbTiles( sourcePath ) );
87  }
88  else
89  {
90  mErrorMessage = tr( "Unsupported source type for writing: " ) + sourceType;
91  return false;
92  }
93 
94  QgsRectangle outputExtent = mExtent;
95  if ( outputExtent.isEmpty() )
96  {
97  outputExtent = fullExtent();
98  if ( outputExtent.isEmpty() )
99  {
100  mErrorMessage = tr( "Failed to calculate output extent" );
101  return false;
102  }
103  }
104 
105  // figure out how many tiles we will need to do
106  int tilesToCreate = 0;
107  for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
108  {
109  const QgsTileMatrix tileMatrix = QgsTileMatrix::fromTileMatrix( zoomLevel, mRootTileMatrix );
110 
111  QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
112  tilesToCreate += ( tileRange.endRow() - tileRange.startRow() + 1 ) *
113  ( tileRange.endColumn() - tileRange.startColumn() + 1 );
114  }
115 
116  if ( tilesToCreate == 0 )
117  {
118  mErrorMessage = tr( "No tiles to generate" );
119  return false;
120  }
121 
122  if ( mbtiles )
123  {
124  if ( !mbtiles->create() )
125  {
126  mErrorMessage = tr( "Failed to create MBTiles file: " ) + sourcePath;
127  return false;
128  }
129 
130  // required metadata
131  mbtiles->setMetadataValue( "format", "pbf" );
132  mbtiles->setMetadataValue( "json", mbtilesJsonSchema() );
133 
134  // metadata specified by the client
135  const QStringList metaKeys = mMetadata.keys();
136  for ( const QString &key : metaKeys )
137  {
138  mbtiles->setMetadataValue( key, mMetadata[key].toString() );
139  }
140 
141  // default metadata that we always write (if not written by the client)
142  if ( !mMetadata.contains( "name" ) )
143  mbtiles->setMetadataValue( "name", "unnamed" ); // required by the spec
144  if ( !mMetadata.contains( "minzoom" ) )
145  mbtiles->setMetadataValue( "minzoom", QString::number( mMinZoom ) );
146  if ( !mMetadata.contains( "maxzoom" ) )
147  mbtiles->setMetadataValue( "maxzoom", QString::number( mMaxZoom ) );
148  if ( !mMetadata.contains( "bounds" ) )
149  {
150  try
151  {
152  QgsCoordinateTransform ct( mRootTileMatrix.crs(), QgsCoordinateReferenceSystem( "EPSG:4326" ), mTransformContext );
154  QgsRectangle wgsExtent = ct.transform( outputExtent );
155  QString boundsStr = QString( "%1,%2,%3,%4" )
156  .arg( wgsExtent.xMinimum() ).arg( wgsExtent.yMinimum() )
157  .arg( wgsExtent.xMaximum() ).arg( wgsExtent.yMaximum() );
158  mbtiles->setMetadataValue( "bounds", boundsStr );
159  }
160  catch ( const QgsCsException & )
161  {
162  // bounds won't be written (not a problem - it is an optional value)
163  }
164  }
165  if ( !mMetadata.contains( "crs" ) )
166  mbtiles->setMetadataValue( "crs", mRootTileMatrix.crs().authid() );
167  }
168 
169  int tilesCreated = 0;
170  for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
171  {
172  const QgsTileMatrix tileMatrix = QgsTileMatrix::fromTileMatrix( zoomLevel, mRootTileMatrix );
173 
174  QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
175  for ( int row = tileRange.startRow(); row <= tileRange.endRow(); ++row )
176  {
177  for ( int col = tileRange.startColumn(); col <= tileRange.endColumn(); ++col )
178  {
179  QgsTileXYZ tileID( col, row, zoomLevel );
180  QgsVectorTileMVTEncoder encoder( tileID );
181  encoder.setTransformContext( mTransformContext );
182 
183  for ( const Layer &layer : std::as_const( mLayers ) )
184  {
185  if ( ( layer.minZoom() >= 0 && zoomLevel < layer.minZoom() ) ||
186  ( 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 == QLatin1String( "xyz" ) )
213  {
214  if ( !writeTileFileXYZ( sourcePath, tileID, tileMatrix, tileData ) )
215  return false; // error message already set
216  }
217  else // mbtiles
218  {
219  QByteArray gzipTileData;
220  QgsMbTiles::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  {
242  QgsRectangle r = ct.transformBoundingBox( vl->extent() );
243  extent.combineExtentWith( r );
244  }
245  catch ( const QgsCsException & )
246  {
247  QgsDebugMsg( "Failed to reproject layer extent to destination CRS" );
248  }
249  }
250  return extent;
251 }
252 
253 bool 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 
282 QString QgsVectorTileWriter::mbtilesJsonSchema()
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() == QVariant::Bool )
295  fieldTypeStr = QStringLiteral( "Boolean" );
296  else if ( field.type() == QVariant::Int || field.type() == QVariant::Double )
297  fieldTypeStr = QStringLiteral( "Number" );
298  else
299  fieldTypeStr = QStringLiteral( "String" );
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 
316 QByteArray 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() ) ||
328  ( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) )
329  continue;
330 
331  encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() );
332  }
333 
334  return encoder.encode();
335 }
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.
QgsRectangle transformBoundingBox(const QgsRectangle &rectangle, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool handle180Crossover=false) const SIP_THROW(QgsCsException)
Transforms a rectangle from the source CRS to the destination CRS.
QgsPointXY transform(const QgsPointXY &point, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward) const SIP_THROW(QgsCsException)
Transform the point from the source CRS to the destination CRS.
Custom exception class for Coordinate Reference System related exceptions.
Definition: qgsexception.h:66
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:45
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
Definition: qgsfeedback.h:54
void setProgress(double progress)
Sets the current progress for the feedback object.
Definition: qgsfeedback.h:63
Encapsulate a field in an attribute table or data source.
Definition: qgsfield.h:51
QString name
Definition: qgsfield.h:60
QVariant::Type type
Definition: qgsfield.h:58
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:76
QgsCoordinateReferenceSystem crs
Definition: qgsmaplayer.h:79
Utility class for reading and writing MBTiles files (which are SQLite3 databases).
Definition: qgsmbtiles.h:39
static bool encodeGzip(const QByteArray &bytesIn, QByteArray &bytesOut)
Encodes gzip byte stream, returns true on success. Useful for writing vector tiles.
Definition: qgsmbtiles.cpp:267
A rectangle specified with double values.
Definition: qgsrectangle.h:42
double yMaximum() const SIP_HOLDGIL
Returns the y maximum value (top side of rectangle).
Definition: qgsrectangle.h:193
double xMaximum() const SIP_HOLDGIL
Returns the x maximum value (right side of rectangle).
Definition: qgsrectangle.h:183
double xMinimum() const SIP_HOLDGIL
Returns the x minimum value (left side of rectangle).
Definition: qgsrectangle.h:188
double yMinimum() const SIP_HOLDGIL
Returns the y minimum value (bottom side of rectangle).
Definition: qgsrectangle.h:198
void combineExtentWith(const QgsRectangle &rect)
Expands the rectangle so that it covers both the original rectangle and the given rectangle.
Definition: qgsrectangle.h:391
bool isEmpty() const
Returns true if the rectangle is empty.
Definition: qgsrectangle.h:469
Defines a matrix of tiles for a single zoom level: it is defined by its size (width *.
Definition: qgstiles.h:108
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:131
bool isRootTileMatrix() const
Returns the root status of the tile matrix (zoom level == 0)
Definition: qgstiles.h:193
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:71
int endColumn() const
Returns index of the last column in the range.
Definition: qgstiles.h:83
int endRow() const
Returns index of the last row in the range.
Definition: qgstiles.h:87
int startRow() const
Returns index of the first row in the range.
Definition: qgstiles.h:85
int startColumn() const
Returns index of the first column in the range.
Definition: qgstiles.h:81
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.
const QgsField & field
Definition: qgsfield.h:463
#define QgsDebugMsg(str)
Definition: qgslogger.h:38