use geopackage_core::ddl;
use geopackage_core::ident::quote;
use geopackage_core::tiles::{
self, TileCoord, TileFormat, TileMatrix, TileMatrixSet, TilePayload, WEBP_EXTENSION_DEFINITION,
WEBP_EXTENSION_NAME, ZOOM_OTHER_EXTENSION_DEFINITION, ZOOM_OTHER_EXTENSION_NAME,
};
use rusqlite::types::ValueRef;
use rusqlite::{CachedStatement, Connection, OptionalExtension};
use crate::transaction::WriteTransaction;
use crate::{
BoundingBox, Error, ExtensionRow, GeoPackage, Result, resolve_table_name, table_exists,
};
#[derive(Debug, Clone)]
pub struct TilePyramidBuilder {
table_name: String,
identifier: Option<String>,
description: Option<String>,
matrix_set: TileMatrixSet,
matrices: Vec<TileMatrix>,
allow_zoom_other: bool,
}
impl TilePyramidBuilder {
pub fn new(table_name: impl Into<String>, matrix_set: TileMatrixSet) -> Self {
Self {
table_name: table_name.into(),
identifier: None,
description: None,
matrix_set,
matrices: Vec::new(),
allow_zoom_other: false,
}
}
#[must_use]
pub fn matrix(mut self, matrix: TileMatrix) -> Self {
self.matrices.push(matrix);
self
}
#[must_use]
pub fn matrices(mut self, matrices: impl IntoIterator<Item = TileMatrix>) -> Self {
self.matrices.extend(matrices);
self
}
#[must_use]
pub fn identifier(mut self, identifier: impl Into<String>) -> Self {
self.identifier = Some(identifier.into());
self
}
#[must_use]
pub fn description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
#[must_use]
pub fn allow_zoom_other(mut self, allow: bool) -> Self {
self.allow_zoom_other = allow;
self
}
pub fn table_name(&self) -> &str {
&self.table_name
}
}
pub struct TilePyramid<'a> {
gpkg: &'a GeoPackage,
table_name: String,
matrix_set: TileMatrixSet,
matrices: Vec<TileMatrix>,
sql: TileSql,
write_block: Option<ExtensionRow>,
}
#[derive(Debug, Clone)]
struct TileSql {
get: String,
exists: String,
count: String,
count_at: String,
scan: String,
scan_at: String,
scan_in: String,
put: String,
delete: String,
}
impl TileSql {
fn new(table: &str) -> Result<Self> {
let table = quote(table)?;
let order = "ORDER BY zoom_level, tile_row, tile_column";
let columns = "zoom_level, tile_column, tile_row, tile_data";
let address = "zoom_level = ?1 AND tile_column = ?2 AND tile_row = ?3";
Ok(Self {
get: format!("SELECT tile_data FROM {table} WHERE {address}"),
exists: format!("SELECT 1 FROM {table} WHERE {address}"),
count: format!("SELECT count(*) FROM {table}"),
count_at: format!("SELECT count(*) FROM {table} WHERE zoom_level = ?1"),
scan: format!("SELECT {columns} FROM {table} {order}"),
scan_at: format!("SELECT {columns} FROM {table} WHERE zoom_level = ?1 {order}"),
scan_in: format!(
"SELECT {columns} FROM {table} \
WHERE zoom_level = ?1 AND tile_column BETWEEN ?2 AND ?3 \
AND tile_row BETWEEN ?4 AND ?5 {order}"
),
put: format!(
"INSERT INTO {table} (zoom_level, tile_column, tile_row, tile_data) \
VALUES (?1, ?2, ?3, ?4) \
ON CONFLICT (zoom_level, tile_column, tile_row) \
DO UPDATE SET tile_data = excluded.tile_data"
),
delete: format!("DELETE FROM {table} WHERE {address}"),
})
}
}
#[derive(Debug)]
pub struct Tile<'a> {
coord: TileCoord,
data: &'a [u8],
}
impl Tile<'_> {
pub fn coord(&self) -> TileCoord {
self.coord
}
pub fn data(&self) -> &[u8] {
self.data
}
pub fn to_vec(&self) -> Vec<u8> {
self.data.to_vec()
}
pub fn probe(&self) -> Result<TilePayload> {
Ok(tiles::probe(self.data)?)
}
}
impl std::fmt::Debug for TilePyramid<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TilePyramid")
.field("table_name", &self.table_name)
.field("matrix_set", &self.matrix_set)
.field("zoom_levels", &self.zoom_levels())
.finish()
}
}
impl GeoPackage {
pub fn create_tile_pyramid(&self, builder: &TilePyramidBuilder) -> Result<TilePyramid<'_>> {
let name = &builder.table_name;
self.check_writable(name)?;
if name
.get(..5)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("gpkg_"))
{
return Err(Error::ReservedTablePrefix {
table_name: name.clone(),
});
}
let conn = self.connection();
if table_exists(conn, name)? {
return Err(Error::TableAlreadyExists {
table_name: name.clone(),
});
}
if self.srs(builder.matrix_set.srs_id)?.is_none() {
return Err(Error::UnknownSrs {
srs_id: builder.matrix_set.srs_id,
});
}
builder.matrix_set.validate(&builder.matrices)?;
let zoom_other = !tiles::is_power_of_two_ladder(&builder.matrices);
if zoom_other && !builder.allow_zoom_other {
return Err(Error::ZoomOtherNotEnabled {
table_name: name.clone(),
});
}
let identifier = builder.identifier.clone().unwrap_or_else(|| name.clone());
let description = builder.description.clone().unwrap_or_default();
let set = &builder.matrix_set;
let tx = WriteTransaction::begin(conn)?;
for (exists, sql) in [
(
table_exists(conn, "gpkg_tile_matrix_set")?,
ddl::CREATE_GPKG_TILE_MATRIX_SET,
),
(
table_exists(conn, "gpkg_tile_matrix")?,
ddl::CREATE_GPKG_TILE_MATRIX,
),
] {
if !exists {
conn.execute_batch(sql)?;
}
}
conn.execute_batch(&tiles::create_tile_table_sql(name)?)?;
conn.execute(
"INSERT INTO gpkg_contents \
(table_name, data_type, identifier, description, min_x, min_y, max_x, max_y, srs_id) \
VALUES (?1, 'tiles', ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
rusqlite::params![
name,
identifier,
description,
set.min_x,
set.min_y,
set.max_x,
set.max_y,
set.srs_id,
],
)?;
conn.execute(
"INSERT INTO gpkg_tile_matrix_set (table_name, srs_id, min_x, min_y, max_x, max_y) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
rusqlite::params![name, set.srs_id, set.min_x, set.min_y, set.max_x, set.max_y],
)?;
{
let mut stmt = conn.prepare(
"INSERT INTO gpkg_tile_matrix \
(table_name, zoom_level, matrix_width, matrix_height, tile_width, tile_height, \
pixel_x_size, pixel_y_size) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
)?;
for matrix in &builder.matrices {
stmt.execute(rusqlite::params![
name,
matrix.zoom_level,
matrix.matrix_width,
matrix.matrix_height,
matrix.tile_width,
matrix.tile_height,
matrix.pixel_x_size,
matrix.pixel_y_size,
])?;
}
}
if zoom_other {
crate::extensions::register(
conn,
Some(name),
Some(tiles::TILE_DATA_COLUMN),
ZOOM_OTHER_EXTENSION_NAME,
ZOOM_OTHER_EXTENSION_DEFINITION,
tiles::TILE_EXTENSION_SCOPE,
)?;
}
tx.commit()?;
self.tiles(name)
}
pub fn tiles(&self, name: &str) -> Result<TilePyramid<'_>> {
let conn = self.connection();
let row = conn
.query_row(
"SELECT table_name, data_type FROM gpkg_contents \
WHERE table_name = ?1 COLLATE NOCASE",
[name],
|r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)),
)
.optional()?;
let (declared_name, data_type) = row.ok_or_else(|| Error::NoSuchLayer {
table_name: name.to_owned(),
})?;
if data_type != "tiles" {
return Err(Error::WrongDataType {
table_name: declared_name,
expected: "tiles",
found: data_type,
});
}
let table_name = resolve_table_name(conn, &declared_name)?.unwrap_or(declared_name);
let matrix_set =
read_matrix_set(conn, &table_name)?.ok_or_else(|| Error::NoTileMatrixSet {
table_name: table_name.clone(),
})?;
let matrices = read_matrices(conn, &table_name)?;
let sql = TileSql::new(&table_name)?;
let write_block = self.blocking_extension(&table_name)?;
Ok(TilePyramid {
gpkg: self,
table_name,
matrix_set,
matrices,
sql,
write_block,
})
}
pub fn tile_pyramids(&self) -> Result<Vec<TilePyramid<'_>>> {
if !table_exists(self.connection(), "gpkg_tile_matrix_set")? {
return Ok(Vec::new());
}
let names: Vec<String> = {
let mut stmt = self.connection().prepare(
"SELECT c.table_name FROM gpkg_contents c \
JOIN gpkg_tile_matrix_set s ON s.table_name = c.table_name COLLATE NOCASE \
WHERE c.data_type = 'tiles' ORDER BY c.table_name",
)?;
stmt.query_map([], |r| r.get(0))?
.collect::<rusqlite::Result<_>>()?
};
names.iter().map(|name| self.tiles(name)).collect()
}
}
impl<'a> TilePyramid<'a> {
pub fn table_name(&self) -> &str {
&self.table_name
}
pub fn gpkg(&self) -> &'a GeoPackage {
self.gpkg
}
pub fn matrix_set(&self) -> &TileMatrixSet {
&self.matrix_set
}
pub fn matrices(&self) -> &[TileMatrix] {
&self.matrices
}
pub fn zoom_levels(&self) -> Vec<i64> {
self.matrices.iter().map(|m| m.zoom_level).collect()
}
pub fn matrix(&self, zoom_level: i64) -> Option<&TileMatrix> {
self.matrices
.binary_search_by_key(&zoom_level, |matrix| matrix.zoom_level)
.ok()
.and_then(|index| self.matrices.get(index))
}
pub fn get_tile(&self, coord: TileCoord) -> Result<Option<Vec<u8>>> {
let conn = self.gpkg.connection();
let mut stmt = conn.prepare_cached(&self.sql.get)?;
Ok(stmt
.query_row(
rusqlite::params![coord.zoom_level, coord.column, coord.row],
|row| tile_blob(row, 0).map(<[u8]>::to_vec),
)
.optional()?)
}
pub fn get_tile_into(&self, coord: TileCoord, buffer: &mut Vec<u8>) -> Result<bool> {
let conn = self.gpkg.connection();
let mut stmt = conn.prepare_cached(&self.sql.get)?;
let found = stmt
.query_row(
rusqlite::params![coord.zoom_level, coord.column, coord.row],
|row| {
let data = tile_blob(row, 0)?;
buffer.clear();
buffer.extend_from_slice(data);
Ok(())
},
)
.optional()?;
Ok(found.is_some())
}
pub fn has_tile(&self, coord: TileCoord) -> Result<bool> {
let conn = self.gpkg.connection();
let mut stmt = conn.prepare_cached(&self.sql.exists)?;
Ok(stmt
.query_row(
rusqlite::params![coord.zoom_level, coord.column, coord.row],
|_| Ok(()),
)
.optional()?
.is_some())
}
pub fn tile_count(&self) -> Result<i64> {
let conn = self.gpkg.connection();
Ok(conn
.prepare_cached(&self.sql.count)?
.query_row([], |r| r.get(0))?)
}
pub fn tile_count_at(&self, zoom_level: i64) -> Result<i64> {
let conn = self.gpkg.connection();
Ok(conn
.prepare_cached(&self.sql.count_at)?
.query_row([zoom_level], |r| r.get(0))?)
}
pub fn cursor(&self) -> Result<TileCursor<'_>> {
self.cursor_with(&self.sql.scan, Vec::new())
}
pub fn cursor_at(&self, zoom_level: i64) -> Result<TileCursor<'_>> {
self.cursor_with(&self.sql.scan_at, vec![zoom_level.into()])
}
pub fn cursor_in(&self, zoom_level: i64, bbox: BoundingBox) -> Result<TileCursor<'_>> {
let matrix = self
.matrix(zoom_level)
.ok_or_else(|| Error::UnknownZoomLevel {
table_name: self.table_name.clone(),
zoom_level,
})?;
let range = self
.matrix_set
.tile_range(matrix, bbox.min_x, bbox.min_y, bbox.max_x, bbox.max_y);
let (min_column, max_column, min_row, max_row) = range.map_or((0, -1, 0, -1), |range| {
(
range.min_column,
range.max_column,
range.min_row,
range.max_row,
)
});
self.cursor_with(
&self.sql.scan_in,
vec![
zoom_level.into(),
min_column.into(),
max_column.into(),
min_row.into(),
max_row.into(),
],
)
}
fn cursor_with(
&self,
sql: &str,
params: Vec<rusqlite::types::Value>,
) -> Result<TileCursor<'_>> {
let stmt = self.gpkg.connection().prepare(sql)?;
Ok(TileCursor { stmt, params })
}
pub fn put_tile(&self, coord: TileCoord, data: &[u8]) -> Result<()> {
let mut writer = self.writer()?;
writer.put(coord, data)?;
writer.commit()
}
pub fn delete_tile(&self, coord: TileCoord) -> Result<bool> {
let mut writer = self.writer()?;
let deleted = writer.delete(coord)?;
writer.commit()?;
Ok(deleted)
}
pub fn writer(&self) -> Result<TileWriter<'a>> {
TileWriter::new(self)
}
pub fn blocking_extension(&self) -> Option<&ExtensionRow> {
self.write_block.as_ref()
}
fn check_writable(&self) -> Result<()> {
match &self.write_block {
None => Ok(()),
Some(row) => Err(Error::UnsupportedExtension {
table_name: self.table_name.clone(),
extension_name: row.name.clone(),
scope: row.scope.as_str().to_owned(),
}),
}
}
pub fn write_all<D: AsRef<[u8]>>(
&self,
tiles: impl IntoIterator<Item = (TileCoord, D)>,
batch_size: usize,
) -> Result<usize> {
let mut tiles = tiles.into_iter();
let mut total = 0;
loop {
let mut writer = self.writer()?;
let mut in_batch = 0;
for (coord, data) in tiles.by_ref() {
writer.put(coord, data.as_ref())?;
in_batch += 1;
if batch_size != 0 && in_batch == batch_size {
break;
}
}
writer.commit()?;
total += in_batch;
if batch_size == 0 || in_batch < batch_size {
return Ok(total);
}
}
}
pub fn validate(&self) -> Result<()> {
self.matrix_set.validate(&self.matrices)?;
Ok(())
}
}
pub struct TileWriter<'conn> {
tx: WriteTransaction<'conn>,
conn: &'conn Connection,
table_name: String,
matrices: Vec<TileMatrix>,
put_stmt: CachedStatement<'conn>,
delete_stmt: CachedStatement<'conn>,
dirty: bool,
webp_registered: Option<bool>,
}
impl<'conn> TileWriter<'conn> {
fn new(pyramid: &TilePyramid<'conn>) -> Result<Self> {
pyramid.check_writable()?;
let conn = pyramid.gpkg.connection();
let tx = WriteTransaction::begin(conn)?;
let put_stmt = conn.prepare_cached(&pyramid.sql.put)?;
let delete_stmt = conn.prepare_cached(&pyramid.sql.delete)?;
Ok(Self {
tx,
conn,
table_name: pyramid.table_name.clone(),
matrices: pyramid.matrices.clone(),
put_stmt,
delete_stmt,
dirty: false,
webp_registered: None,
})
}
pub fn put(&mut self, coord: TileCoord, data: &[u8]) -> Result<()> {
let matrix = self
.matrices
.binary_search_by_key(&coord.zoom_level, |matrix| matrix.zoom_level)
.ok()
.and_then(|index| self.matrices.get(index))
.ok_or_else(|| Error::UnknownZoomLevel {
table_name: self.table_name.clone(),
zoom_level: coord.zoom_level,
})?;
matrix.check_contains(coord.column, coord.row)?;
let payload = tiles::probe(data)?;
matrix.check_payload(&payload)?;
match payload.format {
TileFormat::Webp => self.register_webp()?,
format if format.is_core() => {}
format => {
return Err(Error::TileFormatNotAllowed {
table_name: self.table_name.clone(),
format,
});
}
}
self.put_stmt.execute(rusqlite::params![
coord.zoom_level,
coord.column,
coord.row,
data
])?;
self.dirty = true;
Ok(())
}
pub fn delete(&mut self, coord: TileCoord) -> Result<bool> {
let deleted = self.delete_stmt.execute(rusqlite::params![
coord.zoom_level,
coord.column,
coord.row
])?;
self.dirty |= deleted > 0;
Ok(deleted > 0)
}
pub fn commit(self) -> Result<()> {
let Self {
tx,
conn,
table_name,
dirty,
put_stmt,
delete_stmt,
..
} = self;
drop(put_stmt);
drop(delete_stmt);
if dirty {
conn.execute(
"UPDATE gpkg_contents \
SET last_change = strftime('%Y-%m-%dT%H:%M:%fZ','now') \
WHERE table_name = ?1",
[&table_name],
)?;
}
tx.commit()?;
Ok(())
}
fn register_webp(&mut self) -> Result<()> {
if self.webp_registered.is_none() {
self.webp_registered = Some(crate::extensions::is_registered(
self.conn,
Some(&self.table_name),
WEBP_EXTENSION_NAME,
)?);
}
if self.webp_registered == Some(false) {
crate::extensions::register(
self.conn,
Some(&self.table_name),
Some(tiles::TILE_DATA_COLUMN),
WEBP_EXTENSION_NAME,
WEBP_EXTENSION_DEFINITION,
tiles::TILE_EXTENSION_SCOPE,
)?;
self.webp_registered = Some(true);
}
Ok(())
}
}
impl std::fmt::Debug for TileWriter<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TileWriter")
.field("table_name", &self.table_name)
.field("dirty", &self.dirty)
.finish_non_exhaustive()
}
}
pub struct TileCursor<'a> {
stmt: rusqlite::Statement<'a>,
params: Vec<rusqlite::types::Value>,
}
impl std::fmt::Debug for TileCursor<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TileCursor")
.field("parameters", &self.params.len())
.finish_non_exhaustive()
}
}
impl TileCursor<'_> {
pub fn tiles(&mut self) -> Result<TileStream<'_>> {
let rows = self
.stmt
.query(rusqlite::params_from_iter(self.params.iter()))?;
Ok(TileStream { rows })
}
}
pub struct TileStream<'c> {
rows: rusqlite::Rows<'c>,
}
impl std::fmt::Debug for TileStream<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TileStream").finish_non_exhaustive()
}
}
impl TileStream<'_> {
#[expect(
clippy::should_implement_trait,
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"
)]
pub fn next(&mut self) -> Result<Option<Tile<'_>>> {
let Some(row) = self.rows.next()? else {
return Ok(None);
};
Ok(Some(Tile {
coord: TileCoord::new(row.get(0)?, row.get(1)?, row.get(2)?),
data: tile_blob(row, 3)?,
}))
}
pub fn for_each(&mut self, mut f: impl FnMut(&Tile<'_>) -> Result<()>) -> Result<()> {
while let Some(tile) = self.next()? {
f(&tile)?;
}
Ok(())
}
}
fn tile_blob<'a>(row: &'a rusqlite::Row<'a>, index: usize) -> rusqlite::Result<&'a [u8]> {
match row.get_ref(index)? {
ValueRef::Blob(data) => Ok(data),
_ => Err(rusqlite::Error::InvalidColumnType(
index,
tiles::TILE_DATA_COLUMN.to_owned(),
rusqlite::types::Type::Blob,
)),
}
}
fn read_matrix_set(conn: &Connection, table: &str) -> Result<Option<TileMatrixSet>> {
Ok(conn
.query_row(
"SELECT srs_id, min_x, min_y, max_x, max_y FROM gpkg_tile_matrix_set \
WHERE table_name = ?1 COLLATE NOCASE",
[table],
|r| {
Ok(TileMatrixSet {
srs_id: r.get(0)?,
min_x: r.get(1)?,
min_y: r.get(2)?,
max_x: r.get(3)?,
max_y: r.get(4)?,
})
},
)
.optional()?)
}
fn read_matrices(conn: &Connection, table: &str) -> Result<Vec<TileMatrix>> {
if !table_exists(conn, "gpkg_tile_matrix")? {
return Ok(Vec::new());
}
let mut stmt = conn.prepare(
"SELECT zoom_level, matrix_width, matrix_height, tile_width, tile_height, \
pixel_x_size, pixel_y_size FROM gpkg_tile_matrix \
WHERE table_name = ?1 COLLATE NOCASE ORDER BY zoom_level",
)?;
let rows = stmt.query_map([table], |r| {
Ok(TileMatrix {
zoom_level: r.get(0)?,
matrix_width: r.get(1)?,
matrix_height: r.get(2)?,
tile_width: r.get(3)?,
tile_height: r.get(4)?,
pixel_x_size: r.get(5)?,
pixel_y_size: r.get(6)?,
})
})?;
Ok(rows.collect::<rusqlite::Result<_>>()?)
}