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