QGIS API Documentation 3.30.0-'s-Hertogenbosch (f186b8efe0)
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 "qgsmbtiles.h"
24#include "qgsziputils.h"
25#include "qgslayermetadata.h"
26
27#include <QFile>
28#include <QImage>
29#include <QDomDocument>
30#include <QTextDocumentFragment>
31#include "zip.h"
32#include <iostream>
33
34
35QgsVtpkTiles::QgsVtpkTiles( const QString &filename )
36 : mFilename( filename )
37{
38}
39
41{
42 if ( mZip )
43 {
44 zip_close( mZip );
45 mZip = nullptr;
46 }
47}
48
50{
51 if ( mZip )
52 return true; // already opened
53
54 const QByteArray fileNamePtr = mFilename.toUtf8();
55 int rc = 0;
56 mZip = zip_open( fileNamePtr.constData(), ZIP_CHECKCONS, &rc );
57 if ( rc == ZIP_ER_OK && mZip )
58 {
59 const int count = zip_get_num_files( mZip );
60 if ( count != -1 )
61 {
62 return true;
63 }
64 else
65 {
66 QgsMessageLog::logMessage( QObject::tr( "Error getting files: '%1'" ).arg( zip_strerror( mZip ) ) );
67 zip_close( mZip );
68 mZip = nullptr;
69 return false;
70 }
71 }
72 else
73 {
74 QgsMessageLog::logMessage( QObject::tr( "Error opening zip archive: '%1' (Error code: %2)" ).arg( mZip ? zip_strerror( mZip ) : mFilename ).arg( rc ) );
75 if ( mZip )
76 {
77 zip_close( mZip );
78 mZip = nullptr;
79 }
80 return false;
81 }
82}
83
85{
86 return static_cast< bool >( mZip );
87}
88
89QVariantMap QgsVtpkTiles::metadata() const
90{
91 if ( !mMetadata.isEmpty() )
92 return mMetadata;
93
94 if ( !mZip )
95 return QVariantMap();
96
97 const char *name = "p12/root.json";
98 struct zip_stat stat;
99 zip_stat_init( &stat );
100 zip_stat( mZip, name, 0, &stat );
101
102 const size_t len = stat.size;
103 const std::unique_ptr< char[] > buf( new char[len + 1] );
104
105 //Read the compressed file
106 zip_file *file = zip_fopen( mZip, name, 0 );
107 if ( zip_fread( file, buf.get(), len ) != -1 )
108 {
109 buf[ len ] = '\0';
110 std::string jsonString( buf.get( ) );
111 mMetadata = QgsJsonUtils::parseJson( jsonString ).toMap();
112 zip_fclose( file );
113 file = nullptr;
114 }
115 else
116 {
117 zip_fclose( file );
118 file = nullptr;
119 QgsMessageLog::logMessage( QObject::tr( "Error reading metadata: '%1'" ).arg( zip_strerror( mZip ) ) );
120 }
121 return mMetadata;
122}
123
125{
126 if ( !mZip )
127 return QVariantMap();
128
129 const char *name = "p12/resources/styles/root.json";
130 struct zip_stat stat;
131 zip_stat_init( &stat );
132 zip_stat( mZip, name, 0, &stat );
133
134 const size_t len = stat.size;
135 const std::unique_ptr< char[] > buf( new char[len + 1] );
136
137 QVariantMap style;
138 //Read the compressed file
139 zip_file *file = zip_fopen( mZip, name, 0 );
140 if ( zip_fread( file, buf.get(), len ) != -1 )
141 {
142 buf[ len ] = '\0';
143 std::string jsonString( buf.get( ) );
144 style = QgsJsonUtils::parseJson( jsonString ).toMap();
145 zip_fclose( file );
146 file = nullptr;
147 }
148 else
149 {
150 zip_fclose( file );
151 file = nullptr;
152 QgsMessageLog::logMessage( QObject::tr( "Error reading style definition: '%1'" ).arg( zip_strerror( mZip ) ) );
153 }
154 return style;
155}
156
158{
159 if ( !mZip )
160 return QVariantMap();
161
162 for ( int resolution = 2; resolution > 0; resolution-- )
163 {
164 const QString spriteFileCandidate = QStringLiteral( "p12/resources/sprites/sprite%1.json" ).arg( resolution > 1 ? QStringLiteral( "@%1x" ).arg( resolution ) : QString() );
165 const QByteArray spriteFileCandidateBa = spriteFileCandidate.toLocal8Bit();
166 const char *name = spriteFileCandidateBa.constData();
167 struct zip_stat stat;
168 zip_stat_init( &stat );
169 zip_stat( mZip, name, 0, &stat );
170
171 if ( !stat.valid )
172 continue;
173
174 const size_t len = stat.size;
175 const std::unique_ptr< char[] > buf( new char[len + 1] );
176
177 QVariantMap definition;
178 //Read the compressed file
179 zip_file *file = zip_fopen( mZip, name, 0 );
180 if ( zip_fread( file, buf.get(), len ) != -1 )
181 {
182 buf[ len ] = '\0';
183 std::string jsonString( buf.get( ) );
184 definition = QgsJsonUtils::parseJson( jsonString ).toMap();
185 zip_fclose( file );
186 file = nullptr;
187 }
188 else
189 {
190 zip_fclose( file );
191 file = nullptr;
192 QgsMessageLog::logMessage( QObject::tr( "Error reading sprite definition: '%1'" ).arg( zip_strerror( mZip ) ) );
193 }
194 return definition;
195 }
196
197 return QVariantMap();
198}
199
201{
202 if ( !mZip )
203 return QImage();
204
205 for ( int resolution = 2; resolution > 0; resolution-- )
206 {
207 const QString spriteFileCandidate = QStringLiteral( "p12/resources/sprites/sprite%1.png" ).arg( resolution > 1 ? QStringLiteral( "@%1x" ).arg( resolution ) : QString() );
208 const QByteArray spriteFileCandidateBa = spriteFileCandidate.toLocal8Bit();
209 const char *name = spriteFileCandidateBa.constData();
210 struct zip_stat stat;
211 zip_stat_init( &stat );
212 zip_stat( mZip, name, 0, &stat );
213
214 if ( !stat.valid )
215 continue;
216
217 const size_t len = stat.size;
218 const std::unique_ptr< char[] > buf( new char[len + 1] );
219
220 QImage result;
221 //Read the compressed file
222 zip_file *file = zip_fopen( mZip, name, 0 );
223 if ( zip_fread( file, buf.get(), len ) != -1 )
224 {
225 buf[ len ] = '\0';
226
227 result = QImage::fromData( reinterpret_cast<const uchar *>( buf.get() ), len );
228
229 zip_fclose( file );
230 file = nullptr;
231 }
232 else
233 {
234 zip_fclose( file );
235 file = nullptr;
236 QgsMessageLog::logMessage( QObject::tr( "Error reading sprite image: '%1'" ).arg( zip_strerror( mZip ) ) );
237 }
238 return result;
239 }
240
241 return QImage();
242}
243
245{
246 if ( !mZip )
247 return QgsLayerMetadata();
248
249 const char *name = "esriinfo/iteminfo.xml";
250 struct zip_stat stat;
251 zip_stat_init( &stat );
252 zip_stat( mZip, name, 0, &stat );
253
254 const size_t len = stat.size;
255 QByteArray buf( len, Qt::Uninitialized );
256
258 //Read the compressed file
259 zip_file *file = zip_fopen( mZip, name, 0 );
260 if ( zip_fread( file, buf.data(), len ) != -1 )
261 {
262 zip_fclose( file );
263 file = nullptr;
264
265 QDomDocument doc;
266 QString errorMessage;
267 int errorLine = 0;
268 int errorColumn = 0;
269 if ( !doc.setContent( buf, false, &errorMessage, &errorLine, &errorColumn ) )
270 {
271 QgsMessageLog::logMessage( QObject::tr( "Error reading layer metadata (line %1, col %2): %3" ).arg( errorLine ).arg( errorColumn ).arg( errorMessage ) );
272 }
273 else
274 {
275 metadata.setType( QStringLiteral( "dataset" ) );
276
277 const QDomElement infoElement = doc.firstChildElement( QStringLiteral( "ESRI_ItemInformation" ) );
278
279 metadata.setLanguage( infoElement.attribute( QStringLiteral( "Culture" ) ) );
280
281 const QDomElement guidElement = infoElement.firstChildElement( QStringLiteral( "guid" ) );
282 metadata.setIdentifier( guidElement.text() );
283
284 const QDomElement nameElement = infoElement.firstChildElement( QStringLiteral( "name" ) );
285 metadata.setTitle( nameElement.text() );
286
287 const QDomElement descriptionElement = infoElement.firstChildElement( QStringLiteral( "description" ) );
288 metadata.setAbstract( QTextDocumentFragment::fromHtml( descriptionElement.text() ).toPlainText() );
289
290 const QDomElement tagsElement = infoElement.firstChildElement( QStringLiteral( "tags" ) );
291
292 const QStringList rawTags = tagsElement.text().split( ',' );
293 QStringList tags;
294 tags.reserve( rawTags.size() );
295 for ( const QString &tag : rawTags )
296 tags.append( tag.trimmed() );
297 metadata.addKeywords( QStringLiteral( "keywords" ), tags );
298
299 const QDomElement accessInformationElement = infoElement.firstChildElement( QStringLiteral( "accessinformation" ) );
300 metadata.setRights( { accessInformationElement.text() } );
301
302 const QDomElement licenseInfoElement = infoElement.firstChildElement( QStringLiteral( "licenseinfo" ) );
303 metadata.setLicenses( { QTextDocumentFragment::fromHtml( licenseInfoElement.text() ).toPlainText() } );
304
305 const QDomElement extentElement = infoElement.firstChildElement( QStringLiteral( "extent" ) );
306 const double xMin = extentElement.firstChildElement( QStringLiteral( "xmin" ) ).text().toDouble();
307 const double xMax = extentElement.firstChildElement( QStringLiteral( "xmax" ) ).text().toDouble();
308 const double yMin = extentElement.firstChildElement( QStringLiteral( "ymin" ) ).text().toDouble();
309 const double yMax = extentElement.firstChildElement( QStringLiteral( "ymax" ) ).text().toDouble();
310
312
314 spatialExtent.bounds = QgsBox3d( QgsRectangle( xMin, yMin, xMax, yMax ) );
315 spatialExtent.extentCrs = QgsCoordinateReferenceSystem( "EPSG:4326" );
317 extent.setSpatialExtents( { spatialExtent } );
318 metadata.setExtent( extent );
319 metadata.setCrs( crs );
320
321 return metadata;
322 }
323 }
324 else
325 {
326 zip_fclose( file );
327 file = nullptr;
328 QgsMessageLog::logMessage( QObject::tr( "Error reading layer metadata: '%1'" ).arg( zip_strerror( mZip ) ) );
329 }
330 return metadata;
331}
332
334{
335 if ( !mMatrixSet.isEmpty() )
336 return mMatrixSet;
337
338 mMatrixSet.fromEsriJson( metadata() );
339 return mMatrixSet;
340}
341
343{
344 return matrixSet().crs();
345}
346
348{
349 const QVariantMap md = metadata();
350
351 const QVariantMap fullExtent = md.value( QStringLiteral( "fullExtent" ) ).toMap();
352 if ( !fullExtent.isEmpty() )
353 {
354 QgsRectangle fullExtentRect(
355 fullExtent.value( QStringLiteral( "xmin" ) ).toDouble(),
356 fullExtent.value( QStringLiteral( "ymin" ) ).toDouble(),
357 fullExtent.value( QStringLiteral( "xmax" ) ).toDouble(),
358 fullExtent.value( QStringLiteral( "ymax" ) ).toDouble()
359 );
360
361 const QgsCoordinateReferenceSystem fullExtentCrs = QgsArcGisRestUtils::convertSpatialReference( fullExtent.value( QStringLiteral( "spatialReference" ) ).toMap() );
362 const QgsCoordinateTransform extentTransform( fullExtentCrs, crs(), context );
363 try
364 {
365 return extentTransform.transformBoundingBox( fullExtentRect );
366 }
367 catch ( QgsCsException & )
368 {
369 QgsDebugMsg( QStringLiteral( "Could not transform layer fullExtent to layer CRS" ) );
370 }
371 }
372
373 return QgsRectangle();
374}
375
376QByteArray QgsVtpkTiles::tileData( int z, int x, int y )
377{
378 if ( !mZip )
379 {
380 QgsDebugMsg( QStringLiteral( "VTPK tile package not open: " ) + mFilename );
381 return QByteArray();
382 }
383 if ( mPacketSize < 0 )
384 mPacketSize = metadata().value( QStringLiteral( "resourceInfo" ) ).toMap().value( QStringLiteral( "cacheInfo" ) ).toMap().value( QStringLiteral( "storageInfo" ) ).toMap().value( QStringLiteral( "packetSize" ) ).toInt();
385
386 const int fileRow = mPacketSize * static_cast< int >( std::floor( y / static_cast< double>( mPacketSize ) ) );
387 const int fileCol = mPacketSize * static_cast< int >( std::floor( x / static_cast< double>( mPacketSize ) ) );
388
389 const QString tileName = QStringLiteral( "R%1C%2" )
390 .arg( fileRow, 4, 16, QLatin1Char( '0' ) )
391 .arg( fileCol, 4, 16, QLatin1Char( '0' ) );
392
393 const QString fileName = QStringLiteral( "p12/tile/L%1/%2.bundle" )
394 .arg( z, 2, 10, QLatin1Char( '0' ) ).arg( tileName );
395 struct zip_stat stat;
396 zip_stat_init( &stat );
397 zip_stat( mZip, fileName.toLocal8Bit().constData(), 0, &stat );
398
399 const size_t tileIndexOffset = 64 + 8 * ( mPacketSize * ( y % mPacketSize ) + ( x % mPacketSize ) );
400
401 QByteArray res;
402 const size_t len = stat.size;
403 if ( len <= tileIndexOffset )
404 {
405 QgsMessageLog::logMessage( QObject::tr( "Cannot read gzip contents at offset %1: %2" ).arg( tileIndexOffset ).arg( fileName ) );
406 }
407 else
408 {
409 const std::unique_ptr< char[] > buf( new char[len] );
410
411 //Read the compressed file
412 zip_file *file = zip_fopen( mZip, fileName.toLocal8Bit().constData(), 0 );
413 if ( zip_fread( file, buf.get(), len ) != -1 )
414 {
415 unsigned long long indexValue;
416 memcpy( &indexValue, buf.get() + tileIndexOffset, 8 );
417
418 const std::size_t tileOffset = indexValue % ( 2ULL << 39 );
419 const std::size_t tileSize = static_cast< std::size_t>( std::floor( indexValue / ( 2ULL << 39 ) ) );
420
421 // bundle is a gzip file;
422 if ( !QgsZipUtils::decodeGzip( buf.get() + tileOffset, tileSize, res ) )
423 {
424 QgsMessageLog::logMessage( QObject::tr( "Error extracting bundle contents as gzip: %1" ).arg( fileName ) );
425 }
426 }
427 else
428 {
429 QgsMessageLog::logMessage( QObject::tr( "Error reading tile: '%1'" ).arg( zip_strerror( mZip ) ) );
430 }
431 zip_fclose( file );
432 file = nullptr;
433 }
434
435 return res;
436}
437
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:39
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 SIP_THROW(QgsCsException)
Transforms a rectangle from the source CRS to the destination CRS.
Custom exception class for Coordinate Reference System related exceptions.
Definition: qgsexception.h:66
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:207
bool isEmpty() const
Returns true if the matrix set is empty.
Definition: qgstiles.cpp:132
Encapsulates properties of a vector tile matrix set, including tile origins and scaling information.
bool fromEsriJson(const QVariantMap &json)
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 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 QgsDebugMsg(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.