use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::fs;
use drone_vrp::{DroneSpec, DroneVrpInstance, DroneSolver};
#[derive(Parser)]
#[command(name = "drone-vrp")]
#[command(about = "Drone VRP Solver and Energy Calculator — 9 drone models including Ukrainian combat-proven platforms", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Solve {
#[arg(long, default_value = "FlyCart30")]
drone: String,
#[arg(long)]
customers: String,
#[arg(long)]
depot_lat: f64,
#[arg(long)]
depot_lon: f64,
#[arg(long, default_value_t = 0.0)]
wind: f64,
#[arg(short, long)]
output: Option<String>,
},
Energy {
#[arg(long, default_value = "FlyCart30")]
drone: String,
#[arg(short, long)]
distance: f64,
#[arg(short, long, default_value_t = 0.0)]
payload: f64,
#[arg(short, long, default_value_t = 0.0)]
wind: f64,
},
Range {
#[arg(long, default_value = "FlyCart30")]
drone: String,
#[arg(short, long, default_value_t = 0.0)]
payload: f64,
#[arg(short, long, default_value_t = 0.0)]
wind: f64,
},
Specs {
#[arg(long)]
drone: Option<String>,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Solve {
drone,
customers,
depot_lat,
depot_lon,
wind,
output,
} => {
let spec = DroneSpec::from_name(&drone)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let model = spec.model.clone();
let geojson_str =
fs::read_to_string(&customers).context("Failed to read customers file")?;
let geojson = geojson_str.parse::<geojson::GeoJson>()?;
let mut customer_coords = Vec::new();
let mut demands = Vec::new();
if let geojson::GeoJson::FeatureCollection(collection) = geojson {
for feature in collection.features {
if let Some(ref geometry) = feature.geometry {
if let geojson::Value::Point(ref coords) = geometry.value {
customer_coords.push([coords[1], coords[0]]); let demand = feature
.property("demand")
.and_then(|v| v.as_f64())
.unwrap_or(1.0);
demands.push(demand);
}
}
}
}
let instance = DroneVrpInstance {
drone_model: model.clone(),
depot: [depot_lat, depot_lon],
customers: customer_coords,
demands_kg: demands,
wind_speed_ms: wind,
no_fly_zones: vec![],
};
println!(
"Solving VRP for {} customers using {:?} ({})...",
instance.customers.len(),
model,
spec.manufacturer,
);
let result = DroneSolver::solve(&instance)?;
println!("Solution found:");
println!(" Routes: {}", result.routes.len());
println!(" Energy used: {:.2} Wh", result.energy_used_wh);
if !result.violations.is_empty() {
println!(" Violations: {:?}", result.violations);
}
if let Some(out_path) = output {
let json = serde_json::to_string_pretty(&result)?;
fs::write(out_path, json)?;
println!("Result saved.");
}
}
Commands::Energy {
drone,
distance,
payload,
wind,
} => {
let spec = DroneSpec::from_name(&drone)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let energy = spec.calculate_energy_wh(distance, payload, wind);
println!("Energy consumption: {:.4} Wh", energy);
let pct_battery = (energy / spec.battery_capacity_wh) * 100.0;
println!("Battery used: {:.1}% of {} Wh", pct_battery, spec.battery_capacity_wh);
}
Commands::Range {
drone,
payload,
wind,
} => {
let spec = DroneSpec::from_name(&drone)
.map_err(|e| anyhow::anyhow!("{}", e))?;
let range_m = spec.estimate_max_range_m(payload, wind);
println!("{:?} ({}) range estimate:", spec.model, spec.manufacturer);
println!(" Payload: {:.1} kg / {:.1} kg max", payload, spec.max_payload_kg);
println!(" Wind: {:.1} m/s headwind", wind);
println!(" Max one-way range: {:.1} km (with 20% reserve)", range_m / 1000.0);
println!(" Max round-trip radius: {:.1} km", range_m / 2000.0);
}
Commands::Specs { drone } => {
let models = if let Some(d) = drone {
vec![DroneSpec::from_name(&d)
.map_err(|e| anyhow::anyhow!("{}", e))?]
} else {
DroneSpec::all()
};
for s in &models {
println!("┌─────────────────────────────────────────────────────┐");
println!("│ {:?} ({})", s.model, s.manufacturer);
println!("│ {}", s.description);
println!("│ Country: {} | Propulsion: {:?}", s.country, s.propulsion);
println!("├─────────────────────────────────────────────────────┤");
println!("│ Airframe mass: {:.1} kg", s.mass_kg);
println!("│ Max payload: {:.1} kg", s.max_payload_kg);
println!("│ Battery/Energy: {:.0} Wh", s.battery_capacity_wh);
println!("│ Cruise speed: {:.1} m/s ({:.0} km/h)", s.cruise_speed_ms, s.cruise_speed_ms * 3.6);
println!("│ Power (empty): {:.0} W", s.power_no_load_w);
println!("│ Power (loaded): {:.0} W", s.power_max_load_w);
let range_empty = s.estimate_max_range_m(0.0, 0.0) / 1000.0;
let range_loaded = s.estimate_max_range_m(s.max_payload_kg, 0.0) / 1000.0;
println!("│ Range (empty): {:.1} km one-way (80% reserve)", range_empty);
println!("│ Range (loaded): {:.1} km one-way (80% reserve)", range_loaded);
println!("└─────────────────────────────────────────────────────┘");
println!();
}
if models.len() > 1 {
println!("Aliases: fc30=FlyCart30, aerorozvidka=R18, baba_yaga=Vampire,");
println!(" skyfall=Vampire, gurzuf=HeavyShot, reactive_drone=Kazhan,");
println!(" uforce=Nemesis, m350=Matrice350RTK, ukrspecsystems=PD2");
}
}
}
Ok(())
}