geopackage/tiles.rs
1//! Tile pyramids: [`TilePyramidBuilder`], [`GeoPackage::create_tile_pyramid`],
2//! and the [`TilePyramid`] handle over an existing one.
3//!
4//! A tile pyramid is the container's second data type, alongside features and
5//! attributes, and its payloads are opaque here: this crate stores, indexes and
6//! validates tiles, and decodes none of them. What it does read is each
7//! payload's header, which is how a tile written at the wrong pixel size, or in
8//! a format the table may not contain, is caught rather than stored (see
9//! [`geopackage_core::tiles::probe`]).
10//!
11//! The geometry of a pyramid, and the spec's rules about it, live in
12//! [`geopackage_core::tiles`]. This module is the part that needs a database:
13//! the catalogue rows, the user table, and the extension registrations.
14//!
15//! Creation validates; reading does not. A pyramid another implementation wrote
16//! opens on whatever its `gpkg_tile_matrix` rows say, because a reader that
17//! rejects an imperfect file cannot be used to inspect one.
18
19use geopackage_core::ddl;
20use geopackage_core::ident::quote;
21use geopackage_core::tiles::{
22 self, TileCoord, TileFormat, TileMatrix, TileMatrixSet, TilePayload, WEBP_EXTENSION_DEFINITION,
23 WEBP_EXTENSION_NAME, ZOOM_OTHER_EXTENSION_DEFINITION, ZOOM_OTHER_EXTENSION_NAME,
24};
25use rusqlite::types::ValueRef;
26use rusqlite::{CachedStatement, Connection, OptionalExtension};
27
28use crate::transaction::WriteTransaction;
29use crate::{
30 BoundingBox, Error, ExtensionRow, GeoPackage, Result, resolve_table_name, table_exists,
31};
32
33/// A declarative builder for a tile pyramid.
34///
35/// Declares the pyramid's extent and spatial reference system
36/// ([`TileMatrixSet`]), its zoom levels ([`TileMatrix`]), and the catalogue
37/// metadata, then goes to [`GeoPackage::create_tile_pyramid`].
38///
39/// The zoom levels are usually built from the extent rather than written out:
40/// [`TileMatrixSet::ladder`] derives a power-of-two ladder whose pixel sizes
41/// span the extent exactly.
42///
43/// ```
44/// use geopackage::core::tiles::{TileMatrixSet, ZoomLadder};
45/// use geopackage::{GeoPackage, TilePyramidBuilder};
46///
47/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
48/// # let dir = tempfile::tempdir()?;
49/// # let path = dir.path().join("basemap.gpkg");
50/// let gpkg = GeoPackage::create(path)?;
51/// gpkg.add_epsg_srs(3857)?;
52///
53/// let matrix_set = TileMatrixSet::web_mercator_quad();
54/// let matrices = matrix_set.ladder(ZoomLadder::new(0, 4))?;
55/// let tiles = gpkg.create_tile_pyramid(
56/// &TilePyramidBuilder::new("basemap", matrix_set).matrices(matrices),
57/// )?;
58///
59/// assert_eq!(tiles.zoom_levels(), vec![0, 1, 2, 3, 4]);
60/// assert_eq!(tiles.matrix(4).map(|m| m.matrix_width), Some(16));
61/// # Ok(()) }
62/// ```
63#[derive(Debug, Clone)]
64pub struct TilePyramidBuilder {
65 table_name: String,
66 identifier: Option<String>,
67 description: Option<String>,
68 matrix_set: TileMatrixSet,
69 matrices: Vec<TileMatrix>,
70 allow_zoom_other: bool,
71}
72
73impl TilePyramidBuilder {
74 /// Starts a builder for a pyramid of the given name over the given
75 /// extent.
76 ///
77 /// The name is validated when the builder reaches
78 /// [`GeoPackage::create_tile_pyramid`], not here.
79 pub fn new(table_name: impl Into<String>, matrix_set: TileMatrixSet) -> Self {
80 Self {
81 table_name: table_name.into(),
82 identifier: None,
83 description: None,
84 matrix_set,
85 matrices: Vec::new(),
86 allow_zoom_other: false,
87 }
88 }
89
90 /// Declares one zoom level.
91 #[must_use]
92 pub fn matrix(mut self, matrix: TileMatrix) -> Self {
93 self.matrices.push(matrix);
94 self
95 }
96
97 /// Declares a set of zoom levels, in any order.
98 #[must_use]
99 pub fn matrices(mut self, matrices: impl IntoIterator<Item = TileMatrix>) -> Self {
100 self.matrices.extend(matrices);
101 self
102 }
103
104 /// Sets `gpkg_contents.identifier` (a human-readable name). Defaults to
105 /// the table name when left unset.
106 #[must_use]
107 pub fn identifier(mut self, identifier: impl Into<String>) -> Self {
108 self.identifier = Some(identifier.into());
109 self
110 }
111
112 /// Sets `gpkg_contents.description`.
113 #[must_use]
114 pub fn description(mut self, description: impl Into<String>) -> Self {
115 self.description = Some(description.into());
116 self
117 }
118
119 /// Allows zoom levels that do not step by factors of two, registering the
120 /// `gpkg_zoom_other` extension for the table.
121 ///
122 /// Off by default, and the omission is an error rather than a silent
123 /// registration: a ladder that does not double is usually a mistake in the
124 /// pixel sizes, and a file that quietly registers an extension is one
125 /// whose readers may not have it. Such a pyramid is always *read* whether or not
126 /// this was set, as it is for the file's original writer.
127 #[must_use]
128 pub fn allow_zoom_other(mut self, allow: bool) -> Self {
129 self.allow_zoom_other = allow;
130 self
131 }
132
133 /// Returns the table name.
134 pub fn table_name(&self) -> &str {
135 &self.table_name
136 }
137}
138
139/// A handle to one tile pyramid of a [`GeoPackage`].
140///
141/// Obtained from [`GeoPackage::create_tile_pyramid`], [`GeoPackage::tiles`] or
142/// [`GeoPackage::tile_pyramids`]. The matrix set and the zoom levels are read
143/// once at construction and kept sorted by zoom level, so addressing a tile
144/// costs a binary search rather than a query, and the handle borrows the
145/// [`GeoPackage`] for its lifetime.
146pub struct TilePyramid<'a> {
147 gpkg: &'a GeoPackage,
148 table_name: String,
149 matrix_set: TileMatrixSet,
150 /// Ascending by zoom level, which [`Self::matrix`] binary-searches.
151 matrices: Vec<TileMatrix>,
152 /// Built once per handle: a statement's text is a `prepare_cached` key, and
153 /// formatting one per tile is the per-row rebuild this crate has spent
154 /// releases removing.
155 sql: TileSql,
156 /// The unidentified extension that blocks writes to this pyramid, read
157 /// once for the same reason the statements are built once: `put_tile` is a
158 /// per-tile call, and a catalogue query inside it would be paid per tile.
159 write_block: Option<ExtensionRow>,
160}
161
162/// The statement text a [`TilePyramid`] uses, built once at construction.
163#[derive(Debug, Clone)]
164struct TileSql {
165 get: String,
166 exists: String,
167 count: String,
168 count_at: String,
169 scan: String,
170 scan_at: String,
171 scan_in: String,
172 put: String,
173 delete: String,
174}
175
176impl TileSql {
177 /// Builds the statements for a table, whose name is quoted once here
178 /// rather than at every call.
179 fn new(table: &str) -> Result<Self> {
180 let table = quote(table)?;
181 // Matrix order, which is what a consumer walking a pyramid expects:
182 // zoom level, then north to south, then west to east.
183 let order = "ORDER BY zoom_level, tile_row, tile_column";
184 let columns = "zoom_level, tile_column, tile_row, tile_data";
185 let address = "zoom_level = ?1 AND tile_column = ?2 AND tile_row = ?3";
186 Ok(Self {
187 get: format!("SELECT tile_data FROM {table} WHERE {address}"),
188 exists: format!("SELECT 1 FROM {table} WHERE {address}"),
189 count: format!("SELECT count(*) FROM {table}"),
190 count_at: format!("SELECT count(*) FROM {table} WHERE zoom_level = ?1"),
191 scan: format!("SELECT {columns} FROM {table} {order}"),
192 scan_at: format!("SELECT {columns} FROM {table} WHERE zoom_level = ?1 {order}"),
193 scan_in: format!(
194 "SELECT {columns} FROM {table} \
195 WHERE zoom_level = ?1 AND tile_column BETWEEN ?2 AND ?3 \
196 AND tile_row BETWEEN ?4 AND ?5 {order}"
197 ),
198 put: format!(
199 "INSERT INTO {table} (zoom_level, tile_column, tile_row, tile_data) \
200 VALUES (?1, ?2, ?3, ?4) \
201 ON CONFLICT (zoom_level, tile_column, tile_row) \
202 DO UPDATE SET tile_data = excluded.tile_data"
203 ),
204 delete: format!("DELETE FROM {table} WHERE {address}"),
205 })
206 }
207}
208
209/// One tile of a pyramid, borrowing its payload from the row it was read from.
210///
211/// The payload is the bytes as stored, in whatever format the table contains:
212/// this crate does not decode them. [`Tile::to_vec`] copies, and everything
213/// else here borrows.
214#[derive(Debug)]
215pub struct Tile<'a> {
216 coord: TileCoord,
217 data: &'a [u8],
218}
219
220impl Tile<'_> {
221 /// Returns where this tile sits in the pyramid.
222 pub fn coord(&self) -> TileCoord {
223 self.coord
224 }
225
226 /// Returns the stored payload, borrowed from SQLite's row buffer.
227 pub fn data(&self) -> &[u8] {
228 self.data
229 }
230
231 /// Returns the stored payload, copied into an owned buffer.
232 pub fn to_vec(&self) -> Vec<u8> {
233 self.data.to_vec()
234 }
235
236 /// Returns what the payload's header declares: its encoding and pixel
237 /// size.
238 ///
239 /// # Errors
240 ///
241 /// [`Error::Tile`] when the bytes are not a readable image header.
242 pub fn probe(&self) -> Result<TilePayload> {
243 Ok(tiles::probe(self.data)?)
244 }
245}
246
247impl std::fmt::Debug for TilePyramid<'_> {
248 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249 f.debug_struct("TilePyramid")
250 .field("table_name", &self.table_name)
251 .field("matrix_set", &self.matrix_set)
252 .field("zoom_levels", &self.zoom_levels())
253 .finish()
254 }
255}
256
257impl GeoPackage {
258 /// Creates a tile pyramid from a [`TilePyramidBuilder`].
259 ///
260 /// Emits the tile pyramid user table, a `gpkg_contents` row
261 /// (`data_type = 'tiles'`, bounded by the matrix set extent), the
262 /// `gpkg_tile_matrix_set` row and one `gpkg_tile_matrix` row per zoom
263 /// level, in one transaction, then returns a handle to the new pyramid.
264 /// `gpkg_tile_matrix_set` and `gpkg_tile_matrix` are created on first use,
265 /// as `gpkg_geometry_columns` is for feature layers.
266 ///
267 /// # Errors
268 ///
269 /// - [`Error::ReservedTablePrefix`] if the name begins `gpkg_`.
270 /// - [`Error::TableAlreadyExists`] if a table or view of that name exists.
271 /// - [`Error::UnknownSrs`] if the matrix set's `srs_id` is not registered
272 /// in `gpkg_spatial_ref_sys`.
273 /// - [`Error::Tile`] if the pyramid breaks one of the spec's consistency
274 /// rules (Requirements 45 to 53).
275 /// - [`Error::ZoomOtherNotEnabled`] if its zoom levels do not step by
276 /// factors of two and [`TilePyramidBuilder::allow_zoom_other`] was not
277 /// set.
278 pub fn create_tile_pyramid(&self, builder: &TilePyramidBuilder) -> Result<TilePyramid<'_>> {
279 let name = &builder.table_name;
280 // As with `create_layer`: a whole-GeoPackage extension we cannot
281 // identify covers a table that does not exist yet.
282 self.check_writable(name)?;
283 if name
284 .get(..5)
285 .is_some_and(|prefix| prefix.eq_ignore_ascii_case("gpkg_"))
286 {
287 return Err(Error::ReservedTablePrefix {
288 table_name: name.clone(),
289 });
290 }
291 let conn = self.connection();
292 if table_exists(conn, name)? {
293 return Err(Error::TableAlreadyExists {
294 table_name: name.clone(),
295 });
296 }
297 if self.srs(builder.matrix_set.srs_id)?.is_none() {
298 return Err(Error::UnknownSrs {
299 srs_id: builder.matrix_set.srs_id,
300 });
301 }
302 builder.matrix_set.validate(&builder.matrices)?;
303 let zoom_other = !tiles::is_power_of_two_ladder(&builder.matrices);
304 if zoom_other && !builder.allow_zoom_other {
305 return Err(Error::ZoomOtherNotEnabled {
306 table_name: name.clone(),
307 });
308 }
309
310 let identifier = builder.identifier.clone().unwrap_or_else(|| name.clone());
311 let description = builder.description.clone().unwrap_or_default();
312 let set = &builder.matrix_set;
313
314 let tx = WriteTransaction::begin(conn)?;
315 for (exists, sql) in [
316 (
317 table_exists(conn, "gpkg_tile_matrix_set")?,
318 ddl::CREATE_GPKG_TILE_MATRIX_SET,
319 ),
320 (
321 table_exists(conn, "gpkg_tile_matrix")?,
322 ddl::CREATE_GPKG_TILE_MATRIX,
323 ),
324 ] {
325 if !exists {
326 conn.execute_batch(sql)?;
327 }
328 }
329 conn.execute_batch(&tiles::create_tile_table_sql(name)?)?;
330 // The extent is the matrix set's, not a measurement: for tiles it is
331 // exact by Requirement 144, and gpkg_contents records the same box.
332 conn.execute(
333 "INSERT INTO gpkg_contents \
334 (table_name, data_type, identifier, description, min_x, min_y, max_x, max_y, srs_id) \
335 VALUES (?1, 'tiles', ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
336 rusqlite::params![
337 name,
338 identifier,
339 description,
340 set.min_x,
341 set.min_y,
342 set.max_x,
343 set.max_y,
344 set.srs_id,
345 ],
346 )?;
347 conn.execute(
348 "INSERT INTO gpkg_tile_matrix_set (table_name, srs_id, min_x, min_y, max_x, max_y) \
349 VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
350 rusqlite::params![name, set.srs_id, set.min_x, set.min_y, set.max_x, set.max_y],
351 )?;
352 {
353 let mut stmt = conn.prepare(
354 "INSERT INTO gpkg_tile_matrix \
355 (table_name, zoom_level, matrix_width, matrix_height, tile_width, tile_height, \
356 pixel_x_size, pixel_y_size) \
357 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
358 )?;
359 for matrix in &builder.matrices {
360 stmt.execute(rusqlite::params![
361 name,
362 matrix.zoom_level,
363 matrix.matrix_width,
364 matrix.matrix_height,
365 matrix.tile_width,
366 matrix.tile_height,
367 matrix.pixel_x_size,
368 matrix.pixel_y_size,
369 ])?;
370 }
371 }
372 if zoom_other {
373 crate::extensions::register(
374 conn,
375 Some(name),
376 Some(tiles::TILE_DATA_COLUMN),
377 ZOOM_OTHER_EXTENSION_NAME,
378 ZOOM_OTHER_EXTENSION_DEFINITION,
379 tiles::TILE_EXTENSION_SCOPE,
380 )?;
381 }
382 tx.commit()?;
383 self.tiles(name)
384 }
385
386 /// Opens a tile pyramid by name.
387 ///
388 /// Nothing is validated: the matrix set and zoom levels are reported as the
389 /// file records them.
390 ///
391 /// # Errors
392 ///
393 /// - [`Error::NoSuchLayer`] if `name` is not in `gpkg_contents`.
394 /// - [`Error::WrongDataType`] if it is registered but not as `tiles`.
395 /// - [`Error::NoTileMatrixSet`] if its `gpkg_tile_matrix_set` row is
396 /// missing, which leaves its tiles unlocatable.
397 pub fn tiles(&self, name: &str) -> Result<TilePyramid<'_>> {
398 let conn = self.connection();
399 let row = conn
400 .query_row(
401 "SELECT table_name, data_type FROM gpkg_contents \
402 WHERE table_name = ?1 COLLATE NOCASE",
403 [name],
404 |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
405 )
406 .optional()?;
407 let (declared_name, data_type) = row.ok_or_else(|| Error::NoSuchLayer {
408 table_name: name.to_owned(),
409 })?;
410 if data_type != "tiles" {
411 return Err(Error::WrongDataType {
412 table_name: declared_name,
413 expected: "tiles",
414 found: data_type,
415 });
416 }
417 let table_name = resolve_table_name(conn, &declared_name)?.unwrap_or(declared_name);
418 let matrix_set =
419 read_matrix_set(conn, &table_name)?.ok_or_else(|| Error::NoTileMatrixSet {
420 table_name: table_name.clone(),
421 })?;
422 let matrices = read_matrices(conn, &table_name)?;
423 let sql = TileSql::new(&table_name)?;
424 let write_block = self.blocking_extension(&table_name)?;
425 Ok(TilePyramid {
426 gpkg: self,
427 table_name,
428 matrix_set,
429 matrices,
430 sql,
431 write_block,
432 })
433 }
434
435 /// Returns every tile pyramid in the file, by `gpkg_contents` name.
436 pub fn tile_pyramids(&self) -> Result<Vec<TilePyramid<'_>>> {
437 if !table_exists(self.connection(), "gpkg_tile_matrix_set")? {
438 return Ok(Vec::new());
439 }
440 let names: Vec<String> = {
441 let mut stmt = self.connection().prepare(
442 "SELECT c.table_name FROM gpkg_contents c \
443 JOIN gpkg_tile_matrix_set s ON s.table_name = c.table_name COLLATE NOCASE \
444 WHERE c.data_type = 'tiles' ORDER BY c.table_name",
445 )?;
446 stmt.query_map([], |r| r.get(0))?
447 .collect::<rusqlite::Result<_>>()?
448 };
449 names.iter().map(|name| self.tiles(name)).collect()
450 }
451}
452
453impl<'a> TilePyramid<'a> {
454 /// Returns the physical SQLite table name backing this pyramid.
455 pub fn table_name(&self) -> &str {
456 &self.table_name
457 }
458
459 /// Returns the [`GeoPackage`] this pyramid belongs to.
460 pub fn gpkg(&self) -> &'a GeoPackage {
461 self.gpkg
462 }
463
464 /// Returns the pyramid's extent and spatial reference system.
465 pub fn matrix_set(&self) -> &TileMatrixSet {
466 &self.matrix_set
467 }
468
469 /// Returns every declared zoom level, ascending.
470 pub fn matrices(&self) -> &[TileMatrix] {
471 &self.matrices
472 }
473
474 /// Returns the zoom levels this pyramid declares, ascending.
475 pub fn zoom_levels(&self) -> Vec<i64> {
476 self.matrices.iter().map(|m| m.zoom_level).collect()
477 }
478
479 /// Returns the tile matrix for one zoom level, or `None` if the pyramid
480 /// does not declare that level.
481 pub fn matrix(&self, zoom_level: i64) -> Option<&TileMatrix> {
482 self.matrices
483 .binary_search_by_key(&zoom_level, |matrix| matrix.zoom_level)
484 .ok()
485 .and_then(|index| self.matrices.get(index))
486 }
487
488 /// Returns one tile's payload, or `None` where the pyramid has no tile at
489 /// that address.
490 ///
491 /// Copies the payload out of SQLite once, which is what an owned return
492 /// costs. [`Self::get_tile_into`] reuses a buffer instead, and
493 /// [`Self::cursor`] borrows the bytes without copying at all.
494 ///
495 /// The address is not checked against the zoom level's grid: a tile outside
496 /// it is simply absent, as any other empty address is. The write path is
497 /// where an impossible address is an error.
498 pub fn get_tile(&self, coord: TileCoord) -> Result<Option<Vec<u8>>> {
499 let conn = self.gpkg.connection();
500 let mut stmt = conn.prepare_cached(&self.sql.get)?;
501 Ok(stmt
502 .query_row(
503 rusqlite::params![coord.zoom_level, coord.column, coord.row],
504 |row| tile_blob(row, 0).map(<[u8]>::to_vec),
505 )
506 .optional()?)
507 }
508
509 /// Reads one tile's payload into a caller-owned buffer, returning whether
510 /// a tile was there.
511 ///
512 /// The buffer is cleared first and reused, so a loop over many tiles
513 /// allocates once rather than once per tile. Its contents are untouched
514 /// when the tile is absent.
515 pub fn get_tile_into(&self, coord: TileCoord, buffer: &mut Vec<u8>) -> Result<bool> {
516 let conn = self.gpkg.connection();
517 let mut stmt = conn.prepare_cached(&self.sql.get)?;
518 let found = stmt
519 .query_row(
520 rusqlite::params![coord.zoom_level, coord.column, coord.row],
521 |row| {
522 let data = tile_blob(row, 0)?;
523 buffer.clear();
524 buffer.extend_from_slice(data);
525 Ok(())
526 },
527 )
528 .optional()?;
529 Ok(found.is_some())
530 }
531
532 /// Returns `true` if the pyramid has a tile at an address, without
533 /// reading its payload.
534 pub fn has_tile(&self, coord: TileCoord) -> Result<bool> {
535 let conn = self.gpkg.connection();
536 let mut stmt = conn.prepare_cached(&self.sql.exists)?;
537 Ok(stmt
538 .query_row(
539 rusqlite::params![coord.zoom_level, coord.column, coord.row],
540 |_| Ok(()),
541 )
542 .optional()?
543 .is_some())
544 }
545
546 /// Returns the number of tiles in the pyramid.
547 pub fn tile_count(&self) -> Result<i64> {
548 let conn = self.gpkg.connection();
549 Ok(conn
550 .prepare_cached(&self.sql.count)?
551 .query_row([], |r| r.get(0))?)
552 }
553
554 /// Returns the number of tiles at one zoom level.
555 pub fn tile_count_at(&self, zoom_level: i64) -> Result<i64> {
556 let conn = self.gpkg.connection();
557 Ok(conn
558 .prepare_cached(&self.sql.count_at)?
559 .query_row([zoom_level], |r| r.get(0))?)
560 }
561
562 /// Streams every tile, in matrix order: by zoom level, then north to
563 /// south, then west to east.
564 ///
565 /// Two calls, as the feature read path is: the cursor owns the statement,
566 /// and [`TileCursor::tiles`] borrows it to walk the rows. There is no
567 /// materialising counterpart, because one zoom level of a real pyramid is
568 /// more payload than a `Vec` of them should contain.
569 pub fn cursor(&self) -> Result<TileCursor<'_>> {
570 self.cursor_with(&self.sql.scan, Vec::new())
571 }
572
573 /// Streams the tiles of one zoom level, in matrix order.
574 pub fn cursor_at(&self, zoom_level: i64) -> Result<TileCursor<'_>> {
575 self.cursor_with(&self.sql.scan_at, vec![zoom_level.into()])
576 }
577
578 /// Streams the tiles of one zoom level that a bounding box touches, in
579 /// matrix order.
580 ///
581 /// The box is in the pyramid's own spatial reference system; this crate
582 /// transforms nothing. It is turned into a range of tile indices and asked
583 /// of the table once, so the payloads read are the ones the box selects.
584 /// A box that misses the pyramid's extent yields no tiles.
585 ///
586 /// # Errors
587 ///
588 /// [`Error::UnknownZoomLevel`] when the pyramid declares no such zoom
589 /// level, since without its grid a box cannot be turned into tile indices.
590 pub fn cursor_in(&self, zoom_level: i64, bbox: BoundingBox) -> Result<TileCursor<'_>> {
591 let matrix = self
592 .matrix(zoom_level)
593 .ok_or_else(|| Error::UnknownZoomLevel {
594 table_name: self.table_name.clone(),
595 zoom_level,
596 })?;
597 // A box outside the extent binds an empty range rather than taking a
598 // path of its own: the query then returns nothing, which is the answer.
599 let range = self
600 .matrix_set
601 .tile_range(matrix, bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y);
602 let (min_column, max_column, min_row, max_row) = range.map_or((0, -1, 0, -1), |range| {
603 (
604 range.min_column,
605 range.max_column,
606 range.min_row,
607 range.max_row,
608 )
609 });
610 self.cursor_with(
611 &self.sql.scan_in,
612 vec![
613 zoom_level.into(),
614 min_column.into(),
615 max_column.into(),
616 min_row.into(),
617 max_row.into(),
618 ],
619 )
620 }
621
622 fn cursor_with(
623 &self,
624 sql: &str,
625 params: Vec<rusqlite::types::Value>,
626 ) -> Result<TileCursor<'_>> {
627 let stmt = self.gpkg.connection().prepare(sql)?;
628 Ok(TileCursor { stmt, params })
629 }
630
631 /// Writes one tile, replacing whatever was at that address.
632 ///
633 /// Validated: the zoom level has to be one the pyramid declares, the column
634 /// and row have to fall inside that level's grid, and the payload has to be
635 /// a PNG or JPEG (or a WebP, which registers `gpkg_webp` on the way past)
636 /// of exactly the pixel size the zoom level declares. A tile that fails any
637 /// of those is a tile no conforming reader could use, so it is rejected
638 /// rather than stored.
639 ///
640 /// One tile per transaction. Use [`Self::writer`] or [`Self::write_all`] to
641 /// write many.
642 ///
643 /// # Errors
644 ///
645 /// - [`Error::UnknownZoomLevel`] for a zoom level with no
646 /// `gpkg_tile_matrix` row.
647 /// - [`Error::Tile`] for an address outside the grid, an unreadable
648 /// payload, or one of the wrong pixel size.
649 /// - [`Error::TileFormatNotAllowed`] for a payload that is not PNG, JPEG or
650 /// WebP.
651 pub fn put_tile(&self, coord: TileCoord, data: &[u8]) -> Result<()> {
652 let mut writer = self.writer()?;
653 writer.put(coord, data)?;
654 writer.commit()
655 }
656
657 /// Deletes one tile, returning whether there was one to delete.
658 pub fn delete_tile(&self, coord: TileCoord) -> Result<bool> {
659 let mut writer = self.writer()?;
660 let deleted = writer.delete(coord)?;
661 writer.commit()?;
662 Ok(deleted)
663 }
664
665 /// Opens a [`TileWriter`]: one transaction, prepared statements, and
666 /// per-tile `put`/`delete`.
667 ///
668 /// Nothing is written until [`TileWriter::commit`]; dropping the writer
669 /// rolls its transaction back.
670 pub fn writer(&self) -> Result<TileWriter<'a>> {
671 TileWriter::new(self)
672 }
673
674 /// Returns the extension this crate cannot identify that stops it writing
675 /// to this pyramid, if there is one.
676 ///
677 /// Read when the handle was opened, so a row registered since then is not
678 /// reflected here. See [`GeoPackage::blocking_extension`].
679 pub fn blocking_extension(&self) -> Option<&ExtensionRow> {
680 self.write_block.as_ref()
681 }
682
683 /// Rejects a write when an unidentified extension covers the pyramid.
684 fn check_writable(&self) -> Result<()> {
685 match &self.write_block {
686 None => Ok(()),
687 Some(row) => Err(Error::UnsupportedExtension {
688 table_name: self.table_name.clone(),
689 extension_name: row.name.clone(),
690 scope: row.scope.as_str().to_owned(),
691 }),
692 }
693 }
694
695 /// Writes many tiles, committing every `batch_size` of them (`0` writes
696 /// them all in one transaction), and returns how many were written.
697 ///
698 /// Payloads are borrowed, not consumed: anything that is `AsRef<[u8]>` will
699 /// do, so tiles copied from another pyramid go straight from one row's
700 /// buffer into the other's statement without a copy in between.
701 ///
702 /// # Errors
703 ///
704 /// As [`Self::put_tile`], at the first tile that fails. Tiles committed in
705 /// earlier batches stay written; the batch in flight is rolled back.
706 pub fn write_all<D: AsRef<[u8]>>(
707 &self,
708 tiles: impl IntoIterator<Item = (TileCoord, D)>,
709 batch_size: usize,
710 ) -> Result<usize> {
711 let mut tiles = tiles.into_iter();
712 let mut total = 0;
713 loop {
714 let mut writer = self.writer()?;
715 let mut in_batch = 0;
716 for (coord, data) in tiles.by_ref() {
717 writer.put(coord, data.as_ref())?;
718 in_batch += 1;
719 if batch_size != 0 && in_batch == batch_size {
720 break;
721 }
722 }
723 writer.commit()?;
724 total += in_batch;
725 if batch_size == 0 || in_batch < batch_size {
726 return Ok(total);
727 }
728 }
729 }
730
731 /// Returns `Ok` if this pyramid satisfies the spec's consistency rules.
732 ///
733 /// Creation checks this, so a pyramid this crate wrote always passes. Worth
734 /// asking of one that arrived in a file from elsewhere, since every tile
735 /// bound is calculated from values it does not otherwise question.
736 ///
737 /// # Errors
738 ///
739 /// [`Error::Tile`] naming the first rule the pyramid breaks.
740 pub fn validate(&self) -> Result<()> {
741 self.matrix_set.validate(&self.matrices)?;
742 Ok(())
743 }
744}
745
746/// A transaction over one pyramid, with per-tile `put` and `delete`.
747///
748/// Obtained from [`TilePyramid::writer`]. Writes stage into the transaction the
749/// writer owns; [`Self::commit`] refreshes `gpkg_contents.last_change` and
750/// commits. Dropping a writer without committing rolls it back.
751///
752/// Unless a transaction was already open on the connection, in which case the
753/// writer joins it and committing or rolling back passes to whoever began it.
754/// See
755/// [`Self::commit`], and [`crate::FeatureWriter::commit`] for the reasoning in
756/// full.
757///
758/// The statements come from the connection rather than from the transaction, so
759/// they borrow what it borrows instead of borrowing it, exactly as
760/// [`crate::FeatureWriter`]'s do. They still run inside it: a SQLite
761/// transaction belongs to the connection, not to the statements prepared
762/// against it.
763pub struct TileWriter<'conn> {
764 tx: WriteTransaction<'conn>,
765 conn: &'conn Connection,
766 table_name: String,
767 /// The pyramid's zoom levels, ascending. Copied in rather than borrowed so
768 /// the writer has one lifetime; a pyramid has tens of levels, and this
769 /// is one allocation per writer against a binary search per tile.
770 matrices: Vec<TileMatrix>,
771 /// `INSERT ... ON CONFLICT DO UPDATE`: writing a tile where one already
772 /// sits replaces the payload and keeps the row's id, which the metadata
773 /// extension may reference.
774 put_stmt: CachedStatement<'conn>,
775 delete_stmt: CachedStatement<'conn>,
776 /// Whether anything has been written, so an untouched writer does not stamp
777 /// `last_change`.
778 dirty: bool,
779 /// Whether `gpkg_webp` is registered for this table. Read once, on the
780 /// first WebP payload, so a pyramid of PNGs never asks.
781 webp_registered: Option<bool>,
782}
783
784impl<'conn> TileWriter<'conn> {
785 fn new(pyramid: &TilePyramid<'conn>) -> Result<Self> {
786 // Every tile write reaches a writer, `put_tile` and `delete_tile`
787 // included, so this is the one place the check belongs.
788 pyramid.check_writable()?;
789 let conn = pyramid.gpkg.connection();
790 let tx = WriteTransaction::begin(conn)?;
791 let put_stmt = conn.prepare_cached(&pyramid.sql.put)?;
792 let delete_stmt = conn.prepare_cached(&pyramid.sql.delete)?;
793 Ok(Self {
794 tx,
795 conn,
796 table_name: pyramid.table_name.clone(),
797 matrices: pyramid.matrices.clone(),
798 put_stmt,
799 delete_stmt,
800 dirty: false,
801 webp_registered: None,
802 })
803 }
804
805 /// Writes one tile, replacing whatever was at that address.
806 ///
807 /// # Errors
808 ///
809 /// As [`TilePyramid::put_tile`], which is this call in its own
810 /// transaction.
811 pub fn put(&mut self, coord: TileCoord, data: &[u8]) -> Result<()> {
812 let matrix = self
813 .matrices
814 .binary_search_by_key(&coord.zoom_level, |matrix| matrix.zoom_level)
815 .ok()
816 .and_then(|index| self.matrices.get(index))
817 .ok_or_else(|| Error::UnknownZoomLevel {
818 table_name: self.table_name.clone(),
819 zoom_level: coord.zoom_level,
820 })?;
821 matrix.check_contains(coord.column, coord.row)?;
822 let payload = tiles::probe(data)?;
823 matrix.check_payload(&payload)?;
824 match payload.format {
825 TileFormat::Webp => self.register_webp()?,
826 format if format.is_core() => {}
827 format => {
828 return Err(Error::TileFormatNotAllowed {
829 table_name: self.table_name.clone(),
830 format,
831 });
832 }
833 }
834 // The payload binds as a borrowed slice: a tile read from one pyramid
835 // reaches another's statement without being copied on the way.
836 self.put_stmt.execute(rusqlite::params![
837 coord.zoom_level,
838 coord.column,
839 coord.row,
840 data
841 ])?;
842 self.dirty = true;
843 Ok(())
844 }
845
846 /// Deletes one tile, returning whether there was one to delete.
847 pub fn delete(&mut self, coord: TileCoord) -> Result<bool> {
848 let deleted = self.delete_stmt.execute(rusqlite::params![
849 coord.zoom_level,
850 coord.column,
851 coord.row
852 ])?;
853 self.dirty |= deleted > 0;
854 Ok(deleted > 0)
855 }
856
857 /// Refreshes `gpkg_contents.last_change` and commits.
858 ///
859 /// A writer that wrote nothing commits an empty transaction and leaves
860 /// `last_change` alone.
861 ///
862 /// # When the transaction was the caller's
863 ///
864 /// As [`crate::FeatureWriter::commit`]: a writer opened while a transaction
865 /// was already open joined it, so this stages the `last_change` refresh and
866 /// returns success without committing, and dropping such a writer rolls
867 /// nothing back.
868 pub fn commit(self) -> Result<()> {
869 let Self {
870 tx,
871 conn,
872 table_name,
873 dirty,
874 put_stmt,
875 delete_stmt,
876 ..
877 } = self;
878 // Statements borrow the connection, not the transaction, but dropping
879 // them here keeps the cache tidy before the commit.
880 drop(put_stmt);
881 drop(delete_stmt);
882 if dirty {
883 conn.execute(
884 "UPDATE gpkg_contents \
885 SET last_change = strftime('%Y-%m-%dT%H:%M:%fZ','now') \
886 WHERE table_name = ?1",
887 [&table_name],
888 )?;
889 }
890 tx.commit()?;
891 Ok(())
892 }
893
894 /// Registers `gpkg_webp` for this table, once, on the first WebP
895 /// payload.
896 fn register_webp(&mut self) -> Result<()> {
897 if self.webp_registered.is_none() {
898 self.webp_registered = Some(crate::extensions::is_registered(
899 self.conn,
900 Some(&self.table_name),
901 WEBP_EXTENSION_NAME,
902 )?);
903 }
904 if self.webp_registered == Some(false) {
905 crate::extensions::register(
906 self.conn,
907 Some(&self.table_name),
908 Some(tiles::TILE_DATA_COLUMN),
909 WEBP_EXTENSION_NAME,
910 WEBP_EXTENSION_DEFINITION,
911 tiles::TILE_EXTENSION_SCOPE,
912 )?;
913 self.webp_registered = Some(true);
914 }
915 Ok(())
916 }
917}
918
919impl std::fmt::Debug for TileWriter<'_> {
920 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
921 f.debug_struct("TileWriter")
922 .field("table_name", &self.table_name)
923 .field("dirty", &self.dirty)
924 .finish_non_exhaustive()
925 }
926}
927
928/// A prepared tile scan, owning its statement so that [`TileCursor::tiles`]
929/// can borrow each payload straight out of the row.
930///
931/// The split is the one [`crate::FeatureCursor`] uses, and for the same reason:
932/// rusqlite's row cursor borrows its statement, so an iterator owning both
933/// would be self-referential, which `#![forbid(unsafe_code)]` rules out.
934pub struct TileCursor<'a> {
935 stmt: rusqlite::Statement<'a>,
936 params: Vec<rusqlite::types::Value>,
937}
938
939impl std::fmt::Debug for TileCursor<'_> {
940 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
941 f.debug_struct("TileCursor")
942 .field("parameters", &self.params.len())
943 .finish_non_exhaustive()
944 }
945}
946
947impl TileCursor<'_> {
948 /// Runs the scan and walks its tiles.
949 ///
950 /// Each call re-runs the query from the start, so a cursor can be walked
951 /// more than once.
952 pub fn tiles(&mut self) -> Result<TileStream<'_>> {
953 let rows = self
954 .stmt
955 .query(rusqlite::params_from_iter(self.params.iter()))?;
956 Ok(TileStream { rows })
957 }
958}
959
960/// A scan in progress, borrowing one tile at a time.
961///
962/// Not an [`Iterator`]: an iterator's item cannot borrow from the iterator,
963/// and the whole point here is to return the payload without copying it. Walk
964/// it with `while let Some(tile) = stream.next()?`, or pass a closure to
965/// [`Self::for_each`].
966///
967/// ```
968/// # use geopackage::core::tiles::{TileMatrixSet, ZoomLadder};
969/// # use geopackage::{GeoPackage, TilePyramidBuilder};
970/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
971/// # let dir = tempfile::tempdir()?;
972/// # let gpkg = GeoPackage::create(dir.path().join("t.gpkg"))?;
973/// # gpkg.add_epsg_srs(3857)?;
974/// # let set = TileMatrixSet::web_mercator_quad();
975/// # let matrices = set.ladder(ZoomLadder::new(0, 2))?;
976/// # let pyramid = gpkg.create_tile_pyramid(&TilePyramidBuilder::new("basemap", set).matrices(matrices))?;
977/// let mut cursor = pyramid.cursor()?;
978/// let mut stream = cursor.tiles()?;
979/// let mut bytes = 0;
980/// while let Some(tile) = stream.next()? {
981/// // `tile.data()` borrows the row: nothing is copied to count it.
982/// bytes += tile.data().len();
983/// }
984/// # assert_eq!(bytes, 0);
985/// # Ok(()) }
986/// ```
987pub struct TileStream<'c> {
988 rows: rusqlite::Rows<'c>,
989}
990
991impl std::fmt::Debug for TileStream<'_> {
992 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
993 // `rusqlite::Rows` is not `Debug`, and a scan mid-flight has no state
994 // worth printing beyond the query that produced it.
995 f.debug_struct("TileStream").finish_non_exhaustive()
996 }
997}
998
999impl TileStream<'_> {
1000 /// Returns the next tile of the scan, or `None` at its end.
1001 ///
1002 /// The returned [`Tile`] borrows this stream, so it is dropped before the
1003 /// next call. Copy what you need out of it with [`Tile::to_vec`].
1004 #[expect(
1005 clippy::should_implement_trait,
1006 reason = "a lending cursor cannot implement Iterator: its item borrows the iterator. `next` is the name a caller expects in a `while let` loop, and the fallible, borrowing signature is visibly not Iterator::next"
1007 )]
1008 pub fn next(&mut self) -> Result<Option<Tile<'_>>> {
1009 let Some(row) = self.rows.next()? else {
1010 return Ok(None);
1011 };
1012 Ok(Some(Tile {
1013 coord: TileCoord::new(row.get(0)?, row.get(1)?, row.get(2)?),
1014 data: tile_blob(row, 3)?,
1015 }))
1016 }
1017
1018 /// Runs a closure over every remaining tile.
1019 ///
1020 /// The same walk as [`Self::next`] with the borrow handled for you. The
1021 /// closure's error ends the scan.
1022 ///
1023 /// # Errors
1024 ///
1025 /// Whatever the closure returns, or a read error from the scan itself.
1026 pub fn for_each(&mut self, mut f: impl FnMut(&Tile<'_>) -> Result<()>) -> Result<()> {
1027 while let Some(tile) = self.next()? {
1028 f(&tile)?;
1029 }
1030 Ok(())
1031 }
1032}
1033
1034/// A tile payload, borrowed from the row rather than copied out of it.
1035///
1036/// `tile_data` is `NOT NULL` in every table this crate creates, and a
1037/// non-blob there is a malformed file rather than a value to coerce.
1038fn tile_blob<'a>(row: &'a rusqlite::Row<'a>, index: usize) -> rusqlite::Result<&'a [u8]> {
1039 match row.get_ref(index)? {
1040 ValueRef::Blob(data) => Ok(data),
1041 _ => Err(rusqlite::Error::InvalidColumnType(
1042 index,
1043 tiles::TILE_DATA_COLUMN.to_owned(),
1044 rusqlite::types::Type::Blob,
1045 )),
1046 }
1047}
1048
1049/// Reads a pyramid's `gpkg_tile_matrix_set` row.
1050fn read_matrix_set(conn: &Connection, table: &str) -> Result<Option<TileMatrixSet>> {
1051 Ok(conn
1052 .query_row(
1053 "SELECT srs_id, min_x, min_y, max_x, max_y FROM gpkg_tile_matrix_set \
1054 WHERE table_name = ?1 COLLATE NOCASE",
1055 [table],
1056 |r| {
1057 Ok(TileMatrixSet {
1058 srs_id: r.get(0)?,
1059 min_x: r.get(1)?,
1060 min_y: r.get(2)?,
1061 max_x: r.get(3)?,
1062 max_y: r.get(4)?,
1063 })
1064 },
1065 )
1066 .optional()?)
1067}
1068
1069/// Reads a pyramid's `gpkg_tile_matrix` rows, ascending by zoom level.
1070///
1071/// A file with no `gpkg_tile_matrix` table at all has no zoom levels, which is
1072/// an empty pyramid rather than an error: the tiles table may be empty, and the
1073/// spec requires a row only for a level that contains tiles.
1074fn read_matrices(conn: &Connection, table: &str) -> Result<Vec<TileMatrix>> {
1075 if !table_exists(conn, "gpkg_tile_matrix")? {
1076 return Ok(Vec::new());
1077 }
1078 let mut stmt = conn.prepare(
1079 "SELECT zoom_level, matrix_width, matrix_height, tile_width, tile_height, \
1080 pixel_x_size, pixel_y_size FROM gpkg_tile_matrix \
1081 WHERE table_name = ?1 COLLATE NOCASE ORDER BY zoom_level",
1082 )?;
1083 let rows = stmt.query_map([table], |r| {
1084 Ok(TileMatrix {
1085 zoom_level: r.get(0)?,
1086 matrix_width: r.get(1)?,
1087 matrix_height: r.get(2)?,
1088 tile_width: r.get(3)?,
1089 tile_height: r.get(4)?,
1090 pixel_x_size: r.get(5)?,
1091 pixel_y_size: r.get(6)?,
1092 })
1093 })?;
1094 Ok(rows.collect::<rusqlite::Result<_>>()?)
1095}