Implemented much more robust city selection logic and data
This commit is contained in:
@@ -12,5 +12,8 @@ zbus = { version = "4.0", default-features = false, features = ["tokio"] }
|
||||
tokio = { version = "1.0", features = ["rt-multi-thread", "sync", "time", "macros"] }
|
||||
futures-util = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
toml = "0.8"
|
||||
dirs = "5.0"
|
||||
lazy_static = "1.5"
|
||||
dialoguer = "0.11"
|
||||
|
||||
@@ -177,3 +177,10 @@ All tests verify correct behavior against the legacy C implementation, including
|
||||
## Compatibility
|
||||
|
||||
This rewrite maintains compatibility with the legacy C version's command-line interface for basic operations. The output format and calculation methods are designed to match the original implementation.
|
||||
|
||||
|
||||
## Credits
|
||||
Original author of Redshift for C source code
|
||||
Simple Maps for the country/city data under Creative Commons 4.0 liscense
|
||||
https://creativecommons.org/licenses/by/4.0/
|
||||
https://simplemaps.com/data/world-cities
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Process worldcities.json to filter cities based on:
|
||||
- Population >= 1 million OR capital city
|
||||
- If country has < 5 cities with 1M+ population, keep largest cities + capitals
|
||||
- For city-states (only 1 city), mark for auto-selection
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
# Prompt for input file path
|
||||
input_path = input("Enter the path to worldcities.json (using Simple Maps data): ").strip()
|
||||
|
||||
# Expand user home directory if present
|
||||
input_path = os.path.expanduser(input_path)
|
||||
|
||||
# Check if file exists
|
||||
if not os.path.isfile(input_path):
|
||||
print(f"Error: File not found: {input_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# Prompt for output directory
|
||||
output_dir = input("Enter the directory to save filtered_world_cities.json: ").strip()
|
||||
|
||||
# Expand user home directory if present
|
||||
output_dir = os.path.expanduser(output_dir)
|
||||
|
||||
# Create output directory if it doesn't exist
|
||||
if not os.path.exists(output_dir):
|
||||
os.makedirs(output_dir)
|
||||
|
||||
# Construct output file path
|
||||
output_path = os.path.join(output_dir, 'filtered_world_cities.json')
|
||||
|
||||
# Read the input JSON
|
||||
print(f"\nReading from: {input_path}")
|
||||
with open(input_path, 'r') as f:
|
||||
cities = json.load(f)
|
||||
|
||||
# Group cities by country
|
||||
cities_by_country = defaultdict(list)
|
||||
for city in cities:
|
||||
cities_by_country[city['country']].append(city)
|
||||
|
||||
# Process each country
|
||||
filtered_cities = []
|
||||
countries_with_single_city = {}
|
||||
|
||||
for country, country_cities in cities_by_country.items():
|
||||
# Get cities with population >= 1 million
|
||||
million_plus = []
|
||||
capitals = []
|
||||
|
||||
for city in country_cities:
|
||||
pop = city.get('population')
|
||||
is_capital = bool(city.get('capital') and city.get('capital') not in ("", None))
|
||||
|
||||
# Parse population
|
||||
try:
|
||||
pop_int = int(pop) if pop else 0
|
||||
except (ValueError, TypeError):
|
||||
pop_int = 0
|
||||
|
||||
city['population_int'] = pop_int
|
||||
|
||||
if is_capital:
|
||||
capitals.append(city)
|
||||
|
||||
if pop_int >= 1000000:
|
||||
million_plus.append(city)
|
||||
|
||||
# Apply filtering rules
|
||||
if len(million_plus) >= 5:
|
||||
# Keep cities with 1M+ population and capitals
|
||||
selected = million_plus + [c for c in capitals if c not in million_plus]
|
||||
else:
|
||||
# Keep largest cities + capitals
|
||||
# Sort by population
|
||||
sorted_cities = sorted(country_cities, key=lambda x: x['population_int'], reverse=True)
|
||||
|
||||
# Take top cities and ensure capitals are included
|
||||
num_to_keep = max(5, len(million_plus))
|
||||
selected = sorted_cities[:num_to_keep]
|
||||
|
||||
# Add any capitals not already included
|
||||
for capital in capitals:
|
||||
if capital not in selected:
|
||||
selected.append(capital)
|
||||
|
||||
# Remove duplicates
|
||||
selected_unique = []
|
||||
seen_ids = set()
|
||||
for city in selected:
|
||||
if city['id'] not in seen_ids:
|
||||
selected_unique.append(city)
|
||||
seen_ids.add(city['id'])
|
||||
|
||||
# Track city-states (countries with only 1 city in final selection)
|
||||
is_city_state = len(selected_unique) == 1
|
||||
countries_with_single_city[country] = is_city_state
|
||||
|
||||
filtered_cities.extend(selected_unique)
|
||||
|
||||
# Keep only required fields and sort
|
||||
output_cities = []
|
||||
for city in filtered_cities:
|
||||
output_cities.append({
|
||||
'city': city['city'],
|
||||
'city_ascii': city['city_ascii'],
|
||||
'lat': city['lat'],
|
||||
'lng': city['lng'],
|
||||
'country': city['country'],
|
||||
'admin_name': city['admin_name'],
|
||||
'population': city['population'],
|
||||
'id': city['id'],
|
||||
'isCityCountry': countries_with_single_city.get(city['country'], False)
|
||||
})
|
||||
|
||||
# Sort by country, then by population
|
||||
def safe_pop_int(pop_str):
|
||||
try:
|
||||
return -int(float(pop_str)) if pop_str else 0
|
||||
except (ValueError, TypeError):
|
||||
return 0
|
||||
|
||||
output_cities.sort(key=lambda x: (x['country'], safe_pop_int(x['population'])))
|
||||
|
||||
# Write output
|
||||
print(f"Writing to: {output_path}")
|
||||
with open(output_path, 'w') as f:
|
||||
json.dump(output_cities, f, indent=2, ensure_ascii=False)
|
||||
|
||||
city_states_count = sum(1 for v in countries_with_single_city.values() if v)
|
||||
|
||||
print(f"\nProcessed {len(cities)} cities")
|
||||
print(f"Filtered to {len(output_cities)} cities")
|
||||
print(f"Countries: {len(cities_by_country)}")
|
||||
print(f"City-states (auto-select): {city_states_count}")
|
||||
print(f"\nOutput successfully written to: {output_path}")
|
||||
+153
-262
@@ -1,256 +1,134 @@
|
||||
/// City database for interactive location selection
|
||||
/// Contains major cities organized by country
|
||||
|
||||
use crate::types::Location;
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::env;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct City {
|
||||
pub name: &'static str,
|
||||
pub lat: f32,
|
||||
pub lon: f32,
|
||||
pub city: String,
|
||||
pub city_ascii: String,
|
||||
pub lat: String,
|
||||
pub lng: String,
|
||||
pub country: String,
|
||||
pub admin_name: String,
|
||||
pub population: String,
|
||||
pub id: String,
|
||||
#[serde(rename = "isCityCountry")]
|
||||
pub is_city_country: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Country {
|
||||
pub name: &'static str,
|
||||
pub cities: &'static [City],
|
||||
}
|
||||
|
||||
// Major cities database organized by country
|
||||
pub const COUNTRIES: &[Country] = &[
|
||||
Country {
|
||||
name: "United States",
|
||||
cities: &[
|
||||
City { name: "New York, NY", lat: 40.7128, lon: -74.0060 },
|
||||
City { name: "Los Angeles, CA", lat: 34.0522, lon: -118.2437 },
|
||||
City { name: "Chicago, IL", lat: 41.8781, lon: -87.6298 },
|
||||
City { name: "Houston, TX", lat: 29.7604, lon: -95.3698 },
|
||||
City { name: "Phoenix, AZ", lat: 33.4484, lon: -112.0740 },
|
||||
City { name: "Philadelphia, PA", lat: 39.9526, lon: -75.1652 },
|
||||
City { name: "San Antonio, TX", lat: 29.4241, lon: -98.4936 },
|
||||
City { name: "San Diego, CA", lat: 32.7157, lon: -117.1611 },
|
||||
City { name: "Dallas, TX", lat: 32.7767, lon: -96.7970 },
|
||||
City { name: "San Jose, CA", lat: 37.3382, lon: -121.8863 },
|
||||
City { name: "Austin, TX", lat: 30.2672, lon: -97.7431 },
|
||||
City { name: "Jacksonville, FL", lat: 30.3322, lon: -81.6557 },
|
||||
City { name: "Fort Worth, TX", lat: 32.7555, lon: -97.3308 },
|
||||
City { name: "Columbus, OH", lat: 39.9612, lon: -82.9988 },
|
||||
City { name: "Charlotte, NC", lat: 35.2271, lon: -80.8431 },
|
||||
City { name: "San Francisco, CA", lat: 37.7749, lon: -122.4194 },
|
||||
City { name: "Indianapolis, IN", lat: 39.7684, lon: -86.1581 },
|
||||
City { name: "Seattle, WA", lat: 47.6062, lon: -122.3321 },
|
||||
City { name: "Denver, CO", lat: 39.7392, lon: -104.9903 },
|
||||
City { name: "Boston, MA", lat: 42.3601, lon: -71.0589 },
|
||||
City { name: "Portland, OR", lat: 45.5152, lon: -122.6784 },
|
||||
City { name: "Miami, FL", lat: 25.7617, lon: -80.1918 },
|
||||
City { name: "Atlanta, GA", lat: 33.7490, lon: -84.3880 },
|
||||
City { name: "Las Vegas, NV", lat: 36.1699, lon: -115.1398 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "Canada",
|
||||
cities: &[
|
||||
City { name: "Toronto, ON", lat: 43.6532, lon: -79.3832 },
|
||||
City { name: "Montreal, QC", lat: 45.5017, lon: -73.5673 },
|
||||
City { name: "Vancouver, BC", lat: 49.2827, lon: -123.1207 },
|
||||
City { name: "Calgary, AB", lat: 51.0447, lon: -114.0719 },
|
||||
City { name: "Edmonton, AB", lat: 53.5461, lon: -113.4938 },
|
||||
City { name: "Ottawa, ON", lat: 45.4215, lon: -75.6972 },
|
||||
City { name: "Winnipeg, MB", lat: 49.8951, lon: -97.1384 },
|
||||
City { name: "Quebec City, QC", lat: 46.8139, lon: -71.2080 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "United Kingdom",
|
||||
cities: &[
|
||||
City { name: "London", lat: 51.5074, lon: -0.1278 },
|
||||
City { name: "Manchester", lat: 53.4808, lon: -2.2426 },
|
||||
City { name: "Birmingham", lat: 52.4862, lon: -1.8904 },
|
||||
City { name: "Leeds", lat: 53.8008, lon: -1.5491 },
|
||||
City { name: "Glasgow", lat: 55.8642, lon: -4.2518 },
|
||||
City { name: "Edinburgh", lat: 55.9533, lon: -3.1883 },
|
||||
City { name: "Liverpool", lat: 53.4084, lon: -2.9916 },
|
||||
City { name: "Bristol", lat: 51.4545, lon: -2.5879 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "Germany",
|
||||
cities: &[
|
||||
City { name: "Berlin", lat: 52.5200, lon: 13.4050 },
|
||||
City { name: "Hamburg", lat: 53.5511, lon: 9.9937 },
|
||||
City { name: "Munich", lat: 48.1351, lon: 11.5820 },
|
||||
City { name: "Cologne", lat: 50.9375, lon: 6.9603 },
|
||||
City { name: "Frankfurt", lat: 50.1109, lon: 8.6821 },
|
||||
City { name: "Stuttgart", lat: 48.7758, lon: 9.1829 },
|
||||
City { name: "Düsseldorf", lat: 51.2277, lon: 6.7735 },
|
||||
City { name: "Dortmund", lat: 51.5136, lon: 7.4653 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "France",
|
||||
cities: &[
|
||||
City { name: "Paris", lat: 48.8566, lon: 2.3522 },
|
||||
City { name: "Marseille", lat: 43.2965, lon: 5.3698 },
|
||||
City { name: "Lyon", lat: 45.7640, lon: 4.8357 },
|
||||
City { name: "Toulouse", lat: 43.6047, lon: 1.4442 },
|
||||
City { name: "Nice", lat: 43.7102, lon: 7.2620 },
|
||||
City { name: "Nantes", lat: 47.2184, lon: -1.5536 },
|
||||
City { name: "Strasbourg", lat: 48.5734, lon: 7.7521 },
|
||||
City { name: "Bordeaux", lat: 44.8378, lon: -0.5792 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "Spain",
|
||||
cities: &[
|
||||
City { name: "Madrid", lat: 40.4168, lon: -3.7038 },
|
||||
City { name: "Barcelona", lat: 41.3851, lon: 2.1734 },
|
||||
City { name: "Valencia", lat: 39.4699, lon: -0.3763 },
|
||||
City { name: "Seville", lat: 37.3891, lon: -5.9845 },
|
||||
City { name: "Zaragoza", lat: 41.6488, lon: -0.8891 },
|
||||
City { name: "Málaga", lat: 36.7213, lon: -4.4214 },
|
||||
City { name: "Bilbao", lat: 43.2630, lon: -2.9350 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "Italy",
|
||||
cities: &[
|
||||
City { name: "Rome", lat: 41.9028, lon: 12.4964 },
|
||||
City { name: "Milan", lat: 45.4642, lon: 9.1900 },
|
||||
City { name: "Naples", lat: 40.8518, lon: 14.2681 },
|
||||
City { name: "Turin", lat: 45.0703, lon: 7.6869 },
|
||||
City { name: "Palermo", lat: 38.1157, lon: 13.3615 },
|
||||
City { name: "Florence", lat: 43.7696, lon: 11.2558 },
|
||||
City { name: "Venice", lat: 45.4408, lon: 12.3155 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "Japan",
|
||||
cities: &[
|
||||
City { name: "Tokyo", lat: 35.6762, lon: 139.6503 },
|
||||
City { name: "Osaka", lat: 34.6937, lon: 135.5023 },
|
||||
City { name: "Yokohama", lat: 35.4437, lon: 139.6380 },
|
||||
City { name: "Nagoya", lat: 35.1815, lon: 136.9066 },
|
||||
City { name: "Sapporo", lat: 43.0642, lon: 141.3469 },
|
||||
City { name: "Fukuoka", lat: 33.5904, lon: 130.4017 },
|
||||
City { name: "Kobe", lat: 34.6901, lon: 135.1955 },
|
||||
City { name: "Kyoto", lat: 35.0116, lon: 135.7681 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "China",
|
||||
cities: &[
|
||||
City { name: "Beijing", lat: 39.9042, lon: 116.4074 },
|
||||
City { name: "Shanghai", lat: 31.2304, lon: 121.4737 },
|
||||
City { name: "Guangzhou", lat: 23.1291, lon: 113.2644 },
|
||||
City { name: "Shenzhen", lat: 22.5431, lon: 114.0579 },
|
||||
City { name: "Chengdu", lat: 30.5728, lon: 104.0668 },
|
||||
City { name: "Hangzhou", lat: 30.2741, lon: 120.1551 },
|
||||
City { name: "Wuhan", lat: 30.5928, lon: 114.3055 },
|
||||
City { name: "Xi'an", lat: 34.3416, lon: 108.9398 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "India",
|
||||
cities: &[
|
||||
City { name: "Mumbai", lat: 19.0760, lon: 72.8777 },
|
||||
City { name: "Delhi", lat: 28.7041, lon: 77.1025 },
|
||||
City { name: "Bangalore", lat: 12.9716, lon: 77.5946 },
|
||||
City { name: "Hyderabad", lat: 17.3850, lon: 78.4867 },
|
||||
City { name: "Chennai", lat: 13.0827, lon: 80.2707 },
|
||||
City { name: "Kolkata", lat: 22.5726, lon: 88.3639 },
|
||||
City { name: "Pune", lat: 18.5204, lon: 73.8567 },
|
||||
City { name: "Ahmedabad", lat: 23.0225, lon: 72.5714 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "Australia",
|
||||
cities: &[
|
||||
City { name: "Sydney, NSW", lat: -33.8688, lon: 151.2093 },
|
||||
City { name: "Melbourne, VIC", lat: -37.8136, lon: 144.9631 },
|
||||
City { name: "Brisbane, QLD", lat: -27.4698, lon: 153.0251 },
|
||||
City { name: "Perth, WA", lat: -31.9505, lon: 115.8605 },
|
||||
City { name: "Adelaide, SA", lat: -34.9285, lon: 138.6007 },
|
||||
City { name: "Canberra, ACT", lat: -35.2809, lon: 149.1300 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "Brazil",
|
||||
cities: &[
|
||||
City { name: "São Paulo", lat: -23.5505, lon: -46.6333 },
|
||||
City { name: "Rio de Janeiro", lat: -22.9068, lon: -43.1729 },
|
||||
City { name: "Brasília", lat: -15.8267, lon: -47.9218 },
|
||||
City { name: "Salvador", lat: -12.9714, lon: -38.5014 },
|
||||
City { name: "Fortaleza", lat: -3.7172, lon: -38.5433 },
|
||||
City { name: "Belo Horizonte", lat: -19.9167, lon: -43.9345 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "Mexico",
|
||||
cities: &[
|
||||
City { name: "Mexico City", lat: 19.4326, lon: -99.1332 },
|
||||
City { name: "Guadalajara", lat: 20.6597, lon: -103.3496 },
|
||||
City { name: "Monterrey", lat: 25.6866, lon: -100.3161 },
|
||||
City { name: "Puebla", lat: 19.0414, lon: -98.2063 },
|
||||
City { name: "Tijuana", lat: 32.5149, lon: -117.0382 },
|
||||
City { name: "Cancún", lat: 21.1619, lon: -86.8515 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "Russia",
|
||||
cities: &[
|
||||
City { name: "Moscow", lat: 55.7558, lon: 37.6173 },
|
||||
City { name: "Saint Petersburg", lat: 59.9343, lon: 30.3351 },
|
||||
City { name: "Novosibirsk", lat: 55.0084, lon: 82.9357 },
|
||||
City { name: "Yekaterinburg", lat: 56.8389, lon: 60.6057 },
|
||||
City { name: "Kazan", lat: 55.8304, lon: 49.0661 },
|
||||
City { name: "Vladivostok", lat: 43.1332, lon: 131.9113 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "South Africa",
|
||||
cities: &[
|
||||
City { name: "Johannesburg", lat: -26.2041, lon: 28.0473 },
|
||||
City { name: "Cape Town", lat: -33.9249, lon: 18.4241 },
|
||||
City { name: "Durban", lat: -29.8587, lon: 31.0218 },
|
||||
City { name: "Pretoria", lat: -25.7479, lon: 28.2293 },
|
||||
],
|
||||
},
|
||||
Country {
|
||||
name: "Argentina",
|
||||
cities: &[
|
||||
City { name: "Buenos Aires", lat: -34.6037, lon: -58.3816 },
|
||||
City { name: "Córdoba", lat: -31.4201, lon: -64.1888 },
|
||||
City { name: "Rosario", lat: -32.9442, lon: -60.6505 },
|
||||
City { name: "Mendoza", lat: -32.8895, lon: -68.8458 },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
impl City {
|
||||
pub fn to_location(&self) -> Location {
|
||||
Location {
|
||||
lat: self.lat,
|
||||
lon: self.lon,
|
||||
/// Get display name for the city based on UTF-8 locale support
|
||||
pub fn display_name(&self) -> String {
|
||||
if is_utf8_locale() && self.city != self.city_ascii {
|
||||
format!("{} ({})", self.city, self.city_ascii)
|
||||
} else {
|
||||
self.city_ascii.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse latitude as f64
|
||||
pub fn latitude(&self) -> Result<f64, std::num::ParseFloatError> {
|
||||
self.lat.parse()
|
||||
}
|
||||
|
||||
/// Parse longitude as f64
|
||||
pub fn longitude(&self) -> Result<f64, std::num::ParseFloatError> {
|
||||
self.lng.parse()
|
||||
}
|
||||
}
|
||||
|
||||
/// Search for a city by name (case-insensitive)
|
||||
pub fn find_city(name: &str) -> Option<(usize, usize)> {
|
||||
let name_lower = name.to_lowercase();
|
||||
for (country_idx, country) in COUNTRIES.iter().enumerate() {
|
||||
for (city_idx, city) in country.cities.iter().enumerate() {
|
||||
if city.name.to_lowercase().contains(&name_lower) {
|
||||
return Some((country_idx, city_idx));
|
||||
}
|
||||
lazy_static! {
|
||||
/// Global hash map of countries to cities, loaded at first access
|
||||
pub static ref CITIES_BY_COUNTRY: HashMap<String, Vec<City>> = {
|
||||
let json_data = include_str!("../data/filtered_world_cities.json");
|
||||
let cities: Vec<City> = serde_json::from_str(json_data)
|
||||
.expect("Failed to parse filtered_world_cities.json");
|
||||
|
||||
let mut map: HashMap<String, Vec<City>> = HashMap::new();
|
||||
for city in cities {
|
||||
map.entry(city.country.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(city);
|
||||
}
|
||||
}
|
||||
None
|
||||
map
|
||||
};
|
||||
}
|
||||
|
||||
/// Get total number of cities across all countries
|
||||
pub fn total_cities() -> usize {
|
||||
COUNTRIES.iter().map(|c| c.cities.len()).sum()
|
||||
/// Check if the current locale supports UTF-8
|
||||
fn is_utf8_locale() -> bool {
|
||||
env::var("LANG")
|
||||
.ok()
|
||||
.map(|lang| lang.to_uppercase().contains("UTF-8") || lang.to_uppercase().contains("UTF8"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Get a sorted list of all country names
|
||||
pub fn get_countries() -> Vec<String> {
|
||||
let mut countries: Vec<String> = CITIES_BY_COUNTRY.keys().cloned().collect();
|
||||
countries.sort();
|
||||
countries
|
||||
}
|
||||
|
||||
/// Get cities for a specific country
|
||||
pub fn get_cities_for_country(country: &str) -> Option<&Vec<City>> {
|
||||
CITIES_BY_COUNTRY.get(country)
|
||||
}
|
||||
|
||||
/// Interactive country selection
|
||||
pub fn select_country() -> Result<String, dialoguer::Error> {
|
||||
use dialoguer::Select;
|
||||
|
||||
let countries = get_countries();
|
||||
let selection = Select::new()
|
||||
.with_prompt("Select your country")
|
||||
.items(&countries)
|
||||
.interact()?;
|
||||
|
||||
Ok(countries[selection].clone())
|
||||
}
|
||||
|
||||
/// Interactive city selection for a given country
|
||||
pub fn select_city(country: &str) -> Result<City, String> {
|
||||
use dialoguer::Select;
|
||||
|
||||
let cities = get_cities_for_country(country)
|
||||
.ok_or_else(|| format!("No cities found for country: {}", country))?;
|
||||
|
||||
// If it's a city-state, auto-select the single city
|
||||
if cities.len() == 1 && cities[0].is_city_country {
|
||||
return Ok(cities[0].clone());
|
||||
}
|
||||
|
||||
// Prepare display names
|
||||
let display_names: Vec<String> = cities.iter()
|
||||
.map(|c| c.display_name())
|
||||
.collect();
|
||||
|
||||
let selection = Select::new()
|
||||
.with_prompt("Select your city")
|
||||
.items(&display_names)
|
||||
.interact()
|
||||
.map_err(|e| format!("Selection failed: {}", e))?;
|
||||
|
||||
Ok(cities[selection].clone())
|
||||
}
|
||||
|
||||
/// Interactive location selection - select country then city
|
||||
pub fn select_location() -> Result<(f64, f64), Box<dyn std::error::Error>> {
|
||||
let country = select_country()?;
|
||||
let city = select_city(&country)?;
|
||||
|
||||
let lat = city.latitude()?;
|
||||
let lon = city.longitude()?;
|
||||
|
||||
println!("Selected: {} - {}", country, city.display_name());
|
||||
println!("Coordinates: {:.4}, {:.4}", lat, lon);
|
||||
|
||||
Ok((lat, lon))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -258,40 +136,53 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_countries_not_empty() {
|
||||
assert!(!COUNTRIES.is_empty());
|
||||
fn test_cities_loaded() {
|
||||
assert!(!CITIES_BY_COUNTRY.is_empty(), "Cities should be loaded");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_countries_have_cities() {
|
||||
for country in COUNTRIES {
|
||||
assert!(!country.cities.is_empty(), "{} has no cities", country.name);
|
||||
}
|
||||
fn test_get_countries() {
|
||||
let countries = get_countries();
|
||||
assert!(!countries.is_empty(), "Should have countries");
|
||||
// Verify sorting
|
||||
let mut sorted = countries.clone();
|
||||
sorted.sort();
|
||||
assert_eq!(countries, sorted, "Countries should be sorted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_city() {
|
||||
assert!(find_city("London").is_some());
|
||||
assert!(find_city("Tokyo").is_some());
|
||||
assert!(find_city("New York").is_some());
|
||||
assert!(find_city("NonexistentCity").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_city_to_location() {
|
||||
fn test_city_display_name() {
|
||||
let city = City {
|
||||
name: "Test City",
|
||||
lat: 40.0,
|
||||
lon: -74.0,
|
||||
city: "Āqchah".to_string(),
|
||||
city_ascii: "Aqchah".to_string(),
|
||||
lat: "36.9114".to_string(),
|
||||
lng: "66.1858".to_string(),
|
||||
country: "Afghanistan".to_string(),
|
||||
admin_name: "Jowzjān".to_string(),
|
||||
population: "1012000".to_string(),
|
||||
id: "1004364776".to_string(),
|
||||
is_city_country: false,
|
||||
};
|
||||
let location = city.to_location();
|
||||
assert_eq!(location.lat, 40.0);
|
||||
assert_eq!(location.lon, -74.0);
|
||||
|
||||
// Display name format depends on UTF-8 locale, so just ensure it doesn't panic
|
||||
let _ = city.display_name();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_cities() {
|
||||
let count = total_cities();
|
||||
assert!(count > 100, "Should have over 100 cities");
|
||||
fn test_city_coordinates() {
|
||||
let city = City {
|
||||
city: "Kabul".to_string(),
|
||||
city_ascii: "Kabul".to_string(),
|
||||
lat: "34.5253".to_string(),
|
||||
lng: "69.1783".to_string(),
|
||||
country: "Afghanistan".to_string(),
|
||||
admin_name: "Kābul".to_string(),
|
||||
population: "4273156".to_string(),
|
||||
id: "1004993580".to_string(),
|
||||
is_city_country: false,
|
||||
};
|
||||
|
||||
assert_eq!(city.latitude().unwrap(), 34.5253);
|
||||
assert_eq!(city.longitude().unwrap(), 69.1783);
|
||||
}
|
||||
}
|
||||
|
||||
+23
-142
@@ -1,50 +1,8 @@
|
||||
/// Interactive location selection UI
|
||||
/// Allows users to select their location from a list of countries and cities
|
||||
|
||||
use crate::cities::{COUNTRIES, City, Country};
|
||||
use crate::cities;
|
||||
use crate::types::Location;
|
||||
use std::io::{self, Write};
|
||||
|
||||
/// Display a numbered list and get user selection
|
||||
fn get_selection(prompt: &str, items: &[impl std::fmt::Display], max: usize) -> Result<usize, String> {
|
||||
println!("\n{}", prompt);
|
||||
println!("{}", "=".repeat(prompt.len()));
|
||||
|
||||
for (i, item) in items.iter().take(max).enumerate() {
|
||||
println!("{:3}. {}", i + 1, item);
|
||||
}
|
||||
|
||||
print!("\nEnter number (1-{}): ", max);
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.map_err(|e| format!("Failed to read input: {}", e))?;
|
||||
|
||||
let choice: usize = input
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| "Invalid number entered".to_string())?;
|
||||
|
||||
if choice < 1 || choice > max {
|
||||
return Err(format!("Number must be between 1 and {}", max));
|
||||
}
|
||||
|
||||
Ok(choice - 1)
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Country {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for City {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Interactively select location from country/city lists
|
||||
pub fn select_location_interactive() -> Result<Location, String> {
|
||||
@@ -55,102 +13,24 @@ pub fn select_location_interactive() -> Result<Location, String> {
|
||||
println!("Please select your country and nearest major city.\n");
|
||||
|
||||
// Select country
|
||||
let country_idx = get_selection(
|
||||
"Select your country:",
|
||||
COUNTRIES,
|
||||
COUNTRIES.len(),
|
||||
)?;
|
||||
let country = cities::select_country()
|
||||
.map_err(|e| format!("Country selection failed: {}", e))?;
|
||||
|
||||
let country = &COUNTRIES[country_idx];
|
||||
println!("\nSelected: {}", country.name);
|
||||
println!("\nSelected: {}", country);
|
||||
|
||||
// Select city
|
||||
let city_idx = get_selection(
|
||||
&format!("Select your nearest city in {}:", country.name),
|
||||
country.cities,
|
||||
country.cities.len(),
|
||||
)?;
|
||||
let city = cities::select_city(&country)?;
|
||||
|
||||
let city = &country.cities[city_idx];
|
||||
println!("\nSelected: {}", city.name);
|
||||
println!("Location: {:.4}°, {:.4}°", city.lat, city.lon);
|
||||
println!("\nSelected: {}", city.display_name());
|
||||
println!("Location: {:.4}°, {:.4}°",
|
||||
city.latitude().map_err(|e| format!("Invalid latitude: {}", e))?,
|
||||
city.longitude().map_err(|e| format!("Invalid longitude: {}", e))?
|
||||
);
|
||||
|
||||
Ok(city.to_location())
|
||||
}
|
||||
|
||||
/// Search for a city by name
|
||||
pub fn search_city_interactive() -> Result<Location, String> {
|
||||
println!("\n╔═══════════════════════════════════════════════════════════╗");
|
||||
println!("║ Redshift - City Search ║");
|
||||
println!("╚═══════════════════════════════════════════════════════════╝");
|
||||
|
||||
print!("\nEnter city name to search: ");
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.map_err(|e| format!("Failed to read input: {}", e))?;
|
||||
|
||||
let search_term = input.trim();
|
||||
|
||||
// Find matching cities
|
||||
let mut matches: Vec<(usize, usize)> = Vec::new();
|
||||
let search_lower = search_term.to_lowercase();
|
||||
|
||||
for (country_idx, country) in COUNTRIES.iter().enumerate() {
|
||||
for (city_idx, city) in country.cities.iter().enumerate() {
|
||||
if city.name.to_lowercase().contains(&search_lower) {
|
||||
matches.push((country_idx, city_idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if matches.is_empty() {
|
||||
return Err(format!("No cities found matching '{}'", search_term));
|
||||
}
|
||||
|
||||
if matches.len() == 1 {
|
||||
let (country_idx, city_idx) = matches[0];
|
||||
let city = &COUNTRIES[country_idx].cities[city_idx];
|
||||
println!("\nFound: {} ({})", city.name, COUNTRIES[country_idx].name);
|
||||
println!("Location: {:.4}°, {:.4}°", city.lat, city.lon);
|
||||
return Ok(city.to_location());
|
||||
}
|
||||
|
||||
// Multiple matches - let user choose
|
||||
println!("\nFound {} matching cities:", matches.len());
|
||||
println!("{}", "=".repeat(40));
|
||||
|
||||
for (i, &(country_idx, city_idx)) in matches.iter().enumerate() {
|
||||
let city = &COUNTRIES[country_idx].cities[city_idx];
|
||||
let country = &COUNTRIES[country_idx];
|
||||
println!("{:3}. {} ({})", i + 1, city.name, country.name);
|
||||
}
|
||||
|
||||
print!("\nEnter number (1-{}): ", matches.len());
|
||||
io::stdout().flush().unwrap();
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.map_err(|e| format!("Failed to read input: {}", e))?;
|
||||
|
||||
let choice: usize = input
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|_| "Invalid number entered".to_string())?;
|
||||
|
||||
if choice < 1 || choice > matches.len() {
|
||||
return Err(format!("Number must be between 1 and {}", matches.len()));
|
||||
}
|
||||
|
||||
let (country_idx, city_idx) = matches[choice - 1];
|
||||
let city = &COUNTRIES[country_idx].cities[city_idx];
|
||||
println!("\nSelected: {}", city.name);
|
||||
println!("Location: {:.4}°, {:.4}°", city.lat, city.lon);
|
||||
|
||||
Ok(city.to_location())
|
||||
Ok(Location {
|
||||
lat: city.latitude().map_err(|e| format!("Invalid latitude: {}", e))? as f32,
|
||||
lon: city.longitude().map_err(|e| format!("Invalid longitude: {}", e))? as f32,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -158,16 +38,17 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_country_display() {
|
||||
let country = &COUNTRIES[0];
|
||||
let display = format!("{}", country);
|
||||
assert!(!display.is_empty());
|
||||
fn test_cities_available() {
|
||||
let countries = cities::get_countries();
|
||||
assert!(!countries.is_empty(), "Should have countries available");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_city_display() {
|
||||
let city = &COUNTRIES[0].cities[0];
|
||||
let display = format!("{}", city);
|
||||
assert!(!display.is_empty());
|
||||
fn test_cities_for_country() {
|
||||
let countries = cities::get_countries();
|
||||
if let Some(country) = countries.first() {
|
||||
let cities = cities::get_cities_for_country(country);
|
||||
assert!(cities.is_some(), "Should have cities for first country");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user