mod scan_ids;
mod tag_index;
pub(crate) use scan_ids::scan_block_ids;
pub(crate) use tag_index::{scan_block_tags, TagIndex, TAG_INDEX_VERSION};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ElemKind {
Node,
Way,
Relation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BlobBbox {
pub(crate) min_lat: i32,
pub(crate) max_lat: i32,
pub(crate) min_lon: i32,
pub(crate) max_lon: i32,
}
impl BlobBbox {
pub fn new(min_lat: i32, max_lat: i32, min_lon: i32, max_lon: i32) -> Self {
Self { min_lat, max_lat, min_lon, max_lon }
}
pub fn contains(&self, inner: &BlobBbox) -> bool {
self.min_lat <= inner.min_lat
&& self.max_lat >= inner.max_lat
&& self.min_lon <= inner.min_lon
&& self.max_lon >= inner.max_lon
}
pub fn intersects(&self, other: &BlobBbox) -> bool {
self.min_lat <= other.max_lat
&& self.max_lat >= other.min_lat
&& self.min_lon <= other.max_lon
&& self.max_lon >= other.min_lon
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct BlobIndex {
pub kind: ElemKind,
pub min_id: i64,
pub max_id: i64,
pub count: u64,
pub bbox: Option<BlobBbox>,
}
const INDEX_VERSION_V1: u8 = 0x01;
const INDEX_VERSION: u8 = 0x02;
const INDEX_SIZE_V1: usize = 26;
pub const INDEX_SIZE: usize = 42;
impl BlobIndex {
pub fn serialize(&self) -> [u8; INDEX_SIZE] {
let mut buf = [0u8; INDEX_SIZE];
buf[0] = INDEX_VERSION;
buf[1] = match self.kind {
ElemKind::Node => 0,
ElemKind::Way => 1,
ElemKind::Relation => 2,
};
buf[2..10].copy_from_slice(&self.min_id.to_le_bytes());
buf[10..18].copy_from_slice(&self.max_id.to_le_bytes());
buf[18..26].copy_from_slice(&self.count.to_le_bytes());
if let Some(ref bbox) = self.bbox {
buf[26..30].copy_from_slice(&bbox.min_lat.to_le_bytes());
buf[30..34].copy_from_slice(&bbox.max_lat.to_le_bytes());
buf[34..38].copy_from_slice(&bbox.min_lon.to_le_bytes());
buf[38..42].copy_from_slice(&bbox.max_lon.to_le_bytes());
}
buf
}
pub fn deserialize(data: &[u8]) -> Option<Self> {
if data.len() < INDEX_SIZE_V1 {
return None;
}
let version = data[0];
if version != INDEX_VERSION_V1 && version != INDEX_VERSION {
return None;
}
let kind = match data[1] {
0 => ElemKind::Node,
1 => ElemKind::Way,
2 => ElemKind::Relation,
_ => return None,
};
let min_id = i64::from_le_bytes(data[2..10].try_into().ok()?);
let max_id = i64::from_le_bytes(data[10..18].try_into().ok()?);
let count = u64::from_le_bytes(data[18..26].try_into().ok()?);
let bbox = if version == INDEX_VERSION && data.len() >= INDEX_SIZE && kind == ElemKind::Node {
let min_lat = i32::from_le_bytes(data[26..30].try_into().ok()?);
let max_lat = i32::from_le_bytes(data[30..34].try_into().ok()?);
let min_lon = i32::from_le_bytes(data[34..38].try_into().ok()?);
let max_lon = i32::from_le_bytes(data[38..42].try_into().ok()?);
if min_lat == 0 && max_lat == 0 && min_lon == 0 && max_lon == 0 {
None
} else {
Some(BlobBbox { min_lat, max_lat, min_lon, max_lon })
}
} else {
None
};
Some(BlobIndex { kind, min_id, max_id, count, bbox })
}
}
#[derive(Debug, Clone)]
pub struct BlobFilter {
pub(crate) want_nodes: bool,
pub(crate) want_ways: bool,
pub(crate) want_relations: bool,
pub(crate) node_bbox: Option<BlobBbox>,
pub(crate) required_tag_keys: Option<Box<[Box<[u8]>]>>,
pub(crate) required_tag_prefixes: Option<Box<[Box<[u8]>]>>,
}
impl BlobFilter {
pub fn new(want_nodes: bool, want_ways: bool, want_relations: bool) -> Self {
Self {
want_nodes, want_ways, want_relations,
node_bbox: None, required_tag_keys: None, required_tag_prefixes: None,
}
}
pub fn only_nodes() -> Self {
Self::new(true, false, false)
}
pub fn only_ways() -> Self {
Self::new(false, true, false)
}
pub fn only_relations() -> Self {
Self::new(false, false, true)
}
pub fn with_node_bbox(mut self, bbox: BlobBbox) -> Self {
self.node_bbox = Some(bbox);
self
}
pub(crate) fn wants(&self, kind: ElemKind) -> bool {
match kind {
ElemKind::Node => self.want_nodes,
ElemKind::Way => self.want_ways,
ElemKind::Relation => self.want_relations,
}
}
pub(crate) fn wants_index(&self, index: &BlobIndex) -> bool {
if !self.wants(index.kind) {
return false;
}
if let Some(ref filter_bbox) = self.node_bbox
&& index.kind == ElemKind::Node
&& let Some(ref blob_bbox) = index.bbox
{
return filter_bbox.intersects(blob_bbox);
}
true
}
pub fn with_required_tag_keys(mut self, keys: Vec<String>) -> Self {
self.required_tag_keys = Some(
keys.into_iter()
.map(|s| s.into_bytes().into_boxed_slice())
.collect::<Vec<_>>()
.into_boxed_slice(),
);
self
}
pub fn with_required_tag_prefixes(mut self, prefixes: Vec<String>) -> Self {
self.required_tag_prefixes = Some(
prefixes.into_iter()
.map(|s| s.into_bytes().into_boxed_slice())
.collect::<Vec<_>>()
.into_boxed_slice(),
);
self
}
pub(crate) fn has_tag_filter(&self) -> bool {
self.required_tag_keys.is_some() || self.required_tag_prefixes.is_some()
}
pub(crate) fn wants_tag_index(&self, tag_index: &TagIndex) -> bool {
if let Some(ref keys) = self.required_tag_keys
&& tag_index.has_any_key(keys)
{
return true;
}
if let Some(ref prefixes) = self.required_tag_prefixes
&& tag_index.has_any_prefix(prefixes)
{
return true;
}
!self.has_tag_filter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip_v2_way_no_bbox() {
let index = BlobIndex {
kind: ElemKind::Way,
min_id: 100,
max_id: 9999,
count: 42,
bbox: None,
};
let bytes = index.serialize();
assert_eq!(bytes.len(), INDEX_SIZE);
assert_eq!(bytes[0], INDEX_VERSION);
let recovered = BlobIndex::deserialize(&bytes).expect("deserialize should succeed");
assert_eq!(recovered.kind, ElemKind::Way);
assert_eq!(recovered.min_id, 100);
assert_eq!(recovered.max_id, 9999);
assert_eq!(recovered.count, 42);
assert!(recovered.bbox.is_none());
}
#[test]
fn roundtrip_v2_node_with_bbox() {
let index = BlobIndex {
kind: ElemKind::Node,
min_id: 1,
max_id: 8000,
count: 8000,
bbox: Some(BlobBbox {
min_lat: 510_000_000,
max_lat: 520_000_000,
min_lon: -1_000_000,
max_lon: 10_000_000,
}),
};
let bytes = index.serialize();
let recovered = BlobIndex::deserialize(&bytes).expect("deserialize should succeed");
assert_eq!(recovered.kind, ElemKind::Node);
assert_eq!(recovered.min_id, 1);
assert_eq!(recovered.max_id, 8000);
assert_eq!(recovered.count, 8000);
let bbox = recovered.bbox.expect("should have bbox");
assert_eq!(bbox.min_lat, 510_000_000);
assert_eq!(bbox.max_lat, 520_000_000);
assert_eq!(bbox.min_lon, -1_000_000);
assert_eq!(bbox.max_lon, 10_000_000);
}
#[test]
fn v1_backward_compat() {
let v1_index = BlobIndex {
kind: ElemKind::Node,
min_id: 1,
max_id: 100,
count: 100,
bbox: None,
};
let mut v1_bytes = [0u8; INDEX_SIZE_V1];
v1_bytes[0] = INDEX_VERSION_V1;
v1_bytes[1] = 0; v1_bytes[2..10].copy_from_slice(&v1_index.min_id.to_le_bytes());
v1_bytes[10..18].copy_from_slice(&v1_index.max_id.to_le_bytes());
v1_bytes[18..26].copy_from_slice(&v1_index.count.to_le_bytes());
let recovered = BlobIndex::deserialize(&v1_bytes).expect("v1 should deserialize");
assert_eq!(recovered.kind, ElemKind::Node);
assert_eq!(recovered.min_id, 1);
assert_eq!(recovered.max_id, 100);
assert_eq!(recovered.count, 100);
assert!(recovered.bbox.is_none(), "v1 data should have no bbox");
}
#[test]
fn deserialize_rejects_bad_version() {
let mut bytes = BlobIndex {
kind: ElemKind::Node,
min_id: 0,
max_id: 0,
count: 0,
bbox: None,
}
.serialize();
bytes[0] = 0xFF;
assert!(BlobIndex::deserialize(&bytes).is_none());
}
#[test]
fn deserialize_rejects_short_data() {
assert!(BlobIndex::deserialize(&[0x02, 0x00]).is_none());
}
#[test]
fn deserialize_rejects_bad_type() {
let mut bytes = BlobIndex {
kind: ElemKind::Node,
min_id: 0,
max_id: 0,
count: 0,
bbox: None,
}
.serialize();
bytes[1] = 5; assert!(BlobIndex::deserialize(&bytes).is_none());
}
#[test]
fn roundtrip_negative_ids() {
let index = BlobIndex {
kind: ElemKind::Node,
min_id: -100,
max_id: -1,
count: 100,
bbox: None,
};
let bytes = index.serialize();
let recovered = BlobIndex::deserialize(&bytes).expect("deserialize should succeed");
assert_eq!(recovered.min_id, -100);
assert_eq!(recovered.max_id, -1);
}
#[test]
fn bbox_intersects() {
let a = BlobBbox::new(0, 100, 0, 100);
let b = BlobBbox::new(50, 150, 50, 150);
assert!(a.intersects(&b), "overlapping boxes should intersect");
let c = BlobBbox::new(200, 300, 200, 300);
assert!(!a.intersects(&c), "non-overlapping boxes should not intersect");
let d = BlobBbox::new(100, 200, 100, 200);
assert!(a.intersects(&d), "edge-touching boxes should intersect");
}
#[test]
fn bbox_intersects_negative_coords() {
let a = BlobBbox::new(-900_000_000, -800_000_000, -1_800_000_000, -1_700_000_000);
let b = BlobBbox::new(-850_000_000, -750_000_000, -1_750_000_000, -1_650_000_000);
assert!(a.intersects(&b));
let c = BlobBbox::new(100_000_000, 200_000_000, 100_000_000, 200_000_000);
assert!(!a.intersects(&c));
}
#[test]
fn wants_index_spatial_filter() {
let filter = BlobFilter::new(true, true, true).with_node_bbox(
BlobBbox::new(500_000_000, 520_000_000, 100_000_000, 120_000_000),
);
let inside = BlobIndex {
kind: ElemKind::Node,
min_id: 1,
max_id: 100,
count: 100,
bbox: Some(BlobBbox::new(510_000_000, 515_000_000, 110_000_000, 115_000_000)),
};
assert!(filter.wants_index(&inside));
let outside = BlobIndex {
kind: ElemKind::Node,
min_id: 200,
max_id: 300,
count: 100,
bbox: Some(BlobBbox::new(-100_000_000, -50_000_000, -100_000_000, -50_000_000)),
};
assert!(!filter.wants_index(&outside));
let no_bbox = BlobIndex {
kind: ElemKind::Node,
min_id: 400,
max_id: 500,
count: 100,
bbox: None,
};
assert!(filter.wants_index(&no_bbox));
let way = BlobIndex {
kind: ElemKind::Way,
min_id: 1,
max_id: 100,
count: 100,
bbox: None,
};
assert!(filter.wants_index(&way));
}
}