use std::time::Instant;
use crate::core::optimize::TurnPenalties;
#[derive(Debug, Clone, PartialEq)]
pub enum View {
Home,
Extract,
Compile,
Optimize,
BrowseMaps,
BrowseRoutes,
Help,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DataSource {
Osm,
Overture,
}
impl std::fmt::Display for DataSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DataSource::Osm => write!(f, "OpenStreetMap (OSM)"),
DataSource::Overture => write!(f, "Overture Maps"),
}
}
}
#[derive(Debug, Clone)]
pub struct BoundingBox {
pub min_lon: f64,
pub min_lat: f64,
pub max_lon: f64,
pub max_lat: f64,
}
impl std::fmt::Display for BoundingBox {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{:.2},{:.2},{:.2},{:.2}",
self.min_lat, self.min_lon, self.max_lat, self.max_lon
)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Status {
Ready,
Running { progress: u8, message: String },
Done(String),
Error(String),
}
impl std::fmt::Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Status::Ready => write!(f, "Ready"),
Status::Running { progress, message } => {
write!(f, "{}% - {}", progress, message)
}
Status::Done(msg) => write!(f, "{}", msg),
Status::Error(msg) => write!(f, "Error: {}", msg),
}
}
}
#[derive(Debug, Clone)]
pub struct LogEntry {
pub timestamp: String,
pub level: LogLevel,
pub message: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LogLevel {
Info,
Success,
Warn,
Error,
}
impl std::fmt::Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogLevel::Info => write!(f, "INFO"),
LogLevel::Success => write!(f, "SUCCESS"),
LogLevel::Warn => write!(f, "WARN"),
LogLevel::Error => write!(f, "ERROR"),
}
}
}
#[allow(dead_code)]
pub struct InputMode {
pub active: bool,
pub field: InputField,
pub buffer: String,
}
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub enum InputField {
BoundingBox,
InputFile,
OutputFile,
CacheFile,
RouteFile,
LeftTurnPenalty,
RightTurnPenalty,
UTurnPenalty,
DepotCoordinates,
}
pub struct App {
pub running: bool,
pub current_view: View,
pub workflow_selection: usize,
pub log_entries: Vec<LogEntry>,
pub log_scroll: usize,
pub data_source: DataSource,
pub bounding_box: Option<BoundingBox>,
pub extract_status: Status,
pub input_file: Option<String>,
pub output_file: Option<String>,
pub compile_status: Status,
pub cache_file: Option<String>,
pub route_file: Option<String>,
pub turn_penalties: TurnPenalties,
pub depot_coords: Option<(f64, f64)>,
pub optimize_status: Status,
pub cached_maps: Vec<String>,
pub saved_routes: Vec<String>,
pub browse_selection: usize,
pub input_mode: InputMode,
pub start_time: Instant,
}
impl App {
impl App {
fn scan_cached_maps() -> Vec<String> {
use std::fs;
let mut maps = Vec::new();
if let Ok(entries) = fs::read_dir(".") {
for entry in entries.flatten() {
if let Ok(file_name) = entry.file_name().into_string() {
if file_name.ends_with(".rmp") {
maps.push(file_name);
}
}
}
}
maps.sort();
maps
}
pub fn new() -> Self {
Self {
running: true,
current_view: View::Home,
workflow_selection: 0,
log_entries: Vec::new(),
log_scroll: 0,
data_source: DataSource::Osm,
bounding_box: None,
extract_status: Status::Ready,
input_file: None,
output_file: None,
compile_status: Status::Ready,
cache_file: None,
route_file: None,
turn_penalties: TurnPenalties::default(),
depot_coords: None,
optimize_status: Status::Ready,
cached_maps: Self::scan_cached_maps(),
saved_routes: Vec::new(),
browse_selection: 0,
input_mode: InputMode {
active: false,
field: InputField::BoundingBox,
buffer: String::new(),
},
start_time: Instant::now(),
}
}
pub fn log(&mut self, level: LogLevel, message: impl Into<String>) {
let timestamp = chrono::Local::now().format("%H:%M:%S").to_string();
self.log_entries.push(LogEntry {
timestamp,
level,
message: message.into(),
});
if self.log_entries.len() > 500 {
self.log_entries.remove(0);
}
self.log_scroll = self.log_entries.len().saturating_sub(1);
}
pub fn start_input(&mut self, field: InputField) {
self.input_mode.active = true;
self.input_mode.field = field;
self.input_mode.buffer.clear();
}
pub fn confirm_input(&mut self) {
let value = self.input_mode.buffer.trim().to_string();
if value.is_empty() {
self.input_mode.active = false;
return;
}
match self.input_mode.field.clone() {
InputField::BoundingBox => {
let parts: Vec<&str> = value.split(',').collect();
if parts.len() == 4 {
if let (Ok(min_lon), Ok(min_lat), Ok(max_lon), Ok(max_lat)) = (
parts[0].parse::<f64>(),
parts[1].parse::<f64>(),
parts[2].parse::<f64>(),
parts[3].parse::<f64>(),
) {
self.bounding_box = Some(BoundingBox {
min_lon,
min_lat,
max_lon,
max_lat,
});
self.log(
LogLevel::Success,
format!("Bounding box set: {}", self.bounding_box.as_ref().unwrap()),
);
} else {
self.log(LogLevel::Error, "Invalid coordinates".to_string());
}
} else {
self.log(
LogLevel::Error,
"Expected format: min_lon,min_lat,max_lon,max_lat",
);
}
}
InputField::InputFile => {
self.input_file = Some(value.clone());
if self.output_file.is_none() {
let out = value.replace(".geojson", ".rmp").replace(".json", ".rmp");
self.output_file = Some(out);
}
self.log(LogLevel::Success, format!("Input set: {}", value));
}
InputField::OutputFile => {
self.output_file = Some(value.clone());
self.log(LogLevel::Success, format!("Output set: {}", value));
}
InputField::CacheFile => {
self.cache_file = Some(value.clone());
self.log(LogLevel::Success, format!("Cache file set: {}", value));
}
InputField::RouteFile => {
self.route_file = Some(value.clone());
self.log(LogLevel::Success, format!("Route file set: {}", value));
}
InputField::LeftTurnPenalty => {
if let Ok(v) = value.parse::<f64>() {
self.turn_penalties.left = v;
self.log(LogLevel::Success, format!("Left turn penalty set: {}", v));
}
}
InputField::RightTurnPenalty => {
if let Ok(v) = value.parse::<f64>() {
self.turn_penalties.right = v;
self.log(LogLevel::Success, format!("Right turn penalty set: {}", v));
}
}
InputField::UTurnPenalty => {
if let Ok(v) = value.parse::<f64>() {
self.turn_penalties.u_turn = v;
self.log(LogLevel::Success, format!("U-turn penalty set: {}", v));
}
}
InputField::DepotCoordinates => {
let parts: Vec<&str> = value.split(',').collect();
if parts.len() == 2 {
if let (Ok(lat), Ok(lon)) =
(parts[0].parse::<f64>(), parts[1].parse::<f64>())
{
self.depot_coords = Some((lat, lon));
self.log(
LogLevel::Success,
format!("Depot set: {:.4},{:.4}", lat, lon),
);
}
}
}
}
self.input_mode.active = false;
}
pub fn cancel_input(&mut self) {
self.input_mode.active = false;
self.input_mode.buffer.clear();
self.log(LogLevel::Info, "Input cancelled");
}
pub fn navigate_up(&mut self) {
match self.current_view {
View::Home => {
self.workflow_selection = (self.workflow_selection + 4) % 5;
}
View::BrowseMaps => {
let max = self.cached_maps.len().max(1);
if max > 0 {
self.browse_selection = (self.browse_selection + max - 1) % max;
}
}
View::BrowseRoutes => {
let max = self.saved_routes.len().max(1);
if max > 0 {
self.browse_selection = (self.browse_selection + max - 1) % max;
}
}
_ => {}
}
}
pub fn navigate_down(&mut self) {
match self.current_view {
View::Home => {
self.workflow_selection = (self.workflow_selection + 1) % 5;
}
View::BrowseMaps => {
let max = self.cached_maps.len().max(1);
if max > 0 {
self.browse_selection = (self.browse_selection + 1) % max;
}
}
View::BrowseRoutes => {
let max = self.saved_routes.len().max(1);
if max > 0 {
self.browse_selection = (self.browse_selection + 1) % max;
}
}
_ => {}
}
}
}