use std::collections::HashMap;
use crate::btree::CellValue;
use crate::error::GpkgError;
use crate::gpkg::GeoPackage;
const CELL_BYTES: usize = 24;
const NODE_HEADER_BYTES: usize = 8;
#[derive(Debug, Clone)]
pub struct RTreeEntry {
pub rowid: i64,
pub min_x: f32,
pub max_x: f32,
pub min_y: f32,
pub max_y: f32,
}
impl RTreeEntry {
#[inline]
pub fn intersects(&self, min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> bool {
(self.max_x as f64) >= min_x
&& (self.min_x as f64) <= max_x
&& (self.max_y as f64) >= min_y
&& (self.min_y as f64) <= max_y
}
}
#[derive(Debug, Clone)]
struct InteriorCell {
child_node: i64,
min_x: f32,
max_x: f32,
min_y: f32,
max_y: f32,
}
impl InteriorCell {
#[inline]
fn intersects(&self, min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> bool {
(self.max_x as f64) >= min_x
&& (self.min_x as f64) <= max_x
&& (self.max_y as f64) >= min_y
&& (self.min_y as f64) <= max_y
}
}
#[derive(Debug)]
enum RTreeNode {
Interior(Vec<InteriorCell>),
Leaf(Vec<RTreeEntry>),
}
fn parse_node_header(blob: &[u8]) -> Result<(i32, i32), GpkgError> {
if blob.len() < NODE_HEADER_BYTES {
return Err(GpkgError::InvalidFormat(format!(
"R-tree node blob is only {} bytes; need at least {NODE_HEADER_BYTES}",
blob.len()
)));
}
let node_number = i32::from_be_bytes([blob[0], blob[1], blob[2], blob[3]]);
let num_cells = i32::from_be_bytes([blob[4], blob[5], blob[6], blob[7]]);
Ok((node_number, num_cells))
}
fn parse_raw_cell(data: &[u8], offset: usize) -> Option<(f32, f32, f32, f32, i64)> {
if offset + CELL_BYTES > data.len() {
return None;
}
let min_x = f32::from_be_bytes(data[offset..offset + 4].try_into().ok()?);
let max_x = f32::from_be_bytes(data[offset + 4..offset + 8].try_into().ok()?);
let min_y = f32::from_be_bytes(data[offset + 8..offset + 12].try_into().ok()?);
let max_y = f32::from_be_bytes(data[offset + 12..offset + 16].try_into().ok()?);
let id = i64::from_be_bytes(data[offset + 16..offset + 24].try_into().ok()?);
Some((min_x, max_x, min_y, max_y, id))
}
fn decode_node(
blob: &[u8],
max_node_id: i64,
node_ids: &HashMap<i64, Vec<u8>>,
) -> Result<RTreeNode, GpkgError> {
let (_, num_cells) = parse_node_header(blob)?;
let num_cells = num_cells.max(0) as usize;
let cell_data = &blob[NODE_HEADER_BYTES..];
if num_cells == 0 {
return Ok(RTreeNode::Leaf(Vec::new()));
}
let first_id = {
let (_, _, _, _, id) = parse_raw_cell(cell_data, 0).ok_or_else(|| {
GpkgError::InvalidFormat("R-tree node BLOB too short to hold even one cell".into())
})?;
id
};
let is_interior = first_id > 0 && first_id <= max_node_id && node_ids.contains_key(&first_id);
if is_interior {
let mut cells = Vec::with_capacity(num_cells);
for i in 0..num_cells {
let Some((min_x, max_x, min_y, max_y, child_node)) =
parse_raw_cell(cell_data, i * CELL_BYTES)
else {
break;
};
cells.push(InteriorCell {
child_node,
min_x,
max_x,
min_y,
max_y,
});
}
Ok(RTreeNode::Interior(cells))
} else {
let mut entries = Vec::with_capacity(num_cells);
for i in 0..num_cells {
let Some((min_x, max_x, min_y, max_y, rowid)) =
parse_raw_cell(cell_data, i * CELL_BYTES)
else {
break;
};
entries.push(RTreeEntry {
rowid,
min_x,
max_x,
min_y,
max_y,
});
}
Ok(RTreeNode::Leaf(entries))
}
}
pub struct GpkgRTreeReader {
nodes: HashMap<i64, Vec<u8>>,
max_node_id: i64,
}
impl GpkgRTreeReader {
pub fn open(gpkg: &GeoPackage, table_name: &str, geom_column: &str) -> Result<Self, GpkgError> {
let node_table = format!("rtree_{table_name}_{geom_column}_node");
let rows = gpkg
.scan_table_by_name(&node_table)?
.ok_or_else(|| GpkgError::TableNotFound(node_table.clone()))?;
Self::build_from_rows(&rows, &node_table)
}
fn build_from_rows(
rows: &[(i64, Vec<CellValue>)],
table_name: &str,
) -> Result<Self, GpkgError> {
let mut nodes: HashMap<i64, Vec<u8>> = HashMap::with_capacity(rows.len());
let mut max_node_id: i64 = 0;
for (rowid, values) in rows {
let (node_id, blob) = match values.len() {
0 => {
tracing::warn!(
table = table_name,
rowid = rowid,
"R-tree node row has no columns; skipping"
);
continue;
}
1 => {
let blob = extract_blob(&values[0], table_name, *rowid)?;
(*rowid, blob)
}
_ => {
let node_id = extract_integer(&values[0], *rowid);
let blob = extract_blob(&values[1], table_name, node_id)?;
(node_id, blob)
}
};
if blob.len() < NODE_HEADER_BYTES {
tracing::warn!(
table = table_name,
node_id = node_id,
blob_len = blob.len(),
"R-tree node BLOB too short; skipping"
);
continue;
}
max_node_id = max_node_id.max(node_id);
nodes.insert(node_id, blob);
}
Ok(Self { nodes, max_node_id })
}
#[doc(hidden)]
pub fn for_testing(nodes: HashMap<i64, Vec<u8>>, max_node_id: i64) -> Self {
Self { nodes, max_node_id }
}
pub fn search(&self, min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Vec<i64> {
let mut results = Vec::new();
if let Some(root_blob) = self.nodes.get(&1) {
self.search_node(root_blob, min_x, min_y, max_x, max_y, &mut results);
}
results
}
fn search_node(
&self,
blob: &[u8],
min_x: f64,
min_y: f64,
max_x: f64,
max_y: f64,
results: &mut Vec<i64>,
) {
let node = match decode_node(blob, self.max_node_id, &self.nodes) {
Ok(n) => n,
Err(e) => {
tracing::warn!("Failed to decode R-tree node: {e}");
return;
}
};
match node {
RTreeNode::Interior(cells) => {
for cell in cells {
if cell.intersects(min_x, min_y, max_x, max_y) {
if let Some(child_blob) = self.nodes.get(&cell.child_node) {
self.search_node(child_blob, min_x, min_y, max_x, max_y, results);
}
}
}
}
RTreeNode::Leaf(entries) => {
for entry in entries {
if entry.intersects(min_x, min_y, max_x, max_y) {
results.push(entry.rowid);
}
}
}
}
}
pub fn len(&self) -> usize {
self.nodes.len()
}
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn max_node_id(&self) -> i64 {
self.max_node_id
}
pub fn all_entries(&self) -> Vec<RTreeEntry> {
let mut entries = Vec::new();
if let Some(root_blob) = self.nodes.get(&1) {
self.collect_leaf_entries(root_blob, &mut entries);
}
entries
}
fn collect_leaf_entries(&self, blob: &[u8], out: &mut Vec<RTreeEntry>) {
let node = match decode_node(blob, self.max_node_id, &self.nodes) {
Ok(n) => n,
Err(e) => {
tracing::warn!("Failed to decode R-tree node during full scan: {e}");
return;
}
};
match node {
RTreeNode::Interior(cells) => {
for cell in cells {
if let Some(child_blob) = self.nodes.get(&cell.child_node) {
self.collect_leaf_entries(child_blob, out);
}
}
}
RTreeNode::Leaf(entries) => out.extend(entries),
}
}
}
fn extract_integer(v: &CellValue, rowid_hint: i64) -> i64 {
match v {
CellValue::Integer(i) => *i,
_ => rowid_hint,
}
}
fn extract_blob(v: &CellValue, table: &str, node_id: i64) -> Result<Vec<u8>, GpkgError> {
match v {
CellValue::Blob(b) => Ok(b.clone()),
other => Err(GpkgError::InvalidFormat(format!(
"Expected BLOB for node {node_id} in {table}, got {other:?}"
))),
}
}
#[doc(hidden)]
pub fn build_leaf_node_blob(node_number: i32, entries: &[(i64, f32, f32, f32, f32)]) -> Vec<u8> {
let mut blob = Vec::with_capacity(NODE_HEADER_BYTES + entries.len() * CELL_BYTES);
blob.extend_from_slice(&node_number.to_be_bytes());
blob.extend_from_slice(&(entries.len() as i32).to_be_bytes());
for (rowid, min_x, max_x, min_y, max_y) in entries {
blob.extend_from_slice(&min_x.to_be_bytes());
blob.extend_from_slice(&max_x.to_be_bytes());
blob.extend_from_slice(&min_y.to_be_bytes());
blob.extend_from_slice(&max_y.to_be_bytes());
blob.extend_from_slice(&rowid.to_be_bytes());
}
blob
}
#[doc(hidden)]
pub fn build_interior_node_blob(node_number: i32, cells: &[(i64, f32, f32, f32, f32)]) -> Vec<u8> {
build_leaf_node_blob(node_number, cells)
}