plozone 0.1.0

3D spatial zone engine: geofencing, octree hole-scanning, realtime sync (WebSocket + QUIC + io_uring), voxel pathfinding, and AV sensor fusion.
Documentation
use std::env;

use plozone::{
    EnuConverter, OctreeNode, ScanMode, Zone, ZoneEntry, ZoneStore, run_scan,
};

fn main() {
    let args: Vec<String> = env::args().collect();
    if args.len() < 2 {
        usage();
        return;
    }

    match args[1].as_str() {
        "query" => cmd_query(&args),
        "zones" => cmd_zones(&args),
        "scan" => cmd_scan(&args),
        "bench" => cmd_bench(),
        _ => usage(),
    }
}

fn usage() {
    eprintln!("plozone — 3D spatial zone query tool");
    eprintln!();
    eprintln!("Usage:");
    eprintln!("  plozone query <lat> <lon> <alt>   Query zones at a geodetic point");
    eprintln!("  plozone zones list               List all zone IDs in the store");
    eprintln!("  plozone scan <zone_id> [--depth D]  Scan a zone for unsampled holes");
    eprintln!("  plozone bench                    Quick zone query benchmark");
    eprintln!();
    eprintln!("Examples:");
    eprintln!("  plozone query 10.7626 106.6601 5.0");
    eprintln!("  plozone scan 1 --depth 7");
}

fn default_store() -> (EnuConverter, ZoneStore) {
    let conv = EnuConverter::new(10.7626, 106.6601, 0.0);
    let store = ZoneStore::from_entries(
        &[
            ZoneEntry::new(
                1,
                Zone::Cylinder {
                    center: [10.7626, 106.6601],
                    radius_m: 50.0,
                    z_min: 0.0,
                    z_max: 20.0,
                },
            ),
            ZoneEntry::new(
                2,
                Zone::Aabb {
                    min: [10.7620, 106.6595, 0.0],
                    max: [10.7632, 106.6607, 10.0],
                },
            ),
        ],
        &conv,
    );
    (conv, store)
}

fn cmd_query(args: &[String]) {
    if args.len() < 5 {
        eprintln!("Usage: plozone query <lat> <lon> <alt>");
        return;
    }
    let lat: f64 = args[2].parse().expect("invalid lat");
    let lon: f64 = args[3].parse().expect("invalid lon");
    let alt: f64 = args[4].parse().expect("invalid alt");

    let (conv, store) = default_store();

    let hits = store.query_geodetic(lat, lon, alt, &conv);
    if hits.is_empty() {
        println!("no zones at ({lat}, {lon}, {alt})");
    } else {
        for id in hits {
            let zone_aabb = store.zone_aabb(id);
            let msg = zone_aabb
                .map(|a| format!("[{:.1}, {:.1}, {:.1}] – [{:.1}, {:.1}, {:.1}]", a[0], a[1], a[2], a[3], a[4], a[5]))
                .unwrap_or_else(|| "N/A".to_string());
            println!("zone {id:>4}  ENU AABB {msg}");
        }
    }
}

fn cmd_zones(args: &[String]) {
    let (_, store) = default_store();
    let ids = store.ids();
    if ids.is_empty() {
        println!("no zones loaded");
    } else {
        for id in ids {
            println!("{id}");
        }
    }
    let _ = args;
}

fn cmd_scan(args: &[String]) {
    if args.len() < 3 {
        eprintln!("Usage: plozone scan <zone_id> [--depth D]");
        return;
    }
    let zone_id: u32 = args[2].parse().expect("invalid zone id");
    let mut depth = 4u8;
    for w in args.windows(2) {
        if w[0] == "--depth" {
            depth = w[1].parse().expect("invalid depth");
        }
    }

    let (conv, store) = default_store();
    if !store.ids().contains(&zone_id) {
        eprintln!("zone {} not found", zone_id);
        return;
    }

    let octree = OctreeNode::new([0.0; 3], 64.0);
    let result = run_scan(&octree, &store, zone_id, &ScanMode::Coarse { depth });

    println!("zone {zone_id} scan (depth {depth})");
    println!("  coverage: {:.1}%", result.coverage_pct);
    println!("  holes:    {}", result.holes.len());
    for h in &result.holes {
        println!(
            "    [{:.2}, {:.2}, {:.2}]  size={:.2}m  depth={}",
            h.center[0], h.center[1], h.center[2], h.size_m, h.depth
        );
    }
    let _ = conv;
}

fn cmd_bench() {
    let (conv, store) = default_store();
    let n = 1_000_000u64;
    let start = std::time::Instant::now();
    for _ in 0..n {
        let _ = store.query_geodetic(10.7626, 106.6601, 5.0, &conv);
    }
    let elapsed = start.elapsed();
    let ns_per_query = elapsed.as_nanos() as f64 / n as f64;
    println!(
        "{n} queries in {:.3}s  ({:.0} ns/query)",
        elapsed.as_secs_f64(),
        ns_per_query
    );
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn default_store_has_two_zones() {
        let (_, s) = default_store();
        assert_eq!(s.len(), 2);
    }
}