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"
25 #include "qgsvectortilemvtencoder.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
QgsVectorTileWriter()36 QgsVectorTileWriter::QgsVectorTileWriter()
37 {
38 setRootTileMatrix( QgsTileMatrix::fromWebMercator( 0 ) );
39 }
40
41
setRootTileMatrix(const QgsTileMatrix & tileMatrix)42 bool QgsVectorTileWriter::setRootTileMatrix( const QgsTileMatrix &tileMatrix )
43 {
44 if ( tileMatrix.isRootTileMatrix() )
45 {
46 mRootTileMatrix = tileMatrix;
47 return true;
48 }
49 return false;
50 }
51
52
writeTiles(QgsFeedback * feedback)53 bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback )
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 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 );
153 QgsRectangle wgsExtent = ct.transform( outputExtent );
154 QString boundsStr = QString( "%1,%2,%3,%4" )
155 .arg( wgsExtent.xMinimum() ).arg( wgsExtent.yMinimum() )
156 .arg( wgsExtent.xMaximum() ).arg( wgsExtent.yMaximum() );
157 mbtiles->setMetadataValue( "bounds", boundsStr );
158 }
159 catch ( const QgsCsException & )
160 {
161 // bounds won't be written (not a problem - it is an optional value)
162 }
163 }
164 if ( !mMetadata.contains( "crs" ) )
165 mbtiles->setMetadataValue( "crs", mRootTileMatrix.crs().authid() );
166 }
167
168 int tilesCreated = 0;
169 for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel )
170 {
171 QgsTileMatrix tileMatrix = QgsTileMatrix::fromTileMatrix( zoomLevel, mRootTileMatrix );
172
173 QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent );
174 for ( int row = tileRange.startRow(); row <= tileRange.endRow(); ++row )
175 {
176 for ( int col = tileRange.startColumn(); col <= tileRange.endColumn(); ++col )
177 {
178 QgsTileXYZ tileID( col, row, zoomLevel );
179 QgsVectorTileMVTEncoder encoder( tileID );
180 encoder.setTransformContext( mTransformContext );
181
182 for ( const Layer &layer : std::as_const( mLayers ) )
183 {
184 if ( ( layer.minZoom() >= 0 && zoomLevel < layer.minZoom() ) ||
185 ( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) )
186 continue;
187
188 encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() );
189 }
190
191 if ( feedback && feedback->isCanceled() )
192 {
193 mErrorMessage = tr( "Operation has been canceled" );
194 return false;
195 }
196
197 QByteArray tileData = encoder.encode();
198
199 ++tilesCreated;
200 if ( feedback )
201 {
202 feedback->setProgress( static_cast<double>( tilesCreated ) / tilesToCreate * 100 );
203 }
204
205 if ( tileData.isEmpty() )
206 {
207 // skipping empty tile - no need to write it
208 continue;
209 }
210
211 if ( sourceType == QLatin1String( "xyz" ) )
212 {
213 if ( !writeTileFileXYZ( sourcePath, tileID, tileMatrix, tileData ) )
214 return false; // error message already set
215 }
216 else // mbtiles
217 {
218 QByteArray gzipTileData;
219 QgsMbTiles::encodeGzip( tileData, gzipTileData );
220 int rowTMS = pow( 2, tileID.zoomLevel() ) - tileID.row() - 1;
221 mbtiles->setTileData( tileID.zoomLevel(), tileID.column(), rowTMS, gzipTileData );
222 }
223 }
224 }
225 }
226
227 return true;
228 }
229
fullExtent() const230 QgsRectangle QgsVectorTileWriter::fullExtent() const
231 {
232 QgsRectangle extent;
233
234 for ( const Layer &layer : mLayers )
235 {
236 QgsVectorLayer *vl = layer.layer();
237 QgsCoordinateTransform ct( vl->crs(), mRootTileMatrix.crs(), mTransformContext );
238 try
239 {
240 QgsRectangle r = ct.transformBoundingBox( vl->extent() );
241 extent.combineExtentWith( r );
242 }
243 catch ( const QgsCsException & )
244 {
245 QgsDebugMsg( "Failed to reproject layer extent to destination CRS" );
246 }
247 }
248 return extent;
249 }
250
writeTileFileXYZ(const QString & sourcePath,QgsTileXYZ tileID,const QgsTileMatrix & tileMatrix,const QByteArray & tileData)251 bool QgsVectorTileWriter::writeTileFileXYZ( const QString &sourcePath, QgsTileXYZ tileID, const QgsTileMatrix &tileMatrix, const QByteArray &tileData )
252 {
253 QString filePath = QgsVectorTileUtils::formatXYZUrlTemplate( sourcePath, tileID, tileMatrix );
254
255 // make dirs if needed
256 QFileInfo fi( filePath );
257 QDir fileDir = fi.dir();
258 if ( !fileDir.exists() )
259 {
260 if ( !fileDir.mkpath( "." ) )
261 {
262 mErrorMessage = tr( "Cannot create directory " ) + fileDir.path();
263 return false;
264 }
265 }
266
267 QFile f( filePath );
268 if ( !f.open( QIODevice::WriteOnly ) )
269 {
270 mErrorMessage = tr( "Cannot open file for writing " ) + filePath;
271 return false;
272 }
273
274 f.write( tileData );
275 f.close();
276 return true;
277 }
278
279
mbtilesJsonSchema()280 QString QgsVectorTileWriter::mbtilesJsonSchema()
281 {
282 QVariantList arrayLayers;
283 for ( const Layer &layer : std::as_const( mLayers ) )
284 {
285 QgsVectorLayer *vl = layer.layer();
286 const QgsFields fields = vl->fields();
287
288 QVariantMap fieldsObj;
289 for ( const QgsField &field : fields )
290 {
291 QString fieldTypeStr;
292 if ( field.type() == QVariant::Bool )
293 fieldTypeStr = QStringLiteral( "Boolean" );
294 else if ( field.type() == QVariant::Int || field.type() == QVariant::Double )
295 fieldTypeStr = QStringLiteral( "Number" );
296 else
297 fieldTypeStr = QStringLiteral( "String" );
298
299 fieldsObj[field.name()] = fieldTypeStr;
300 }
301
302 QVariantMap layerObj;
303 layerObj["id"] = vl->name();
304 layerObj["fields"] = fieldsObj;
305 arrayLayers.append( layerObj );
306 }
307
308 QVariantMap rootObj;
309 rootObj["vector_layers"] = arrayLayers;
310 return QString::fromStdString( QgsJsonUtils::jsonFromVariant( rootObj ).dump() );
311 }
312
313
writeSingleTile(QgsTileXYZ tileID,QgsFeedback * feedback,int buffer,int resolution) const314 QByteArray QgsVectorTileWriter::writeSingleTile( QgsTileXYZ tileID, QgsFeedback *feedback, int buffer, int resolution ) const
315 {
316 int zoomLevel = tileID.zoomLevel();
317
318 QgsVectorTileMVTEncoder encoder( tileID );
319 encoder.setTileBuffer( buffer );
320 encoder.setResolution( resolution );
321 encoder.setTransformContext( mTransformContext );
322
323 for ( const QgsVectorTileWriter::Layer &layer : std::as_const( mLayers ) )
324 {
325 if ( ( layer.minZoom() >= 0 && zoomLevel < layer.minZoom() ) ||
326 ( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) )
327 continue;
328
329 encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() );
330 }
331
332 return encoder.encode();
333 }
334