QGIS API Documentation  3.24.2-Tisler (13c1a02865)
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  // this is not entirely unexpected -- user may have just requested a tile outside of the extent of the mbtiles package
166  QgsDebugMsgLevel( QStringLiteral( "MBTile not found: z=%1 x=%2 y=%3" ).arg( z ).arg( x ).arg( y ), 2 );
167  return QByteArray();
168  }
169 
170  return preparedStatement.columnAsBlob( 0 );
171 }
172 
173 QImage QgsMbTiles::tileDataAsImage( int z, int x, int y )
174 {
175  QImage tileImage;
176  const QByteArray tileBlob = tileData( z, x, y );
177  if ( !tileImage.loadFromData( tileBlob ) )
178  {
179  QgsDebugMsg( QStringLiteral( "MBTile data failed to load: z=%1 x=%2 y=%3" ).arg( z ).arg( x ).arg( y ) );
180  return QImage();
181  }
182  return tileImage;
183 }
184 
185 void QgsMbTiles::setTileData( int z, int x, int y, const QByteArray &data )
186 {
187  if ( !mDatabase )
188  {
189  QgsDebugMsg( QStringLiteral( "MBTiles database not open: " ) + mFilename );
190  return;
191  }
192 
193  int result;
194  const QString sql = QStringLiteral( "insert into tiles values (%1, %2, %3, ?)" ).arg( z ).arg( x ).arg( y );
195  sqlite3_statement_unique_ptr preparedStatement = mDatabase.prepare( sql, result );
196  if ( result != SQLITE_OK )
197  {
198  QgsDebugMsg( QStringLiteral( "MBTile failed to prepare statement: " ) + sql );
199  return;
200  }
201 
202  sqlite3_bind_blob( preparedStatement.get(), 1, data.constData(), data.size(), SQLITE_TRANSIENT );
203 
204  if ( preparedStatement.step() != SQLITE_DONE )
205  {
206  QgsDebugMsg( QStringLiteral( "MBTile tile failed to be set: %1,%2,%3" ).arg( z ).arg( x ).arg( y ) );
207  return;
208  }
209 }
210 
211 bool QgsMbTiles::decodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut )
212 {
213  unsigned char *bytesInPtr = reinterpret_cast<unsigned char *>( const_cast<char *>( bytesIn.constData() ) );
214  uint bytesInLeft = static_cast<uint>( bytesIn.count() );
215 
216  const uint CHUNK = 16384;
217  unsigned char out[CHUNK];
218  const int DEC_MAGIC_NUM_FOR_GZIP = 16;
219 
220  // allocate inflate state
221  z_stream strm;
222  strm.zalloc = Z_NULL;
223  strm.zfree = Z_NULL;
224  strm.opaque = Z_NULL;
225  strm.avail_in = 0;
226  strm.next_in = Z_NULL;
227 
228  int ret = inflateInit2( &strm, MAX_WBITS + DEC_MAGIC_NUM_FOR_GZIP );
229  if ( ret != Z_OK )
230  return false;
231 
232  while ( ret != Z_STREAM_END ) // done when inflate() says it's done
233  {
234  // prepare next chunk
235  const uint bytesToProcess = std::min( CHUNK, bytesInLeft );
236  strm.next_in = bytesInPtr;
237  strm.avail_in = bytesToProcess;
238  bytesInPtr += bytesToProcess;
239  bytesInLeft -= bytesToProcess;
240 
241  if ( bytesToProcess == 0 )
242  break; // we end with an error - no more data but inflate() wants more data
243 
244  // run inflate() on input until output buffer not full
245  do
246  {
247  strm.avail_out = CHUNK;
248  strm.next_out = out;
249  ret = inflate( &strm, Z_NO_FLUSH );
250  Q_ASSERT( ret != Z_STREAM_ERROR ); // state not clobbered
251  if ( ret == Z_NEED_DICT || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR )
252  {
253  inflateEnd( &strm );
254  return false;
255  }
256  const unsigned have = CHUNK - strm.avail_out;
257  bytesOut.append( QByteArray::fromRawData( reinterpret_cast<const char *>( out ), static_cast<int>( have ) ) );
258  }
259  while ( strm.avail_out == 0 );
260  }
261 
262  inflateEnd( &strm );
263  return ret == Z_STREAM_END;
264 }
265 
266 
267 bool QgsMbTiles::encodeGzip( const QByteArray &bytesIn, QByteArray &bytesOut )
268 {
269  unsigned char *bytesInPtr = reinterpret_cast<unsigned char *>( const_cast<char *>( bytesIn.constData() ) );
270  const uint bytesInLeft = static_cast<uint>( bytesIn.count() );
271 
272  const uint CHUNK = 16384;
273  unsigned char out[CHUNK];
274  const int DEC_MAGIC_NUM_FOR_GZIP = 16;
275 
276  // allocate deflate state
277  z_stream strm;
278  strm.zalloc = Z_NULL;
279  strm.zfree = Z_NULL;
280  strm.opaque = Z_NULL;
281 
282  int ret = deflateInit2( &strm, Z_DEFAULT_COMPRESSION, Z_DEFLATED, MAX_WBITS + DEC_MAGIC_NUM_FOR_GZIP, 8, Z_DEFAULT_STRATEGY );
283  if ( ret != Z_OK )
284  return false;
285 
286  strm.avail_in = bytesInLeft;
287  strm.next_in = bytesInPtr;
288 
289  // run deflate() on input until output buffer not full, finish
290  // compression if all of source has been read in
291  do
292  {
293  strm.avail_out = CHUNK;
294  strm.next_out = out;
295  ret = deflate( &strm, Z_FINISH ); // no bad return value
296  Q_ASSERT( ret != Z_STREAM_ERROR ); // state not clobbered
297 
298  const unsigned have = CHUNK - strm.avail_out;
299  bytesOut.append( QByteArray::fromRawData( reinterpret_cast<const char *>( out ), static_cast<int>( have ) ) );
300  }
301  while ( strm.avail_out == 0 );
302  Q_ASSERT( ret == Z_STREAM_END ); // stream will be complete
303 
304  // clean up and return
305  deflateEnd( &strm );
306  return true;
307 }
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:173
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:211
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:185
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:267
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 QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38