use serde::Deserialize;
use std::collections::HashMap;
use std::sync::OnceLock;
#[derive(Clone, Debug, PartialEq, Deserialize)]
pub struct VisaAccess {
pub country: String,
#[serde(rename = "type")]
pub access_type: String,
pub days: Option<u32>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Country {
pub name: String,
pub code: String,
pub flag: String,
pub region: String,
pub population: u64,
pub land_area_km2: u64,
pub currency_code: String,
pub currency_name: String,
pub money_supply_b_usd: f64,
pub token_price_usd: f64,
pub visa_free_destinations: u32,
pub visa_free_inbound: u32,
}
#[derive(Clone, Debug)]
pub struct CountryIndex {
pub eco_out_pct: f64, pub eco_in_pct: f64, pub pop_out_pct: f64, pub pop_in_pct: f64, pub freedom: f64, pub openness: f64, }
include!(concat!(env!("OUT_DIR"), "/countries.rs"));
static VISA_DATA: OnceLock<HashMap<String, Vec<VisaAccess>>> = OnceLock::new();
pub fn get_visa_data() -> &'static HashMap<String, Vec<VisaAccess>> {
VISA_DATA.get_or_init(|| {
serde_json::from_str(VISA_DATA_JSON).unwrap_or_default()
})
}
pub fn get_visa_outgoing(code: &str) -> Vec<VisaAccess> {
get_visa_data()
.get(code)
.cloned()
.unwrap_or_default()
}
pub fn get_visa_incoming(country_name: &str) -> Vec<VisaAccess> {
let data = get_visa_data();
let countries = load_countries();
let mut results = Vec::new();
for (code, entries) in data.iter() {
if let Some(entry) = entries.iter().find(|e| e.country == country_name) {
let holder_name = countries.iter()
.find(|c| c.code == code.to_uppercase())
.map(|c| c.name.clone())
.unwrap_or_else(|| code.to_uppercase());
results.push(VisaAccess {
country: holder_name,
access_type: entry.access_type.clone(),
days: entry.days,
});
}
}
results.sort_by(|a, b| a.access_type.cmp(&b.access_type).then(a.country.cmp(&b.country)));
results
}
pub fn access_type_weight(t: &str) -> f64 {
match t {
"visa-free" => 1.0,
"visa-on-arrival" => 0.8,
"eta" | "e-visa" => 0.5,
"visa-required" => 0.1,
"no-admission" => 0.0,
_ => 0.0,
}
}
pub fn access_type_color(t: &str) -> &'static str {
match t {
"visa-free" => "var(--cyber-green)",
"visa-on-arrival" => "var(--cyber-cyan)",
"eta" | "e-visa" => "var(--cyber-yellow)",
"visa-required" => "var(--cyber-red)",
"no-admission" => "#555",
_ => "#666",
}
}
pub fn access_type_label(t: &str) -> &'static str {
match t {
"visa-free" => "FREE",
"visa-on-arrival" => "ARRIVAL",
"eta" | "e-visa" => "ONLINE",
"visa-required" => "INTERVIEW",
"no-admission" => "DENIED",
_ => "UNKNOWN",
}
}
#[derive(Clone, Debug)]
pub struct Token {
pub code: String,
pub name: String,
pub price_usd: f64,
pub total_supply_b_usd: f64,
pub countries: Vec<(String, String, String)>, }
pub fn get_tokens() -> Vec<Token> {
let countries = load_countries();
let mut map: HashMap<String, Token> = HashMap::new();
for c in &countries {
let entry = map.entry(c.currency_code.clone()).or_insert_with(|| Token {
code: c.currency_code.clone(),
name: c.currency_name.clone(),
price_usd: c.token_price_usd,
total_supply_b_usd: 0.0,
countries: Vec::new(),
});
entry.total_supply_b_usd += c.money_supply_b_usd;
entry.countries.push((c.code.clone(), c.name.clone(), c.flag.clone()));
if entry.price_usd <= 0.0 && c.token_price_usd > 0.0 {
entry.price_usd = c.token_price_usd;
}
}
let mut tokens: Vec<Token> = map.into_values().collect();
tokens.sort_by(|a, b| b.total_supply_b_usd.partial_cmp(&a.total_supply_b_usd).unwrap_or(std::cmp::Ordering::Equal));
tokens
}
pub fn get_token(code: &str) -> Option<Token> {
get_tokens().into_iter().find(|t| t.code.to_uppercase() == code.to_uppercase())
}
impl Country {
pub fn population_fmt(&self) -> String {
format_number(self.population)
}
pub fn land_area_fmt(&self) -> String {
format!("{} kmยฒ", format_number(self.land_area_km2))
}
pub fn cap_fmt(&self) -> String {
fmt_usd_billions(self.money_supply_b_usd)
}
pub fn supply(&self) -> f64 {
if self.token_price_usd > 0.0 {
self.money_supply_b_usd / self.token_price_usd
} else {
0.0
}
}
pub fn supply_fmt(&self) -> String {
let s = self.supply();
if s <= 0.0 {
"N/A".to_string()
} else if s >= 1_000_000.0 {
format!("{:.1}Q", s / 1_000_000.0)
} else if s >= 1000.0 {
format!("{:.0}T", s / 1000.0)
} else if s >= 1.0 {
format!("{:.0}B", s)
} else {
format!("{:.1}B", s)
}
}
pub fn price_fmt(&self) -> String {
if self.token_price_usd <= 0.0 {
"N/A".to_string()
} else if self.token_price_usd >= 1.0 {
format!("${:.2}", self.token_price_usd)
} else if self.token_price_usd >= 0.01 {
format!("${:.4}", self.token_price_usd)
} else {
format!("${:.6}", self.token_price_usd)
}
}
pub fn index(&self) -> CountryIndex {
let countries = load_countries();
let data = get_visa_data();
let total_cap: f64 = countries.iter().map(|c| c.money_supply_b_usd).sum();
let total_pop: f64 = countries.iter().map(|c| c.population as f64).sum();
let by_name: std::collections::HashMap<&str, &Country> =
countries.iter().map(|c| (c.name.as_str(), c)).collect();
let outgoing = get_visa_outgoing(&self.code);
let mut eco_out: f64 = 0.0;
let mut pop_out: f64 = 0.0;
for e in &outgoing {
let w = access_type_weight(&e.access_type);
if let Some(dest) = by_name.get(e.country.as_str()) {
eco_out += w * dest.money_supply_b_usd;
pop_out += w * dest.population as f64;
}
}
let mut eco_in: f64 = 0.0;
let mut pop_in: f64 = 0.0;
for (code, entries) in data.iter() {
if let Some(entry) = entries.iter().find(|e| e.country == self.name) {
let w = access_type_weight(&entry.access_type);
if let Some(holder) = countries.iter().find(|c| c.code == code.to_uppercase()) {
eco_in += w * holder.money_supply_b_usd;
pop_in += w * holder.population as f64;
}
}
}
let eco_out_pct = if total_cap > 0.0 { eco_out / total_cap * 100.0 } else { 0.0 };
let eco_in_pct = if total_cap > 0.0 { eco_in / total_cap * 100.0 } else { 0.0 };
let pop_out_pct = if total_pop > 0.0 { pop_out / total_pop * 100.0 } else { 0.0 };
let pop_in_pct = if total_pop > 0.0 { pop_in / total_pop * 100.0 } else { 0.0 };
let freedom = (eco_out_pct * pop_out_pct).sqrt();
let openness = (eco_in_pct * pop_in_pct).sqrt();
CountryIndex { eco_out_pct, eco_in_pct, pop_out_pct, pop_in_pct, freedom, openness }
}
}
fn fmt_usd_billions(v: f64) -> String {
if v <= 0.0 {
"N/A".to_string()
} else if v >= 1000.0 {
format!("${:.1}T", v / 1000.0)
} else {
format!("${:.0}B", v)
}
}
impl Token {
pub fn cap_fmt(&self) -> String {
fmt_usd_billions(self.total_supply_b_usd)
}
pub fn supply(&self) -> f64 {
if self.price_usd > 0.0 {
self.total_supply_b_usd / self.price_usd
} else {
0.0
}
}
pub fn supply_fmt(&self) -> String {
let s = self.supply();
if s <= 0.0 {
"N/A".to_string()
} else if s >= 1_000_000.0 {
format!("{:.1}Q", s / 1_000_000.0)
} else if s >= 1000.0 {
format!("{:.0}T", s / 1000.0)
} else if s >= 1.0 {
format!("{:.0}B", s)
} else {
format!("{:.1}B", s)
}
}
pub fn price_fmt(&self) -> String {
if self.price_usd <= 0.0 {
"N/A".to_string()
} else if self.price_usd >= 1.0 {
format!("${:.2}", self.price_usd)
} else if self.price_usd >= 0.01 {
format!("${:.4}", self.price_usd)
} else {
format!("${:.6}", self.price_usd)
}
}
}
fn format_number(n: u64) -> String {
let s = n.to_string();
let mut result = String::new();
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(c);
}
result.chars().rev().collect()
}
pub const REGIONS: &[&str] = &[
"All",
"Africa",
"Asia",
"Europe",
"Eurasia",
"Latin America",
"Middle East",
"North America",
"Oceania",
];
#[derive(Clone, Copy, PartialEq)]
pub enum SortField {
Name,
Population,
LandArea,
Token,
Price,
Supply,
Cap,
EcoOut,
EcoIn,
PopOut,
PopIn,
Freedom,
Openness,
}
impl SortField {
pub fn label(&self) -> &'static str {
match self {
Self::Name => "COUNTRY",
Self::Population => "POPULATION",
Self::LandArea => "LAND AREA",
Self::Token => "TOKEN",
Self::Price => "PRICE",
Self::Supply => "SUPPLY",
Self::Cap => "CAP",
Self::EcoOut => "ECO OUT%",
Self::EcoIn => "ECO IN%",
Self::PopOut => "POP OUT%",
Self::PopIn => "POP IN%",
Self::Freedom => "FREEDOM",
Self::Openness => "OPENNESS",
}
}
}