#![expect(
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::cast_sign_loss
)]
use std::fmt::{Display, Formatter};
use std::path::PathBuf;
use std::str::FromStr as _;
use martin_tile_utils::{get_zoom_precision, xyz_to_bbox};
use serde::Serialize;
use size_format::SizeFormatterSI;
use sqlx::{SqliteExecutor, query};
use tilejson::Bounds;
use crate::{MbtResult, MbtType, Mbtiles, invert_y_value};
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ZoomInfo {
pub zoom: u8,
pub tile_count: u64,
pub min_tile_size: u64,
pub max_tile_size: u64,
pub avg_tile_size: f64,
pub bbox: Bounds,
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct Summary {
pub file_path: String,
pub file_size: Option<u64>,
pub mbt_type: MbtType,
pub page_size: u64,
pub page_count: u64,
pub tile_count: u64,
pub min_tile_size: Option<u64>,
pub max_tile_size: Option<u64>,
pub avg_tile_size: f64,
pub bbox: Option<Bounds>,
pub min_zoom: Option<u8>,
pub max_zoom: Option<u8>,
pub zoom_info: Vec<ZoomInfo>,
}
impl Display for Summary {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
writeln!(f, "{:15} {}", "File path:", self.file_path)?;
writeln!(f, "{:15} {}", "Schema:", self.mbt_type)?;
if let Some(file_size) = self.file_size {
let file_size = SizeFormatterSI::new(file_size);
writeln!(f, "{:15} {file_size:.2}B", "File size:")?;
} else {
writeln!(f, "{:15} unknown", "File size:")?;
}
let page_size = SizeFormatterSI::new(self.page_size);
writeln!(f, "{:15} {page_size:.2}B", "SQL page size:")?;
writeln!(f, "{:15} {:.2}", "SQL page count:", self.page_count)?;
writeln!(f)?;
if self.zoom_info.is_empty() {
writeln!(f, " There are no tiles in this mbtiles file")?;
} else {
writeln!(
f,
" {:^4} | {:^9} | {:^9} | {:^9} | {:^9} | Bounding Box",
"Zoom", "Count", "Smallest", "Largest", "Average"
)?;
}
for l in &self.zoom_info {
let min = SizeFormatterSI::new(l.min_tile_size);
let max = SizeFormatterSI::new(l.max_tile_size);
let avg = SizeFormatterSI::new(l.avg_tile_size as u64);
let prec = get_zoom_precision(l.zoom);
writeln!(
f,
" {:>4} | {:>9} | {:>9} | {:>9} | {:>9} | {:.prec$}",
l.zoom,
l.tile_count,
format!("{min:.1}B"),
format!("{max:.1}B"),
format!("{avg:.1}B"),
l.bbox,
)?;
}
if self.zoom_info.len() > 1
&& let (Some(min), Some(max), Some(bbox), Some(max_zoom)) = (
self.min_tile_size,
self.max_tile_size,
self.bbox,
self.max_zoom,
)
{
let min = SizeFormatterSI::new(min);
let max = SizeFormatterSI::new(max);
let avg = SizeFormatterSI::new(self.avg_tile_size as u64);
let prec = get_zoom_precision(max_zoom);
writeln!(
f,
" {:>4} | {:>9} | {:>9} | {:>9} | {:>9} | {bbox:.prec$}",
"all",
self.tile_count,
format!("{min}B"),
format!("{max}B"),
format!("{avg}B"),
)?;
}
Ok(())
}
}
impl Mbtiles {
#[hotpath::measure]
pub async fn summary<T>(&self, conn: &mut T) -> MbtResult<Summary>
where
for<'e> &'e mut T: SqliteExecutor<'e>,
{
let mbt_type = self.detect_type(&mut *conn).await?;
let file_size = PathBuf::from_str(self.filepath())
.ok()
.and_then(|p| p.metadata().ok())
.map(|m| m.len());
let sql = query!("PRAGMA page_size;");
let page_size = sql
.fetch_one(&mut *conn)
.await?
.page_size
.expect("page_size is not null") as u64;
let sql = query!("PRAGMA page_count;");
let page_count = sql
.fetch_one(&mut *conn)
.await?
.page_count
.expect("page_count is not null") as u64;
let zoom_info = query!(
"
SELECT zoom_level AS zoom,
count() AS count,
min(length(tile_data)) AS smallest,
max(length(tile_data)) AS largest,
avg(length(tile_data)) AS average,
min(tile_column) AS min_tile_x,
min(tile_row) AS min_tile_y,
max(tile_column) AS max_tile_x,
max(tile_row) AS max_tile_y
FROM tiles
GROUP BY zoom_level"
)
.fetch_all(&mut *conn)
.await?;
let zoom_info: Vec<ZoomInfo> = zoom_info
.into_iter()
.map(|r| {
let zoom = u8::try_from(r.zoom.expect("zoom_level should not be NULL"))
.expect("zoom_level should fit in a u8");
ZoomInfo {
zoom,
tile_count: r.count as u64,
min_tile_size: r.smallest.unwrap_or(0) as u64,
max_tile_size: r.largest.unwrap_or(0) as u64,
avg_tile_size: r.average.unwrap_or(0.0),
bbox: xyz_to_bbox(
zoom,
r.min_tile_x.expect("min_tile_x should not be None") as u32,
invert_y_value(
zoom,
r.max_tile_y.expect("max_tile_y should not be None") as u32,
),
r.max_tile_x.expect("max_tile_x should not be None") as u32,
invert_y_value(
zoom,
r.min_tile_y.expect("min_tile_y should not be None") as u32,
),
)
.into(),
}
})
.collect();
let tile_count = zoom_info.iter().map(|l| l.tile_count).sum();
let avg_sum = zoom_info
.iter()
.map(|l| l.avg_tile_size * l.tile_count as f64)
.sum::<f64>();
Ok(Summary {
file_path: self.filepath().to_string(),
file_size,
mbt_type,
page_size,
page_count,
tile_count,
min_tile_size: zoom_info.iter().map(|l| l.min_tile_size).reduce(u64::min),
max_tile_size: zoom_info.iter().map(|l| l.max_tile_size).reduce(u64::max),
avg_tile_size: avg_sum / tile_count as f64,
bbox: zoom_info.iter().map(|l| l.bbox).reduce(|a, b| a + b),
min_zoom: zoom_info.iter().map(|l| l.zoom).reduce(u8::min),
max_zoom: zoom_info.iter().map(|l| l.zoom).reduce(u8::max),
zoom_info,
})
}
}
#[cfg(test)]
mod tests {
use insta::assert_yaml_snapshot;
use crate::metadata::anonymous_mbtiles;
use crate::{MbtType, Mbtiles, init_mbtiles_schema};
#[actix_rt::test]
async fn summary_empty_file() {
let mbt = Mbtiles::new(":memory:").unwrap();
let mut conn = mbt.open().await.unwrap();
init_mbtiles_schema(&mut conn, MbtType::Flat, false)
.await
.unwrap();
let res = mbt.summary(&mut conn).await.unwrap();
assert_yaml_snapshot!(res, @r#"
file_path: ":memory:"
file_size: ~
mbt_type: Flat
page_size: 512
page_count: 6
tile_count: 0
min_tile_size: ~
max_tile_size: ~
avg_tile_size: NaN
bbox: ~
min_zoom: ~
max_zoom: ~
zoom_info: []
"#);
}
#[actix_rt::test]
async fn summary() {
let script = include_str!("../../tests/fixtures/mbtiles/world_cities.sql");
let (mbt, mut conn) = anonymous_mbtiles(script).await;
let res = mbt.summary(&mut conn).await.unwrap();
assert_yaml_snapshot!(res, @r#"
file_path: ":memory:"
file_size: ~
mbt_type: Flat
page_size: 4096
page_count: 5
tile_count: 8
min_tile_size: 20
max_tile_size: 1107
avg_tile_size: 202.625
bbox:
- -180
- -85.0511287798066
- 180.00000000000003
- 85.0511287798066
min_zoom: 0
max_zoom: 6
zoom_info:
- zoom: 0
tile_count: 1
min_tile_size: 1107
max_tile_size: 1107
avg_tile_size: 1107
bbox:
- -180
- -85.0511287798066
- 180
- 85.0511287798066
- zoom: 1
tile_count: 1
min_tile_size: 20
max_tile_size: 20
avg_tile_size: 20
bbox:
- -180
- -85.0511287798066
- 0
- 0
- zoom: 2
tile_count: 2
min_tile_size: 151
max_tile_size: 263
avg_tile_size: 207
bbox:
- 90.00000000000001
- -66.51326044311186
- 180.00000000000003
- 66.51326044311186
- zoom: 3
tile_count: 1
min_tile_size: 20
max_tile_size: 20
avg_tile_size: 20
bbox:
- 134.99999999999997
- -40.97989806962013
- 179.99999999999997
- 0
- zoom: 4
tile_count: 1
min_tile_size: 20
max_tile_size: 20
avg_tile_size: 20
bbox:
- -22.500000000000014
- 0.000000000000012549319548339412
- -0.000000000000012549319548339412
- 21.943045533438188
- zoom: 5
tile_count: 1
min_tile_size: 20
max_tile_size: 20
avg_tile_size: 20
bbox:
- 0
- 40.97989806962013
- 11.25
- 48.92249926375824
- zoom: 6
tile_count: 1
min_tile_size: 20
max_tile_size: 20
avg_tile_size: 20
bbox:
- 73.12500000000001
- 27.059125784374054
- 78.75000000000001
- 31.952162238024968
"#);
}
}