QGIS API Documentation  3.22.4-Białowieża (ce8e65e95e)
qgsmbtiles.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsmbtiles.cpp
3  --------------------------------------
4  Date : January 2020
5  Copyright : (C) 2020 by Martin Dobias
6  Email : wonder dot sk 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 "qgsmbtiles.h"
17 
18 #include "qgslogger.h"
19 #include "qgsrectangle.h"
20 
21 #include <QFile>
22 #include <QImage>
23 
24 #include <zlib.h>
25 
26 
27 QgsMbTiles::QgsMbTiles( const QString &filename )
28  : mFilename( filename )
29 {
30 }
31 
33 {
34  if ( mDatabase )
35  return true; // already opened
36 
37  const sqlite3_database_unique_ptr database;
38  const int result = mDatabase.open_v2( mFilename, SQLITE_OPEN_READONLY, nullptr );
39  if ( result != SQLITE_OK )
40  {
41  QgsDebugMsg( QStringLiteral( "Can't open MBTiles database: %1" ).arg( database.errorMessage() ) );
42  return false;
43  }
44  return true;
45 }
46 
47 bool QgsMbTiles::isOpen() const
48 {
49  return bool( mDatabase );
50 }
51 
53 {
54  if ( mDatabase )
55  return false;
56 
57  if ( QFile::exists( mFilename ) )
58  return false;
59 
60  const sqlite3_database_unique_ptr database;
61  int result = mDatabase.open_v2( mFilename, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr );
62  if ( result != SQLITE_OK )
63  {
64  QgsDebugMsg( QStringLiteral( "Can't create MBTiles database: %1" ).arg( database.errorMessage() ) );
65  return false;
66  }
67 
68  const QString sql = \
69  "CREATE TABLE metadata (name text, value text);" \
70  "CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob);" \
71  "CREATE UNIQUE INDEX tile_index on tiles (zoom_level, tile_column, tile_row);";
72  QString errorMessage;
73  result = mDatabase.exec( sql, errorMessage );
74  if ( result != SQLITE_OK )
75  {
76  QgsDebugMsg( QStringLiteral( "Failed to initialize MBTiles database: " ) + errorMessage );
77  return false;
78  }
79 
80  return true;
81 }
82 
83 QString QgsMbTiles::metadataValue( const QString &key )
84 {
85  if ( !mDatabase )
86  {
87  QgsDebugMsg( QStringLiteral( "MBTiles database not open: " ) + mFilename );
88  return QString();
89  }
90 
91  int result;
92  const QString sql = QStringLiteral( "select value from metadata where name='%1'" ).arg( key );
93  sqlite3_statement_unique_ptr preparedStatement = mDatabase.prepare( sql, result );
94  if ( result != SQLITE_OK )
95  {
96  QgsDebugMsg( QStringLiteral( "MBTile failed to prepare statement: " ) + sql );
97  return QString();
98  }
99 
100  if ( preparedStatement.step() != SQLITE_ROW )
101  {
102  QgsDebugMsg( QStringLiteral( "MBTile metadata value not found: " ) + key );
103  return QString();
104  }
105 
106  return preparedStatement.columnAsText( 0 );
107 }
108 
109 void QgsMbTiles::setMetadataValue( const QString &key, const QString &value )
110 {
111  if ( !mDatabase )
112  {
113  QgsDebugMsg( QStringLiteral( "MBTiles database not open: " ) + mFilename );
114  return;
115  }
116 
117  int result;
118  const QString sql = QStringLiteral( "insert into metadata values (%1, %2)" ).arg( QgsSqliteUtils::quotedValue( key ), QgsSqliteUtils::quotedValue( value ) );
119  sqlite3_statement_unique_ptr preparedStatement = mDatabase.prepare( sql, result );
120  if ( result != SQLITE_OK )
121  {
122  QgsDebugMsg( QStringLiteral( "MBTile failed to prepare statement: " ) + sql );
123  return;
124  }
125 
126  if ( preparedStatement.step() != SQLITE_DONE )
127  {
128  QgsDebugMsg( QStringLiteral( "MBTile metadata value failed to be set: " ) + key );
129  return;
130  }
131 }
132 
134 {
135  const QString boundsStr = metadataValue( "bounds" );
136  if ( boundsStr.isEmpty() )
137  return QgsRectangle();
138  QStringList boundsArray = boundsStr.split( ',' );
139  if ( boundsArray.count() != 4 )
140  return QgsRectangle();
141 
142  return QgsRectangle( boundsArray[0].toDouble(), boundsArray[1].toDouble(),
143  boundsArray[2].toDouble(), boundsArray[3].toDouble() );
144 }
145 
146 QByteArray QgsMbTiles::tileData( int z, int x, int y )
147 {
148  if ( !mDatabase )
149  {
150  QgsDebugMsg( QStringLiteral( "MBTiles database not open: " ) + mFilename );
151  return QByteArray();
152  }
153 
154  int result;
155  const QString sql = QStringLiteral( "select tile_data from tiles where zoom_level=%1 and tile_column=%2 and tile_row=%3" ).arg( z ).arg( x ).arg( y );
156  sqlite3_statement_unique_ptr preparedStatement = mDatabase.prepare( sql, result );
157  if ( result != SQLITE_OK )
158  {
159  QgsDebugMsg( QStringLiteral( "MBTile failed to prepare statement: " ) + sql );
160  return QByteArray();
161  }
162 
163  if ( preparedStatement.step() != SQLITE_ROW )
164  {
165  QgsDebugMsg( QStringLiteral( "MBTile not found: z=%1 x=%2 y=%3" ).arg( z ).arg( x ).arg( y ) );
166  return QByteArray();
167  }
168 
169  return preparedStatement.columnAsBlob( 0 );
170 }
171 
172 QImage QgsMbTiles::tileDataAsImage( int z, int x, int y )
173 {
174  QImage tileImage;
175  const QByteArray tileBlob = tileData( z, x, y );
176  if ( !tileImage.loadFromData( tileBlob ) )
177  {
178  QgsDebugMsg( QStringLiteral( "MBTile data failed to load: z=%1 x=%2 y=%3" ).arg( z ).arg( x ).arg( y ) );
179  return QImage();
180  }
181  return tileImage;
182 }
183 
184 void QgsMbTiles::setTileData( int z, int x, int y, const QByteArray &data )
185 {
186  if ( !mDatabase )
187  {
188  QgsDebugMsg( QStringLiteral( "MBTiles database not open: " ) + mFilename );
189  return;
190  }
191 
192  int result;
193  const QString sql = QStringLiteral( "insert into tiles values (%1, %2, %3, ?)" ).arg( z ).arg( x ).arg( y );
194  sqlite3_statement_unique_ptr preparedStatement = mDatabase.prepare( sql, result );
195  if ( result != SQLITE_OK )
196  {
197  QgsDebugMsg( QStringLiteral( "MBTile failed to prepare statement: " ) + sql );
198  return;
199  }
200 
201  sqlite3_bind_blob( preparedStatement.get(), 1, data.constData(), data.size(), SQLITE_TRANSIENT );
202 
203  if ( preparedStatement.step() != SQLITE_DONE )
204  {
205  QgsDebugMsg( QStringLiteral( "MBTile tile failed to be set: %1,%2,%3" ).arg( z ).arg( x ).arg( y ) );
206  return;
207  }
208 }
209 
210 bool QgsMbTiles::decodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut )
211 {
212  unsigned char *bytesInPtr = reinterpret_cast<unsigned char *>( const_cast<char *>( bytesIn.constData() ) );
213  uint bytesInLeft = static_cast<uint>( bytesIn.count() );
214 
215  const uint CHUNK = 16384;
216  unsigned char out[CHUNK];
217  const int DEC_MAGIC_NUM_FOR_GZIP = 16;
218 
219  // allocate inflate state
220  z_stream strm;
221  strm.zalloc = Z_NULL;
222  strm.zfree = Z_NULL;
223  strm.opaque = Z_NULL;
224  strm.avail_in = 0;
225  strm.next_in = Z_NULL;
226 
227  int ret = inflateInit2( &strm, MAX_WBITS + DEC_MAGIC_NUM_FOR_GZIP );
228  if ( ret != Z_OK )
229  return false;
230 
231  while ( ret != Z_STREAM_END ) // done when inflate() says it's done
232  {
233  // prepare next chunk
234  const uint bytesToProcess = std::min( CHUNK, bytesInLeft );
235  strm.next_in = bytesInPtr;
236  strm.avail_in = bytesToProcess;
237  bytesInPtr += bytesToProcess;
238  bytesInLeft -= bytesToProcess;
239 
240  if ( bytesToProcess == 0 )
241  break; // we end with an error - no more data but inflate() wants more data
242 
243  // run inflate() on input until output buffer not full
244  do
245  {
246  strm.avail_out = CHUNK;
247  strm.next_out = out;
248  ret = inflate( &strm, Z_NO_FLUSH );
249  Q_ASSERT( ret != Z_STREAM_ERROR ); // state not clobbered
250  if ( ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR )
251  {
252  inflateEnd( &strm );
253  return false;
254  }
255  const unsigned have = CHUNK - strm.avail_out;
256  bytesOut.append( QByteArray::fromRawData( reinterpret_cast<const char *>( out ), static_cast<int>( have ) ) );
257  }
258  while ( strm.avail_out == 0 );
259  }
260 
261  inflateEnd( &strm );
262  return ret == Z_STREAM_END;
263 }
264 
265 
266 bool QgsMbTiles::encodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut )
267 {
268  unsigned char *bytesInPtr = reinterpret_cast<unsigned char *>( const_cast<char *>( bytesIn.constData() ) );
269  const uint bytesInLeft = static_cast<uint>( bytesIn.count() );
270 
271  const uint CHUNK = 16384;
272  unsigned char out[CHUNK];
273  const int DEC_MAGIC_NUM_FOR_GZIP = 16;
274 
275  // allocate deflate state
276  z_stream strm;
277  strm.zalloc = Z_NULL;
278  strm.zfree = Z_NULL;
279  strm.opaque = Z_NULL;
280 
281  int ret = deflateInit2( &strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS + DEC_MAGIC_NUM_FOR_GZIP, 8, Z_DEFAULT_STRATEGY );
282  if ( ret != Z_OK )
283  return false;
284 
285  strm.avail_in = bytesInLeft;
286  strm.next_in = bytesInPtr;
287 
288  // run deflate() on input until output buffer not full, finish
289  // compression if all of source has been read in
290  do
291  {
292  strm.avail_out = CHUNK;
293  strm.next_out = out;
294  ret = deflate( &strm, Z_FINISH ); // no bad return value
295  Q_ASSERT( ret != Z_STREAM_ERROR ); // state not clobbered
296 
297  const unsigned have = CHUNK - strm.avail_out;
298  bytesOut.append( QByteArray::fromRawData( reinterpret_cast<const char *>( out ), static_cast<int>( have ) ) );
299  }
300  while ( strm.avail_out == 0 );
301  Q_ASSERT( ret == Z_STREAM_END ); // stream will be complete
302 
303  // clean up and return
304  deflateEnd( &strm );
305  return true;
306 }
bool create()
Creates a new MBTiles file and initializes it with metadata and tiles tables.
Definition: qgsmbtiles.cpp:52
QImage tileDataAsImage(int z, int x, int y)
Returns tile decoded as a raster image (if stored in a known format like JPG or PNG)
Definition: qgsmbtiles.cpp:172
QgsMbTiles(const QString &filename)
Constructs MBTiles reader (but it does not open the file yet)
Definition: qgsmbtiles.cpp:27
bool open()
Tries to open the file, returns true on success.
Definition: qgsmbtiles.cpp:32
QByteArray tileData(int z, int x, int y)
Returns raw tile data for given tile.
Definition: qgsmbtiles.cpp:146
static bool decodeGzip(const QByteArray &bytesIn, QByteArray &bytesOut)
Decodes gzip byte stream, returns true on success. Useful for reading vector tiles.
Definition: qgsmbtiles.cpp:210
bool isOpen() const
Returns whether the MBTiles file is currently opened.
Definition: qgsmbtiles.cpp:47
void setTileData(int z, int x, int y, const QByteArray &data)
Adds tile data for the given tile coordinates.
Definition: qgsmbtiles.cpp:184
QString metadataValue(const QString &key)
Requests metadata value for the given key.
Definition: qgsmbtiles.cpp:83
QgsRectangle extent()
Returns bounding box from metadata, given in WGS 84 (if available)
Definition: qgsmbtiles.cpp:133
static bool encodeGzip(const QByteArray &bytesIn, QByteArray &bytesOut)
Encodes gzip byte stream, returns true on success. Useful for writing vector tiles.
Definition: qgsmbtiles.cpp:266
void setMetadataValue(const QString &key, const QString &value)
Sets metadata value for the given key.
Definition: qgsmbtiles.cpp:109
A rectangle specified with double values.
Definition: qgsrectangle.h:42
static QString quotedValue(const QVariant &value)
Returns a properly quoted and escaped version of value for use in SQL strings.
Unique pointer for sqlite3 databases, which automatically closes the database when the pointer goes o...
sqlite3_statement_unique_ptr prepare(const QString &sql, int &resultCode) const
Prepares a sql statement, returning the result.
QString errorMessage() const
Returns the most recent error message encountered by the database.
int open_v2(const QString &path, int flags, const char *zVfs)
Opens the database at the specified file path.
int exec(const QString &sql, QString &errorMessage) const
Executes the sql command in the database.
Unique pointer for sqlite3 prepared statements, which automatically finalizes the statement when the ...
QString columnAsText(int column) const
Returns the column value from the current statement row as a string.
int step()
Steps to the next record in the statement, returning the sqlite3 result code.
QByteArray columnAsBlob(int column) const
Returns the column value from the current statement row as raw byte array.
#define QgsDebugMsg(str)
Definition: qgslogger.h:38