use rstar::{RTree, RTreeObject, AABB};
use vibesql_types::SqlValue;
#[derive(Debug, Clone)]
pub struct SpatialIndexEntry {
pub row_id: usize,
pub mbr: AABB<[f64; 2]>,
}
impl RTreeObject for SpatialIndexEntry {
type Envelope = AABB<[f64; 2]>;
fn envelope(&self) -> Self::Envelope {
self.mbr
}
}
impl PartialEq for SpatialIndexEntry {
fn eq(&self, other: &Self) -> bool {
self.row_id == other.row_id
}
}
#[derive(Debug, Clone)]
pub struct SpatialIndex {
rtree: RTree<SpatialIndexEntry>,
pub column_name: String,
}
impl SpatialIndex {
pub fn new(column_name: String) -> Self {
SpatialIndex { rtree: RTree::new(), column_name }
}
pub fn bulk_load(column_name: String, entries: Vec<SpatialIndexEntry>) -> Self {
SpatialIndex { rtree: RTree::bulk_load(entries), column_name }
}
pub fn insert(&mut self, row_id: usize, mbr: AABB<[f64; 2]>) {
let entry = SpatialIndexEntry { row_id, mbr };
self.rtree.insert(entry);
}
pub fn remove(&mut self, row_id: usize, mbr: &AABB<[f64; 2]>) -> bool {
let entry = SpatialIndexEntry { row_id, mbr: *mbr };
self.rtree.remove(&entry).is_some()
}
pub fn locate_in_envelope(&self, mbr: &AABB<[f64; 2]>) -> Vec<usize> {
self.rtree.locate_in_envelope(*mbr).map(|entry| entry.row_id).collect()
}
pub fn locate_at_point(&self, point: &[f64; 2]) -> Vec<usize> {
let point_mbr = AABB::from_point(*point);
self.locate_in_envelope(&point_mbr)
}
pub fn all_row_ids(&self) -> Vec<usize> {
self.rtree.iter().map(|entry| entry.row_id).collect()
}
pub fn len(&self) -> usize {
self.rtree.size()
}
pub fn is_empty(&self) -> bool {
self.rtree.size() == 0
}
pub fn clear(&mut self) {
self.rtree = RTree::new();
}
}
pub fn extract_mbr_from_sql_value(value: &SqlValue) -> Option<AABB<[f64; 2]>> {
const GEOMETRY_TYPE_MARKER: &str = "__GEOMETRY__";
match value {
SqlValue::Varchar(s) | SqlValue::Character(s) => {
if !s.starts_with(GEOMETRY_TYPE_MARKER) {
return parse_wkt_mbr(s);
}
let parts: Vec<&str> = s.splitn(3, '|').collect();
if parts.len() >= 3 {
let wkt = parts[2];
parse_wkt_mbr(wkt)
} else {
None
}
}
SqlValue::Null => None,
_ => None,
}
}
fn parse_wkt_mbr(wkt: &str) -> Option<AABB<[f64; 2]>> {
let wkt = if wkt.starts_with("SRID=") {
wkt.split_once(';').map(|(_, w)| w).unwrap_or(wkt)
} else {
wkt
};
let wkt = wkt.trim();
if wkt.starts_with("POINT") {
parse_point_mbr(wkt)
} else if wkt.starts_with("LINESTRING") {
parse_linestring_mbr(wkt)
} else if wkt.starts_with("POLYGON") {
parse_polygon_mbr(wkt)
} else if wkt.starts_with("MULTIPOINT") {
parse_multipoint_mbr(wkt)
} else if wkt.starts_with("MULTILINESTRING") {
parse_multilinestring_mbr(wkt)
} else if wkt.starts_with("MULTIPOLYGON") {
parse_multipolygon_mbr(wkt)
} else {
None
}
}
fn parse_point_mbr(wkt: &str) -> Option<AABB<[f64; 2]>> {
let coords_str = wkt.strip_prefix("POINT")?.trim();
let coords_str = coords_str.strip_prefix('(')?.strip_suffix(')')?;
let parts: Vec<&str> = coords_str.split_whitespace().collect();
if parts.len() >= 2 {
let x: f64 = parts[0].parse().ok()?;
let y: f64 = parts[1].parse().ok()?;
Some(AABB::from_point([x, y]))
} else {
None
}
}
fn parse_linestring_mbr(wkt: &str) -> Option<AABB<[f64; 2]>> {
let coords_str = wkt.strip_prefix("LINESTRING")?.trim();
let coords_str = coords_str.strip_prefix('(')?.strip_suffix(')')?;
let points = parse_coordinate_list(coords_str)?;
calculate_mbr_from_points(&points)
}
fn parse_polygon_mbr(wkt: &str) -> Option<AABB<[f64; 2]>> {
let coords_str = wkt.strip_prefix("POLYGON")?.trim();
let coords_str = coords_str.strip_prefix('(')?.strip_suffix(')')?;
let exterior = if coords_str.contains("),") {
let rings: Vec<&str> = coords_str.split("),").collect();
rings[0].trim().strip_prefix('(').unwrap_or(rings[0])
} else {
coords_str.strip_prefix('(')?.strip_suffix(')')?
};
let points = parse_coordinate_list(exterior)?;
calculate_mbr_from_points(&points)
}
fn parse_multipoint_mbr(wkt: &str) -> Option<AABB<[f64; 2]>> {
let coords_str = wkt.strip_prefix("MULTIPOINT")?.trim();
let coords_str = coords_str.strip_prefix('(')?.strip_suffix(')')?;
let coords_str = coords_str.replace("(", "").replace(")", "");
let points = parse_coordinate_list(&coords_str)?;
calculate_mbr_from_points(&points)
}
fn parse_multilinestring_mbr(wkt: &str) -> Option<AABB<[f64; 2]>> {
let coords_str = wkt.strip_prefix("MULTILINESTRING")?.trim();
let coords_str = coords_str.strip_prefix('(')?.strip_suffix(')')?;
let mut all_points = Vec::new();
let lines: Vec<&str> = coords_str.split("),").collect();
for line in lines {
let line = line.trim().strip_prefix('(').unwrap_or(line);
let line = line.trim().strip_suffix(')').unwrap_or(line);
if let Some(mut points) = parse_coordinate_list(line) {
all_points.append(&mut points);
}
}
calculate_mbr_from_points(&all_points)
}
fn parse_multipolygon_mbr(wkt: &str) -> Option<AABB<[f64; 2]>> {
let coords_str = wkt.strip_prefix("MULTIPOLYGON")?.trim();
let coords_str = coords_str.strip_prefix('(')?.strip_suffix(')')?;
let mut all_points = Vec::new();
let polygons: Vec<&str> = coords_str.split(")),").collect();
for polygon in polygons {
let polygon = polygon.trim().strip_prefix('(').unwrap_or(polygon);
let polygon = polygon.trim().strip_suffix(')').unwrap_or(polygon);
let rings: Vec<&str> = polygon.split("),").collect();
if !rings.is_empty() {
let exterior = rings[0].trim().strip_prefix('(').unwrap_or(rings[0]);
if let Some(mut points) = parse_coordinate_list(exterior) {
all_points.append(&mut points);
}
}
}
calculate_mbr_from_points(&all_points)
}
fn parse_coordinate_list(coords_str: &str) -> Option<Vec<[f64; 2]>> {
let mut points = Vec::new();
for coord_pair in coords_str.split(',') {
let parts: Vec<&str> = coord_pair.split_whitespace().collect();
if parts.len() >= 2 {
let x: f64 = parts[0].parse().ok()?;
let y: f64 = parts[1].parse().ok()?;
points.push([x, y]);
}
}
if points.is_empty() {
None
} else {
Some(points)
}
}
fn calculate_mbr_from_points(points: &[[f64; 2]]) -> Option<AABB<[f64; 2]>> {
if points.is_empty() {
return None;
}
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for &[x, y] in points {
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x);
max_y = max_y.max(y);
}
Some(AABB::from_corners([min_x, min_y], [max_x, max_y]))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spatial_index_insert_and_query() {
let mut index = SpatialIndex::new("location".to_string());
index.insert(0, AABB::from_point([0.0, 0.0]));
index.insert(1, AABB::from_point([10.0, 10.0]));
index.insert(2, AABB::from_point([20.0, 20.0]));
assert_eq!(index.len(), 3);
let query_mbr = AABB::from_corners([5.0, 5.0], [15.0, 15.0]);
let results = index.locate_in_envelope(&query_mbr);
assert!(results.contains(&1));
assert_eq!(results.len(), 1);
}
#[test]
fn test_spatial_index_remove() {
let mut index = SpatialIndex::new("location".to_string());
let mbr = AABB::from_point([10.0, 10.0]);
index.insert(0, mbr);
assert_eq!(index.len(), 1);
let removed = index.remove(0, &mbr);
assert!(removed);
assert_eq!(index.len(), 0);
}
#[test]
fn test_extract_mbr_from_point() {
let sql_value = SqlValue::Varchar(arcstr::ArcStr::from("__GEOMETRY__|0|POINT(10 20)"));
let mbr = extract_mbr_from_sql_value(&sql_value);
assert!(mbr.is_some());
let mbr = mbr.unwrap();
assert_eq!(mbr.lower(), [10.0, 20.0]);
assert_eq!(mbr.upper(), [10.0, 20.0]);
}
#[test]
fn test_extract_mbr_from_polygon() {
let sql_value = SqlValue::Varchar(arcstr::ArcStr::from(
"__GEOMETRY__|0|POLYGON((0 0, 100 0, 100 100, 0 100, 0 0))",
));
let mbr = extract_mbr_from_sql_value(&sql_value);
assert!(mbr.is_some());
let mbr = mbr.unwrap();
assert_eq!(mbr.lower(), [0.0, 0.0]);
assert_eq!(mbr.upper(), [100.0, 100.0]);
}
#[test]
fn test_parse_point_mbr() {
let mbr = parse_point_mbr("POINT(5 10)");
assert!(mbr.is_some());
let mbr = mbr.unwrap();
assert_eq!(mbr.lower(), [5.0, 10.0]);
assert_eq!(mbr.upper(), [5.0, 10.0]);
}
#[test]
fn test_parse_linestring_mbr() {
let mbr = parse_linestring_mbr("LINESTRING(0 0, 10 10, 20 5)");
assert!(mbr.is_some());
let mbr = mbr.unwrap();
assert_eq!(mbr.lower(), [0.0, 0.0]);
assert_eq!(mbr.upper(), [20.0, 10.0]);
}
#[test]
fn test_spatial_index_bulk_load() {
let entries = vec![
SpatialIndexEntry { row_id: 0, mbr: AABB::from_point([0.0, 0.0]) },
SpatialIndexEntry { row_id: 1, mbr: AABB::from_point([10.0, 10.0]) },
SpatialIndexEntry { row_id: 2, mbr: AABB::from_point([20.0, 20.0]) },
];
let index = SpatialIndex::bulk_load("location".to_string(), entries);
assert_eq!(index.len(), 3);
let query_mbr = AABB::from_corners([5.0, 5.0], [15.0, 15.0]);
let results = index.locate_in_envelope(&query_mbr);
assert!(results.contains(&1));
}
#[test]
fn test_extract_mbr_null_value() {
let sql_value = SqlValue::Null;
let mbr = extract_mbr_from_sql_value(&sql_value);
assert!(mbr.is_none());
}
#[test]
fn test_extract_mbr_with_srid() {
let sql_value =
SqlValue::Varchar(arcstr::ArcStr::from("__GEOMETRY__|4326|SRID=4326;POINT(10 20)"));
let mbr = extract_mbr_from_sql_value(&sql_value);
assert!(mbr.is_some());
let mbr = mbr.unwrap();
assert_eq!(mbr.lower(), [10.0, 20.0]);
}
}