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