QGIS API Documentation 4.3.0-Master (0ff9723465c)
Loading...
Searching...
No Matches
qgsgpkgtiles.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsgpkgtiles.cpp
3 --------------------------------------
4 Date : September 2026
5 Copyright : (C) 2026 by Nyall Dawson
6 Email : nyall.dawson@gmail.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 "qgsgpkgtiles.h"
17
18#include <sqlite3.h>
19
21#include "qgslogger.h"
22#include "qgsrectangle.h"
23#include "qgstiles.h"
24
25#include <QFile>
26#include <QString>
27
28using namespace Qt::StringLiterals;
29
31 : mFilename( filename )
32{}
33
38
40{
41 if ( !mDatabase )
42 return false;
43
44 const int result = mDatabase.exec( u"CREATE UNIQUE INDEX IF NOT EXISTS uk_tiles ON tiles (zoom_level, tile_column, tile_row);"_s, mLastError );
45 if ( result != SQLITE_OK )
46 {
47 QgsDebugError( u"Failed to create GeoPackage tile index: %1"_s.arg( mLastError ) );
48 return false;
49 }
50
51 return true;
52}
53
54bool QgsGeoPackageTiles::create( const QgsTileMatrix &z0matrix, const QgsRectangle &contentsExtent, int minZoom, int maxZoom, int tileWidth, int tileHeight )
55{
56 if ( mFilename.isEmpty() )
57 return false;
58
59 if ( QFile::exists( mFilename ) )
60 {
61 mLastError = u"File already exists"_s;
62 return false;
63 }
64
65 int result = mDatabase.open_v2( mFilename, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, nullptr );
66 if ( result != SQLITE_OK )
67 {
68 mLastError = mDatabase.errorMessage();
69 QgsDebugError( u"Can't create GeoPackage database: %1"_s.arg( mLastError ) );
70 return false;
71 }
72
73 QString errorMessage;
74 // ignore errors from these, they aren't critical
75 // optimise writing speed
76 mDatabase.exec( u"PRAGMA journal_mode = WAL;"_s, errorMessage );
77 mDatabase.exec( u"PRAGMA synchronous = OFF;"_s, errorMessage );
78 mDatabase.exec( u"PRAGMA temp_store = MEMORY;"_s, errorMessage );
79 mDatabase.exec( u"PRAGMA cache_size = -64000;"_s, errorMessage );
80
81 mDatabase.exec( u"PRAGMA application_id = 1196444487;"_s, mLastError );
82 mDatabase.exec( u"PRAGMA user_version = 10200;"_s, mLastError );
83 mDatabase.exec( u"BEGIN TRANSACTION;"_s, errorMessage );
84
85 const QString sql = "CREATE TABLE gpkg_spatial_ref_sys ("
86 " srs_name TEXT NOT NULL, srs_id INTEGER NOT NULL PRIMARY KEY,"
87 " organization TEXT NOT NULL, organization_coordsys_id INTEGER NOT NULL,"
88 " definition TEXT NOT NULL, description TEXT"
89 ");"
90 "CREATE TABLE gpkg_contents ("
91 " table_name TEXT NOT NULL PRIMARY KEY, data_type TEXT NOT NULL,"
92 " identifier TEXT UNIQUE, description TEXT DEFAULT '',"
93 " last_change DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),"
94 " min_x DOUBLE, min_y DOUBLE, max_x DOUBLE, max_y DOUBLE, srs_id INTEGER"
95 ");"
96 "CREATE TABLE gpkg_tile_matrix_set ("
97 " table_name TEXT NOT NULL PRIMARY KEY, srs_id INTEGER NOT NULL,"
98 " min_x DOUBLE NOT NULL, min_y DOUBLE NOT NULL,"
99 " max_x DOUBLE NOT NULL, max_y DOUBLE NOT NULL"
100 ");"
101 "CREATE TABLE gpkg_tile_matrix ("
102 " table_name TEXT NOT NULL, zoom_level INTEGER NOT NULL,"
103 " matrix_width INTEGER NOT NULL, matrix_height INTEGER NOT NULL,"
104 " tile_width INTEGER NOT NULL, tile_height INTEGER NOT NULL,"
105 " pixel_x_size DOUBLE NOT NULL, pixel_y_size DOUBLE NOT NULL,"
106 " CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level)"
107 ");"
108 "CREATE TABLE tiles ("
109 " id INTEGER PRIMARY KEY AUTOINCREMENT, zoom_level INTEGER NOT NULL,"
110 " tile_column INTEGER NOT NULL, tile_row INTEGER NOT NULL,"
111 " tile_data BLOB NOT NULL"
112 ");";
113
114 result = mDatabase.exec( sql, mLastError );
115 if ( result != SQLITE_OK )
116 {
117 QgsDebugError( u"Failed to initialize GeoPackage database tables: "_s + mLastError );
118 return false;
119 }
120
121 // Insert standard GeoPackage SRS entries
122 QString srsSql = "INSERT INTO gpkg_spatial_ref_sys VALUES "
123 "('Undefined Cartesian SRS', -1, 'NONE', -1, 'undefined', 'undefined Cartesian coordinate reference system')," // #spellok
124 "('Undefined geographic SRS', 0, 'NONE', 0, 'undefined', 'undefined geographic coordinate reference system'),";
125 srsSql += "('WGS 84 geodetic', 4326, 'EPSG', 4326, 'GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AXIS[\"Latitude\",NORTH],AXIS[\"Longitude\",EAST],AUTHORITY[\"EPSG\",\"4326\"]]', 'longitude/latitude coordinates in decimal degrees on the WGS 84 spheroid'),"_L1;
126 srsSql += "('WGS 84 / Pseudo-Mercator', 3857, 'EPSG', 3857, 'GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]],AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.0174532925199433,AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4326\"]],PROJECTION[\"Mercator_1SP\"],PARAMETER[\"central_meridian\",0],PARAMETER[\"scale_factor\",1],PARAMETER[\"false_easting\",0],PARAMETER[\"false_northing\",0],UNIT[\"metre\",1,AUTHORITY[\"EPSG\",\"9001\"]],AXIS[\"Easting\",EAST],AXIS[\"Northing\",NORTH],EXTENSION[\"PROJ4\",\"+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs\"],AUTHORITY[\"EPSG\",\"3857\"]]', '')"_L1;
127
128 result = mDatabase.exec( srsSql, mLastError );
129 if ( result != SQLITE_OK )
130 {
131 QgsDebugError( u"Failed to populate GeoPackage gpkg_spatial_ref_sys table: "_s + mLastError );
132 return false;
133 }
134
135 const QgsCoordinateReferenceSystem crs = z0matrix.crs();
136 long srsId = crs.postgisSrid();
137 if ( srsId <= 0 )
138 srsId = crs.srsid();
139
140 if ( crs != QgsCoordinateReferenceSystem( "EPSG:4326" ) && crs != QgsCoordinateReferenceSystem( "EPSG:3857" ) )
141 {
142 const QStringList authidParts = crs.authid().split( ':' );
143 const QString org = authidParts.size() == 2 && !authidParts.at( 0 ).isEmpty() ? authidParts.at( 0 ) : u"NONE"_s;
144 const int orgId = authidParts.size() == 2 ? authidParts.at( 1 ).toInt() : 0;
145 const QString wkt = crs.toWkt( Qgis::CrsWktVariant::Wkt1Gdal );
146
147 const QString customSrsSql = QString(
148 "INSERT OR IGNORE INTO gpkg_spatial_ref_sys (srs_name, srs_id, organization, organization_coordsys_id, definition, description) "
149 "VALUES (%1, %2, %3, %4, %5, %6);"
150 )
151 .arg( QgsSqliteUtils::quotedValue( crs.description().isEmpty() ? crs.authid() : crs.description() ) )
152 .arg( srsId )
153 .arg( QgsSqliteUtils::quotedValue( org ) )
154 .arg( orgId )
155 .arg( QgsSqliteUtils::quotedValue( wkt ) )
157
158 result = mDatabase.exec( customSrsSql, mLastError );
159 if ( result != SQLITE_OK )
160 {
161 QgsDebugError( u"Failed to insert target SRS into GeoPackage gpkg_spatial_ref_sys table: "_s + mLastError );
162 return false;
163 }
164 }
165
166
167 const QString contentsSql = QString(
168 "INSERT INTO gpkg_contents (table_name, data_type, identifier, min_x, min_y, max_x, max_y, srs_id) "
169 "VALUES ('tiles', 'tiles', 'tiles', %1, %2, %3, %4, %5);"
170 )
171 .arg( contentsExtent.xMinimum(), 0, 'g', 17 )
172 .arg( contentsExtent.yMinimum(), 0, 'g', 17 )
173 .arg( contentsExtent.xMaximum(), 0, 'g', 17 )
174 .arg( contentsExtent.yMaximum(), 0, 'g', 17 )
175 .arg( srsId );
176 result = mDatabase.exec( contentsSql, mLastError );
177 if ( result != SQLITE_OK )
178 {
179 QgsDebugError( u"Failed to populate GeoPackage gpkg_contents table: "_s + mLastError );
180 return false;
181 }
182
183 const QgsRectangle tileMatrixSetExtent = z0matrix.extent();
184 const QString tmsSql = QString( "INSERT INTO gpkg_tile_matrix_set VALUES ('tiles', %1, %2, %3, %4, %5);" )
185 .arg( srsId )
186 .arg( tileMatrixSetExtent.xMinimum(), 0, 'g', 17 )
187 .arg( tileMatrixSetExtent.yMinimum(), 0, 'g', 17 )
188 .arg( tileMatrixSetExtent.xMaximum(), 0, 'g', 17 )
189 .arg( tileMatrixSetExtent.yMaximum(), 0, 'g', 17 );
190
191 result = mDatabase.exec( tmsSql, mLastError );
192 if ( result != SQLITE_OK )
193 {
194 QgsDebugError( u"Failed to populate GeoPackage gpkg_tile_matrix_set: "_s + mLastError );
195 return false;
196 }
197
198
199 const double tmsWidth = tileMatrixSetExtent.width();
200 const double tmsHeight = tileMatrixSetExtent.height();
201
202 for ( int z = minZoom; z <= maxZoom; ++z )
203 {
204 const long long matrixWidth = z0matrix.derivedMatrixWidthAtZoomLevel( z );
205 ;
206 const long long matrixHeight = z0matrix.derivedMatrixHeightAtZoomLevel( z );
207
208 const double pixelXSize = tmsWidth / ( matrixWidth * tileWidth );
209 const double pixelYSize = tmsHeight / ( matrixHeight * tileHeight );
210
211 const QString matrixSql = QString( "INSERT INTO gpkg_tile_matrix VALUES ('tiles', %1, %2, %3, %4, %5, %6, %7);" )
212 .arg( z )
213 .arg( matrixWidth )
214 .arg( matrixHeight )
215 .arg( tileWidth )
216 .arg( tileHeight )
217 .arg( pixelXSize, 0, 'g', 17 )
218 .arg( pixelYSize, 0, 'g', 17 );
219
220 result = mDatabase.exec( matrixSql, mLastError );
221 if ( result != SQLITE_OK )
222 {
223 QgsDebugError( u"Failed to populate GeoPackage gpkg_tile_matrix for zoom %1: %2"_s.arg( z ).arg( mLastError ) );
224 return false;
225 }
226 }
227
228 result = mDatabase.exec( u"COMMIT TRANSACTION;"_s, mLastError );
229 if ( result != SQLITE_OK )
230 {
231 QgsDebugError( u"Failed to commit transaction: %1"_s.arg( mLastError ) );
232 }
233
234 // index creation is deferred to finalize()
235
236 return true;
237}
238
239void QgsGeoPackageTiles::setTileData( const QList<TileData> &tiles ) const
240{
241 if ( tiles.isEmpty() )
242 {
243 return;
244 }
245
246 if ( !mDatabase )
247 {
248 QgsDebugError( u"GeoPackage database not open: "_s + mFilename );
249 return;
250 }
251
252 int result = mDatabase.exec( u"BEGIN TRANSACTION;"_s, mLastError );
253 if ( result != SQLITE_OK )
254 {
255 QgsDebugError( u"Failed to begin transaction: %1"_s.arg( mLastError ) );
256 return;
257 }
258
259 const QString sql = u"INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (?, ?, ?, ?)"_s;
260 sqlite3_statement_unique_ptr preparedStatement = mDatabase.prepare( sql, result );
261 if ( result != SQLITE_OK )
262 {
263 QgsDebugError( u"GeoPackage failed to prepare statement: %1"_s.arg( sql ) );
264 mLastError = mDatabase.errorMessage();
265 mDatabase.exec( u"ROLLBACK TRANSACTION;"_s, mLastError );
266 return;
267 }
268
269 for ( const TileData &tile : tiles )
270 {
271 sqlite3_reset( preparedStatement.get() );
272 sqlite3_clear_bindings( preparedStatement.get() );
273
274 sqlite3_bind_int( preparedStatement.get(), 1, tile.z );
275 sqlite3_bind_int( preparedStatement.get(), 2, tile.x );
276 sqlite3_bind_int( preparedStatement.get(), 3, tile.y );
277 sqlite3_bind_blob( preparedStatement.get(), 4, tile.data.constData(), tile.data.size(), SQLITE_TRANSIENT );
278
279 if ( preparedStatement.step() != SQLITE_DONE )
280 {
281 mLastError = mDatabase.errorMessage();
282 QgsDebugError( u"GeoPackage tile failed to be set: %1,%2,%3"_s.arg( tile.z ).arg( tile.x ).arg( tile.y ) );
283 }
284 }
285
286 result = mDatabase.exec( u"COMMIT TRANSACTION;"_s, mLastError );
287 if ( result != SQLITE_OK )
288 {
289 QgsDebugError( u"Failed to commit transaction: %1"_s.arg( mLastError ) );
290 }
291}
292
294{
295 mDatabase.reset();
296 return true;
297}
@ Wkt1Gdal
WKT1 as traditionally output by GDAL, deriving from OGC 01-009. A notable departure from WKT1_GDAL wi...
Definition qgis.h:2599
Represents a coordinate reference system (CRS).
long postgisSrid() const
Returns PostGIS SRID for the CRS.
QString toWkt(Qgis::CrsWktVariant variant=Qgis::CrsWktVariant::Wkt1Gdal, bool multiline=false, int indentationWidth=4) const
Returns a WKT representation of this CRS.
long srsid() const
Returns the internal CRS ID, if available.
QgsGeoPackageTiles(const QString &filename)
Constructs the GeoPackage reader (but it does not open the file yet).
bool close()
Explicitly closes the database.
void setTileData(const QList< TileData > &tiles) const
Adds a batch of tile data within a single SQLite transaction.
bool finalize()
Finalizes the database after writing all tiles.
bool create(const QgsTileMatrix &z0matrix, const QgsRectangle &contentsExtent, int minZoom, int maxZoom, int tileWidth=256, int tileHeight=256)
Creates a new GeoPackage database and initializes it with metadata and tiles tables.
A rectangle specified with double values.
double xMinimum
double yMinimum
double xMaximum
double yMaximum
static QString quotedValue(const QVariant &value)
Returns a properly quoted and escaped version of value for use in SQL strings.
Defines a matrix of tiles for a single zoom level: it is defined by its size (width *.
Definition qgstiles.h:171
QgsRectangle extent() const
Returns extent of the tile matrix.
Definition qgstiles.h:243
QgsCoordinateReferenceSystem crs() const
Returns the crs of the tile matrix.
Definition qgstiles.h:191
long long derivedMatrixHeightAtZoomLevel(int zoomLevel) const
Returns the calculated matrix height at the specified zoom level.
Definition qgstiles.cpp:90
long long derivedMatrixWidthAtZoomLevel(int zoomLevel) const
Returns the calculated matrix width at the specified zoom level.
Definition qgstiles.cpp:84
Unique pointer for sqlite3 prepared statements, which automatically finalizes the statement when the ...
int step()
Steps to the next record in the statement, returning the sqlite3 result code.
#define QgsDebugError(str)
Definition qgslogger.h:71
Struct representing a raw image tile.