QGIS API Documentation  3.26.3-Buenos Aires (65e4edfdad)
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 
35 QgsVtpkTiles::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 
89 QVariantMap 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 
124 QVariantMap QgsVtpkTiles::styleDefinition() const
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 
313  QgsLayerMetadata::SpatialExtent spatialExtent;
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 
376 QByteArray 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 
QgsVtpkTiles::metadata
QVariantMap metadata() const
Returns the VTPK metadata.
Definition: qgsvtpktiles.cpp:89
QgsLayerMetadata::SpatialExtent::extentCrs
QgsCoordinateReferenceSystem extentCrs
Coordinate reference system for spatial extent.
Definition: qgslayermetadata.h:72
QgsCoordinateTransformContext
Contains information about the context in which a coordinate transform is executed.
Definition: qgscoordinatetransformcontext.h:57
qgsziputils.h
QgsVectorTileMatrixSet
Encapsulates properties of a vector tile matrix set, including tile origins and scaling information.
Definition: qgsvectortilematrixset.h:31
qgsrectangle.h
QgsLayerMetadata::SpatialExtent
Metadata spatial extent structure.
Definition: qgslayermetadata.h:63
QgsLayerMetadata
A structured metadata store for a map layer.
Definition: qgslayermetadata.h:56
QgsCoordinateTransform::transformBoundingBox
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.
Definition: qgscoordinatetransform.cpp:560
QgsVtpkTiles::tileData
QByteArray tileData(int z, int x, int y)
Returns the raw tile data for the matching tile.
Definition: qgsvtpktiles.cpp:376
QgsVtpkTiles::extent
QgsRectangle extent(const QgsCoordinateTransformContext &context) const
Returns bounding box from metadata, given in the tiles crs().
Definition: qgsvtpktiles.cpp:347
qgsvtpktiles.h
QgsArcGisRestUtils::convertSpatialReference
static QgsCoordinateReferenceSystem convertSpatialReference(const QVariantMap &spatialReferenceMap)
Converts a spatial reference JSON definition to a QgsCoordinateReferenceSystem value.
Definition: qgsarcgisrestutils.cpp:403
QgsDebugMsg
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QgsVectorTileMatrixSet::fromEsriJson
bool fromEsriJson(const QVariantMap &json)
Initializes the tile structure settings from an ESRI REST VectorTileService json map.
Definition: qgsvectortilematrixset.cpp:29
QgsRectangle
A rectangle specified with double values.
Definition: qgsrectangle.h:41
QgsLayerMetadata::SpatialExtent::bounds
QgsBox3d bounds
Geospatial extent of the resource.
Definition: qgslayermetadata.h:82
qgsmbtiles.h
QgsVtpkTiles::layerMetadata
QgsLayerMetadata layerMetadata() const
Reads layer metadata from the VTPK file.
Definition: qgsvtpktiles.cpp:244
qgslayermetadata.h
QgsCsException
Custom exception class for Coordinate Reference System related exceptions.
Definition: qgsexception.h:65
QgsVtpkTiles::open
bool open()
Tries to open the file, returns true on success.
Definition: qgsvtpktiles.cpp:49
QgsTileMatrixSet::isEmpty
bool isEmpty() const
Returns true if the matrix set is empty.
Definition: qgstiles.cpp:132
QgsZipUtils::decodeGzip
CORE_EXPORT bool decodeGzip(const QByteArray &bytesIn, QByteArray &bytesOut)
Decodes gzip byte stream, returns true on success.
Definition: qgsziputils.cpp:195
QgsVtpkTiles::spriteDefinition
QVariantMap spriteDefinition() const
Returns the VTPK sprites definitions.
Definition: qgsvtpktiles.cpp:157
QgsVtpkTiles::crs
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system of the tiles.
Definition: qgsvtpktiles.cpp:342
QgsVtpkTiles::styleDefinition
QVariantMap styleDefinition() const
Returns the VTPK style definition.
Definition: qgsvtpktiles.cpp:124
QgsMessageLog::logMessage
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).
Definition: qgsmessagelog.cpp:27
QgsVtpkTiles::QgsVtpkTiles
QgsVtpkTiles(const QString &filename)
Constructs VTPK reader (but it does not open the file yet)
Definition: qgsvtpktiles.cpp:35
QgsVtpkTiles::matrixSet
QgsVectorTileMatrixSet matrixSet() const
Returns the vector tile matrix set representing the tiles.
Definition: qgsvtpktiles.cpp:333
QgsVtpkTiles::~QgsVtpkTiles
~QgsVtpkTiles()
Definition: qgsvtpktiles.cpp:40
QgsLayerMetadata::Extent
Metadata extent structure.
Definition: qgslayermetadata.h:91
QgsCoordinateReferenceSystem
This class represents a coordinate reference system (CRS).
Definition: qgscoordinatereferencesystem.h:211
QgsVtpkTiles::isOpen
bool isOpen() const
Returns whether the VTPK file is currently opened.
Definition: qgsvtpktiles.cpp:84
qgsarcgisrestutils.h
QgsTileMatrixSet::crs
QgsCoordinateReferenceSystem crs() const
Returns the coordinate reference system associated with the tiles.
Definition: qgstiles.cpp:195
QgsBox3d
A 3-dimensional box composed of x, y, z coordinates.
Definition: qgsbox3d.h:38
qgsjsonutils.h
qgslogger.h
QgsCoordinateTransform
Class for doing transforms between two map coordinate systems.
Definition: qgscoordinatetransform.h:57
QgsVtpkTiles::spriteImage
QImage spriteImage() const
Returns the VTPK sprite image, if it exists.
Definition: qgsvtpktiles.cpp:200
qgsmessagelog.h
QgsJsonUtils::parseJson
static QVariant parseJson(const std::string &jsonString)
Converts JSON jsonString to a QVariant, in case of parsing error an invalid QVariant is returned and ...
Definition: qgsjsonutils.cpp:456