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