use anyhow::Result;
use clap::{Parser, Subcommand};
use howler_core::{Config, Database, Source};
use std::collections::HashMap;
mod server;
#[derive(Parser)]
#[command(name = "howler")]
#[command(about = "Real-world wolf tracking and sighting data", long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Commands>,
#[arg(long)]
fetch: bool,
}
#[derive(Subcommand)]
enum Commands {
Tui,
Gui,
Report,
Server {
#[arg(long, default_value_t = 8080)]
port: u16,
},
}
fn generate_report(db: &Database) -> Result<()> {
let sightings = db.get_all_sightings()?;
if sightings.is_empty() {
println!("No sightings data available. Run with --fetch to populate the database.");
return Ok(());
}
println!("=== Wolf Sighting Report ===\n");
println!("Total sightings: {}", sightings.len());
let mut by_source: HashMap<Source, usize> = HashMap::new();
for sighting in &sightings {
*by_source.entry(sighting.source.clone()).or_insert(0) += 1;
}
println!("\nBy source:");
for (source, count) in by_source {
println!(" {}: {}", source, count);
}
let mut by_species: HashMap<String, usize> = HashMap::new();
for sighting in &sightings {
*by_species.entry(sighting.species.clone()).or_insert(0) += 1;
}
println!("\nBy species:");
for (species, count) in by_species {
println!(" {}: {}", species, count);
}
let min_lat = sightings
.iter()
.map(|s| s.latitude)
.fold(f64::INFINITY, f64::min);
let max_lat = sightings
.iter()
.map(|s| s.latitude)
.fold(f64::NEG_INFINITY, f64::max);
let min_lon = sightings
.iter()
.map(|s| s.longitude)
.fold(f64::INFINITY, f64::min);
let max_lon = sightings
.iter()
.map(|s| s.longitude)
.fold(f64::NEG_INFINITY, f64::max);
println!("\nGeographic range:");
println!(" Latitude: {:.4} to {:.4}", min_lat, max_lat);
println!(" Longitude: {:.4} to {:.4}", min_lon, max_lon);
let dates: Vec<_> = sightings.iter().map(|s| s.observed_on).collect();
if let (Some(min_date), Some(max_date)) = (dates.iter().min(), dates.iter().max()) {
println!("\nDate range:");
println!(" From: {}", min_date.format("%Y-%m-%d"));
println!(" To: {}", max_date.format("%Y-%m-%d"));
}
if let Some(status) = db.get_species_status("Canis lupus")? {
println!("\nIUCN Red List Status:");
println!(" Scientific name: {}", status.scientific_name);
if let Some(common) = status.common_name {
println!(" Common name: {}", common);
}
if let Some(category) = status.red_list_category {
println!(" Category: {}", category);
}
if let Some(trend) = status.population_trend {
println!(" Population trend: {}", trend);
}
if let Some(threats) = status.threats {
println!(" Threats: {}", threats);
}
} else {
println!("\nIUCN status not available (run with --fetch and IUCN_TOKEN set)");
}
println!("\nRecent sightings (last 5):");
let mut recent = sightings.clone();
recent.sort_by_key(|b| std::cmp::Reverse(b.observed_on));
for (i, sighting) in recent.iter().take(5).enumerate() {
println!(
" {}. {} - {} ({}) at ({:.4}, {:.4})",
i + 1,
sighting.species,
sighting.observed_on.format("%Y-%m-%d"),
sighting.source,
sighting.latitude,
sighting.longitude
);
}
Ok(())
}
async fn fetch_data(config: &Config) -> Result<()> {
let db = Database::new("howler.db")?;
println!("Fetching data from available sources...");
println!("Fetching from GBIF...");
match howler_core::gbif::fetch_and_cache_gbif(&db, 100).await {
Ok(count) => println!(" GBIF: {} sightings fetched", count),
Err(e) => eprintln!(" GBIF error: {}", e),
}
println!("Fetching from Movebank...");
match howler_core::movebank::fetch_and_cache_movebank(&db, config, 100).await {
Ok(count) => println!(" Movebank: {} sightings fetched", count),
Err(e) => eprintln!(" Movebank error: {}", e),
}
println!("Fetching from iNaturalist...");
match howler_core::inaturalist::fetch_and_cache_inaturalist(&db, config, 100).await {
Ok(count) => println!(" iNaturalist: {} sightings fetched", count),
Err(e) => eprintln!(" iNaturalist error: {}", e),
}
println!("Fetching from IUCN...");
match howler_core::iucn::fetch_and_cache_iucn(&db, config).await {
Ok(count) => println!(" IUCN: {} species statuses fetched", count),
Err(e) => eprintln!(" IUCN error: {}", e),
}
println!("\nData fetching complete.");
Ok(())
}
fn main() -> Result<()> {
let cli = Cli::parse();
let config = Config::from_env();
if cli.fetch {
tokio::runtime::Runtime::new()?.block_on(fetch_data(&config))?;
}
match cli.command {
Some(Commands::Tui) => {
howler_tui::run()
}
Some(Commands::Gui) => {
howler_gui::run()
}
Some(Commands::Report) => {
let db = Database::new("howler.db")?;
generate_report(&db)
}
Some(Commands::Server { port }) => {
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(server::run_server(port))
}
None => {
howler_tui::run()
}
}
}