Branch data Line data Source code
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 : : 36 : 0 : QgsVectorTileWriter::QgsVectorTileWriter() 37 : : { 38 : 0 : } 39 : : 40 : : 41 : 0 : bool QgsVectorTileWriter::writeTiles( QgsFeedback *feedback ) 42 : : { 43 : 0 : if ( mMinZoom < 0 ) 44 : : { 45 : 0 : mErrorMessage = tr( "Invalid min. zoom level" ); 46 : 0 : return false; 47 : : } 48 : 0 : if ( mMaxZoom > 24 ) 49 : : { 50 : 0 : mErrorMessage = tr( "Invalid max. zoom level" ); 51 : 0 : return false; 52 : : } 53 : : 54 : 0 : std::unique_ptr<QgsMbTiles> mbtiles; 55 : : 56 : 0 : QgsDataSourceUri dsUri; 57 : 0 : dsUri.setEncodedUri( mDestinationUri ); 58 : : 59 : 0 : QString sourceType = dsUri.param( QStringLiteral( "type" ) ); 60 : 0 : QString sourcePath = dsUri.param( QStringLiteral( "url" ) ); 61 : 0 : if ( sourceType == QLatin1String( "xyz" ) ) 62 : : { 63 : : // remove the initial file:// scheme 64 : 0 : sourcePath = QUrl( sourcePath ).toLocalFile(); 65 : : 66 : 0 : if ( !QgsVectorTileUtils::checkXYZUrlTemplate( sourcePath ) ) 67 : : { 68 : 0 : mErrorMessage = tr( "Invalid template for XYZ: " ) + sourcePath; 69 : 0 : return false; 70 : : } 71 : 0 : } 72 : 0 : else if ( sourceType == QLatin1String( "mbtiles" ) ) 73 : : { 74 : 0 : mbtiles.reset( new QgsMbTiles( sourcePath ) ); 75 : 0 : } 76 : : else 77 : : { 78 : 0 : mErrorMessage = tr( "Unsupported source type for writing: " ) + sourceType; 79 : 0 : return false; 80 : : } 81 : : 82 : 0 : QgsRectangle outputExtent = mExtent; 83 : 0 : if ( outputExtent.isEmpty() ) 84 : : { 85 : 0 : outputExtent = fullExtent(); 86 : 0 : if ( outputExtent.isEmpty() ) 87 : : { 88 : 0 : mErrorMessage = tr( "Failed to calculate output extent" ); 89 : 0 : return false; 90 : : } 91 : 0 : } 92 : : 93 : : // figure out how many tiles we will need to do 94 : 0 : int tilesToCreate = 0; 95 : 0 : for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel ) 96 : : { 97 : 0 : QgsTileMatrix tileMatrix = QgsTileMatrix::fromWebMercator( zoomLevel ); 98 : : 99 : 0 : QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent ); 100 : 0 : tilesToCreate += ( tileRange.endRow() - tileRange.startRow() + 1 ) * 101 : 0 : ( tileRange.endColumn() - tileRange.startColumn() + 1 ); 102 : 0 : } 103 : : 104 : 0 : if ( tilesToCreate == 0 ) 105 : : { 106 : 0 : mErrorMessage = tr( "No tiles to generate" ); 107 : 0 : return false; 108 : : } 109 : : 110 : 0 : if ( mbtiles ) 111 : : { 112 : 0 : if ( !mbtiles->create() ) 113 : : { 114 : 0 : mErrorMessage = tr( "Failed to create MBTiles file: " ) + sourcePath; 115 : 0 : return false; 116 : : } 117 : : 118 : : // required metadata 119 : 0 : mbtiles->setMetadataValue( "format", "pbf" ); 120 : 0 : mbtiles->setMetadataValue( "json", mbtilesJsonSchema() ); 121 : : 122 : : // metadata specified by the client 123 : 0 : const QStringList metaKeys = mMetadata.keys(); 124 : 0 : for ( const QString &key : metaKeys ) 125 : : { 126 : 0 : mbtiles->setMetadataValue( key, mMetadata[key].toString() ); 127 : : } 128 : : 129 : : // default metadata that we always write (if not written by the client) 130 : 0 : if ( !mMetadata.contains( "name" ) ) 131 : 0 : mbtiles->setMetadataValue( "name", "unnamed" ); // required by the spec 132 : 0 : if ( !mMetadata.contains( "minzoom" ) ) 133 : 0 : mbtiles->setMetadataValue( "minzoom", QString::number( mMinZoom ) ); 134 : 0 : if ( !mMetadata.contains( "maxzoom" ) ) 135 : 0 : mbtiles->setMetadataValue( "maxzoom", QString::number( mMaxZoom ) ); 136 : 0 : if ( !mMetadata.contains( "bounds" ) ) 137 : : { 138 : : try 139 : : { 140 : 0 : QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( "EPSG:3857" ), QgsCoordinateReferenceSystem( "EPSG:4326" ), mTransformContext ); 141 : 0 : QgsRectangle wgsExtent = ct.transform( outputExtent ); 142 : 0 : QString boundsStr = QString( "%1,%2,%3,%4" ) 143 : 0 : .arg( wgsExtent.xMinimum() ).arg( wgsExtent.yMinimum() ) 144 : 0 : .arg( wgsExtent.xMaximum() ).arg( wgsExtent.yMaximum() ); 145 : 0 : mbtiles->setMetadataValue( "bounds", boundsStr ); 146 : 0 : } 147 : : catch ( const QgsCsException & ) 148 : : { 149 : : // bounds won't be written (not a problem - it is an optional value) 150 : 0 : } 151 : 0 : } 152 : 0 : } 153 : : 154 : 0 : int tilesCreated = 0; 155 : 0 : for ( int zoomLevel = mMinZoom; zoomLevel <= mMaxZoom; ++zoomLevel ) 156 : : { 157 : 0 : QgsTileMatrix tileMatrix = QgsTileMatrix::fromWebMercator( zoomLevel ); 158 : : 159 : 0 : QgsTileRange tileRange = tileMatrix.tileRangeFromExtent( outputExtent ); 160 : 0 : for ( int row = tileRange.startRow(); row <= tileRange.endRow(); ++row ) 161 : : { 162 : 0 : for ( int col = tileRange.startColumn(); col <= tileRange.endColumn(); ++col ) 163 : : { 164 : 0 : QgsTileXYZ tileID( col, row, zoomLevel ); 165 : 0 : QgsVectorTileMVTEncoder encoder( tileID ); 166 : 0 : encoder.setTransformContext( mTransformContext ); 167 : : 168 : 0 : for ( const Layer &layer : std::as_const( mLayers ) ) 169 : : { 170 : 0 : if ( ( layer.minZoom() >= 0 && zoomLevel < layer.minZoom() ) || 171 : 0 : ( layer.maxZoom() >= 0 && zoomLevel > layer.maxZoom() ) ) 172 : 0 : continue; 173 : : 174 : 0 : encoder.addLayer( layer.layer(), feedback, layer.filterExpression(), layer.layerName() ); 175 : 0 : } 176 : : 177 : 0 : if ( feedback && feedback->isCanceled() ) 178 : : { 179 : 0 : mErrorMessage = tr( "Operation has been canceled" ); 180 : 0 : return false; 181 : : } 182 : : 183 : 0 : QByteArray tileData = encoder.encode(); 184 : : 185 : 0 : ++tilesCreated; 186 : 0 : if ( feedback ) 187 : : { 188 : 0 : feedback->setProgress( static_cast<double>( tilesCreated ) / tilesToCreate * 100 ); 189 : 0 : } 190 : : 191 : 0 : if ( tileData.isEmpty() ) 192 : : { 193 : : // skipping empty tile - no need to write it 194 : 0 : continue; 195 : : } 196 : : 197 : 0 : if ( sourceType == QLatin1String( "xyz" ) ) 198 : : { 199 : 0 : if ( !writeTileFileXYZ( sourcePath, tileID, tileMatrix, tileData ) ) 200 : 0 : return false; // error message already set 201 : 0 : } 202 : : else // mbtiles 203 : : { 204 : 0 : QByteArray gzipTileData; 205 : 0 : QgsMbTiles::encodeGzip( tileData, gzipTileData ); 206 : 0 : int rowTMS = pow( 2, tileID.zoomLevel() ) - tileID.row() - 1; 207 : 0 : mbtiles->setTileData( tileID.zoomLevel(), tileID.column(), rowTMS, gzipTileData ); 208 : 0 : } 209 : 0 : } 210 : 0 : } 211 : 0 : } 212 : : 213 : 0 : return true; 214 : 0 : } 215 : : 216 : 0 : QgsRectangle QgsVectorTileWriter::fullExtent() const 217 : : { 218 : 0 : QgsRectangle extent; 219 : 0 : QgsCoordinateReferenceSystem destCrs( "EPSG:3857" ); 220 : : 221 : 0 : for ( const Layer &layer : mLayers ) 222 : : { 223 : 0 : QgsVectorLayer *vl = layer.layer(); 224 : 0 : QgsCoordinateTransform ct( vl->crs(), destCrs, mTransformContext ); 225 : : try 226 : : { 227 : 0 : QgsRectangle r = ct.transformBoundingBox( vl->extent() ); 228 : 0 : extent.combineExtentWith( r ); 229 : 0 : } 230 : : catch ( const QgsCsException & ) 231 : : { 232 : 0 : QgsDebugMsg( "Failed to reproject layer extent to destination CRS" ); 233 : 0 : } 234 : 0 : } 235 : : return extent; 236 : 0 : } 237 : : 238 : 0 : bool QgsVectorTileWriter::writeTileFileXYZ( const QString &sourcePath, QgsTileXYZ tileID, const QgsTileMatrix &tileMatrix, const QByteArray &tileData ) 239 : : { 240 : 0 : QString filePath = QgsVectorTileUtils::formatXYZUrlTemplate( sourcePath, tileID, tileMatrix ); 241 : : 242 : : // make dirs if needed 243 : 0 : QFileInfo fi( filePath ); 244 : 0 : QDir fileDir = fi.dir(); 245 : 0 : if ( !fileDir.exists() ) 246 : : { 247 : 0 : if ( !fileDir.mkpath( "." ) ) 248 : : { 249 : 0 : mErrorMessage = tr( "Cannot create directory " ) + fileDir.path(); 250 : 0 : return false; 251 : : } 252 : 0 : } 253 : : 254 : 0 : QFile f( filePath ); 255 : 0 : if ( !f.open( QIODevice::WriteOnly ) ) 256 : : { 257 : 0 : mErrorMessage = tr( "Cannot open file for writing " ) + filePath; 258 : 0 : return false; 259 : : } 260 : : 261 : 0 : f.write( tileData ); 262 : 0 : f.close(); 263 : 0 : return true; 264 : 0 : } 265 : : 266 : : 267 : 0 : QString QgsVectorTileWriter::mbtilesJsonSchema() 268 : : { 269 : 0 : QVariantList arrayLayers; 270 : 0 : for ( const Layer &layer : std::as_const( mLayers ) ) 271 : : { 272 : 0 : QgsVectorLayer *vl = layer.layer(); 273 : 0 : const QgsFields fields = vl->fields(); 274 : : 275 : 0 : QVariantMap fieldsObj; 276 : 0 : for ( const QgsField &field : fields ) 277 : : { 278 : 0 : QString fieldTypeStr; 279 : 0 : if ( field.type() == QVariant::Bool ) 280 : 0 : fieldTypeStr = QStringLiteral( "Boolean" ); 281 : 0 : else if ( field.type() == QVariant::Int || field.type() == QVariant::Double ) 282 : 0 : fieldTypeStr = QStringLiteral( "Number" ); 283 : : else 284 : 0 : fieldTypeStr = QStringLiteral( "String" ); 285 : : 286 : 0 : fieldsObj[field.name()] = fieldTypeStr; 287 : 0 : } 288 : : 289 : 0 : QVariantMap layerObj; 290 : 0 : layerObj["id"] = vl->name(); 291 : 0 : layerObj["fields"] = fieldsObj; 292 : 0 : arrayLayers.append( layerObj ); 293 : 0 : } 294 : : 295 : 0 : QVariantMap rootObj; 296 : 0 : rootObj["vector_layers"] = arrayLayers; 297 : 0 : return QString::fromStdString( QgsJsonUtils::jsonFromVariant( rootObj ).dump() ); 298 : 0 : }