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 = 4;
#[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<u16, 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 num_cells = u16::from_be_bytes([blob[2], blob[3]]);
Ok(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 id = i64::from_be_bytes(data[offset..offset + 8].try_into().ok()?);
let min_x = f32::from_be_bytes(data[offset + 8..offset + 12].try_into().ok()?);
let max_x = f32::from_be_bytes(data[offset + 12..offset + 16].try_into().ok()?);
let min_y = f32::from_be_bytes(data[offset + 16..offset + 20].try_into().ok()?);
let max_y = f32::from_be_bytes(data[offset + 20..offset + 24].try_into().ok()?);
Some((min_x, max_x, min_y, max_y, id))
}
fn read_root_depth(blob: &[u8]) -> u16 {
match blob.first_chunk::<2>() {
Some(bytes) => u16::from_be_bytes(*bytes),
None => 0,
}
}
fn decode_node(blob: &[u8], depth: u16) -> Result<RTreeNode, GpkgError> {
let num_cells = parse_node_header(blob)? as usize;
let cell_data = &blob[NODE_HEADER_BYTES..];
if depth == 0 {
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))
} else {
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))
}
}
pub struct GpkgRTreeReader {
nodes: HashMap<i64, Vec<u8>>,
max_node_id: i64,
root_depth: u16,
}
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);
}
let root_depth = nodes.get(&1).map(|blob| read_root_depth(blob)).unwrap_or(0);
Ok(Self {
nodes,
max_node_id,
root_depth,
})
}
#[doc(hidden)]
pub fn for_testing(nodes: HashMap<i64, Vec<u8>>, max_node_id: i64) -> Self {
let root_depth = nodes.get(&1).map(|blob| read_root_depth(blob)).unwrap_or(0);
Self {
nodes,
max_node_id,
root_depth,
}
}
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,
self.root_depth,
min_x,
min_y,
max_x,
max_y,
&mut results,
);
}
results
}
#[allow(clippy::too_many_arguments)]
fn search_node(
&self,
blob: &[u8],
depth: u16,
min_x: f64,
min_y: f64,
max_x: f64,
max_y: f64,
results: &mut Vec<i64>,
) {
let node = match decode_node(blob, depth) {
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)
&& let Some(child_blob) = self.nodes.get(&cell.child_node)
{
self.search_node(
child_blob,
depth.saturating_sub(1),
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, self.root_depth, &mut entries);
}
entries
}
fn collect_leaf_entries(&self, blob: &[u8], depth: u16, out: &mut Vec<RTreeEntry>) {
let node = match decode_node(blob, depth) {
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, depth.saturating_sub(1), 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 _ = node_number; let mut blob = Vec::with_capacity(NODE_HEADER_BYTES + entries.len() * CELL_BYTES);
blob.extend_from_slice(&0u16.to_be_bytes()); blob.extend_from_slice(&(entries.len() as u16).to_be_bytes());
write_node_cells(&mut blob, entries);
blob
}
#[doc(hidden)]
pub fn build_interior_node_blob(
node_number: i32,
depth: u16,
cells: &[(i64, f32, f32, f32, f32)],
) -> Vec<u8> {
let _ = node_number; let mut blob = Vec::with_capacity(NODE_HEADER_BYTES + cells.len() * CELL_BYTES);
blob.extend_from_slice(&depth.to_be_bytes());
blob.extend_from_slice(&(cells.len() as u16).to_be_bytes());
write_node_cells(&mut blob, cells);
blob
}
fn write_node_cells(blob: &mut Vec<u8>, cells: &[(i64, f32, f32, f32, f32)]) {
for (id, min_x, max_x, min_y, max_y) in cells {
blob.extend_from_slice(&id.to_be_bytes());
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());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[rustfmt::skip]
const REAL_SQLITE_RTREE_NODE_BLOB: [u8; 76] = [
0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x3F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x3F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
0x40, 0x00, 0x00, 0x00, 0x40, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00,
0x40, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03,
0x40, 0x80, 0x00, 0x00, 0x40, 0xA0, 0x00, 0x00, 0x40, 0x80, 0x00, 0x00,
0x40, 0xA0, 0x00, 0x00,
];
#[test]
fn parse_node_header_reads_real_sqlite_cell_count() {
let num_cells = parse_node_header(&REAL_SQLITE_RTREE_NODE_BLOB).expect("header must parse");
assert_eq!(num_cells, 3, "real fixture has exactly 3 inserted rows");
assert_eq!(NODE_HEADER_BYTES, 4);
}
#[test]
fn parse_raw_cell_reads_real_sqlite_id_first_layout() {
let cell_data = &REAL_SQLITE_RTREE_NODE_BLOB[NODE_HEADER_BYTES..];
let (min_x, max_x, min_y, max_y, id) =
parse_raw_cell(cell_data, 0).expect("cell 0 must parse");
assert_eq!(id, 1, "id field must be read FIRST, not last");
assert_eq!((min_x, max_x, min_y, max_y), (0.0, 1.0, 0.0, 1.0));
let (min_x, max_x, min_y, max_y, id) =
parse_raw_cell(cell_data, CELL_BYTES).expect("cell 1 must parse");
assert_eq!(id, 2);
assert_eq!((min_x, max_x, min_y, max_y), (2.0, 3.0, 2.0, 3.0));
let (min_x, max_x, min_y, max_y, id) =
parse_raw_cell(cell_data, 2 * CELL_BYTES).expect("cell 2 must parse");
assert_eq!(id, 3);
assert_eq!((min_x, max_x, min_y, max_y), (4.0, 5.0, 4.0, 5.0));
}
#[test]
fn gpkg_rtree_reader_decodes_real_sqlite_node_blob_correctly() {
let mut nodes = HashMap::new();
nodes.insert(1i64, REAL_SQLITE_RTREE_NODE_BLOB.to_vec());
let reader = GpkgRTreeReader::for_testing(nodes, 1);
assert_eq!(reader.len(), 1);
let mut results = reader.search(-10.0, -10.0, 10.0, 10.0);
results.sort_unstable();
assert_eq!(results, vec![1, 2, 3]);
let results = reader.search(2.0, 2.0, 3.0, 3.0);
assert_eq!(results, vec![2]);
let results = reader.search(100.0, 100.0, 200.0, 200.0);
assert!(results.is_empty());
let mut entries = reader.all_entries();
entries.sort_by_key(|e| e.rowid);
assert_eq!(entries.len(), 3);
assert_eq!(entries[0].rowid, 1);
assert_eq!((entries[0].min_x, entries[0].max_x), (0.0, 1.0));
assert_eq!(entries[1].rowid, 2);
assert_eq!((entries[1].min_x, entries[1].max_x), (2.0, 3.0));
assert_eq!(entries[2].rowid, 3);
assert_eq!((entries[2].min_x, entries[2].max_x), (4.0, 5.0));
}
#[test]
fn parse_node_header_rejects_short_blob() {
let err = parse_node_header(&[0u8, 0u8, 0u8]).expect_err("3 bytes is too short");
match err {
GpkgError::InvalidFormat(msg) => assert!(msg.contains("only 3 bytes")),
other => panic!("expected InvalidFormat, got {other:?}"),
}
}
}