QGIS API Documentation  3.27.0-Master (bef583a8ef)
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 
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:195
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.