QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
qgsvtpktiles.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsvtpktiles.cpp
3 --------------------------------------
4 Date : March 2022
5 Copyright : (C) 2022 by Nyall Dawson
6 Email : nyall dot dawson 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 "qgsvtpktiles.h"
17
18#include "qgslogger.h"
19#include "qgsrectangle.h"
20#include "qgsmessagelog.h"
21#include "qgsjsonutils.h"
22#include "qgsarcgisrestutils.h"
23#include "qgsziputils.h"
24#include "qgslayermetadata.h"
25
26#include <QFile>
27#include <QImage>
28#include <QDomDocument>
29#include <QTextDocumentFragment>
30#include "zip.h"
31#include <iostream>
32
33
34QgsVtpkTiles::QgsVtpkTiles( const QString &filename )
35 : mFilename( filename )
36{
37}
38
40{
41 if ( mZip )
42 {
43 zip_close( mZip );
44 mZip = nullptr;
45 }
46}
47
49{
50 if ( mZip )
51 return true; // already opened
52
53 const QByteArray fileNamePtr = mFilename.toUtf8();
54 int rc = 0;
55 mZip = zip_open( fileNamePtr.constData(), ZIP_CHECKCONS, &rc );
56 if ( rc == ZIP_ER_OK && mZip )
57 {
58 const int count = zip_get_num_entries( mZip, ZIP_FL_UNCHANGED );
59 if ( count != -1 )
60 {
61 return true;
62 }
63 else
64 {
65 QgsMessageLog::logMessage( QObject::tr( "Error getting files: '%1'" ).arg( zip_strerror( mZip ) ) );
66 zip_close( mZip );
67 mZip = nullptr;
68 return false;
69 }
70 }
71 else
72 {
73 QgsMessageLog::logMessage( QObject::tr( "Error opening zip archive: '%1' (Error code: %2)" ).arg( mZip ? zip_strerror( mZip ) : mFilename ).arg( rc ) );
74 if ( mZip )
75 {
76 zip_close( mZip );
77 mZip = nullptr;
78 }
79 return false;
80 }
81}
82
84{
85 return static_cast< bool >( mZip );
86}
87
88QVariantMap QgsVtpkTiles::metadata() const
89{
90 if ( !mMetadata.isEmpty() )
91 return mMetadata;
92
93 if ( !mZip )
94 return QVariantMap();
95
96 const char *name = "p12/root.json";
97 struct zip_stat stat;
98 zip_stat_init( &stat );
99 zip_stat( mZip, name, 0, &stat );
100
101 const size_t len = stat.size;
102 const std::unique_ptr< char[] > buf( new char[len + 1] );
103
104 //Read the compressed file
105 zip_file *file = zip_fopen( mZip, name, 0 );
106 if ( zip_fread( file, buf.get(), len ) != -1 )
107 {
108 buf[ len ] = '\0';
109 std::string jsonString( buf.get( ) );
110 mMetadata = QgsJsonUtils::parseJson( jsonString ).toMap();
111 zip_fclose( file );
112 file = nullptr;
113 }
114 else
115 {
116 if ( file )
117 zip_fclose( file );
118 file = nullptr;
119 QgsMessageLog::logMessage( QObject::tr( "Error reading metadata: '%1'" ).arg( zip_strerror( mZip ) ) );
120 }
121
122 mTileMapPath = mMetadata.value( QStringLiteral( "tileMap" ) ).toString();
123
124 return mMetadata;
125}
126
128{
129 if ( !mZip )
130 return QVariantMap();
131
132 const char *name = "p12/resources/styles/root.json";
133 struct zip_stat stat;
134 zip_stat_init( &stat );
135 zip_stat( mZip, name, 0, &stat );
136
137 const size_t len = stat.size;
138 const std::unique_ptr< char[] > buf( new char[len + 1] );
139
140 QVariantMap style;
141 //Read the compressed file
142 zip_file *file = zip_fopen( mZip, name, 0 );
143 if ( zip_fread( file, buf.get(), len ) != -1 )
144 {
145 buf[ len ] = '\0';
146 std::string jsonString( buf.get( ) );
147 style = QgsJsonUtils::parseJson( jsonString ).toMap();
148 zip_fclose( file );
149 file = nullptr;
150 }
151 else
152 {
153 if ( file )
154 zip_fclose( file );
155 file = nullptr;
156 QgsMessageLog::logMessage( QObject::tr( "Error reading style definition: '%1'" ).arg( zip_strerror( mZip ) ) );
157 }
158 return style;
159}
160
162{
163 if ( !mZip )
164 return QVariantMap();
165
166 for ( int resolution = 2; resolution > 0; resolution-- )
167 {
168 const QString spriteFileCandidate = QStringLiteral( "p12/resources/sprites/sprite%1.json" ).arg( resolution > 1 ? QStringLiteral( "@%1x" ).arg( resolution ) : QString() );
169 const QByteArray spriteFileCandidateBa = spriteFileCandidate.toLocal8Bit();
170 const char *name = spriteFileCandidateBa.constData();
171 struct zip_stat stat;
172 zip_stat_init( &stat );
173 zip_stat( mZip, name, 0, &stat );
174
175 if ( !stat.valid )
176 continue;
177
178 const size_t len = stat.size;
179 const std::unique_ptr< char[] > buf( new char[len + 1] );
180
181 QVariantMap definition;
182 //Read the compressed file
183 zip_file *file = zip_fopen( mZip, name, 0 );
184 if ( zip_fread( file, buf.get(), len ) != -1 )
185 {
186 buf[ len ] = '\0';
187 std::string jsonString( buf.get( ) );
188 definition = QgsJsonUtils::parseJson( jsonString ).toMap();
189 zip_fclose( file );
190 file = nullptr;
191 }
192 else
193 {
194 if ( file )
195 zip_fclose( file );
196 file = nullptr;
197 QgsMessageLog::logMessage( QObject::tr( "Error reading sprite definition: '%1'" ).arg( zip_strerror( mZip ) ) );
198 }
199 return definition;
200 }
201
202 return QVariantMap();
203}
204
206{
207 if ( !mZip )
208 return QImage();
209
210 for ( int resolution = 2; resolution > 0; resolution-- )
211 {
212 const QString spriteFileCandidate = QStringLiteral( "p12/resources/sprites/sprite%1.png" ).arg( resolution > 1 ? QStringLiteral( "@%1x" ).arg( resolution ) : QString() );
213 const QByteArray spriteFileCandidateBa = spriteFileCandidate.toLocal8Bit();
214 const char *name = spriteFileCandidateBa.constData();
215 struct zip_stat stat;
216 zip_stat_init( &stat );
217 zip_stat( mZip, name, 0, &stat );
218
219 if ( !stat.valid )
220 continue;
221
222 const size_t len = stat.size;
223 const std::unique_ptr< char[] > buf( new char[len + 1] );
224
225 QImage result;
226 //Read the compressed file
227 zip_file *file = zip_fopen( mZip, name, 0 );
228 if ( zip_fread( file, buf.get(), len ) != -1 )
229 {
230 buf[ len ] = '\0';
231
232 result = QImage::fromData( reinterpret_cast<const uchar *>( buf.get() ), len );
233
234 zip_fclose( file );
235 file = nullptr;
236 }
237 else
238 {
239 if ( file )
240 zip_fclose( file );
241 file = nullptr;
242 QgsMessageLog::logMessage( QObject::tr( "Error reading sprite image: '%1'" ).arg( zip_strerror( mZip ) ) );
243 }
244 return result;
245 }
246
247 return QImage();
248}
249
251{
252 if ( !mZip )
253 return QgsLayerMetadata();
254
255 const char *name = "esriinfo/iteminfo.xml";
256 struct zip_stat stat;
257 zip_stat_init( &stat );
258 zip_stat( mZip, name, 0, &stat );
259
260 const size_t len = stat.size;
261 QByteArray buf( len, Qt::Uninitialized );
262
264 //Read the compressed file
265 zip_file *file = zip_fopen( mZip, name, 0 );
266 if ( zip_fread( file, buf.data(), len ) != -1 )
267 {
268 zip_fclose( file );
269 file = nullptr;
270
271 QDomDocument doc;
272 QString errorMessage;
273 int errorLine = 0;
274 int errorColumn = 0;
275 if ( !doc.setContent( buf, false, &errorMessage, &errorLine, &errorColumn ) )
276 {
277 QgsMessageLog::logMessage( QObject::tr( "Error reading layer metadata (line %1, col %2): %3" ).arg( errorLine ).arg( errorColumn ).arg( errorMessage ) );
278 }
279 else
280 {
281 metadata.setType( QStringLiteral( "dataset" ) );
282
283 const QDomElement infoElement = doc.firstChildElement( QStringLiteral( "ESRI_ItemInformation" ) );
284
285 metadata.setLanguage( infoElement.attribute( QStringLiteral( "Culture" ) ) );
286
287 const QDomElement guidElement = infoElement.firstChildElement( QStringLiteral( "guid" ) );
288 metadata.setIdentifier( guidElement.text() );
289
290 const QDomElement nameElement = infoElement.firstChildElement( QStringLiteral( "name" ) );
291 metadata.setTitle( nameElement.text() );
292
293 const QDomElement descriptionElement = infoElement.firstChildElement( QStringLiteral( "description" ) );
294 metadata.setAbstract( QTextDocumentFragment::fromHtml( descriptionElement.text() ).toPlainText() );
295
296 const QDomElement tagsElement = infoElement.firstChildElement( QStringLiteral( "tags" ) );
297
298 const QStringList rawTags = tagsElement.text().split( ',' );
299 QStringList tags;
300 tags.reserve( rawTags.size() );
301 for ( const QString &tag : rawTags )
302 tags.append( tag.trimmed() );
303 metadata.addKeywords( QStringLiteral( "keywords" ), tags );
304
305 const QDomElement accessInformationElement = infoElement.firstChildElement( QStringLiteral( "accessinformation" ) );
306 metadata.setRights( { accessInformationElement.text() } );
307
308 const QDomElement licenseInfoElement = infoElement.firstChildElement( QStringLiteral( "licenseinfo" ) );
309 metadata.setLicenses( { QTextDocumentFragment::fromHtml( licenseInfoElement.text() ).toPlainText() } );
310
311 const QDomElement extentElement = infoElement.firstChildElement( QStringLiteral( "extent" ) );
312 const double xMin = extentElement.firstChildElement( QStringLiteral( "xmin" ) ).text().toDouble();
313 const double xMax = extentElement.firstChildElement( QStringLiteral( "xmax" ) ).text().toDouble();
314 const double yMin = extentElement.firstChildElement( QStringLiteral( "ymin" ) ).text().toDouble();
315 const double yMax = extentElement.firstChildElement( QStringLiteral( "ymax" ) ).text().toDouble();
316
318
320 spatialExtent.bounds = QgsBox3D( QgsRectangle( xMin, yMin, xMax, yMax ) );
321 spatialExtent.extentCrs = QgsCoordinateReferenceSystem( "EPSG:4326" );
323 extent.setSpatialExtents( { spatialExtent } );
324 metadata.setExtent( extent );
325 metadata.setCrs( crs );
326
327 return metadata;
328 }
329 }
330 else
331 {
332 if ( file )
333 zip_fclose( file );
334 file = nullptr;
335 QgsMessageLog::logMessage( QObject::tr( "Error reading layer metadata: '%1'" ).arg( zip_strerror( mZip ) ) );
336 }
337 return metadata;
338}
339
340QVariantMap QgsVtpkTiles::rootTileMap() const
341{
342 // make sure metadata has been read already
343 ( void )metadata();
344
345 if ( mHasReadTileMap || mTileMapPath.isEmpty() )
346 return mRootTileMap;
347
348 if ( !mZip )
349 return QVariantMap();
350
351 const QString tileMapPath = QStringLiteral( "p12/%1/root.json" ).arg( mTileMapPath );
352 struct zip_stat stat;
353 zip_stat_init( &stat );
354 zip_stat( mZip, tileMapPath.toLocal8Bit().constData(), 0, &stat );
355
356 const size_t len = stat.size;
357 const std::unique_ptr< char[] > buf( new char[len + 1] );
358
359 //Read the compressed file
360 zip_file *file = zip_fopen( mZip, tileMapPath.toLocal8Bit().constData(), 0 );
361 if ( !file )
362 {
363 QgsDebugError( QStringLiteral( "Tilemap %1 was not found in vtpk archive" ).arg( tileMapPath ) );
364 mTileMapPath.clear();
365 return mRootTileMap;
366 }
367
368 if ( zip_fread( file, buf.get(), len ) != -1 )
369 {
370 buf[ len ] = '\0';
371 std::string jsonString( buf.get( ) );
372 mRootTileMap = QgsJsonUtils::parseJson( jsonString ).toMap();
373 zip_fclose( file );
374 file = nullptr;
375 }
376 else
377 {
378 if ( file )
379 zip_fclose( file );
380 file = nullptr;
381 QgsDebugError( QStringLiteral( "Tilemap %1 could not be read from vtpk archive" ).arg( tileMapPath ) );
382 mTileMapPath.clear();
383 }
384 mHasReadTileMap = true;
385 return mRootTileMap;
386}
387
389{
390 if ( !mMatrixSet.isEmpty() )
391 return mMatrixSet;
392
393 mMatrixSet.fromEsriJson( metadata(), rootTileMap() );
394 return mMatrixSet;
395}
396
398{
399 return matrixSet().crs();
400}
401
403{
404 const QVariantMap md = metadata();
405
406 const QVariantMap fullExtent = md.value( QStringLiteral( "fullExtent" ) ).toMap();
407 if ( !fullExtent.isEmpty() )
408 {
409 QgsRectangle fullExtentRect(
410 fullExtent.value( QStringLiteral( "xmin" ) ).toDouble(),
411 fullExtent.value( QStringLiteral( "ymin" ) ).toDouble(),
412 fullExtent.value( QStringLiteral( "xmax" ) ).toDouble(),
413 fullExtent.value( QStringLiteral( "ymax" ) ).toDouble()
414 );
415
416 const QgsCoordinateReferenceSystem fullExtentCrs = QgsArcGisRestUtils::convertSpatialReference( fullExtent.value( QStringLiteral( "spatialReference" ) ).toMap() );
417 const QgsCoordinateTransform extentTransform( fullExtentCrs, crs(), context );
418 try
419 {
420 return extentTransform.transformBoundingBox( fullExtentRect );
421 }
422 catch ( QgsCsException & )
423 {
424 QgsDebugError( QStringLiteral( "Could not transform layer fullExtent to layer CRS" ) );
425 }
426 }
427
428 return QgsRectangle();
429}
430
431QByteArray QgsVtpkTiles::tileData( int z, int x, int y )
432{
433 if ( !mZip )
434 {
435 QgsDebugError( QStringLiteral( "VTPK tile package not open: " ) + mFilename );
436 return QByteArray();
437 }
438 if ( mPacketSize < 0 )
439 mPacketSize = metadata().value( QStringLiteral( "resourceInfo" ) ).toMap().value( QStringLiteral( "cacheInfo" ) ).toMap().value( QStringLiteral( "storageInfo" ) ).toMap().value( QStringLiteral( "packetSize" ) ).toInt();
440
441 const int fileRow = mPacketSize * static_cast< int >( std::floor( y / static_cast< double>( mPacketSize ) ) );
442 const int fileCol = mPacketSize * static_cast< int >( std::floor( x / static_cast< double>( mPacketSize ) ) );
443
444 const QString tileName = QStringLiteral( "R%1C%2" )
445 .arg( fileRow, 4, 16, QLatin1Char( '0' ) )
446 .arg( fileCol, 4, 16, QLatin1Char( '0' ) );
447
448 const QString fileName = QStringLiteral( "p12/tile/L%1/%2.bundle" )
449 .arg( z, 2, 10, QLatin1Char( '0' ) ).arg( tileName );
450 struct zip_stat stat;
451 zip_stat_init( &stat );
452 zip_stat( mZip, fileName.toLocal8Bit().constData(), 0, &stat );
453
454 const size_t tileIndexOffset = 64 + 8 * ( mPacketSize * ( y % mPacketSize ) + ( x % mPacketSize ) );
455
456 QByteArray res;
457 const size_t len = stat.size;
458 if ( len <= tileIndexOffset )
459 {
460 // seems this should be treated as "no content" here, rather then a broken VTPK
461 res = QByteArray( "" );
462 }
463 else
464 {
465 const std::unique_ptr< char[] > buf( new char[len] );
466
467 //Read the compressed file
468 zip_file *file = zip_fopen( mZip, fileName.toLocal8Bit().constData(), 0 );
469 if ( zip_fread( file, buf.get(), len ) != -1 )
470 {
471 unsigned long long indexValue;
472 memcpy( &indexValue, buf.get() + tileIndexOffset, 8 );
473
474 const std::size_t tileOffset = indexValue % ( 2ULL << 39 );
475 const std::size_t tileSize = static_cast< std::size_t>( std::floor( indexValue / ( 2ULL << 39 ) ) );
476 // bundle is a gzip file;
477 if ( tileSize == 0 )
478 {
479 // construct a non-null bytearray
480 res = QByteArray( "" );
481 }
482 else if ( !QgsZipUtils::decodeGzip( buf.get() + tileOffset, tileSize, res ) )
483 {
484 QgsMessageLog::logMessage( QObject::tr( "Error extracting bundle contents as gzip: %1" ).arg( fileName ) );
485 }
486 }
487 else
488 {
489 QgsMessageLog::logMessage( QObject::tr( "Error reading tile: '%1'" ).arg( zip_strerror( mZip ) ) );
490 }
491 if ( file )
492 zip_fclose( file );
493 file = nullptr;
494 }
495
496 return res;
497}
498
static QgsCoordinateReferenceSystem convertSpatialReference(const QVariantMap &spatialReferenceMap)
Converts a spatial reference JSON definition to a QgsCoordinateReferenceSystem value.
A 3-dimensional box composed of x, y, z coordinates.
Definition: qgsbox3d.h:43
This class represents a coordinate reference system (CRS).
Contains information about the context in which a coordinate transform is executed.
Class for doing transforms between two map coordinate systems.
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.
Definition: qgsexception.h:67
static QVariant parseJson(const std::string &jsonString)
Converts JSON jsonString to a QVariant, in case of parsing error an invalid QVariant is returned and ...
A structured metadata store for a map layer.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
A rectangle specified with double values.
Definition: qgsrectangle.h:42
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system associated with the tiles.
Definition: qgstiles.cpp:218
bool isEmpty() const
Returns true if the matrix set is empty.
Definition: qgstiles.cpp:138
Encapsulates properties of a vector tile matrix set, including tile origins and scaling information.
bool fromEsriJson(const QVariantMap &json, const QVariantMap &rootTileMap=QVariantMap())
Initializes the tile structure settings from an ESRI REST VectorTileService json map.
QByteArray tileData(int z, int x, int y)
Returns the raw tile data for the matching tile.
QgsRectangle extent(const QgsCoordinateTransformContext &context) const
Returns bounding box from metadata, given in the tiles crs().
bool isOpen() const
Returns whether the VTPK file is currently opened.
QVariantMap rootTileMap() const
Returns the root tilemap content, if it exists.
QVariantMap spriteDefinition() const
Returns the VTPK sprites definitions.
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system of the tiles.
QgsLayerMetadata layerMetadata() const
Reads layer metadata from the VTPK file.
bool open()
Tries to open the file, returns true on success.
QgsVtpkTiles(const QString &filename)
Constructs VTPK reader (but it does not open the file yet)
QgsVectorTileMatrixSet matrixSet() const
Returns the vector tile matrix set representing the tiles.
QVariantMap styleDefinition() const
Returns the VTPK style definition.
QVariantMap metadata() const
Returns the VTPK metadata.
QImage spriteImage() const
Returns the VTPK sprite image, if it exists.
CORE_EXPORT bool decodeGzip(const QByteArray &bytesIn, QByteArray &bytesOut)
Decodes gzip byte stream, returns true on success.
#define QgsDebugError(str)
Definition: qgslogger.h:38
Metadata extent structure.
Metadata spatial extent structure.
QgsCoordinateReferenceSystem extentCrs
Coordinate reference system for spatial extent.
QgsBox3D bounds
Geospatial extent of the resource.