From e6737bc63300fec8ad728384c158e9bac5007397 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Tue, 22 Jul 2025 15:49:31 +0100 Subject: [PATCH 01/12] Start adding llm integration (not working at all) --- src/llm/client.rs | 50 ++++++++++++++++++++++++++++++++ src/llm/mod.rs | 12 ++++++++ src/llm/openrouter_types.rs | 1 + src/main.rs | 1 + src/workspace/mod.rs | 2 ++ src/workspace/worker.rs | 5 ++-- src/workspace/workspace_types.rs | 3 ++ 7 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/llm/client.rs create mode 100644 src/llm/mod.rs create mode 100644 src/llm/openrouter_types.rs diff --git a/src/llm/client.rs b/src/llm/client.rs new file mode 100644 index 0000000..376a690 --- /dev/null +++ b/src/llm/client.rs @@ -0,0 +1,50 @@ +// https://openrouter.ai/docs/api-reference/overview +use super::OpenrouterClient; + +impl OpenrouterClient { + // We want to also parse in the get_data_from_string_osm but maybe we want to format it differently (we should try that first) + // We also need to parse through the selection data like size and location! + pub fn send_openrouter_chat_string(&self, query: String) -> Result { + let mut status = 429; + while status == 429 { + if let Ok(mut response) = self.agent.post(&self.url).send(&query) { + if response.status() == 200 { + return response.body_mut().read_to_string(); + } else if response.status() == 429 { + std::thread::sleep(std::time::Duration::from_secs(5)); + } else { + status = 0; + } + } + } + Err(ureq::Error::BadUri( + "Error sending/making request!".to_string(), + )) + } + + pub fn send_overpass_query(&self) -> Result { + if let Ok(query) = self.build_overpass_query_string() { + if query.is_empty() { + return Err(ureq::Error::BadUri("Empty query".into())); + } + let mut status = 429; + while status == 429 { + if let Ok(mut response) = self.agent.post(&self.url).send(&query) { + if response.status() == 200 { + return response.body_mut().read_to_string(); + } else if response.status() == 429 { + std::thread::sleep(std::time::Duration::from_secs(5)); + } else { + status = 0; + } + } + } + return Err(ureq::Error::BadUri( + "Error sending/making request!".to_string(), + )); + } + Err(ureq::Error::BadUri( + "Error sending/making request!".to_string(), + )) + } +} diff --git a/src/llm/mod.rs b/src/llm/mod.rs new file mode 100644 index 0000000..e9f7a75 --- /dev/null +++ b/src/llm/mod.rs @@ -0,0 +1,12 @@ +mod client; +mod openrouter_types; + +pub use client::*; +pub use openrouter_types::*; +use ureq::Agent; + +#[derive(Clone)] +pub struct OpenrouterClient { + pub token: Option, + pub agent: Agent, +} diff --git a/src/llm/openrouter_types.rs b/src/llm/openrouter_types.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/llm/openrouter_types.rs @@ -0,0 +1 @@ + diff --git a/src/main.rs b/src/main.rs index 519d0f4..81efe83 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ pub mod camera; pub mod debug; pub mod geojson; pub mod interaction; +pub mod llm; pub mod overpass; pub mod settings; pub mod tools; diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index 2fd6ce3..42cb0a0 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -28,6 +28,7 @@ pub struct Workspace { // Request Clients: pub overpass_agent: OverpassClient, + pub overpass_agent: OpenrouterClient, } impl Default for Workspace { @@ -37,6 +38,7 @@ impl Default for Workspace { loaded_requests: Arc::new(Mutex::new(HashMap::new())), worker: WorkspaceWorker::new(4), overpass_agent: OverpassClient::new("https://overpass-api.de/api/interpreter"), + llm_agent: OpenrouterClient::new(), } } } diff --git a/src/workspace/worker.rs b/src/workspace/worker.rs index 9bb4177..83d7acc 100644 --- a/src/workspace/worker.rs +++ b/src/workspace/worker.rs @@ -66,19 +66,20 @@ pub fn process_requests(mut commands: Commands, mut workspace: ResMut info!("No workspace found"); return; } - let client = workspace.overpass_agent.clone(); + let overpass_client = workspace.overpass_agent.clone(); // Clone the loaded_requests Arc instead of holding the MutexGuard let loaded_requests = workspace.loaded_requests.clone(); let task = task_pool.spawn(async move { let mut result = Vec::new(); match request.get_request() { RequestType::OverpassTurboRequest(ref query) => { - if let Ok(q) = client.send_overpass_query_string(query.clone()) { + if let Ok(q) = overpass_client.send_overpass_query_string(query.clone()) { if !q.is_empty() { result = q.as_bytes().to_vec(); } } } + RequestType::OpenRouterRequest(_) => todo!(), RequestType::OpenMeteoRequest(_open_meteo_request) => {} } diff --git a/src/workspace/workspace_types.rs b/src/workspace/workspace_types.rs index abe9cb4..1e4ec2d 100644 --- a/src/workspace/workspace_types.rs +++ b/src/workspace/workspace_types.rs @@ -191,6 +191,7 @@ impl WorkspaceRequest { } } crate::workspace::RequestType::OpenMeteoRequest(_) => {} + crate::workspace::RequestType::OpenRouterRequest(_) => {} } } @@ -223,6 +224,7 @@ pub enum RequestType { // If we want to add more requests we can just add them here. OpenMeteoRequest(OpenMeteoRequest), OverpassTurboRequest(String), + OpenRouterRequest(String), } impl std::fmt::Debug for RequestType { @@ -230,6 +232,7 @@ impl std::fmt::Debug for RequestType { match self { RequestType::OpenMeteoRequest(_) => write!(f, "OpenMeteoRequest"), RequestType::OverpassTurboRequest(_) => write!(f, "OverpassTurboRequest"), + RequestType::OpenRouterRequest(_) => write!(f, "OpenRouterRequest"), } } } From bfaf04a39b4648b001c0716222d8821c15db8d81 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Tue, 22 Jul 2025 16:04:58 +0100 Subject: [PATCH 02/12] Add more detail notes and add request struct --- src/llm/client.rs | 34 ++------- src/llm/mod.rs | 1 + src/llm/openrouter_types.rs | 145 ++++++++++++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 27 deletions(-) diff --git a/src/llm/client.rs b/src/llm/client.rs index 376a690..b380f70 100644 --- a/src/llm/client.rs +++ b/src/llm/client.rs @@ -1,10 +1,16 @@ +use crate::llm::Request; + // https://openrouter.ai/docs/api-reference/overview use super::OpenrouterClient; impl OpenrouterClient { // We want to also parse in the get_data_from_string_osm but maybe we want to format it differently (we should try that first) // We also need to parse through the selection data like size and location! - pub fn send_openrouter_chat_string(&self, query: String) -> Result { + pub fn send_openrouter_chat_string(&self, request: Request) -> Result { + // This will be a put request + // want to parse throguh the request in the body as a json + // Remember hearder needs bearer token + // Would be good to find out how to do the streaing but first just await response. let mut status = 429; while status == 429 { if let Ok(mut response) = self.agent.post(&self.url).send(&query) { @@ -21,30 +27,4 @@ impl OpenrouterClient { "Error sending/making request!".to_string(), )) } - - pub fn send_overpass_query(&self) -> Result { - if let Ok(query) = self.build_overpass_query_string() { - if query.is_empty() { - return Err(ureq::Error::BadUri("Empty query".into())); - } - let mut status = 429; - while status == 429 { - if let Ok(mut response) = self.agent.post(&self.url).send(&query) { - if response.status() == 200 { - return response.body_mut().read_to_string(); - } else if response.status() == 429 { - std::thread::sleep(std::time::Duration::from_secs(5)); - } else { - status = 0; - } - } - } - return Err(ureq::Error::BadUri( - "Error sending/making request!".to_string(), - )); - } - Err(ureq::Error::BadUri( - "Error sending/making request!".to_string(), - )) - } } diff --git a/src/llm/mod.rs b/src/llm/mod.rs index e9f7a75..34d94c5 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -9,4 +9,5 @@ use ureq::Agent; pub struct OpenrouterClient { pub token: Option, pub agent: Agent, + pub url: String, } diff --git a/src/llm/openrouter_types.rs b/src/llm/openrouter_types.rs index 8b13789..1e05d5d 100644 --- a/src/llm/openrouter_types.rs +++ b/src/llm/openrouter_types.rs @@ -1 +1,146 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct Request { + pub messages: Option>, + pub prompt: Option, + pub model: Option, + pub response_format: Option, + pub stop: Option, + pub stream: Option, + pub max_tokens: Option, + pub temperature: Option, + pub tools: Option>, + pub tool_choice: Option, + pub seed: Option, + pub top_p: Option, + pub top_k: Option, + pub frequency_penalty: Option, + pub presence_penalty: Option, + pub repetition_penalty: Option, + pub logit_bias: Option>, + pub top_logprobs: Option, + pub min_p: Option, + pub top_a: Option, + pub prediction: Option, + pub transforms: Option>, + pub models: Option>, + pub route: Option, + pub user: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub struct ResponseFormat { + pub r#type: String, // "json_object" +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Stop { + Single(String), + Multiple(Vec), +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Role { + User, + Assistant, + System, + Tool, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ContentPart { + Text(TextContent), + Image(ImageContentPart), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TextContent { + pub r#type: String, // "text" + pub text: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ImageContentPart { + pub r#type: String, // "image_url" + pub image_url: ImageUrl, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ImageUrl { + pub url: String, + pub detail: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Message { + UserAssistantSystem { + role: Role, + content: MessageContent, + name: Option, + }, + Tool { + role: Role, // must be Tool + content: String, + tool_call_id: String, + name: Option, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum MessageContent { + Text(String), + Parts(Vec), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Tool { + pub r#type: String, // "function" + pub function: FunctionDescription, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FunctionDescription { + pub name: String, + pub description: Option, + pub parameters: serde_json::Value, // JSON Schema +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ToolChoice { + None, + Auto, + Function { + r#type: String, // "function" + function: ToolFunctionChoice, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ToolFunctionChoice { + pub name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Prediction { + pub r#type: String, // "content" + pub content: String, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Route { + Fallback, +} + +// pub fn Addget_data_from_string_osm +// add workspace info! From 7c74d64dc2ab4189bf3ac0cfa8d1a570b8859961 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Tue, 22 Jul 2025 16:20:38 +0100 Subject: [PATCH 03/12] Make somewhat compile. Todo: Think of ui redesign, right panel chant with send and it takes the left selection to get sent to the llm! --- src/llm/client.rs | 4 +++- src/llm/mod.rs | 27 +++++++++++++++++++++++++-- src/workspace/mod.rs | 8 +++++--- src/workspace/ui.rs | 2 ++ src/workspace/worker.rs | 1 + src/workspace/workspace_types.rs | 1 + 6 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/llm/client.rs b/src/llm/client.rs index b380f70..9aa7a46 100644 --- a/src/llm/client.rs +++ b/src/llm/client.rs @@ -6,6 +6,7 @@ use super::OpenrouterClient; impl OpenrouterClient { // We want to also parse in the get_data_from_string_osm but maybe we want to format it differently (we should try that first) // We also need to parse through the selection data like size and location! + /* pub fn send_openrouter_chat_string(&self, request: Request) -> Result { // This will be a put request // want to parse throguh the request in the body as a json @@ -13,7 +14,7 @@ impl OpenrouterClient { // Would be good to find out how to do the streaing but first just await response. let mut status = 429; while status == 429 { - if let Ok(mut response) = self.agent.post(&self.url).send(&query) { + if let Ok(mut response) = self.agent.post(&self.url).send(&request) { if response.status() == 200 { return response.body_mut().read_to_string(); } else if response.status() == 429 { @@ -27,4 +28,5 @@ impl OpenrouterClient { "Error sending/making request!".to_string(), )) } + */ } diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 34d94c5..73ffb2e 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -1,9 +1,10 @@ mod client; mod openrouter_types; -pub use client::*; +use std::fmt::Display; + pub use openrouter_types::*; -use ureq::Agent; +use ureq::{Agent, unversioned::transport::time::Duration}; #[derive(Clone)] pub struct OpenrouterClient { @@ -11,3 +12,25 @@ pub struct OpenrouterClient { pub agent: Agent, pub url: String, } + +impl OpenrouterClient { + pub fn new(url: impl Display, token: Option) -> Self { + let config = Agent::config_builder() + .timeout_global(Some(*Duration::from_secs(5))) + .build(); + let agent: Agent = config.into(); + OpenrouterClient { + agent, + url: url.to_string(), + token, + } + } + + pub fn set_url(&mut self, url: impl Display) { + self.url = url.to_string(); + } + + pub fn set_token(&mut self, token: impl Display) { + self.token = Some(token.to_string()); + } +} diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index 42cb0a0..b98a090 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -10,7 +10,7 @@ use serde_json::Value; use worker::WorkspaceWorker; pub use workspace_types::*; -use crate::{geojson::MapFeature, overpass::OverpassClient}; +use crate::{geojson::MapFeature, llm::OpenrouterClient, overpass::OverpassClient}; mod renderer; mod ui; @@ -28,7 +28,7 @@ pub struct Workspace { // Request Clients: pub overpass_agent: OverpassClient, - pub overpass_agent: OpenrouterClient, + pub llm_agent: OpenrouterClient, } impl Default for Workspace { @@ -38,7 +38,7 @@ impl Default for Workspace { loaded_requests: Arc::new(Mutex::new(HashMap::new())), worker: WorkspaceWorker::new(4), overpass_agent: OverpassClient::new("https://overpass-api.de/api/interpreter"), - llm_agent: OpenrouterClient::new(), + llm_agent: OpenrouterClient::new("https://openrouter.ai/api/v1/chat/completions", None), } } } @@ -68,6 +68,8 @@ pub struct WorkspaceRequest { raw_data: Vec, // Raw data from the request maybe have this as a id list aswell... #[serde(skip)] processed_data: RTree, + #[serde(skip)] + llm_analysis: Vec, last_query_date: i64, // When the OSM data was fetched } diff --git a/src/workspace/ui.rs b/src/workspace/ui.rs index 5dee3d6..e89b075 100644 --- a/src/workspace/ui.rs +++ b/src/workspace/ui.rs @@ -139,10 +139,12 @@ pub fn workspace_actions_ui( } }); // https://blog.afi.io/blog/how-to-draw-and-view-boundary-data-with-openstreetmap-osm/ + /* ui.horizontal(|ui| { ui.label("Workspaces"); if ui.button("Add").clicked() {} }); + */ }); }); }); diff --git a/src/workspace/worker.rs b/src/workspace/worker.rs index 83d7acc..2be32a3 100644 --- a/src/workspace/worker.rs +++ b/src/workspace/worker.rs @@ -79,6 +79,7 @@ pub fn process_requests(mut commands: Commands, mut workspace: ResMut } } } + here needs to be done and this is purposfully broken so that it gets flagged! RequestType::OpenRouterRequest(_) => todo!(), RequestType::OpenMeteoRequest(_open_meteo_request) => {} } diff --git a/src/workspace/workspace_types.rs b/src/workspace/workspace_types.rs index 1e4ec2d..d0ce7c0 100644 --- a/src/workspace/workspace_types.rs +++ b/src/workspace/workspace_types.rs @@ -261,6 +261,7 @@ impl WorkspaceRequest { raw_data, processed_data: RTree::new(), last_query_date: chrono::Utc::now().timestamp(), + llm_analysis: Vec::new(), } } From e2e27967cd7a288190500d9c60d5cc670bd8254c Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Thu, 24 Jul 2025 13:41:17 +0100 Subject: [PATCH 04/12] Add ai ui and progress in adding analysis functions --- src/geojson/loader.rs | 2 +- src/llm/client.rs | 58 +++++++++ src/llm/openrouter_types.rs | 2 + src/workspace/commands.rs | 211 ++++++++++++++++++++++++++++++ src/workspace/mod.rs | 2 + src/workspace/ui.rs | 213 ++++++++++++++++++++++++++++++- src/workspace/worker.rs | 2 +- src/workspace/workspace_types.rs | 50 +++++++- 8 files changed, 534 insertions(+), 6 deletions(-) create mode 100644 src/workspace/commands.rs diff --git a/src/geojson/loader.rs b/src/geojson/loader.rs index 2320e68..0e52734 100644 --- a/src/geojson/loader.rs +++ b/src/geojson/loader.rs @@ -2,7 +2,7 @@ use std::{fs::File, io::BufReader}; use bevy::log::info; use bevy_map_viewer::Coord; -use geojson::GeoJson; +use geojson::{GeoJson, JsonValue}; use rstar::RTree; use serde::{Deserialize, Serialize}; diff --git a/src/llm/client.rs b/src/llm/client.rs index 9aa7a46..d4dbab4 100644 --- a/src/llm/client.rs +++ b/src/llm/client.rs @@ -2,6 +2,64 @@ use crate::llm::Request; // https://openrouter.ai/docs/api-reference/overview use super::OpenrouterClient; +// ============================================================================ +// LLM Request Command Specification +// ============================================================================ +// +// Use "rq:" for data requests. Format: +// rq: +// +// Commands: +// nb : Nearby features ex: rq: nb {51.5,-0.09} r500 +// cnt : Count features ex: rq: cnt {51.5,-0.09} r500 +// sm : Summarize features ex: rq: sm {51.5,-0.09} r500 +// gt : Feature details ex: rq: gt 123456 +// t : Feature tags ex: rq: t 123456 +// bb : Features in bbox ex: rq: bb {51.4,-0.1,51.6,-0.08} +// d : Distance ex: rq: d {51.5,-0.09} {51.6,-0.10} +// n : Nearest feature ex: rq: n {51.5,-0.09} +// ply : Features in polygon ex: rq: ply {[51.5,-0.1],[51.6,-0.1],[51.6,-0.09]} +// i : General info/stats ex: rq: i {51.5,-0.09} r500 +// +// Rules: +// - Points as {lat,lon}, radius as r (e.g., r200). +// - bbox: {minLat,minLon,maxLat,maxLon}. +// - polygons: {[lat,lon],...}. +// - Always prefix data requests with "rq:". +// +// Example: +// LLM -> rq: nb {51.5,-0.09} r200 +// App -> Returns JSON of features. +// +// ============================================================================ + +pub const LLM_PROMPT: &str = r#" +You are a geo-analysis assistant. + +Answer user queries (e.g., "What is the population density here, and what if I build 200 more houses?"). +If data is required, use an rq: command. + +Commands: +// nb : Nearby features. ex: rq: nb {51.5,-0.09} r500 +// cnt : Count features. ex: rq: cnt {51.5,-0.09} r500 +// sm : Summarize features. ex: rq: sm {51.5,-0.09} r500 +// gt : Feature details by ID. ex: rq: gt 123456 +// t : Tags for feature. ex: rq: t 123456 +// bb : Features in bbox. ex: rq: bb {51.4,-0.1,51.6,-0.08} +// d : Distance between two. ex: rq: d {51.5,-0.09} {51.6,-0.10} +// n : Nearest feature. ex: rq: n {51.5,-0.09} +// ply : Features in polygon. ex: rq: ply {[51.5,-0.1],[51.6,-0.1],[51.6,-0.09]} +// i : General info/stats. ex: rq: i {51.5,-0.09} r500 + +Rules: +- Only use rq: commands to request data. +- Interpret returned JSON and respond concisely. + +Example: +User: What is the population density here? +Assistant: +rq: sm {51.5,-0.09} r500 +"#; impl OpenrouterClient { // We want to also parse in the get_data_from_string_osm but maybe we want to format it differently (we should try that first) diff --git a/src/llm/openrouter_types.rs b/src/llm/openrouter_types.rs index 1e05d5d..1bd131f 100644 --- a/src/llm/openrouter_types.rs +++ b/src/llm/openrouter_types.rs @@ -1,6 +1,8 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; +pub struct AnalysisRequestType {} + #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub struct Request { diff --git a/src/workspace/commands.rs b/src/workspace/commands.rs new file mode 100644 index 0000000..8b7c931 --- /dev/null +++ b/src/workspace/commands.rs @@ -0,0 +1,211 @@ +use crate::{ + geojson::MapFeature, + workspace::{Workspace, WorkspaceData}, +}; +use bevy_map_viewer::Coord; +use geo::Centroid; +use rstar::{AABB, PointDistance, RTreeObject}; +use std::fmt::Display; + +impl Workspace { + // i : General info/stats. + pub fn get_info(&self) -> String { + let count = self.count_workspace(); + if let Some(workspace) = &self.workspace { + let area = workspace.get_area(); + let selection = &workspace.selection; + return format!( + "Features: {}, Area: {:?}, Selection: {:?}", + count, area, selection + ); + } + format!("Features: {}", count) + } + + // t : Tags for feature. ex: rq: t 123456 + pub fn get_feature_tags(&self, id: impl Display) -> Option { + for request in self.get_requests() { + for feature in request.get_processed_data() { + if feature.id == id.to_string() { + return Some(feature.properties.clone()); + } + } + } + None + } + + // gt : Feature details by ID. ex: rq: gt 123456 + pub fn get_feature_by_id(&self, id: impl Display) -> Option { + for request in self.get_requests() { + for feature in request.get_processed_data() { + if feature.id == id.to_string() { + return Some(feature.clone()); + } + } + } + None + } + + // cnt : Count features. ex: rq: cnt {51.5,-0.09} r500 or blank for all workspace + pub fn count_workspace(&self) -> i32 { + self.get_requests() + .iter() + .map(|r| r.processed_data.size() as i32) + .sum() + } + + // nb : Nearby features. ex: rq: nb {51.5,-0.09} r500 + pub fn nearby_point(&self, point: Coord, radius: f64) -> Vec { + let mut matches = Vec::new(); + for request in self.get_requests() { + let mut m: Vec = request + .processed_data + .iter() + .filter(|feature| { + let c = feature.geometry.centroid().unwrap(); + let coord_c = Coord { + lat: c.y() as f32, + long: c.x() as f32, + }; + point.distance_haversine(&coord_c) <= radius + }) + .cloned() + .collect(); + matches.append(&mut m); + } + matches + } + + // sm : Summarize features. ex: rq: sm {51.5,-0.09} r500 + pub fn summarize_features(&self, point: Coord, radius: f64) -> String { + let features = self.nearby_point(point, radius); + let count = features.len(); + let mut tags_count = std::collections::HashMap::new(); + + for f in &features { + if let Some(props) = f.properties.as_object() { + for (k, v) in props { + *tags_count.entry(k.clone()).or_insert(0) += 1; + } + } + } + + format!("Count: {}, Tags Summary: {:?}", count, tags_count) + } + + // bb : Features in bbox. ex: rq: bb {51.4,-0.1,51.6,-0.08} + pub fn features_in_bbox( + &self, + min_lat: f64, + min_lon: f64, + max_lat: f64, + max_lon: f64, + ) -> Vec { + let envelope = AABB::from_corners([min_lon, min_lat], [max_lon, max_lat]); + let mut matches = Vec::new(); + + for request in self.get_requests() { + let mut m: Vec = request + .processed_data + .locate_in_envelope(&envelope) + .cloned() + .collect(); + matches.append(&mut m); + } + + matches + } + + // d : Distance between two. ex: rq: d {51.5,-0.09} {51.6,-0.10} + pub fn distance_between(&self, a: Coord, b: Coord) -> f64 { + a.distance_haversine(&b) + } + + // n : Nearest feature. ex: rq: n {51.5,-0.09} + pub fn nearest_feature(&self, point: Coord) -> Option { + let mut nearest: Option = None; + let mut min_dist = f64::MAX; + + for request in self.get_requests() { + for feature in request.get_processed_data() { + let c = feature.geometry.centroid().unwrap(); + let coord_c = Coord { + lat: c.y() as f32, + long: c.x() as f32, + }; + let dist: f64 = point.distance_haversine(&coord_c); + if dist < min_dist { + min_dist = dist; + nearest = Some(feature.clone()); + } + } + } + + nearest + } + + // ply : Features in polygon. ex: rq: ply {[51.5,-0.1],[51.6,-0.1],[51.6,-0.09]} + pub fn features_in_polygon(&self, polygon: &[Coord]) -> Vec { + fn point_in_polygon(point: &Coord, polygon: &[Coord]) -> bool { + // Ray casting algorithm + let mut inside = false; + let mut j = polygon.len() - 1; + for i in 0..polygon.len() { + let xi = polygon[i].long; + let yi = polygon[i].lat; + let xj = polygon[j].long; + let yj = polygon[j].lat; + let intersect = ((yi > point.lat) != (yj > point.lat)) + && (point.long < (xj - xi) * (point.lat - yi) / (yj - yi) + xi); + if intersect { + inside = !inside; + } + j = i; + } + inside + } + + let mut matches = Vec::new(); + for request in self.get_requests() { + let mut m: Vec = request + .processed_data + .iter() + .filter(|feature| { + point_in_polygon( + &Coord { + lat: feature.geometry.centroid().unwrap().y() as f32, + long: feature.geometry.centroid().unwrap().x() as f32, + }, + polygon, + ) + }) + .cloned() + .collect(); + matches.append(&mut m); + } + matches + } +} + +pub trait HaversineDistance { + fn distance_haversine(&self, other: &Self) -> f64; +} + +impl HaversineDistance for Coord { + fn distance_haversine(&self, other: &Self) -> f64 { + let radius_earth = 6_371_000.0_f64; // Earth's radius in meters + + let lat1 = self.lat.to_radians(); + let lon1 = self.long.to_radians(); + let lat2 = other.lat.to_radians(); + let lon2 = other.long.to_radians(); + + let dlat = lat2 - lat1; + let dlon = lon2 - lon1; + + let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2); + let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt()); + + radius_earth * c as f64 + } +} diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index b98a090..c4fff25 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -12,6 +12,7 @@ pub use workspace_types::*; use crate::{geojson::MapFeature, llm::OpenrouterClient, overpass::OverpassClient}; +mod commands; mod renderer; mod ui; mod worker; @@ -29,6 +30,7 @@ pub struct Workspace { // Request Clients: pub overpass_agent: OverpassClient, pub llm_agent: OpenrouterClient, + // Add llm requests agent! } impl Default for Workspace { diff --git a/src/workspace/ui.rs b/src/workspace/ui.rs index e89b075..a3542ff 100644 --- a/src/workspace/ui.rs +++ b/src/workspace/ui.rs @@ -20,6 +20,20 @@ use crate::{ use super::{RequestType, WorkspaceRequest}; +#[derive(Resource, Default)] +pub struct ChatState { + pub input_text: String, + pub chat_history: Vec, + pub is_processing: bool, +} + +#[derive(Clone)] +pub struct ChatMessage { + pub content: String, + pub is_user: bool, + pub timestamp: String, +} + pub fn workspace_actions_ui( mut tile_map_res: ResMut, mut contexts: EguiContexts, @@ -475,7 +489,7 @@ pub fn workspace_analysis_ui(mut contexts: EguiContexts, workspace_res: ResMut, + workspace: Res, +) { + let ctx = contexts.ctx_mut(); + let screen_rect = ctx.screen_rect(); + + let chat_width = if screen_rect.width() / 4 as f32 > 200.0 { + screen_rect.width() / 4 as f32 + } else { + 200.0 + }; // Same width as workspace_analysis_ui + let chat_height = (screen_rect.height() - 50.0); + let chat_pos = egui::pos2( + screen_rect.width() - chat_width - 10.0, + 40.0, // Position under item_info with some spacing + ); + + egui::Area::new("chat_box".into()) + .fixed_pos(chat_pos) + .show(ctx, |ui| { + egui::Frame::new() + .fill(egui::Color32::from_rgba_premultiplied(25, 25, 25, 255)) + .corner_radius(10.0) + .shadow(egui::epaint::Shadow { + color: egui::Color32::from_black_alpha(60), + offset: [5, 5], + blur: 10, + spread: 5, + }) + .show(ui, |ui| { + ui.set_width(chat_width); + ui.set_height(chat_height); + + ui.vertical(|ui| { + // Header + ui.horizontal(|ui| { + ui.label( + RichText::new("AI Chat Assistant") + .strong() + .color(egui::Color32::WHITE), + ); + ui.with_layout( + egui::Layout::right_to_left(egui::Align::Center), + |ui| { + if ui.small_button("πŸ—‘").on_hover_text("Clear chat").clicked() + { + chat_state.chat_history.clear(); + } + }, + ); + }); + + ui.separator(); + + // Messages area (scrollable) + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .stick_to_bottom(true) + .max_height(chat_height - 80.0) // Leave space for input + .show(ui, |ui| { + ui.set_width(chat_width - 20.0); + + if chat_state.chat_history.is_empty() { + ui.vertical_centered(|ui| { + ui.add_space(20.0); + ui.label( + RichText::new("Ask me about the map data!") + .color(egui::Color32::GRAY) + .italics(), + ); + }); + } else { + for message in &chat_state.chat_history { + render_chat_message(ui, message, chat_width - 30.0); + ui.add_space(8.0); + } + } + + // Show loading indicator if processing + if chat_state.is_processing { + ui.horizontal(|ui| { + ui.add_space(10.0); + ui.spinner(); + ui.label( + RichText::new("AI is thinking...") + .color(egui::Color32::GRAY), + ); + }); + } + }); + + ui.add_space(5.0); + ui.separator(); + + // Input area + ui.horizontal(|ui| { + let text_edit = egui::TextEdit::singleline(&mut chat_state.input_text) + .desired_width(chat_width - 60.0) + .hint_text("Ask about the map data..."); + + let response = ui.add(text_edit); + + let send_clicked = + ui.button("πŸ“€").on_hover_text("Send message").clicked(); + + let enter_pressed = response.lost_focus() + && ui.input(|i| i.key_pressed(egui::Key::Enter)); + + if (send_clicked || enter_pressed) + && !chat_state.input_text.trim().is_empty() + && !chat_state.is_processing + { + send_chat_message(&mut chat_state, &workspace); + } + }); + }); + }); + }); +} + +fn render_chat_message(ui: &mut egui::Ui, message: &ChatMessage, max_width: f32) { + let bg_color = if message.is_user { + egui::Color32::from_rgba_premultiplied(70, 130, 180, 200) // Steel blue for user + } else { + egui::Color32::from_rgba_premultiplied(60, 60, 60, 200) // Dark gray for AI + }; + + let text_color = egui::Color32::WHITE; + + ui.horizontal(|ui| { + ui.add_space(10.0); + if !message.is_user { + ui.add_space(20.0); + } + + egui::Frame::new() + .fill(bg_color) + .corner_radius(8.0) + .inner_margin(8.0) + .show(ui, |ui| { + ui.set_max_width(max_width * 0.8); + + // Message header + ui.horizontal(|ui| { + let icon = if message.is_user { "πŸ‘€" } else { "πŸ€–" }; + let sender = if message.is_user { "You" } else { "AI" }; + ui.label( + RichText::new(format!("{} {}", icon, sender)) + .small() + .color(egui::Color32::LIGHT_GRAY), + ); + }); + + // Message content with wrapping + ui.add(egui::Label::new(RichText::new(&message.content).color(text_color)).wrap()); + }); + + if message.is_user { + ui.add_space(10.0); + } + }); +} + +fn send_chat_message(chat_state: &mut ChatState, workspace: &Workspace) { + let user_message = chat_state.input_text.trim().to_string(); + + // Add user message to chat + chat_state.chat_history.push(ChatMessage { + content: user_message.clone(), + is_user: true, + timestamp: chrono::Utc::now().format("%H:%M:%S").to_string(), + }); + + // Clear input + chat_state.input_text.clear(); + + // Set processing state + chat_state.is_processing = true; + + // TODO: Replace this with actual LLM integration + // For now, simulate AI response + chat_state.chat_history.push(ChatMessage { + content: format!("I received your message: '{}'. LLM integration coming soon! I can analyze map data, features, and spatial relationships.", user_message), + is_user: false, + timestamp: chrono::Utc::now().format("%H:%M:%S").to_string(), + }); + + chat_state.is_processing = false; + + // Keep only last 50 messages to prevent memory issues + while chat_state.chat_history.len() > 50 { + chat_state.chat_history.remove(0); + } +} diff --git a/src/workspace/worker.rs b/src/workspace/worker.rs index 2be32a3..2210c28 100644 --- a/src/workspace/worker.rs +++ b/src/workspace/worker.rs @@ -79,9 +79,9 @@ pub fn process_requests(mut commands: Commands, mut workspace: ResMut } } } - here needs to be done and this is purposfully broken so that it gets flagged! RequestType::OpenRouterRequest(_) => todo!(), RequestType::OpenMeteoRequest(_open_meteo_request) => {} + RequestType::AnalysisRequest(_) => todo!(), } request.raw_data = result.clone(); diff --git a/src/workspace/workspace_types.rs b/src/workspace/workspace_types.rs index d0ce7c0..2c07ec4 100644 --- a/src/workspace/workspace_types.rs +++ b/src/workspace/workspace_types.rs @@ -7,18 +7,24 @@ use rstar::{AABB, RTree, RTreeObject}; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::geojson::{MapFeature, get_data_from_string_osm}; +use crate::{ + geojson::{MapFeature, get_data_from_string_osm}, + workspace::ui::chat_box_ui, +}; use super::{ Workspace, WorkspaceData, WorkspacePlugin, WorkspaceRequest, renderer::render_workspace_requests, - ui::{PersistentInfoWindows, item_info, workspace_actions_ui, workspace_analysis_ui}, + ui::{ + ChatState, PersistentInfoWindows, item_info, workspace_actions_ui, workspace_analysis_ui, + }, worker::{cleanup_tasks, process_requests}, }; impl Plugin for WorkspacePlugin { fn build(&self, app: &mut App) { app.insert_resource(Workspace::default()) + .insert_resource(ChatState::default()) .add_systems(FixedUpdate, (process_requests, cleanup_tasks)) .add_systems(Update, render_workspace_requests) .insert_resource(PersistentInfoWindows::default()) @@ -26,7 +32,7 @@ impl Plugin for WorkspacePlugin { Update, (( workspace_actions_ui.after(EguiPreUpdateSet::InitContexts), - workspace_analysis_ui.after(EguiPreUpdateSet::InitContexts), + chat_box_ui.after(EguiPreUpdateSet::InitContexts), item_info.after(EguiPreUpdateSet::InitContexts), ),), ); @@ -66,6 +72,18 @@ impl Workspace { rendered_requests } + pub fn get_requests(&self) -> Vec { + let loaded_requests = self.loaded_requests.lock().unwrap(); + let mut rendered_requests = Vec::new(); + if let Some(workspace) = &self.workspace { + for j in workspace.get_requests().iter() { + if let Some(request) = loaded_requests.get(j) { + rendered_requests.push(request.clone()); + } + } + } + rendered_requests + } pub fn get_rendered_requests(&self) -> Vec { let loaded_requests = self.loaded_requests.lock().unwrap(); let mut rendered_requests = Vec::new(); @@ -192,6 +210,10 @@ impl WorkspaceRequest { } crate::workspace::RequestType::OpenMeteoRequest(_) => {} crate::workspace::RequestType::OpenRouterRequest(_) => {} + RequestType::OpenMeteoRequest(open_meteo_request) => todo!(), + RequestType::OverpassTurboRequest(_) => todo!(), + RequestType::OpenRouterRequest(_) => todo!(), + RequestType::AnalysisRequest(_) => todo!(), } } @@ -225,6 +247,7 @@ pub enum RequestType { OpenMeteoRequest(OpenMeteoRequest), OverpassTurboRequest(String), OpenRouterRequest(String), + AnalysisRequest(String), } impl std::fmt::Debug for RequestType { @@ -233,6 +256,7 @@ impl std::fmt::Debug for RequestType { RequestType::OpenMeteoRequest(_) => write!(f, "OpenMeteoRequest"), RequestType::OverpassTurboRequest(_) => write!(f, "OverpassTurboRequest"), RequestType::OpenRouterRequest(_) => write!(f, "OpenRouterRequest"), + RequestType::AnalysisRequest(_) => todo!(), } } } @@ -282,6 +306,17 @@ impl WorkspaceData { properties: HashMap::new(), } } + pub fn empty() -> Self { + Self { + id: Uuid::new_v4().to_string(), + name: "Empty".to_string(), + selection: Selection::empty(), + creation_date: chrono::Utc::now().timestamp(), + last_modified: chrono::Utc::now().timestamp(), + requests: HashSet::new(), + properties: HashMap::new(), + } + } } impl RTreeObject for WorkspaceData { @@ -396,6 +431,15 @@ impl Selection { points: Some(vec![start]), } } + + pub fn empty() -> Self { + Self { + selection_type: SelectionType::NONE, + start: None, + end: None, + points: None, + } + } } /// These implementations are for the RTreeObject trait. From 9da3fcccd7a24d24204bce379e24f32fbf3e6645 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Thu, 24 Jul 2025 13:54:26 +0100 Subject: [PATCH 05/12] Add documentation (claude generated) --- API_DOCUMENTATION.md | 129 ++++++++++++++++++ DETAILED_API_DOCS.md | 311 +++++++++++++++++++++++++++++++++++++++++++ FILE_STRUCTURE.md | 249 ++++++++++++++++++++++++++++++++++ src/camera.rs | 22 +++ src/debug.rs | 22 +++ src/geojson/mod.rs | 24 ++++ src/interaction.rs | 22 +++ src/llm/mod.rs | 26 ++++ src/main.rs | 26 ++++ src/overpass/mod.rs | 28 ++++ src/settings.rs | 23 ++++ src/tools/mod.rs | 31 +++++ src/workspace/mod.rs | 33 +++++ src/workspace/ui.rs | 114 ++++++++++++---- 14 files changed, 1037 insertions(+), 23 deletions(-) create mode 100644 API_DOCUMENTATION.md create mode 100644 DETAILED_API_DOCS.md create mode 100644 FILE_STRUCTURE.md diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..8eeb850 --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,129 @@ +# Map-RS API Documentation + +## Project Overview + +Map-RS is a Rust-based interactive map viewer built with the Bevy game engine. It provides advanced geospatial data visualization, real-time data analysis, and AI-powered insights for geographic information systems (GIS) applications. + +## Key Features + +- **Interactive Map Visualization**: Real-time map rendering with multiple tile providers +- **Geospatial Data Processing**: Support for GeoJSON, Overpass API integration +- **AI-Powered Analysis**: LLM integration for map data insights +- **Advanced Tools**: Measurement, shape drawing, area selection +- **Workspace Management**: Save and load different map configurations +- **Multi-API Support**: Weather data, environmental data, and spatial queries + +## Architecture + +The application follows a modular plugin architecture using Bevy's ECS (Entity Component System): + +``` +map-rs/ +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ main.rs # Application entry point and plugin registration +β”‚ β”œβ”€β”€ camera.rs # Camera controls and movement systems +β”‚ β”œβ”€β”€ debug.rs # Debug utilities and performance monitoring +β”‚ β”œβ”€β”€ interaction.rs # User input handling and map interactions +β”‚ β”œβ”€β”€ settings.rs # Application configuration and preferences +β”‚ β”œβ”€β”€ apis/ # External API integrations +β”‚ β”‚ β”œβ”€β”€ weather/ # Weather data providers (OpenMeteo, NWS, EarthData) +β”‚ β”‚ └── environmental/ # Environmental data (Europa, LandCover) +β”‚ β”œβ”€β”€ geojson/ # GeoJSON processing and rendering +β”‚ β”‚ β”œβ”€β”€ mod.rs # Module exports and main functionality +β”‚ β”‚ β”œβ”€β”€ loader.rs # GeoJSON file loading and parsing +β”‚ β”‚ β”œβ”€β”€ renderer.rs # 2D rendering of geographic features +β”‚ β”‚ β”œβ”€β”€ shapes_plugin.rs # Shape creation and manipulation +β”‚ β”‚ └── types.rs # Data structures for geographic features +β”‚ β”œβ”€β”€ llm/ # AI/LLM integration +β”‚ β”‚ β”œβ”€β”€ mod.rs # Module exports +β”‚ β”‚ β”œβ”€β”€ client.rs # OpenRouter API client +β”‚ β”‚ └── openrouter_types.rs # API request/response types +β”‚ β”œβ”€β”€ overpass/ # OpenStreetMap Overpass API +β”‚ β”‚ β”œβ”€β”€ mod.rs # Module exports +β”‚ β”‚ β”œβ”€β”€ client.rs # Overpass query client +β”‚ β”‚ └── overpass_types.rs # OSM data structures +β”‚ β”œβ”€β”€ storage/ # Data persistence and caching +β”‚ β”œβ”€β”€ tools/ # Interactive map tools +β”‚ β”‚ β”œβ”€β”€ mod.rs # Tool management system +β”‚ β”‚ β”œβ”€β”€ tool.rs # Base tool trait and implementations +β”‚ β”‚ β”œβ”€β”€ ui.rs # Tool UI components +β”‚ β”‚ β”œβ”€β”€ measure.rs # Distance and area measurement +β”‚ β”‚ β”œβ”€β”€ pin.rs # Map marker placement +β”‚ β”‚ β”œβ”€β”€ feature_picker.rs # Feature selection and info display +β”‚ β”‚ └── work_space_selector.rs # Workspace area selection +β”‚ └── workspace/ # Workspace and data management +β”‚ β”œβ”€β”€ mod.rs # Core workspace functionality +β”‚ β”œβ”€β”€ ui.rs # Workspace UI components +β”‚ β”œβ”€β”€ renderer.rs # Workspace-specific rendering +β”‚ β”œβ”€β”€ worker.rs # Background task processing +β”‚ β”œβ”€β”€ commands.rs # Workspace operations +β”‚ └── workspace_types.rs # Data structures and plugin setup +β”œβ”€β”€ assets/ # Static assets +β”‚ β”œβ”€β”€ buttons/ # UI button icons +β”‚ β”œβ”€β”€ fonts/ # Typography assets +β”‚ β”œβ”€β”€ icon/ # Application icons +β”‚ β”œβ”€β”€ shaders/ # Custom WGSL shaders +β”‚ └── test/ # Test data files +└── target/ # Compiled artifacts +``` + +## Core Systems + +### 1. Main Application (`src/main.rs`) +The entry point that initializes all plugins and systems. + +### 2. Camera System (`src/camera.rs`) +Handles map navigation, zoom controls, and viewport management. + +### 3. Workspace System (`src/workspace/`) +Manages different map configurations, data layers, and user sessions. + +### 4. GeoJSON Processing (`src/geojson/`) +Handles loading, parsing, and rendering of geographic data. + +### 5. Tools System (`src/tools/`) +Interactive tools for map manipulation and measurement. + +### 6. API Integrations (`src/apis/`) +External data providers for weather, environmental, and geographic data. + +## Plugin Architecture + +Each major system is implemented as a Bevy plugin: + +- `WorkspacePlugin`: Core workspace management +- `CameraSystemPlugin`: Camera controls +- `InteractionSystemPlugin`: User input handling +- `RenderPlugin`: GeoJSON rendering +- `SettingsPlugin`: Configuration management +- `ToolsPlugin`: Interactive tools +- `DebugPlugin`: Development utilities + +## Data Flow + +1. **Input**: User interactions, API responses, file imports +2. **Processing**: Data parsing, spatial calculations, filtering +3. **Storage**: Workspace persistence, caching, session management +4. **Rendering**: 2D map rendering, UI updates, visual effects +5. **Output**: Interactive map display, analysis results, exports + +## External Dependencies + +- **Bevy**: Game engine and ECS framework +- **egui**: Immediate mode GUI +- **rstar**: Spatial indexing (R-tree) +- **lyon**: 2D path tessellation +- **ureq**: HTTP client for API requests +- **serde**: Serialization framework +- **geojson**: GeoJSON parsing +- **chrono**: Date and time handling + +## Development Setup + +1. Install Rust toolchain +2. Clone repository +3. Run `cargo build` to compile +4. Run `cargo run` to start application +5. Set environment variable `OPENROUTER_API_KEY` for AI features + +This documentation provides a high-level overview of the system architecture. Detailed API documentation for each module follows in the subsequent sections. diff --git a/DETAILED_API_DOCS.md b/DETAILED_API_DOCS.md new file mode 100644 index 0000000..64d8bd7 --- /dev/null +++ b/DETAILED_API_DOCS.md @@ -0,0 +1,311 @@ +# Map-RS Detailed API Documentation + +## Core Data Structures + +### Workspace System + +#### `Workspace` Resource +The main resource that manages workspace state and external service connections. + +```rust +#[derive(Resource)] +pub struct Workspace { + pub workspace: Option, + pub loaded_requests: Arc>>, + pub worker: WorkspaceWorker, + pub overpass_agent: OverpassClient, + pub llm_agent: OpenrouterClient, +} +``` + +**Fields:** +- `workspace`: Current workspace configuration and data +- `loaded_requests`: Thread-safe storage for active data requests +- `worker`: Background task processor for async operations +- `overpass_agent`: Client for OpenStreetMap Overpass API +- `llm_agent`: Client for AI/LLM integration via OpenRouter + +**Methods:** +- `get_rendered_requests()`: Returns currently rendered workspace requests +- `process_request(request: WorkspaceRequest)`: Processes and adds request to workspace + +#### `WorkspaceData` Structure +Contains workspace configuration and metadata. + +```rust +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq)] +pub struct WorkspaceData { + id: String, + name: String, + selection: Selection, + creation_date: i64, + last_modified: i64, + requests: HashSet, + properties: HashMap<(String, Value), Srgba>, +} +``` + +**Key Methods:** +- `get_name()`: Returns workspace display name +- `get_id()`: Returns unique workspace identifier +- `get_selection()`: Returns geographic selection area +- `get_area()`: Calculates total area of workspace region + +#### `WorkspaceRequest` Structure +Represents a data request with processing state. + +```rust +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WorkspaceRequest { + id: String, + layer: u32, + visible: bool, + request: RequestType, + raw_data: Vec, + processed_data: RTree, + llm_analysis: Vec, + last_query_date: i64, +} +``` + +**Request Types:** +- `OverpassTurboRequest`: OpenStreetMap data query +- `OpenRouterRequest`: AI/LLM analysis request +- `OpenMeteoRequest`: Weather data request + +### Geographic Data System + +#### `MapFeature` Structure +Core geographic feature representation. + +```rust +pub struct MapFeature { + pub id: String, + pub geometry: GeometryType, + pub properties: serde_json::Value, + pub closed: bool, + pub feature_type: FeatureType, +} +``` + +**Methods:** +- `get_in_world_space(tile_manager)`: Converts to world coordinates +- `get_bounds()`: Returns feature bounding box +- `get_center()`: Calculates geometric center point + +#### `GeometryType` Enum +Supported geometry types for geographic features. + +```rust +pub enum GeometryType { + Point(Point), + LineString(LineString), + Polygon(Polygon), + MultiPoint(MultiPoint), + MultiLineString(MultiLineString), + MultiPolygon(MultiPolygon), +} +``` + +### Tool System + +#### `ToolResources` Resource +Manages interactive tool state and selections. + +```rust +#[derive(Resource)] +pub struct ToolResources { + pub pointer: bool, + pub measure: bool, + pub pin: bool, + pub selection_areas: WorkspaceSelector, +} +``` + +#### Tool Trait +Base trait for all interactive tools. + +```rust +pub trait Tool { + fn activate(&mut self); + fn deactivate(&mut self); + fn handle_input(&mut self, input: &InputEvent) -> ToolResult; + fn render(&self, ui: &mut egui::Ui); +} +``` + +### UI System + +#### `ChatState` Resource +Manages AI chat interface state. + +```rust +#[derive(Resource, Default)] +pub struct ChatState { + pub input_text: String, + pub chat_history: Vec, + pub is_processing: bool, + pub api_token: String, + pub token_verified: bool, +} +``` + +#### `PersistentInfoWindows` Resource +Manages feature information popup windows. + +```rust +#[derive(Resource, Default)] +pub struct PersistentInfoWindows { + pub windows: HashMap, +} +``` + +## API Integration + +### Overpass API Client + +#### `OverpassClient` Structure +```rust +#[derive(Clone)] +pub struct OverpassClient { + url: String, + agent: Agent, + bounds: String, + settings: Settings, +} +``` + +**Methods:** +- `new(url: &str)`: Creates new client instance +- `query(query_string: &str)`: Executes Overpass query +- `get_bounds(selection: Selection)`: Converts selection to bounds string +- `build_query(bounds: String, settings: Settings)`: Builds Overpass QL query + +### LLM Integration + +#### `OpenrouterClient` Structure +```rust +#[derive(Clone)] +pub struct OpenrouterClient { + pub token: Option, + pub agent: Agent, + pub url: String, +} +``` + +**Methods:** +- `new(url: impl Display, token: Option)`: Creates client +- `send_chat_request(request: Request)`: Sends chat completion request +- `analyze_features(features: &[MapFeature])`: Analyzes geographic features + +## System Architecture + +### Plugin System +Each major feature is implemented as a Bevy plugin: + +1. **WorkspacePlugin**: Core workspace management + - Systems: `process_requests`, `cleanup_tasks`, `render_workspace_requests` + - Resources: `Workspace`, `ChatState`, `PersistentInfoWindows` + +2. **CameraSystemPlugin**: Camera controls and rendering + - Systems: Camera movement, zoom handling, coordinate transforms + - Components: `OldMonitorSettings`, camera entities + +3. **RenderPlugin**: Geographic data rendering + - Systems: GeoJSON tessellation, mesh construction, styling + - Components: Rendered shapes, materials, visibility + +4. **ToolsPlugin**: Interactive tools + - Systems: Tool activation, input handling, UI rendering + - Resources: `ToolResources`, tool state management + +5. **InteractionSystemPlugin**: User input handling + - Systems: File drop processing, mouse/keyboard input + - Events: Input events, interaction state changes + +### Data Flow + +1. **Input Processing**: User interactions β†’ Tool systems β†’ State updates +2. **Data Requests**: Tool actions β†’ Workspace worker β†’ API clients +3. **Data Processing**: API responses β†’ Parser β†’ Spatial indexing β†’ Storage +4. **Rendering**: Processed data β†’ Tessellation β†’ Mesh generation β†’ GPU +5. **UI Updates**: State changes β†’ UI systems β†’ Visual feedback + +### Rendering Pipeline + +1. **Tessellation**: Complex polygons β†’ Triangle meshes (using Lyon) +2. **Styling**: Feature properties β†’ Colors, stroke widths, visibility +3. **Batching**: Similar features β†’ Optimized draw calls +4. **Culling**: Viewport bounds β†’ Visible features only +5. **GPU Rendering**: Mesh data β†’ Vertex/fragment shaders β†’ Frame buffer + +## Performance Considerations + +### Spatial Indexing +- Uses R-tree data structure for efficient spatial queries +- Enables fast intersection testing and nearest neighbor searches +- Supports dynamic updates as data changes + +### Memory Management +- Thread-safe data structures for concurrent access +- Efficient memory pooling for frequently allocated objects +- Streaming data processing for large datasets + +### Rendering Optimization +- Frustum culling for off-screen features +- Level-of-detail based on zoom level +- Instanced rendering for similar features +- GPU-based tessellation for complex shapes + +## Error Handling + +### Result Types +Most operations return `Result` for graceful error handling: +- API requests: Network errors, rate limiting, invalid responses +- Data processing: Parse errors, invalid geometry, format issues +- File operations: I/O errors, permission issues, corrupted data + +### Logging +Uses Bevy's logging system with configurable levels: +- `error!`: Critical failures requiring attention +- `warn!`: Non-critical issues that may affect functionality +- `info!`: General operational information +- `debug!`: Detailed debugging information +- `trace!`: Verbose execution tracing + +## Extension Points + +### Custom Tools +Implement the `Tool` trait to create new interactive tools: + +```rust +pub struct CustomTool { + // Tool state +} + +impl Tool for CustomTool { + fn activate(&mut self) { /* Activation logic */ } + fn deactivate(&mut self) { /* Cleanup logic */ } + fn handle_input(&mut self, input: &InputEvent) -> ToolResult { /* Input handling */ } + fn render(&self, ui: &mut egui::Ui) { /* UI rendering */ } +} +``` + +### Custom Data Sources +Add new data sources by implementing request types and processing: + +```rust +pub enum CustomRequestType { + MyApiRequest(MyApiQuery), +} + +// Add to RequestType enum and implement processing logic +``` + +### Custom Rendering +Extend the rendering system with custom shaders and materials: +- Add custom vertex/fragment shaders in `assets/shaders/` +- Implement custom material types +- Add specialized rendering systems for new feature types + +This documentation provides a comprehensive overview of the Map-RS API structure and usage patterns. For specific implementation details, refer to the individual module source files. diff --git a/FILE_STRUCTURE.md b/FILE_STRUCTURE.md new file mode 100644 index 0000000..67ed880 --- /dev/null +++ b/FILE_STRUCTURE.md @@ -0,0 +1,249 @@ +# Map-RS File Structure Documentation + +## Project Root Structure + +``` +map-rs/ +β”œβ”€β”€ Cargo.toml # Project configuration and dependencies +β”œβ”€β”€ Cargo.lock # Dependency lock file +β”œβ”€β”€ LICENSE # Project license (MIT/Apache) +β”œβ”€β”€ README.md # Project overview and setup instructions +β”œβ”€β”€ API_DOCUMENTATION.md # High-level API documentation +β”œβ”€β”€ DETAILED_API_DOCS.md # Comprehensive API reference +β”œβ”€β”€ FILE_STRUCTURE.md # This documentation file +β”œβ”€β”€ assets/ # Static assets and resources +β”œβ”€β”€ src/ # Source code directory +└── target/ # Compiled output (build artifacts) +``` + +## Assets Directory (`assets/`) + +Static resources used by the application at runtime. + +``` +assets/ +β”œβ”€β”€ buttons/ # UI button icons and graphics +β”‚ β”œβ”€β”€ arrow.svg # Navigation arrow icon +β”‚ β”œβ”€β”€ circle-o.svg # Circle selection tool icon +β”‚ β”œβ”€β”€ measure.svg # Measurement tool icon +β”‚ β”œβ”€β”€ north-arrow-n.svg # North direction indicator +β”‚ β”œβ”€β”€ pin.svg # Map pin/marker icon +β”‚ β”œβ”€β”€ polygon-pt.svg # Polygon tool icon +β”‚ └── rectangle-pt.svg # Rectangle selection tool icon +β”œβ”€β”€ fonts/ # Typography assets +β”‚ └── BagnardSans.otf # Custom font for UI elements +β”œβ”€β”€ icon/ # Application icons +β”‚ └── icon1080.png # High-resolution app icon +β”œβ”€β”€ shaders/ # Custom WGSL shaders +β”‚ β”œβ”€β”€ full_screen_pass.wgsl # Post-processing shader +β”‚ └── old_monitor.wgsl # Retro display effect shader +└── test/ # Test data files + └── green-belt.geojson # Sample GeoJSON for testing +``` + +## Source Code Directory (`src/`) + +Main application source code organized by functionality. + +``` +src/ +β”œβ”€β”€ main.rs # Application entry point +β”œβ”€β”€ camera.rs # Camera system and controls +β”œβ”€β”€ debug.rs # Debug utilities and diagnostics +β”œβ”€β”€ interaction.rs # User input and file handling +β”œβ”€β”€ settings.rs # Application configuration +β”œβ”€β”€ apis/ # External API integrations +β”œβ”€β”€ geojson/ # Geographic data processing +β”œβ”€β”€ llm/ # AI/LLM integration +β”œβ”€β”€ overpass/ # OpenStreetMap API client +β”œβ”€β”€ storage/ # Data persistence (future) +β”œβ”€β”€ tools/ # Interactive map tools +└── workspace/ # Workspace management +``` + +### APIs Directory (`src/apis/`) + +Integration modules for external data services. + +``` +apis/ +β”œβ”€β”€ NOTE # Development notes and API keys +β”œβ”€β”€ environmental/ # Environmental data providers +β”‚ β”œβ”€β”€ europa.rs # European environmental data API +β”‚ └── landcover.rs # Land cover classification API +└── weather/ # Weather data providers + β”œβ”€β”€ earthdata.rs # NASA Earth data API + β”œβ”€β”€ nws.rs # National Weather Service API + └── openmeteo.rs # Open-Meteo weather API +``` + +### GeoJSON Module (`src/geojson/`) + +Geographic data processing and rendering components. + +``` +geojson/ +β”œβ”€β”€ mod.rs # Module exports and main functionality +β”œβ”€β”€ loader.rs # GeoJSON file loading and parsing +β”œβ”€β”€ renderer.rs # 2D rendering using tessellation +β”œβ”€β”€ shapes_plugin.rs # Interactive shape creation tools +└── types.rs # Geographic feature data structures +``` + +**Key Components:** +- **loader.rs**: Handles GeoJSON file import, validation, and parsing +- **renderer.rs**: Converts geographic features to renderable meshes using Lyon tessellation +- **shapes_plugin.rs**: Provides tools for creating and editing geometric shapes +- **types.rs**: Defines `MapFeature`, `GeometryType`, and related data structures + +### LLM Module (`src/llm/`) + +AI and Large Language Model integration for data analysis. + +``` +llm/ +β”œβ”€β”€ mod.rs # Module exports and client setup +β”œβ”€β”€ client.rs # OpenRouter API client implementation +└── openrouter_types.rs # API request/response data structures +``` + +**Functionality:** +- Chat-based interaction with geographic data +- Automated analysis and insight generation +- Natural language querying of spatial information +- Integration with multiple LLM providers through OpenRouter + +### Overpass Module (`src/overpass/`) + +OpenStreetMap data integration via Overpass API. + +``` +overpass/ +β”œβ”€β”€ mod.rs # Module exports and client setup +β”œβ”€β”€ client.rs # Overpass API client and query builder +└── overpass_types.rs # OSM data structures and query types +``` + +**Features:** +- Complex spatial queries using Overpass QL +- Efficient OSM data parsing and conversion +- Bounding box and feature-based filtering +- Rate limiting and respectful API usage + +### Storage Module (`src/storage/`) + +Data persistence and caching system (in development). + +``` +storage/ +└── WORK ON ME NEXT WITH URGRANCY # Development priority marker +``` + +**Planned Features:** +- Workspace persistence to disk +- Cached API response storage +- User preference management +- Data export/import functionality + +### Tools Module (`src/tools/`) + +Interactive tools for map manipulation and analysis. + +``` +tools/ +β”œβ”€β”€ mod.rs # Tool management and exports +β”œβ”€β”€ tool.rs # Base tool trait and common functionality +β”œβ”€β”€ ui.rs # Tool UI components and panels +β”œβ”€β”€ feature_picker.rs # Feature selection and information display +β”œβ”€β”€ measure.rs # Distance and area measurement tools +β”œβ”€β”€ pin.rs # Map marker placement and management +└── work_space_selector.rs # Area selection for workspace definition +``` + +**Tool Categories:** +- **Measurement**: Distance, area, and perimeter calculation +- **Selection**: Rectangle, polygon, and circle selection tools +- **Annotation**: Pin placement and labeling +- **Analysis**: Feature information and attribute display + +### Workspace Module (`src/workspace/`) + +Core workspace management and data organization. + +``` +workspace/ +β”œβ”€β”€ mod.rs # Core workspace functionality and resources +β”œβ”€β”€ commands.rs # Workspace operation commands +β”œβ”€β”€ renderer.rs # Workspace-specific rendering logic +β”œβ”€β”€ ui.rs # User interface components (analysis panel, chat) +β”œβ”€β”€ worker.rs # Background task processing +└── workspace_types.rs # Data structures and plugin implementation +``` + +**Key Responsibilities:** +- Multi-layered data organization +- Background processing of API requests +- Real-time data updates and synchronization +- User interface for workspace interaction +- Integration between different data sources + +## Build Output (`target/`) + +Rust compiler output and build artifacts. + +``` +target/ +β”œβ”€β”€ CACHEDIR.TAG # Build cache metadata +└── debug/ # Debug build outputs + β”œβ”€β”€ map-rs # Main executable (debug) + β”œβ”€β”€ map-rs.d # Dependency information + β”œβ”€β”€ build/ # Intermediate build files + β”œβ”€β”€ deps/ # Compiled dependencies + β”œβ”€β”€ examples/ # Example program outputs + └── incremental/ # Incremental compilation cache +``` + +## File Naming Conventions + +### Rust Source Files +- **Module files**: `mod.rs` - Main module entry point +- **Implementation files**: Descriptive names matching functionality +- **Plugin files**: `*_plugin.rs` - Bevy plugin implementations +- **Type files**: `*_types.rs` - Data structure definitions +- **Client files**: `*_client.rs` - External service clients + +### Asset Files +- **Icons**: SVG format for scalability +- **Fonts**: OpenType format (.otf) for typography +- **Shaders**: WGSL format for WebGPU compatibility +- **Test data**: Standard format extensions (.geojson, .json) + +### Documentation Files +- **README.md**: Project overview and setup +- **API_DOCUMENTATION.md**: High-level API guide +- **DETAILED_API_DOCS.md**: Comprehensive API reference +- **FILE_STRUCTURE.md**: This file structure guide + +## Development Workflow + +### Adding New Features +1. Create appropriate module structure in `src/` +2. Implement core functionality with proper documentation +3. Add corresponding UI components if needed +4. Update plugin registration in relevant `mod.rs` files +5. Add assets to `assets/` directory if required +6. Update documentation files + +### Asset Management +- Place UI icons in `assets/buttons/` +- Add custom fonts to `assets/fonts/` +- Store shader files in `assets/shaders/` +- Keep test data in `assets/test/` + +### Module Organization +- Each major feature gets its own module directory +- Use `mod.rs` for module exports and main functionality +- Separate concerns: types, client code, UI, and logic +- Maintain consistent naming conventions + +This file structure supports modular development, clear separation of concerns, and easy navigation of the codebase. Each module has a specific purpose and well-defined interfaces with other parts of the system. diff --git a/src/camera.rs b/src/camera.rs index d10ec1c..b6689fb 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,3 +1,25 @@ +//! # Camera System Module +//! +//! This module handles all camera-related functionality for the map viewer. +//! +//! ## Purpose +//! - Manages 2D camera movement and navigation across the map +//! - Provides zoom controls and viewport management +//! - Implements custom shader effects for visual enhancements +//! - Handles camera-to-world coordinate transformations +//! +//! ## Key Components +//! - `CameraSystemPlugin`: Main plugin for camera functionality +//! - `OldMonitorSettings`: Custom shader settings for visual effects +//! - Camera movement and zoom systems +//! - Coordinate transformation utilities +//! +//! ## Features +//! - Smooth camera movement and zoom +//! - Custom post-processing effects via shaders +//! - Map coordinate system integration +//! - Viewport-based rendering optimizations + use bevy::{ core_pipeline::core_2d::graph::{Core2d, Node2d}, core_pipeline::fullscreen_vertex_shader::fullscreen_shader_vertex_state, diff --git a/src/debug.rs b/src/debug.rs index 4f40257..5bed16c 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -1,3 +1,25 @@ +//! # Debug System Module +//! +//! This module provides debugging utilities and performance monitoring for the application. +//! +//! ## Purpose +//! - Real-time performance metrics display (FPS, frame time) +//! - Debug overlays and visual debugging tools +//! - Development-time diagnostics and profiling +//! - Error reporting and logging utilities +//! +//! ## Key Components +//! - `DebugPlugin`: Main debug plugin with conditional compilation +//! - `DebugText`: Component for debug information display +//! - Performance monitoring systems +//! - Debug UI rendering +//! +//! ## Features +//! - FPS counter and frame time diagnostics +//! - Debug text overlays +//! - Conditional debug builds (only active in debug mode) +//! - Performance profiling integration + use bevy::{ color::palettes::css::GOLD, diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin}, diff --git a/src/geojson/mod.rs b/src/geojson/mod.rs index be66714..b4ff217 100644 --- a/src/geojson/mod.rs +++ b/src/geojson/mod.rs @@ -1,3 +1,27 @@ +//! # GeoJSON Processing Module +//! +//! This module handles all GeoJSON-related functionality including loading, parsing, +//! rendering, and manipulation of geographic data. +//! +//! ## Purpose +//! - Load and parse GeoJSON files and data streams +//! - Render geographic features as 2D shapes on the map +//! - Provide tools for creating and editing geographic shapes +//! - Manage spatial indexing and querying of geographic data +//! +//! ## Sub-modules +//! - `loader`: GeoJSON file loading and parsing utilities +//! - `renderer`: 2D rendering of geographic features using tessellation +//! - `shapes_plugin`: Interactive shape creation and editing tools +//! - `types`: Data structures and types for geographic features +//! +//! ## Key Features +//! - Support for all GeoJSON geometry types (Point, LineString, Polygon, etc.) +//! - Efficient spatial indexing using R-trees +//! - Real-time tessellation for complex polygons +//! - Style-based rendering with customizable colors and stroke properties +//! - Integration with OpenStreetMap data via Overpass API + mod loader; mod renderer; mod shapes_plugin; diff --git a/src/interaction.rs b/src/interaction.rs index 0d7d697..5d8729f 100644 --- a/src/interaction.rs +++ b/src/interaction.rs @@ -1,3 +1,25 @@ +//! # Interaction System Module +//! +//! This module handles user input and interaction with the map viewer. +//! +//! ## Purpose +//! - Processes user input events (mouse, keyboard, file drops) +//! - Handles file drag-and-drop functionality for data import +//! - Manages interaction states and user interface events +//! - Coordinates between UI and map interactions +//! +//! ## Key Components +//! - `InteractionSystemPlugin`: Main plugin for user interactions +//! - File drop handling system +//! - Input event processing +//! - Interaction state management +//! +//! ## Features +//! - Drag-and-drop file import (GeoJSON, etc.) +//! - Mouse and keyboard input handling +//! - Touch and gesture support preparation +//! - Context-sensitive interaction modes + use bevy::prelude::*; use bevy_map_viewer::ZoomChangedEvent; diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 73ffb2e..97923d2 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -1,3 +1,29 @@ +//! # LLM Integration Module +//! +//! This module provides Large Language Model (LLM) integration for AI-powered +//! analysis and insights of geographic and map data. +//! +//! ## Purpose +//! - Connect to OpenRouter API for access to various LLM providers +//! - Analyze geographic data and provide natural language insights +//! - Enable conversational interaction with map data +//! - Generate automated reports and summaries of spatial analysis +//! +//! ## Sub-modules +//! - `client`: OpenRouter API client implementation +//! - `openrouter_types`: Request/response data structures for API communication +//! +//! ## Key Features +//! - Support for multiple LLM providers through OpenRouter +//! - Contextual analysis of map features and spatial relationships +//! - Natural language querying of geographic data +//! - Integration with workspace data for comprehensive analysis +//! - Secure API key management and authentication +//! +//! ## Usage +//! The LLM client can analyze map features, answer questions about geographic data, +//! and provide insights based on spatial relationships and attribute data. + mod client; mod openrouter_types; diff --git a/src/main.rs b/src/main.rs index 81efe83..7baf172 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,29 @@ +//! # Map-RS Main Application +//! +//! This is the entry point for the Map-RS interactive map viewer application. +//! +//! ## Purpose +//! - Initializes the Bevy application with all necessary plugins +//! - Configures window settings and renderer options +//! - Sets up the main game loop and system scheduling +//! - Manages input absorption for UI interactions +//! +//! ## Architecture +//! The application uses a modular plugin system where each major feature +//! is implemented as a separate Bevy plugin: +//! - WorkspacePlugin: Core workspace and data management +//! - CameraSystemPlugin: Map navigation and camera controls +//! - InteractionSystemPlugin: User input handling +//! - RenderPlugin: GeoJSON and geographic data rendering +//! - SettingsPlugin: Application configuration +//! - ToolsPlugin: Interactive map tools +//! - DebugPlugin: Development and debugging utilities +//! +//! ## Key Systems +//! - `absorb_egui_inputs`: Prevents game input when UI is active +//! - Plugin initialization and dependency management +//! - Window and renderer configuration + use bevy::{ prelude::*, winit::{UpdateMode, WinitSettings}, diff --git a/src/overpass/mod.rs b/src/overpass/mod.rs index 8f1bce5..fe2f3da 100644 --- a/src/overpass/mod.rs +++ b/src/overpass/mod.rs @@ -1,3 +1,31 @@ +//! # Overpass API Module +//! +//! This module provides integration with the OpenStreetMap Overpass API for +//! querying and retrieving geographic data from OpenStreetMap. +//! +//! ## Purpose +//! - Connect to Overpass API endpoints for OSM data queries +//! - Build and execute complex spatial queries +//! - Parse and process OpenStreetMap data formats +//! - Provide efficient caching and data management for OSM data +//! +//! ## Sub-modules +//! - `client`: Overpass API client with query building and execution +//! - `overpass_types`: Data structures for OSM features and query responses +//! +//! ## Key Features +//! - Support for complex Overpass QL (Query Language) queries +//! - Spatial filtering and bounding box queries +//! - Feature type filtering (nodes, ways, relations) +//! - Efficient data parsing and conversion to internal formats +//! - Rate limiting and respectful API usage +//! +//! ## Query Types +//! - Bounding box queries for specific geographic areas +//! - Feature-based queries (buildings, roads, amenities, etc.) +//! - Attribute-based filtering and selection +//! - Spatial relationship queries + mod client; mod overpass_types; diff --git a/src/settings.rs b/src/settings.rs index 17ab70f..ac2c11d 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,3 +1,26 @@ +//! # Settings System Module +//! +//! This module manages application configuration and user preferences. +//! +//! ## Purpose +//! - Stores and manages user preferences and application settings +//! - Provides UI for configuring various application options +//! - Handles settings persistence and loading +//! - Manages theme and visual customization options +//! +//! ## Key Components +//! - `SettingsPlugin`: Main plugin for settings management +//! - Settings UI components and dialogs +//! - Configuration persistence systems +//! - Theme and visual customization +//! +//! ## Features +//! - User preference management +//! - Theme and color scheme customization +//! - Performance and rendering settings +//! - API configuration and credentials +//! - Import/export of settings + use bevy::prelude::*; #[allow(unused_imports)] use bevy_egui::{ diff --git a/src/tools/mod.rs b/src/tools/mod.rs index 5dffe0a..7b720f6 100644 --- a/src/tools/mod.rs +++ b/src/tools/mod.rs @@ -1,3 +1,34 @@ +//! # Interactive Tools Module +//! +//! This module provides interactive tools for map manipulation, measurement, +//! and data analysis within the map viewer interface. +//! +//! ## Purpose +//! - Provide interactive tools for map interaction and analysis +//! - Enable measurement of distances, areas, and spatial relationships +//! - Support creation and placement of map markers and annotations +//! - Facilitate workspace area selection and boundary definition +//! +//! ## Sub-modules +//! - `measure`: Distance and area measurement tools +//! - `pin`: Map marker and annotation placement tools +//! - `tool`: Base tool trait and common tool functionality +//! - `ui`: User interface components for tool interaction +//! - `work_space_selector`: Area selection tools for workspace definition +//! +//! ## Key Features +//! - Distance measurement between points +//! - Area calculation for polygonal regions +//! - Interactive marker placement and editing +//! - Workspace boundary selection and management +//! - Tool state management and mode switching +//! +//! ## Tool Types +//! - Measurement tools (distance, area, perimeter) +//! - Annotation tools (pins, labels, notes) +//! - Selection tools (rectangle, polygon, circle) +//! - Drawing tools (freehand, shapes) + mod measure; mod pin; mod tool; diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index c4fff25..c1b7490 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -1,3 +1,36 @@ +//! # Workspace Management Module +//! +//! This module provides comprehensive workspace management functionality for +//! organizing, persisting, and analyzing geographic data and map configurations. +//! +//! ## Purpose +//! - Manage different map workspace configurations and data layers +//! - Provide data persistence and session management +//! - Handle background processing of geographic data requests +//! - Coordinate between different data sources and analysis tools +//! - Enable collaborative workspace sharing and management +//! +//! ## Sub-modules +//! - `commands`: Workspace operation commands and state management +//! - `renderer`: Workspace-specific rendering and visualization +//! - `ui`: User interface components for workspace interaction +//! - `worker`: Background task processing and data pipeline management +//! - `workspace_types`: Core data structures and plugin implementation +//! +//! ## Key Features +//! - Multi-layered data organization and management +//! - Persistent workspace storage and loading +//! - Background processing of API requests and data analysis +//! - Integration with multiple data sources (OSM, weather, environmental) +//! - Real-time data updates and synchronization +//! - Collaborative workspace features preparation +//! +//! ## Workspace Components +//! - Data layers with independent styling and visibility +//! - Analysis results and cached computations +//! - User annotations and custom features +//! - API integration settings and credentials + use std::{ collections::{HashMap, HashSet}, sync::{Arc, Mutex}, diff --git a/src/workspace/ui.rs b/src/workspace/ui.rs index a3542ff..f8d2a89 100644 --- a/src/workspace/ui.rs +++ b/src/workspace/ui.rs @@ -25,6 +25,8 @@ pub struct ChatState { pub input_text: String, pub chat_history: Vec, pub is_processing: bool, + pub api_token: String, + pub token_verified: bool, } #[derive(Clone)] @@ -580,12 +582,61 @@ pub fn chat_box_ui( { chat_state.chat_history.clear(); } + + if chat_state.token_verified { + if ui.small_button("πŸ”‘").on_hover_text("Change API token").clicked() { + chat_state.token_verified = false; + chat_state.api_token.clear(); + chat_state.chat_history.clear(); + } + } }, ); }); ui.separator(); + // API Token input area + if !chat_state.token_verified { + ui.vertical(|ui| { + ui.label( + RichText::new("πŸ”‘ API Token Required") + .strong() + .color(egui::Color32::YELLOW), + ); + ui.add_space(5.0); + + ui.horizontal(|ui| { + let token_edit = egui::TextEdit::singleline(&mut chat_state.api_token) + .desired_width(chat_width - 80.0) + .hint_text("Enter your OpenRouter API token...") + .password(true); + + ui.add(token_edit); + + if ui.button("βœ“").on_hover_text("Verify token").clicked() && !chat_state.api_token.trim().is_empty() { + // Simple validation - just check if token is not empty + // In a real implementation, you might want to test the token with an API call + chat_state.token_verified = true; + chat_state.chat_history.push(ChatMessage { + content: "API token verified! You can now chat with the AI assistant.".to_string(), + is_user: false, + timestamp: chrono::Utc::now().format("%H:%M:%S").to_string(), + }); + } + }); + + ui.add_space(5.0); + ui.label( + RichText::new("Get your API token from: https://openrouter.ai/keys") + .small() + .color(egui::Color32::LIGHT_BLUE), + ); + }); + + ui.separator(); + } + // Messages area (scrollable) egui::ScrollArea::vertical() .auto_shrink([false, false]) @@ -597,11 +648,19 @@ pub fn chat_box_ui( if chat_state.chat_history.is_empty() { ui.vertical_centered(|ui| { ui.add_space(20.0); - ui.label( - RichText::new("Ask me about the map data!") - .color(egui::Color32::GRAY) - .italics(), - ); + if chat_state.token_verified { + ui.label( + RichText::new("Ask me about the map data!") + .color(egui::Color32::GRAY) + .italics(), + ); + } else { + ui.label( + RichText::new("Please enter your API token above to start chatting") + .color(egui::Color32::GRAY) + .italics(), + ); + } }); } else { for message in &chat_state.chat_history { @@ -626,27 +685,37 @@ pub fn chat_box_ui( ui.add_space(5.0); ui.separator(); - // Input area - ui.horizontal(|ui| { - let text_edit = egui::TextEdit::singleline(&mut chat_state.input_text) - .desired_width(chat_width - 60.0) - .hint_text("Ask about the map data..."); + // Input area - only show if token is verified + if chat_state.token_verified { + ui.horizontal(|ui| { + let text_edit = egui::TextEdit::singleline(&mut chat_state.input_text) + .desired_width(chat_width - 60.0) + .hint_text("Ask about the map data..."); - let response = ui.add(text_edit); + let response = ui.add(text_edit); - let send_clicked = - ui.button("πŸ“€").on_hover_text("Send message").clicked(); + let send_clicked = + ui.button("πŸ“€").on_hover_text("Send message").clicked(); - let enter_pressed = response.lost_focus() - && ui.input(|i| i.key_pressed(egui::Key::Enter)); + let enter_pressed = response.lost_focus() + && ui.input(|i| i.key_pressed(egui::Key::Enter)); - if (send_clicked || enter_pressed) - && !chat_state.input_text.trim().is_empty() - && !chat_state.is_processing - { - send_chat_message(&mut chat_state, &workspace); - } - }); + if (send_clicked || enter_pressed) + && !chat_state.input_text.trim().is_empty() + && !chat_state.is_processing + { + send_chat_message(&mut chat_state, &workspace); + } + }); + } else { + ui.vertical_centered(|ui| { + ui.label( + RichText::new("πŸ’‘ Enter your API token above to start chatting") + .color(egui::Color32::DARK_GRAY) + .italics(), + ); + }); + } }); }); }); @@ -685,7 +754,6 @@ fn render_chat_message(ui: &mut egui::Ui, message: &ChatMessage, max_width: f32) ); }); - // Message content with wrapping ui.add(egui::Label::new(RichText::new(&message.content).color(text_color)).wrap()); }); From ca9b91e73335fcc899793b0bf9b646a4ea8615b0 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Mon, 28 Jul 2025 20:08:41 +0100 Subject: [PATCH 06/12] Add support for llm agent --- src/llm/client.rs | 157 +++++++++--- src/llm/mod.rs | 35 ++- src/llm/openrouter_types.rs | 184 +++----------- src/workspace/mod.rs | 19 +- src/workspace/ui.rs | 418 ++++++++++++------------------- src/workspace/worker.rs | 51 +++- src/workspace/workspace_types.rs | 34 ++- 7 files changed, 432 insertions(+), 466 deletions(-) diff --git a/src/llm/client.rs b/src/llm/client.rs index d4dbab4..02865df 100644 --- a/src/llm/client.rs +++ b/src/llm/client.rs @@ -1,7 +1,9 @@ -use crate::llm::Request; +use serde_json::json; + +use crate::llm::LlmResponse; // https://openrouter.ai/docs/api-reference/overview -use super::OpenrouterClient; +use super::{Message, OpenrouterClient}; // ============================================================================ // LLM Request Command Specification // ============================================================================ @@ -33,48 +35,128 @@ use super::OpenrouterClient; // // ============================================================================ +// We should add a populiation decnity command pub const LLM_PROMPT: &str = r#" You are a geo-analysis assistant. -Answer user queries (e.g., "What is the population density here, and what if I build 200 more houses?"). -If data is required, use an rq: command. - -Commands: -// nb : Nearby features. ex: rq: nb {51.5,-0.09} r500 -// cnt : Count features. ex: rq: cnt {51.5,-0.09} r500 -// sm : Summarize features. ex: rq: sm {51.5,-0.09} r500 -// gt : Feature details by ID. ex: rq: gt 123456 -// t : Tags for feature. ex: rq: t 123456 -// bb : Features in bbox. ex: rq: bb {51.4,-0.1,51.6,-0.08} -// d : Distance between two. ex: rq: d {51.5,-0.09} {51.6,-0.10} -// n : Nearest feature. ex: rq: n {51.5,-0.09} -// ply : Features in polygon. ex: rq: ply {[51.5,-0.1],[51.6,-0.1],[51.6,-0.09]} -// i : General info/stats. ex: rq: i {51.5,-0.09} r500 - -Rules: -- Only use rq: commands to request data. -- Interpret returned JSON and respond concisely. - -Example: -User: What is the population density here? -Assistant: -rq: sm {51.5,-0.09} r500 +Your job is to answer user questions about geographic areas. Always follow this strict process: + +1. Use an `rq:` command to request data DO NOT ADD ANY OTHER TEXT TO THIS MESSAGE. +2. Wait for JSON data to be returned. +3. Interpret the data and give a clear, concise answer. +4. Do NOT guess or assume anything without data. +5. When words like here are used assume it means workspace and data can be gotten through commands + +--- Commands --- + +Needs specific parameters: +- rq: nb {lat,lon} r{radius} β†’ Nearby features +- rq: bb {lat1,lon1,lat2,lon2} β†’ Features in bounding box +- rq: d {lat1,lon1} {lat2,lon2} β†’ Distance between two points +- rq: n {lat,lon} β†’ Nearest feature +- rq: ply {[lat,lon],[lat,lon],...} β†’ Features inside polygon + +Context from workspace (no parameters) + +- rq: s β†’ Get the selection information +- rq: i β†’ General summary for an area +- rq: cnt β†’ Count features +- rq: sm β†’ Summarize attributes (e.g. population, area) + +Id parameter +- rq: gt β†’ Get full details for a feature +- rq: t β†’ Get tag metadata + +Points must be in {lat,lon} format. Distances like r500 mean 500 meters. + +--- Examples --- + +User: What's nearby at {51.5,-0.09}? + +Assistant: rq: nb {51.5,-0.09} r500 + +{Wait wait for data} + +{Analyse data or request more} + +Assistant: There are 18 nearby features within 500 meters, including residential buildings, a park, and a school. + +--- + +User: What's the population density at {51.5,-0.09}? + +Assistant: rq: sm {51.5,-0.09} r500 + +{Wait wait for data} + +{Analyse data or request more} + +Assistant: The population density is 15 people per 100 mΒ² (or 15,000 per kmΒ²). + +--- + +User: What happens if I add 200 houses here? + +Assistant: rq: sm {51.5,-0.09} r500 + +{Wait wait for data} + +{Analyse data or request more} + +Assistant: Currently, there are 500 households. Adding 200 would increase that by 40%. If average household size stays the same, population would increase from 1500 to ~2100. + +--- + +Rules Recap: +- Only use `rq:` to request data DO NOT ADD ANY OTHER TEXT TO THIS MESSAGE. +- Don’t guess β€” answer only after data is returned. +- Be concise and directly answer the user's question. + "#; +// I really should make my own llm api using ureq! impl OpenrouterClient { - // We want to also parse in the get_data_from_string_osm but maybe we want to format it differently (we should try that first) - // We also need to parse through the selection data like size and location! - /* - pub fn send_openrouter_chat_string(&self, request: Request) -> Result { - // This will be a put request - // want to parse throguh the request in the body as a json - // Remember hearder needs bearer token - // Would be good to find out how to do the streaing but first just await response. + fn build_messages_json(&self, mess: &Vec) -> serde_json::Value { + let mut messages = vec![json!({ + "role": "system", + "content": self.prompt + })]; + + for message in mess { + messages.push(json!({ + "role": message.role, + "content": message.content + })); + } + + json!(messages) + } + + pub fn send_openrouter_chat( + &self, + messages: &Vec, + ) -> Result { + let body = json!({ + "temperature": 0.0, + "model": "deepseek/deepseek-chat-v3-0324:free", + "messages": self.build_messages_json(messages) + }); + let mut status = 429; while status == 429 { - if let Ok(mut response) = self.agent.post(&self.url).send(&request) { + let body_clone = body.clone(); // Clone the body for each request attempt + if let Ok(mut response) = self + .agent + .post(&self.url) + .header( + "Authorization", + format!("Bearer {}", self.token.as_ref().unwrap().to_string()), + ) + .send_json(body_clone) + { if response.status() == 200 { - return response.body_mut().read_to_string(); + let res: LlmResponse = response.body_mut().read_json()?; + return Ok(res); } else if response.status() == 429 { std::thread::sleep(std::time::Duration::from_secs(5)); } else { @@ -82,9 +164,6 @@ impl OpenrouterClient { } } } - Err(ureq::Error::BadUri( - "Error sending/making request!".to_string(), - )) + Err(ureq::Error::ConnectionFailed) } - */ } diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 97923d2..0d40f5f 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -1,25 +1,25 @@ //! # LLM Integration Module -//! +//! //! This module provides Large Language Model (LLM) integration for AI-powered //! analysis and insights of geographic and map data. -//! +//! //! ## Purpose //! - Connect to OpenRouter API for access to various LLM providers //! - Analyze geographic data and provide natural language insights //! - Enable conversational interaction with map data //! - Generate automated reports and summaries of spatial analysis -//! +//! //! ## Sub-modules //! - `client`: OpenRouter API client implementation //! - `openrouter_types`: Request/response data structures for API communication -//! +//! //! ## Key Features //! - Support for multiple LLM providers through OpenRouter //! - Contextual analysis of map features and spatial relationships //! - Natural language querying of geographic data //! - Integration with workspace data for comprehensive analysis //! - Secure API key management and authentication -//! +//! //! ## Usage //! The LLM client can analyze map features, answer questions about geographic data, //! and provide insights based on spatial relationships and attribute data. @@ -30,25 +30,42 @@ mod openrouter_types; use std::fmt::Display; pub use openrouter_types::*; +use serde::{Deserialize, Serialize}; use ureq::{Agent, unversioned::transport::time::Duration}; -#[derive(Clone)] +use crate::llm::client::LLM_PROMPT; + +#[derive(Clone, Serialize)] pub struct OpenrouterClient { pub token: Option, + #[serde(skip)] pub agent: Agent, pub url: String, + pub prompt: String, +} + +impl Default for OpenrouterClient { + fn default() -> Self { + let config = Agent::config_builder().build(); + let agent: Agent = config.into(); + OpenrouterClient { + agent, + url: String::new(), + token: None, + prompt: LLM_PROMPT.to_string(), + } + } } impl OpenrouterClient { pub fn new(url: impl Display, token: Option) -> Self { - let config = Agent::config_builder() - .timeout_global(Some(*Duration::from_secs(5))) - .build(); + let config = Agent::config_builder().build(); let agent: Agent = config.into(); OpenrouterClient { agent, url: url.to_string(), token, + prompt: LLM_PROMPT.to_string(), } } diff --git a/src/llm/openrouter_types.rs b/src/llm/openrouter_types.rs index 1bd131f..20c264c 100644 --- a/src/llm/openrouter_types.rs +++ b/src/llm/openrouter_types.rs @@ -1,148 +1,46 @@ use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -pub struct AnalysisRequestType {} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct Request { - pub messages: Option>, - pub prompt: Option, - pub model: Option, - pub response_format: Option, - pub stop: Option, - pub stream: Option, - pub max_tokens: Option, - pub temperature: Option, - pub tools: Option>, - pub tool_choice: Option, - pub seed: Option, - pub top_p: Option, - pub top_k: Option, - pub frequency_penalty: Option, - pub presence_penalty: Option, - pub repetition_penalty: Option, - pub logit_bias: Option>, - pub top_logprobs: Option, - pub min_p: Option, - pub top_a: Option, - pub prediction: Option, - pub transforms: Option>, - pub models: Option>, - pub route: Option, - pub user: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct ResponseFormat { - pub r#type: String, // "json_object" -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum Stop { - Single(String), - Multiple(Vec), -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Role { - User, - Assistant, - System, - Tool, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum ContentPart { - Text(TextContent), - Image(ImageContentPart), -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct TextContent { - pub r#type: String, // "text" - pub text: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ImageContentPart { - pub r#type: String, // "image_url" - pub image_url: ImageUrl, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ImageUrl { - pub url: String, - pub detail: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum Message { - UserAssistantSystem { - role: Role, - content: MessageContent, - name: Option, - }, - Tool { - role: Role, // must be Tool - content: String, - tool_call_id: String, - name: Option, - }, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum MessageContent { - Text(String), - Parts(Vec), -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Tool { - pub r#type: String, // "function" - pub function: FunctionDescription, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FunctionDescription { - pub name: String, - pub description: Option, - pub parameters: serde_json::Value, // JSON Schema -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum ToolChoice { - None, - Auto, - Function { - r#type: String, // "function" - function: ToolFunctionChoice, - }, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct ToolFunctionChoice { - pub name: String, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Prediction { - pub r#type: String, // "content" +use serde_json::Value; + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LlmResponse { + pub id: String, + pub provider: String, + pub model: String, + pub object: String, + pub created: i64, + pub choices: Vec, + pub usage: Usage, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Choice { + pub logprobs: Value, + #[serde(rename = "finish_reason")] + pub finish_reason: String, + #[serde(rename = "native_finish_reason")] + pub native_finish_reason: String, + pub index: i64, + pub message: Message, +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Message { + pub role: String, pub content: String, + pub refusal: Value, + pub reasoning: Value, } -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum Route { - Fallback, +#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Usage { + #[serde(rename = "prompt_tokens")] + pub prompt_tokens: i64, + #[serde(rename = "completion_tokens")] + pub completion_tokens: i64, + #[serde(rename = "total_tokens")] + pub total_tokens: i64, } - -// pub fn Addget_data_from_string_osm -// add workspace info! diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index c1b7490..c0e79eb 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -1,22 +1,22 @@ //! # Workspace Management Module -//! +//! //! This module provides comprehensive workspace management functionality for //! organizing, persisting, and analyzing geographic data and map configurations. -//! +//! //! ## Purpose //! - Manage different map workspace configurations and data layers //! - Provide data persistence and session management //! - Handle background processing of geographic data requests //! - Coordinate between different data sources and analysis tools //! - Enable collaborative workspace sharing and management -//! +//! //! ## Sub-modules //! - `commands`: Workspace operation commands and state management //! - `renderer`: Workspace-specific rendering and visualization //! - `ui`: User interface components for workspace interaction //! - `worker`: Background task processing and data pipeline management //! - `workspace_types`: Core data structures and plugin implementation -//! +//! //! ## Key Features //! - Multi-layered data organization and management //! - Persistent workspace storage and loading @@ -24,7 +24,7 @@ //! - Integration with multiple data sources (OSM, weather, environmental) //! - Real-time data updates and synchronization //! - Collaborative workspace features preparation -//! +//! //! ## Workspace Components //! - Data layers with independent styling and visibility //! - Analysis results and cached computations @@ -43,7 +43,11 @@ use serde_json::Value; use worker::WorkspaceWorker; pub use workspace_types::*; -use crate::{geojson::MapFeature, llm::OpenrouterClient, overpass::OverpassClient}; +use crate::{ + geojson::MapFeature, + llm::{LlmResponse, Message, OpenrouterClient}, + overpass::OverpassClient, +}; mod commands; mod renderer; @@ -92,6 +96,7 @@ pub struct WorkspaceData { last_modified: i64, requests: HashSet, properties: HashMap<(String, Value), Srgba>, + messages: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -104,7 +109,7 @@ pub struct WorkspaceRequest { #[serde(skip)] processed_data: RTree, #[serde(skip)] - llm_analysis: Vec, + llm_analysis: Vec, last_query_date: i64, // When the OSM data was fetched } diff --git a/src/workspace/ui.rs b/src/workspace/ui.rs index f8d2a89..4e45680 100644 --- a/src/workspace/ui.rs +++ b/src/workspace/ui.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::sync::{Arc, Mutex}; use bevy::prelude::*; use bevy_egui::{ @@ -8,7 +9,7 @@ use bevy_egui::{ use bevy_map_viewer::{ Coord, EguiBlockInputState, MapViewerMarker, TileMapResources, ZoomChangedEvent, game_to_coord, }; -use rstar::{AABB, Envelope, RTreeObject}; +use rstar::{AABB, Envelope}; use uuid::Uuid; use crate::{ @@ -20,20 +21,39 @@ use crate::{ use super::{RequestType, WorkspaceRequest}; -#[derive(Resource, Default)] +// This should go into workspace so it can be saved. +#[derive(Resource)] pub struct ChatState { + pub inner: Arc>, +} + +impl Default for ChatState { + fn default() -> Self { + Self { + inner: Arc::new(Mutex::new(ChatStateInner::default())), + } + } +} + +impl Clone for ChatState { + fn clone(&self) -> Self { + Self { + inner: Arc::clone(&self.inner), + } + } +} + +#[derive(Default, Clone)] +pub struct ChatStateInner { pub input_text: String, pub chat_history: Vec, pub is_processing: bool, - pub api_token: String, - pub token_verified: bool, } #[derive(Clone)] pub struct ChatMessage { pub content: String, pub is_user: bool, - pub timestamp: String, } pub fn workspace_actions_ui( @@ -404,138 +424,10 @@ fn center(selection: &super::Selection, tile_map_res: &TileMapResources) -> Vec2 } } -// We should add a right panel for area analysis -/* -Right Panel: Area Analysis -1. Customizable Styles - - Color by Attribute: - A dropdown for selecting the attribute to color by - (e.g., "Building type," "Land use," "Population density"). - This dynamically updates the map with chosen color schemes. - - Size by Attribute: - Allow users to adjust point size based on specific attributes, - such as the "size of buildings" or "household income." - - Icons for Points: - Enable users to select different icons for points on the map based on attributes - (e.g., a house icon for residences, a tree for parks). - -2. Thematic Mapping: Choropleths - - Dropdown for Layer Selection: - A list to choose which layers to apply choropleth styling to. - - Legend for Choropleth: - Automatically generate a color scale showing the range of values - (e.g., darker shades of blue for higher population density). - -3. Legend Generation - - Auto-generated Legends: - Once you style a layer or apply thematic mapping, - the panel will automatically generate a legend explaining the color, size, and icons used on the map. - -4. Charts & Statistics - - House Type Breakdown: - Show a pie chart or bar graph breaking down the area by different house types - (e.g., "Detached," "Semi-detached," "Apartments"). - - Area Size: - Display the total size (area) of a selected polygon or area (e.g., "Total area: 150,000 mΒ²"). - - Population Density: - Show the population density for the selected area (e.g., people per kmΒ²). - - Weather Analysis: - Include a weather summary for the selected area, - such as temperature, precipitation, or other relevant weather parameters. - - UV & Sunlight Analysis: - Provide insights about the UV index or sunlight exposure in the area, - possibly overlaying heat maps to indicate high/low sunlight regions. - - Attribute Analysis: - A detailed breakdown of any attributes present in the area - (e.g., "Number of residential buildings," "Average household income"). - -5. Layer Pickers - - Add Layers: - Allow users to add additional data layers, such as road networks, land use, pollution levels, etc. - - Layer Toggle: - Let users enable/disable specific layers in the analysis. - -UI Layout Suggestions: - - Top Section: - A title or summary of the selected area (e.g., "Cambridge - Central Area"). - - Left Section: - A panel with sliders/dropdowns for custom styles, color by attribute, and thematic mapping options. - - Middle Section: - Interactive charts (pie charts, bar graphs, heatmaps) showing key statistics, - such as population density and area size. - - Bottom Section: - A mini map preview (if space allows), or layer picker dropdown with checkboxes to enable/disable data layers. -*/ -// Have a little option to hide to the side. Also allow resizing. -pub fn workspace_analysis_ui(mut contexts: EguiContexts, workspace_res: ResMut) { - if let Some(workspace) = &workspace_res.workspace { - let ctx = contexts.ctx_mut(); - let screen_rect = ctx.screen_rect(); - - let tilebox_width = 200.0; - let tilebox_height = (screen_rect.height() - 40.0) * 0.25; // 1/4 of the right panel height - - let tilebox_pos = egui::pos2(screen_rect.width() - tilebox_width - 10.0, 30.0); - - // Number of items: - let mut item_count = 0; - for lists in workspace_res.get_rendered_requests().iter() { - item_count += lists.get_processed_data().size(); - } - let workspace_area = workspace.get_area(); - egui::Area::new("info".into()) - .fixed_pos(tilebox_pos) - .show(ctx, |ui| { - egui::Frame::new() - .fill(egui::Color32::from_rgba_premultiplied(30, 30, 30, 255)) - .corner_radius(10.0) - .shadow(egui::epaint::Shadow { - color: egui::Color32::from_black_alpha(60), - offset: [5, 5], - blur: 10, - spread: 5, - }) - .show(ui, |ui| { - ui.set_width(tilebox_width); - ui.set_height(tilebox_height); - ui.vertical_centered(|ui| { - ui.label(""); - ui.spacing_mut().item_spacing = egui::vec2(8.0, 10.0); - ui.label(RichText::new("Workspace Analysis").strong()); - ui.separator(); - ui.label(format!("Number of items: {}", item_count)); - ui.label(format!( - "Workspace area: {} {:#?}", - workspace_area.0, workspace_area.1 - )); - }); - }); - }); - } -} - pub fn chat_box_ui( mut contexts: EguiContexts, - mut chat_state: ResMut, - workspace: Res, + chat_state: Res, + mut workspace: ResMut, ) { let ctx = contexts.ctx_mut(); let screen_rect = ctx.screen_rect(); @@ -545,7 +437,7 @@ pub fn chat_box_ui( } else { 200.0 }; // Same width as workspace_analysis_ui - let chat_height = (screen_rect.height() - 50.0); + let chat_height = screen_rect.height() - 50.0; let chat_pos = egui::pos2( screen_rect.width() - chat_width - 10.0, 40.0, // Position under item_info with some spacing @@ -580,14 +472,8 @@ pub fn chat_box_ui( |ui| { if ui.small_button("πŸ—‘").on_hover_text("Clear chat").clicked() { - chat_state.chat_history.clear(); - } - - if chat_state.token_verified { - if ui.small_button("πŸ”‘").on_hover_text("Change API token").clicked() { - chat_state.token_verified = false; - chat_state.api_token.clear(); - chat_state.chat_history.clear(); + if let Ok(mut inner) = chat_state.inner.lock() { + inner.chat_history.clear(); } } }, @@ -596,48 +482,6 @@ pub fn chat_box_ui( ui.separator(); - // API Token input area - if !chat_state.token_verified { - ui.vertical(|ui| { - ui.label( - RichText::new("πŸ”‘ API Token Required") - .strong() - .color(egui::Color32::YELLOW), - ); - ui.add_space(5.0); - - ui.horizontal(|ui| { - let token_edit = egui::TextEdit::singleline(&mut chat_state.api_token) - .desired_width(chat_width - 80.0) - .hint_text("Enter your OpenRouter API token...") - .password(true); - - ui.add(token_edit); - - if ui.button("βœ“").on_hover_text("Verify token").clicked() && !chat_state.api_token.trim().is_empty() { - // Simple validation - just check if token is not empty - // In a real implementation, you might want to test the token with an API call - chat_state.token_verified = true; - chat_state.chat_history.push(ChatMessage { - content: "API token verified! You can now chat with the AI assistant.".to_string(), - is_user: false, - timestamp: chrono::Utc::now().format("%H:%M:%S").to_string(), - }); - } - }); - - ui.add_space(5.0); - ui.label( - RichText::new("Get your API token from: https://openrouter.ai/keys") - .small() - .color(egui::Color32::LIGHT_BLUE), - ); - }); - - ui.separator(); - } - - // Messages area (scrollable) egui::ScrollArea::vertical() .auto_shrink([false, false]) .stick_to_bottom(true) @@ -645,50 +489,42 @@ pub fn chat_box_ui( .show(ui, |ui| { ui.set_width(chat_width - 20.0); - if chat_state.chat_history.is_empty() { - ui.vertical_centered(|ui| { - ui.add_space(20.0); - if chat_state.token_verified { + if let Ok(inner) = chat_state.inner.lock() { + if inner.chat_history.is_empty() { + ui.vertical_centered(|ui| { + ui.add_space(20.0); ui.label( RichText::new("Ask me about the map data!") .color(egui::Color32::GRAY) .italics(), ); - } else { + }); + } else { + for message in &inner.chat_history { + render_chat_message(ui, message, chat_width - 30.0); + ui.add_space(8.0); + } + } // Show loading indicator if processing + if inner.is_processing { + ui.horizontal(|ui| { + ui.add_space(10.0); + ui.spinner(); ui.label( - RichText::new("Please enter your API token above to start chatting") - .color(egui::Color32::GRAY) - .italics(), + RichText::new("AI is thinking...") + .color(egui::Color32::GRAY), ); - } - }); - } else { - for message in &chat_state.chat_history { - render_chat_message(ui, message, chat_width - 30.0); - ui.add_space(8.0); + }); } } - - // Show loading indicator if processing - if chat_state.is_processing { - ui.horizontal(|ui| { - ui.add_space(10.0); - ui.spinner(); - ui.label( - RichText::new("AI is thinking...") - .color(egui::Color32::GRAY), - ); - }); - } }); ui.add_space(5.0); ui.separator(); - // Input area - only show if token is verified - if chat_state.token_verified { - ui.horizontal(|ui| { - let text_edit = egui::TextEdit::singleline(&mut chat_state.input_text) + // Input area + ui.horizontal(|ui| { + if let Ok(mut inner) = chat_state.inner.lock() { + let text_edit = egui::TextEdit::singleline(&mut inner.input_text) .desired_width(chat_width - 60.0) .hint_text("Ask about the map data..."); @@ -701,21 +537,29 @@ pub fn chat_box_ui( && ui.input(|i| i.key_pressed(egui::Key::Enter)); if (send_clicked || enter_pressed) - && !chat_state.input_text.trim().is_empty() - && !chat_state.is_processing + && !inner.input_text.trim().is_empty() + && !inner.is_processing { - send_chat_message(&mut chat_state, &workspace); + let user_message = inner.input_text.trim().to_string(); + inner.input_text.clear(); + inner.is_processing = true; + + // Add user message to history + inner.chat_history.push(ChatMessage { + content: user_message.clone(), + is_user: true, + }); + + // Drop the lock before calling send_chat_message_background + drop(inner); + send_chat_message_background( + &chat_state, + &mut workspace, + user_message, + ); } - }); - } else { - ui.vertical_centered(|ui| { - ui.label( - RichText::new("πŸ’‘ Enter your API token above to start chatting") - .color(egui::Color32::DARK_GRAY) - .italics(), - ); - }); - } + } + }); }); }); }); @@ -763,34 +607,102 @@ fn render_chat_message(ui: &mut egui::Ui, message: &ChatMessage, max_width: f32) }); } -fn send_chat_message(chat_state: &mut ChatState, workspace: &Workspace) { - let user_message = chat_state.input_text.trim().to_string(); - - // Add user message to chat - chat_state.chat_history.push(ChatMessage { - content: user_message.clone(), - is_user: true, - timestamp: chrono::Utc::now().format("%H:%M:%S").to_string(), - }); +fn send_chat_message_background( + chat_state: &ChatState, + workspace: &mut Workspace, + user_message: String, +) { + let mut llm_client = workspace.llm_agent.clone(); + llm_client.set_token("!! REPLACE ME WITH TOKEN !!"); + + // Build context information about the current workspace and selection + let mut context_info = String::new(); + + if let Some(workspace_data) = &workspace.workspace { + let area = workspace_data.get_area(); + let selection = workspace_data.get_selection(); + + context_info.push_str(&format!( + "Current workspace: {}\n", + workspace_data.get_name() + )); + context_info.push_str(&format!("Selection area: {:.2} {:#?}\n", area.0, area.1)); + context_info.push_str(&format!( + "Selection type: {:#?}\n", + selection.selection_type + )); + + // Add coordinate information based on selection type + match selection.selection_type { + crate::workspace::SelectionType::RECTANGLE => { + if let (Some(start), Some(end)) = (selection.start, selection.end) { + context_info.push_str(&format!( + "Bounding box: SW({:.6}, {:.6}) to NE({:.6}, {:.6})\n", + start.lat, start.long, end.lat, end.long + )); + } + } + crate::workspace::SelectionType::CIRCLE => { + if let (Some(center), Some(edge)) = (selection.start, selection.end) { + let radius = center.distance(&edge); + context_info.push_str(&format!( + "Center: ({:.6}, {:.6}), Radius: {:.2} {:#?}\n", + center.lat, center.long, radius.0, radius.1 + )); + } + } + crate::workspace::SelectionType::POLYGON => { + if let Some(points) = &selection.points { + context_info.push_str(&format!("Polygon with {} vertices\n", points.len())); + for (i, point) in points.iter().take(5).enumerate() { + context_info.push_str(&format!( + " Point {}: ({:.6}, {:.6})\n", + i + 1, + point.lat, + point.long + )); + } + if points.len() > 5 { + context_info.push_str(" ...\n"); + } + } + } + _ => {} + } - // Clear input - chat_state.input_text.clear(); + // Add information about loaded data + let requests = workspace.get_rendered_requests(); + let total_features: usize = requests.iter().map(|r| r.get_processed_data().size()).sum(); + context_info.push_str(&format!("Total features loaded: {}\n", total_features)); + context_info.push_str(&format!("Data layers: {}\n", requests.len())); + } else { + context_info.push_str("No workspace selected\n"); + } - // Set processing state - chat_state.is_processing = true; + // Create the enhanced user message with context + let enhanced_message = if context_info.trim().is_empty() { + user_message.clone() + } else { + format!("CONTEXT:\n{}\nUSER QUERY: {}", context_info, user_message) + }; + if let Some(workspace) = workspace.workspace.as_mut() { + workspace.add_message("user", &enhanced_message); + } - // TODO: Replace this with actual LLM integration - // For now, simulate AI response - chat_state.chat_history.push(ChatMessage { - content: format!("I received your message: '{}'. LLM integration coming soon! I can analyze map data, features, and spatial relationships.", user_message), - is_user: false, - timestamp: chrono::Utc::now().format("%H:%M:%S").to_string(), - }); + // Attempt to get response from LLM - chat_state.is_processing = false; + let request = WorkspaceRequest::new( + Uuid::new_v4().to_string(), + 1, + RequestType::OpenRouterRequest(), + Vec::new(), + ); + workspace.worker.queue_request(request); - // Keep only last 50 messages to prevent memory issues - while chat_state.chat_history.len() > 50 { - chat_state.chat_history.remove(0); + // Clean up old messages + if let Ok(mut inner) = chat_state.inner.lock() { + while inner.chat_history.len() > 50 { + inner.chat_history.remove(0); + } } } diff --git a/src/workspace/worker.rs b/src/workspace/worker.rs index 2210c28..ed7419a 100644 --- a/src/workspace/worker.rs +++ b/src/workspace/worker.rs @@ -1,3 +1,4 @@ +use crate::workspace::ui::{ChatMessage, ChatState}; use crate::workspace::{RequestType, WorkspaceRequest}; use bevy::prelude::*; use bevy::tasks::{AsyncComputeTaskPool, Task}; @@ -32,7 +33,11 @@ impl WorkspaceWorker { } } -pub fn process_requests(mut commands: Commands, mut workspace: ResMut) { +pub fn process_requests( + mut commands: Commands, + mut workspace: ResMut, + chat_state: ResMut, +) { let task_pool = AsyncComputeTaskPool::get(); let pending_requests = workspace.worker.pending_requests.clone(); let active_tasks = workspace.worker.active_tasks.clone(); @@ -67,8 +72,11 @@ pub fn process_requests(mut commands: Commands, mut workspace: ResMut return; } let overpass_client = workspace.overpass_agent.clone(); + let openrouter_client = workspace.llm_agent.clone(); // Clone the loaded_requests Arc instead of holding the MutexGuard let loaded_requests = workspace.loaded_requests.clone(); + let ws: Option = workspace.workspace.clone(); + let cs = chat_state.clone(); let task = task_pool.spawn(async move { let mut result = Vec::new(); match request.get_request() { @@ -79,9 +87,46 @@ pub fn process_requests(mut commands: Commands, mut workspace: ResMut } } } - RequestType::OpenRouterRequest(_) => todo!(), + RequestType::OpenRouterRequest() => { + if let Some(mut workspace) = ws { + if let Ok(q) = openrouter_client.send_openrouter_chat(&workspace.messages) { + if let Some(choice) = q.choices.first() { + let ai_message = choice.message.content.clone(); + + workspace.add_message("assistant", &ai_message); + + // Update chat state with thread-safe access + if let Ok(mut inner) = cs.inner.lock() { + inner.chat_history.push(ChatMessage { + content: ai_message.clone(), + is_user: false, + }); + inner.is_processing = false; + } + if &ai_message[0..2] == "rq" { + // Now we need to handle the request sent by the agent! + } + } else { + // Handle case where no choices are returned + workspace.add_message("assistant", "Sorry, I didn't receive a proper response from the AI. Please try again."); + + // Update chat state with thread-safe access + if let Ok(mut inner) = cs.inner.lock() { + inner.chat_history.push(ChatMessage { + content: + "Sorry, I didn't receive a proper response from the AI. Please try again." + .to_string(), + is_user: false, + }); + inner.is_processing = false; + } + bevy::log::info!("here3"); + + } + } + } + } RequestType::OpenMeteoRequest(_open_meteo_request) => {} - RequestType::AnalysisRequest(_) => todo!(), } request.raw_data = result.clone(); diff --git a/src/workspace/workspace_types.rs b/src/workspace/workspace_types.rs index 2c07ec4..2aa860c 100644 --- a/src/workspace/workspace_types.rs +++ b/src/workspace/workspace_types.rs @@ -9,15 +9,14 @@ use uuid::Uuid; use crate::{ geojson::{MapFeature, get_data_from_string_osm}, + llm::{Message, OpenrouterClient}, workspace::ui::chat_box_ui, }; use super::{ Workspace, WorkspaceData, WorkspacePlugin, WorkspaceRequest, renderer::render_workspace_requests, - ui::{ - ChatState, PersistentInfoWindows, item_info, workspace_actions_ui, workspace_analysis_ui, - }, + ui::{ChatState, PersistentInfoWindows, item_info, workspace_actions_ui}, worker::{cleanup_tasks, process_requests}, }; @@ -103,6 +102,21 @@ impl Workspace { } } +impl WorkspaceData { + pub fn add_message(&mut self, role: &str, content: &str) { + self.messages.push(Message { + role: role.to_string(), + content: content.to_string(), + refusal: serde_json::Value::Null, + reasoning: serde_json::Value::Null, + }); + } + + pub fn clear_history(&mut self) { + self.messages.clear(); + } +} + impl WorkspaceData { pub fn get_color_properties(&self) -> HashMap<(String, serde_json::Value), Srgba> { self.properties.clone() @@ -209,11 +223,7 @@ impl WorkspaceRequest { } } crate::workspace::RequestType::OpenMeteoRequest(_) => {} - crate::workspace::RequestType::OpenRouterRequest(_) => {} - RequestType::OpenMeteoRequest(open_meteo_request) => todo!(), - RequestType::OverpassTurboRequest(_) => todo!(), - RequestType::OpenRouterRequest(_) => todo!(), - RequestType::AnalysisRequest(_) => todo!(), + crate::workspace::RequestType::OpenRouterRequest() => {} } } @@ -246,8 +256,7 @@ pub enum RequestType { // If we want to add more requests we can just add them here. OpenMeteoRequest(OpenMeteoRequest), OverpassTurboRequest(String), - OpenRouterRequest(String), - AnalysisRequest(String), + OpenRouterRequest(), } impl std::fmt::Debug for RequestType { @@ -255,8 +264,7 @@ impl std::fmt::Debug for RequestType { match self { RequestType::OpenMeteoRequest(_) => write!(f, "OpenMeteoRequest"), RequestType::OverpassTurboRequest(_) => write!(f, "OverpassTurboRequest"), - RequestType::OpenRouterRequest(_) => write!(f, "OpenRouterRequest"), - RequestType::AnalysisRequest(_) => todo!(), + RequestType::OpenRouterRequest() => write!(f, "OpenRouterRequest"), } } } @@ -304,6 +312,7 @@ impl WorkspaceData { last_modified: chrono::Utc::now().timestamp(), requests: HashSet::new(), properties: HashMap::new(), + messages: Vec::new(), } } pub fn empty() -> Self { @@ -315,6 +324,7 @@ impl WorkspaceData { last_modified: chrono::Utc::now().timestamp(), requests: HashSet::new(), properties: HashMap::new(), + messages: Vec::new(), } } } From 4d69ea1dca382910a39b71163570a7541e010282 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Mon, 28 Jul 2025 20:13:27 +0100 Subject: [PATCH 07/12] Clippy fix --- src/camera.rs | 1 - src/debug.rs | 2 +- src/geojson/loader.rs | 4 ++-- src/geojson/renderer.rs | 8 ++++---- src/geojson/types.rs | 13 +------------ src/interaction.rs | 22 +++++++--------------- src/llm/client.rs | 2 +- src/llm/mod.rs | 4 ++-- src/overpass/client.rs | 4 ++-- src/workspace/commands.rs | 13 ++++++------- src/workspace/mod.rs | 5 +---- src/workspace/ui.rs | 12 ++++++------ src/workspace/workspace_types.rs | 11 +++++------ 13 files changed, 38 insertions(+), 63 deletions(-) diff --git a/src/camera.rs b/src/camera.rs index b6689fb..b792ff2 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -46,7 +46,6 @@ use bevy_map_viewer::{Coord, MapViewerMarker, MapViewerPlugin, TileMapResources} use bevy_pancam::{DirectionKeys, PanCam, PanCamPlugin}; use bevy_map_viewer::EguiBlockInputState; -use directories::UserDirs; use platform_dirs::AppDirs; const SHADER_ASSET_PATH: &str = "shaders/full_screen_pass.wgsl"; diff --git a/src/debug.rs b/src/debug.rs index 5bed16c..4034671 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -131,6 +131,6 @@ pub fn count_entities( ) { for mut span in &mut query { let entity_count = query_entity.iter().count(); - **span = format!("{}", entity_count); + **span = format!("{entity_count}"); } } diff --git a/src/geojson/loader.rs b/src/geojson/loader.rs index 0e52734..d389ffb 100644 --- a/src/geojson/loader.rs +++ b/src/geojson/loader.rs @@ -2,7 +2,7 @@ use std::{fs::File, io::BufReader}; use bevy::log::info; use bevy_map_viewer::Coord; -use geojson::{GeoJson, JsonValue}; +use geojson::GeoJson; use rstar::RTree; use serde::{Deserialize, Serialize}; @@ -140,7 +140,7 @@ pub fn get_map_data(file_path: &str) -> Result, Box, tile_map_manager: Res, workspace: Res, - mut zoom_change: EventReader, + zoom_change: EventReader, mut meshes: ResMut>, mut materials: ResMut>, ) { @@ -192,12 +192,12 @@ impl MeshConstructor { self.index_offset += geometry.vertices.len() as u32; } fn to_mesh(&self) -> Mesh { - let mesh = Mesh::new( + + Mesh::new( PrimitiveTopology::TriangleList, RenderAssetUsages::default(), ) .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, self.vertices.clone()) - .with_inserted_indices(Indices::U32(self.indices.clone())); - mesh + .with_inserted_indices(Indices::U32(self.indices.clone())) } } diff --git a/src/geojson/types.rs b/src/geojson/types.rs index 3882ee1..3963078 100644 --- a/src/geojson/types.rs +++ b/src/geojson/types.rs @@ -1,4 +1,3 @@ -use std::collections::HashSet; use bevy::prelude::*; use bevy_map_viewer::{Coord, TileMapResources}; @@ -25,18 +24,8 @@ impl MapFeature { } new_points } - - fn extract_attributes_from_properties(&self) -> HashSet { - let mut attributes = HashSet::new(); - - if let serde_json::Value::Object(properties) = &self.properties { - for key in properties.keys() { - attributes.insert(key.clone()); - } - } - attributes - } } + impl RTreeObject for MapFeature { type Envelope = AABB<[f64; 2]>; diff --git a/src/interaction.rs b/src/interaction.rs index 5d8729f..1cc8edd 100644 --- a/src/interaction.rs +++ b/src/interaction.rs @@ -1,19 +1,19 @@ //! # Interaction System Module -//! +//! //! This module handles user input and interaction with the map viewer. -//! +//! //! ## Purpose //! - Processes user input events (mouse, keyboard, file drops) //! - Handles file drag-and-drop functionality for data import //! - Manages interaction states and user interface events //! - Coordinates between UI and map interactions -//! +//! //! ## Key Components //! - `InteractionSystemPlugin`: Main plugin for user interactions //! - File drop handling system //! - Input event processing //! - Interaction state management -//! +//! //! ## Features //! - Drag-and-drop file import (GeoJSON, etc.) //! - Mouse and keyboard input handling @@ -21,9 +21,7 @@ //! - Context-sensitive interaction modes use bevy::prelude::*; -use bevy_map_viewer::ZoomChangedEvent; -use crate::workspace::Workspace; pub struct InteractionSystemPlugin; @@ -33,26 +31,20 @@ impl Plugin for InteractionSystemPlugin { } } -fn file_drop( - mut evr_dnd: EventReader, - workspace_res: ResMut, - zoom_event: EventWriter, -) { +fn file_drop(mut evr_dnd: EventReader) { for ev in evr_dnd.read() { if let FileDragAndDrop::HoveredFile { window, path_buf } = ev { if path_buf.extension().unwrap() == "geojson" { // Make this so the UI respods to a hover for example we can chanage the ui to have a gray overlay and say "Drop file here" and if it will be accepted println!( - "Hovered file with path: {:?}, in window id: {:?}", - path_buf, window + "Hovered file with path: {path_buf:?}, in window id: {window:?}" ); } } if let FileDragAndDrop::DroppedFile { window, path_buf } = ev { if path_buf.extension().unwrap() == "geojson" { println!( - "Dropped file with path: {:?}, in window id: {:?}", - path_buf, window + "Dropped file with path: {path_buf:?}, in window id: {window:?}" ); // TODO ADD ADD WORKSPACE REQUEST /* diff --git a/src/llm/client.rs b/src/llm/client.rs index 02865df..4a67816 100644 --- a/src/llm/client.rs +++ b/src/llm/client.rs @@ -150,7 +150,7 @@ impl OpenrouterClient { .post(&self.url) .header( "Authorization", - format!("Bearer {}", self.token.as_ref().unwrap().to_string()), + format!("Bearer {}", self.token.as_ref().unwrap()), ) .send_json(body_clone) { diff --git a/src/llm/mod.rs b/src/llm/mod.rs index 0d40f5f..47c4eaf 100644 --- a/src/llm/mod.rs +++ b/src/llm/mod.rs @@ -30,8 +30,8 @@ mod openrouter_types; use std::fmt::Display; pub use openrouter_types::*; -use serde::{Deserialize, Serialize}; -use ureq::{Agent, unversioned::transport::time::Duration}; +use serde::Serialize; +use ureq::Agent; use crate::llm::client::LLM_PROMPT; diff --git a/src/overpass/client.rs b/src/overpass/client.rs index bc73d5e..537365d 100644 --- a/src/overpass/client.rs +++ b/src/overpass/client.rs @@ -124,7 +124,7 @@ pub fn get_bounds(selection: Selection) -> String { .map(|point| format!("{} {}", point.lat, point.long)) .collect::>() .join(" "); - format!("poly:\"{}\"", points_string) + format!("poly:\"{points_string}\"") } else { String::default() } @@ -164,7 +164,7 @@ pub fn get_bounds(selection: Selection) -> String { .map(|point| format!("{} {}", point.lat, point.long)) .collect::>() .join(" "); - format!("poly:\"{}\"", points_string) + format!("poly:\"{points_string}\"") } else { String::new() // Return an empty string if no points are provided } diff --git a/src/workspace/commands.rs b/src/workspace/commands.rs index 8b7c931..2ba5fce 100644 --- a/src/workspace/commands.rs +++ b/src/workspace/commands.rs @@ -1,10 +1,10 @@ use crate::{ geojson::MapFeature, - workspace::{Workspace, WorkspaceData}, + workspace::Workspace, }; use bevy_map_viewer::Coord; use geo::Centroid; -use rstar::{AABB, PointDistance, RTreeObject}; +use rstar::AABB; use std::fmt::Display; impl Workspace { @@ -15,11 +15,10 @@ impl Workspace { let area = workspace.get_area(); let selection = &workspace.selection; return format!( - "Features: {}, Area: {:?}, Selection: {:?}", - count, area, selection + "Features: {count}, Area: {area:?}, Selection: {selection:?}" ); } - format!("Features: {}", count) + format!("Features: {count}") } // t : Tags for feature. ex: rq: t 123456 @@ -84,13 +83,13 @@ impl Workspace { for f in &features { if let Some(props) = f.properties.as_object() { - for (k, v) in props { + for (k, _v) in props { *tags_count.entry(k.clone()).or_insert(0) += 1; } } } - format!("Count: {}, Tags Summary: {:?}", count, tags_count) + format!("Count: {count}, Tags Summary: {tags_count:?}") } // bb : Features in bbox. ex: rq: bb {51.4,-0.1,51.6,-0.08} diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index c0e79eb..445ac6e 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -45,7 +45,7 @@ pub use workspace_types::*; use crate::{ geojson::MapFeature, - llm::{LlmResponse, Message, OpenrouterClient}, + llm::{Message, OpenrouterClient}, overpass::OverpassClient, }; @@ -108,8 +108,5 @@ pub struct WorkspaceRequest { raw_data: Vec, // Raw data from the request maybe have this as a id list aswell... #[serde(skip)] processed_data: RTree, - #[serde(skip)] - llm_analysis: Vec, - last_query_date: i64, // When the OSM data was fetched } diff --git a/src/workspace/ui.rs b/src/workspace/ui.rs index 4e45680..fae25d5 100644 --- a/src/workspace/ui.rs +++ b/src/workspace/ui.rs @@ -324,7 +324,7 @@ pub fn item_info( ui.horizontal(|ui| { ui.label(key); - ui.label(format!("{}", value)); + ui.label(format!("{value}")); if let Some(color) = workspace .workspace @@ -432,8 +432,8 @@ pub fn chat_box_ui( let ctx = contexts.ctx_mut(); let screen_rect = ctx.screen_rect(); - let chat_width = if screen_rect.width() / 4 as f32 > 200.0 { - screen_rect.width() / 4 as f32 + let chat_width = if screen_rect.width() / 4_f32 > 200.0 { + screen_rect.width() / 4_f32 } else { 200.0 }; // Same width as workspace_analysis_ui @@ -592,7 +592,7 @@ fn render_chat_message(ui: &mut egui::Ui, message: &ChatMessage, max_width: f32) let icon = if message.is_user { "πŸ‘€" } else { "πŸ€–" }; let sender = if message.is_user { "You" } else { "AI" }; ui.label( - RichText::new(format!("{} {}", icon, sender)) + RichText::new(format!("{icon} {sender}")) .small() .color(egui::Color32::LIGHT_GRAY), ); @@ -673,7 +673,7 @@ fn send_chat_message_background( // Add information about loaded data let requests = workspace.get_rendered_requests(); let total_features: usize = requests.iter().map(|r| r.get_processed_data().size()).sum(); - context_info.push_str(&format!("Total features loaded: {}\n", total_features)); + context_info.push_str(&format!("Total features loaded: {total_features}\n")); context_info.push_str(&format!("Data layers: {}\n", requests.len())); } else { context_info.push_str("No workspace selected\n"); @@ -683,7 +683,7 @@ fn send_chat_message_background( let enhanced_message = if context_info.trim().is_empty() { user_message.clone() } else { - format!("CONTEXT:\n{}\nUSER QUERY: {}", context_info, user_message) + format!("CONTEXT:\n{context_info}\nUSER QUERY: {user_message}") }; if let Some(workspace) = workspace.workspace.as_mut() { workspace.add_message("user", &enhanced_message); diff --git a/src/workspace/workspace_types.rs b/src/workspace/workspace_types.rs index 2aa860c..4395a51 100644 --- a/src/workspace/workspace_types.rs +++ b/src/workspace/workspace_types.rs @@ -9,7 +9,7 @@ use uuid::Uuid; use crate::{ geojson::{MapFeature, get_data_from_string_osm}, - llm::{Message, OpenrouterClient}, + llm::Message, workspace::ui::chat_box_ui, }; @@ -174,21 +174,21 @@ impl WorkspaceData { .distance(&Coord::new(start.lat, end.long)); return (top.0 * left.0, left.1); } - return (0.0, bevy_map_viewer::DistanceType::Km); + (0.0, bevy_map_viewer::DistanceType::Km) } SelectionType::CIRCLE => { if let (Some(center), Some(edge)) = (self.selection.start, self.selection.end) { let radius = center.distance(&edge); return (std::f32::consts::PI * radius.0 * radius.0, radius.1); } - return (0.0, bevy_map_viewer::DistanceType::Km); + (0.0, bevy_map_viewer::DistanceType::Km) } SelectionType::POLYGON => { if let Some(_points) = &self.selection.points { return (0.0, bevy_map_viewer::DistanceType::Km); // TODO: Implement polygon area calculation } - return (0.0, bevy_map_viewer::DistanceType::Km); + (0.0, bevy_map_viewer::DistanceType::Km) } _ => (0.0, bevy_map_viewer::DistanceType::Km), } @@ -197,7 +197,7 @@ impl WorkspaceData { impl WorkspaceRequest { pub fn get_visible(&self) -> bool { - self.visible.clone() + self.visible } pub fn get_id(&self) -> String { self.id.clone() @@ -293,7 +293,6 @@ impl WorkspaceRequest { raw_data, processed_data: RTree::new(), last_query_date: chrono::Utc::now().timestamp(), - llm_analysis: Vec::new(), } } From a1ca94e3f672deb5c4b66b8d0bc8a31a706dc247 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Mon, 28 Jul 2025 21:58:36 +0100 Subject: [PATCH 08/12] Interface llm with commands --- src/llm/client.rs | 43 +++-- src/workspace/commands.rs | 10 +- src/workspace/mod.rs | 2 +- src/workspace/ui.rs | 3 +- src/workspace/worker.rs | 374 +++++++++++++++++++++++++++++++++----- 5 files changed, 360 insertions(+), 72 deletions(-) diff --git a/src/llm/client.rs b/src/llm/client.rs index 4a67816..32277f5 100644 --- a/src/llm/client.rs +++ b/src/llm/client.rs @@ -1,3 +1,4 @@ +use bevy::log; use serde_json::json; use crate::llm::LlmResponse; @@ -12,21 +13,19 @@ use super::{Message, OpenrouterClient}; // rq: // // Commands: +// i : General info/stats ex: rq: i (workspace info) +// cnt : Count features ex: rq: cnt (count all workspace features) // nb : Nearby features ex: rq: nb {51.5,-0.09} r500 -// cnt : Count features ex: rq: cnt {51.5,-0.09} r500 // sm : Summarize features ex: rq: sm {51.5,-0.09} r500 // gt : Feature details ex: rq: gt 123456 // t : Feature tags ex: rq: t 123456 // bb : Features in bbox ex: rq: bb {51.4,-0.1,51.6,-0.08} // d : Distance ex: rq: d {51.5,-0.09} {51.6,-0.10} // n : Nearest feature ex: rq: n {51.5,-0.09} -// ply : Features in polygon ex: rq: ply {[51.5,-0.1],[51.6,-0.1],[51.6,-0.09]} -// i : General info/stats ex: rq: i {51.5,-0.09} r500 // // Rules: // - Points as {lat,lon}, radius as r (e.g., r200). // - bbox: {minLat,minLon,maxLat,maxLon}. -// - polygons: {[lat,lon],...}. // - Always prefix data requests with "rq:". // // Example: @@ -35,7 +34,8 @@ use super::{Message, OpenrouterClient}; // // ============================================================================ -// We should add a populiation decnity command +// We should add a population density command +// Command list updated to match actual implementations in commands.rs pub const LLM_PROMPT: &str = r#" You are a geo-analysis assistant. @@ -49,23 +49,22 @@ Your job is to answer user questions about geographic areas. Always follow this --- Commands --- -Needs specific parameters: -- rq: nb {lat,lon} r{radius} β†’ Nearby features -- rq: bb {lat1,lon1,lat2,lon2} β†’ Features in bounding box -- rq: d {lat1,lon1} {lat2,lon2} β†’ Distance between two points -- rq: n {lat,lon} β†’ Nearest feature -- rq: ply {[lat,lon],[lat,lon],...} β†’ Features inside polygon +Workspace-level commands (no parameters needed): +- rq: i β†’ General workspace info and stats +- rq: cnt β†’ Count all features in workspace -Context from workspace (no parameters) +Location-based commands (need coordinates): +- rq: nb {lat,lon} r{radius} β†’ Nearby features within radius +- rq: sm {lat,lon} r{radius} β†’ Summarize features in area +- rq: n {lat,lon} β†’ Nearest feature to point +- rq: d {lat1,lon1} {lat2,lon2} β†’ Distance between two points -- rq: s β†’ Get the selection information -- rq: i β†’ General summary for an area -- rq: cnt β†’ Count features -- rq: sm β†’ Summarize attributes (e.g. population, area) +Bounding box command: +- rq: bb {minLat,minLon,maxLat,maxLon} β†’ Features in bounding box -Id parameter +Feature-specific commands (need feature ID): - rq: gt β†’ Get full details for a feature -- rq: t β†’ Get tag metadata +- rq: t β†’ Get tag metadata for a feature Points must be in {lat,lon} format. Distances like r500 mean 500 meters. @@ -114,7 +113,7 @@ Rules Recap: "#; -// I really should make my own llm api using ureq! +// This really needs to reflect how many tokens are left! impl OpenrouterClient { fn build_messages_json(&self, mess: &Vec) -> serde_json::Value { let mut messages = vec![json!({ @@ -142,6 +141,8 @@ impl OpenrouterClient { "messages": self.build_messages_json(messages) }); + // We want to parse throught the error message. Rate limiting probably means that the tokens are all used up! + // We can take "backup" keys so that it doesnt end it just switches keys. let mut status = 429; while status == 429 { let body_clone = body.clone(); // Clone the body for each request attempt @@ -156,14 +157,18 @@ impl OpenrouterClient { { if response.status() == 200 { let res: LlmResponse = response.body_mut().read_json()?; + log::info!("Got the llm response!"); return Ok(res); } else if response.status() == 429 { + log::info!("Timeout!"); std::thread::sleep(std::time::Duration::from_secs(5)); } else { + log::info!("Error"); status = 0; } } } + Err(ureq::Error::ConnectionFailed) } } diff --git a/src/workspace/commands.rs b/src/workspace/commands.rs index 2ba5fce..440d663 100644 --- a/src/workspace/commands.rs +++ b/src/workspace/commands.rs @@ -1,12 +1,10 @@ -use crate::{ - geojson::MapFeature, - workspace::Workspace, -}; +use crate::{geojson::MapFeature, workspace::Workspace}; use bevy_map_viewer::Coord; use geo::Centroid; use rstar::AABB; use std::fmt::Display; +// TODO: Make the geojson simplifyer to parse to the llm. impl Workspace { // i : General info/stats. pub fn get_info(&self) -> String { @@ -14,9 +12,7 @@ impl Workspace { if let Some(workspace) = &self.workspace { let area = workspace.get_area(); let selection = &workspace.selection; - return format!( - "Features: {count}, Area: {area:?}, Selection: {selection:?}" - ); + return format!("Features: {count}, Area: {area:?}, Selection: {selection:?}"); } format!("Features: {count}") } diff --git a/src/workspace/mod.rs b/src/workspace/mod.rs index 445ac6e..fc0d6e7 100644 --- a/src/workspace/mod.rs +++ b/src/workspace/mod.rs @@ -57,7 +57,7 @@ mod workspace_types; pub struct WorkspacePlugin; -#[derive(Resource)] +#[derive(Resource, Clone)] pub struct Workspace { pub workspace: Option, // (id, request) diff --git a/src/workspace/ui.rs b/src/workspace/ui.rs index fae25d5..f3a4abb 100644 --- a/src/workspace/ui.rs +++ b/src/workspace/ui.rs @@ -612,8 +612,7 @@ fn send_chat_message_background( workspace: &mut Workspace, user_message: String, ) { - let mut llm_client = workspace.llm_agent.clone(); - llm_client.set_token("!! REPLACE ME WITH TOKEN !!"); + workspace.llm_agent.set_token("!!! YOUR TOKEN HERE !!!"); // Build context information about the current workspace and selection let mut context_info = String::new(); diff --git a/src/workspace/worker.rs b/src/workspace/worker.rs index ed7419a..01f4576 100644 --- a/src/workspace/worker.rs +++ b/src/workspace/worker.rs @@ -2,12 +2,12 @@ use crate::workspace::ui::{ChatMessage, ChatState}; use crate::workspace::{RequestType, WorkspaceRequest}; use bevy::prelude::*; use bevy::tasks::{AsyncComputeTaskPool, Task}; +use bevy_map_viewer::Coord; use bevy_tasks::futures_lite::future; use std::sync::{Arc, Mutex}; use super::Workspace; - -#[derive(Default)] +#[derive(Default, Clone)] pub struct WorkspaceWorker { /// A thread-safe queue of pending Overpass requests. pending_requests: Arc>>, @@ -71,59 +71,26 @@ pub fn process_requests( info!("No workspace found"); return; } - let overpass_client = workspace.overpass_agent.clone(); - let openrouter_client = workspace.llm_agent.clone(); - // Clone the loaded_requests Arc instead of holding the MutexGuard let loaded_requests = workspace.loaded_requests.clone(); - let ws: Option = workspace.workspace.clone(); + let workspace_clone = workspace.clone(); let cs = chat_state.clone(); let task = task_pool.spawn(async move { let mut result = Vec::new(); match request.get_request() { RequestType::OverpassTurboRequest(ref query) => { - if let Ok(q) = overpass_client.send_overpass_query_string(query.clone()) { + if let Ok(q) = workspace_clone + .overpass_agent + .send_overpass_query_string(query.clone()) + { if !q.is_empty() { result = q.as_bytes().to_vec(); } } } RequestType::OpenRouterRequest() => { - if let Some(mut workspace) = ws { - if let Ok(q) = openrouter_client.send_openrouter_chat(&workspace.messages) { - if let Some(choice) = q.choices.first() { - let ai_message = choice.message.content.clone(); - - workspace.add_message("assistant", &ai_message); - - // Update chat state with thread-safe access - if let Ok(mut inner) = cs.inner.lock() { - inner.chat_history.push(ChatMessage { - content: ai_message.clone(), - is_user: false, - }); - inner.is_processing = false; - } - if &ai_message[0..2] == "rq" { - // Now we need to handle the request sent by the agent! - } - } else { - // Handle case where no choices are returned - workspace.add_message("assistant", "Sorry, I didn't receive a proper response from the AI. Please try again."); - - // Update chat state with thread-safe access - if let Ok(mut inner) = cs.inner.lock() { - inner.chat_history.push(ChatMessage { - content: - "Sorry, I didn't receive a proper response from the AI. Please try again." - .to_string(), - is_user: false, - }); - inner.is_processing = false; - } - bevy::log::info!("here3"); - - } - } + if let Some(workspace_data) = &workspace_clone.workspace { + // Process the LLM request with automatic follow-up capability + process_llm_request(&workspace_clone, workspace_data, &cs, 0); } } RequestType::OpenMeteoRequest(_open_meteo_request) => {} @@ -156,3 +123,324 @@ pub fn cleanup_tasks(mut commands: Commands, mut tasks: Query<(Entity, &mut Task } } } + +// Recursive function to handle LLM requests with automatic follow-up +fn process_llm_request( + workspace_clone: &Workspace, + workspace_data: &super::WorkspaceData, + cs: &ChatState, + recursion_depth: usize, +) { + const MAX_RECURSION_DEPTH: usize = 5; // Prevent infinite loops + + if recursion_depth >= MAX_RECURSION_DEPTH { + bevy::log::warn!("Maximum recursion depth reached for LLM requests"); + if let Ok(mut inner) = cs.inner.lock() { + inner.is_processing = false; + } + return; + } + + if let Ok(q) = workspace_clone + .llm_agent + .send_openrouter_chat(&workspace_data.messages) + { + if let Some(choice) = q.choices.first() { + let ai_message = choice.message.content.clone(); + let mut workspace_data_mut = workspace_data.clone(); + workspace_data_mut.add_message("assistant", &ai_message); + + // Update chat state with thread-safe access + if let Ok(mut inner) = cs.inner.lock() { + inner.chat_history.push(ChatMessage { + content: ai_message.clone(), + is_user: false, + }); + } + + if ai_message.len() > 2 && &ai_message[0..3] == "rq:" { + let command_part = &ai_message[3..].trim(); + let mut parts = command_part.split_whitespace(); + + if let Some(cmd) = parts.next() { + let response = match cmd { + "i" => { + // General info/stats + workspace_clone.get_info() + } + "cnt" => { + // Count features - can be for whole workspace or specific area + format!("Feature count: {}", workspace_clone.count_workspace()) + } + "nb" => { + // Nearby features: rq: nb {51.5,-0.09} r500 + if let (Some(coord_str), Some(radius_str)) = + (parts.next(), parts.next()) + { + if let (Ok(coord), Ok(radius)) = + (parse_coord(coord_str), parse_radius(radius_str)) + { + let features = workspace_clone.nearby_point(coord, radius); + format!( + "Found {} nearby features: {:?}", + features.len(), + features.iter().map(|f| &f.id).collect::>() + ) + } else { + "Invalid coordinate or radius format. Use: rq: nb {lat,lon} r".to_string() + } + } else { + "Missing parameters. Use: rq: nb {lat,lon} r".to_string() + } + } + "sm" => { + // Summarize features: rq: sm {51.5,-0.09} r500 + if let (Some(coord_str), Some(radius_str)) = + (parts.next(), parts.next()) + { + if let (Ok(coord), Ok(radius)) = + (parse_coord(coord_str), parse_radius(radius_str)) + { + workspace_clone.summarize_features(coord, radius) + } else { + "Invalid coordinate or radius format. Use: rq: sm {lat,lon} r".to_string() + } + } else { + "Missing parameters. Use: rq: sm {lat,lon} r".to_string() + } + } + "gt" => { + // Feature details by ID: rq: gt 123456 + if let Some(id) = parts.next() { + if let Some(feature) = workspace_clone.get_feature_by_id(id) { + format!("Feature {}: {:?}", id, feature) + } else { + format!("Feature {} not found", id) + } + } else { + "Missing feature ID. Use: rq: gt ".to_string() + } + } + "t" => { + // Feature tags: rq: t 123456 + if let Some(id) = parts.next() { + if let Some(tags) = workspace_clone.get_feature_tags(id) { + format!("Tags for feature {}: {}", id, tags) + } else { + format!("Feature {} not found or has no tags", id) + } + } else { + "Missing feature ID. Use: rq: t ".to_string() + } + } + "bb" => { + // Features in bbox: rq: bb {51.4,-0.1,51.6,-0.08} + if let Some(bbox_str) = parts.next() { + if let Ok((min_lat, min_lon, max_lat, max_lon)) = + parse_bbox(bbox_str) + { + let features = workspace_clone + .features_in_bbox(min_lat, min_lon, max_lat, max_lon); + format!( + "Found {} features in bbox: {:?}", + features.len(), + features.iter().map(|f| &f.id).collect::>() + ) + } else { + "Invalid bbox format. Use: rq: bb {min_lat,min_lon,max_lat,max_lon}".to_string() + } + } else { + "Missing bbox. Use: rq: bb {min_lat,min_lon,max_lat,max_lon}" + .to_string() + } + } + "d" => { + // Distance between points: rq: d {51.5,-0.09} {51.6,-0.10} + if let (Some(coord1_str), Some(coord2_str)) = + (parts.next(), parts.next()) + { + if let (Ok(coord1), Ok(coord2)) = + (parse_coord(coord1_str), parse_coord(coord2_str)) + { + let distance = workspace_clone.distance_between(coord1, coord2); + format!("Distance: {:.2} meters", distance) + } else { + "Invalid coordinate format. Use: rq: d {lat1,lon1} {lat2,lon2}" + .to_string() + } + } else { + "Missing coordinates. Use: rq: d {lat1,lon1} {lat2,lon2}" + .to_string() + } + } + "n" => { + // Nearest feature: rq: n {51.5,-0.09} + if let Some(coord_str) = parts.next() { + if let Ok(coord) = parse_coord(coord_str) { + if let Some(feature) = workspace_clone.nearest_feature(coord) { + format!("Nearest feature: {} at {:?}", feature.id, feature) + } else { + "No features found".to_string() + } + } else { + "Invalid coordinate format. Use: rq: n {lat,lon}".to_string() + } + } else { + "Missing coordinate. Use: rq: n {lat,lon}".to_string() + } + } + _ => { + format!( + "Unknown command: {}. Available commands: i, cnt, nb, sm, gt, t, bb, d, n", + cmd + ) + } + }; + + bevy::log::info!("Command executed: {} -> {}", command_part, response); + workspace_data_mut.add_message("user", &response); + bevy::log::info!("Command executed: {} -> {}", command_part, response); + + bevy::log::info!("Automatically following up with LLM after providing data..."); + process_llm_request( + workspace_clone, + &workspace_data_mut, + cs, + recursion_depth + 1, + ); + } + } else { + // This is a final answer, not a data request - stop processing + bevy::log::info!( + "LLM provided final answer (no command detected): '{}'", + ai_message + ); + bevy::log::info!("Setting is_processing = false"); + if let Ok(mut inner) = cs.inner.lock() { + inner.is_processing = false; + bevy::log::info!("Successfully set is_processing = false"); + } else { + bevy::log::error!("Failed to lock chat state to set is_processing = false"); + } + } + } else { + bevy::log::error!("No choices returned from LLM"); + let mut workspace_data_mut = workspace_data.clone(); + workspace_data_mut.add_message( + "assistant", + "Sorry, I didn't receive a proper response from the AI. Please try again.", + ); + + if let Ok(mut inner) = cs.inner.lock() { + inner.chat_history.push(ChatMessage { + content: + "Sorry, I didn't receive a proper response from the AI. Please try again." + .to_string(), + is_user: false, + }); + inner.is_processing = false; + } + } + } else { + bevy::log::error!("Failed to send chat request to LLM"); + if let Ok(mut inner) = cs.inner.lock() { + inner.is_processing = false; + } + } +} + +// Helper functions for parsing LLM command parameters +fn parse_coord(coord_str: &str) -> Result { + let coord_str = coord_str.trim_start_matches('{').trim_end_matches('}'); + let parts: Vec<&str> = coord_str.split(',').collect(); + + if parts.len() != 2 { + return Err("Coordinate must have exactly 2 parts".to_string()); + } + + let lat = parts[0] + .trim() + .parse::() + .map_err(|_| "Invalid latitude")?; + let long = parts[1] + .trim() + .parse::() + .map_err(|_| "Invalid longitude")?; + + Ok(Coord { lat, long }) +} + +fn parse_radius(radius_str: &str) -> Result { + if !radius_str.starts_with('r') { + return Err("Radius must start with 'r'".to_string()); + } + + radius_str[1..] + .parse::() + .map_err(|_| "Invalid radius value".to_string()) +} + +fn parse_bbox(bbox_str: &str) -> Result<(f64, f64, f64, f64), String> { + let bbox_str = bbox_str.trim_start_matches('{').trim_end_matches('}'); + let parts: Vec<&str> = bbox_str.split(',').collect(); + + if parts.len() != 4 { + return Err("Bbox must have exactly 4 parts".to_string()); + } + + let min_lat = parts[0] + .trim() + .parse::() + .map_err(|_| "Invalid min_lat")?; + let min_lon = parts[1] + .trim() + .parse::() + .map_err(|_| "Invalid min_lon")?; + let max_lat = parts[2] + .trim() + .parse::() + .map_err(|_| "Invalid max_lat")?; + let max_lon = parts[3] + .trim() + .parse::() + .map_err(|_| "Invalid max_lon")?; + + Ok((min_lat, min_lon, max_lat, max_lon)) +} + +fn parse_polygon(polygon_str: &str) -> Result, String> { + // Parse format: {[lat1,lon1],[lat2,lon2],[lat3,lon3]} + let polygon_str = polygon_str.trim_start_matches('{').trim_end_matches('}'); + let mut coords = Vec::new(); + + // Split by '],' to separate coordinate pairs + let parts: Vec<&str> = polygon_str.split("],[").collect(); + + for (i, part) in parts.iter().enumerate() { + let coord_part = if i == 0 { + part.trim_start_matches('[') + } else if i == parts.len() - 1 { + part.trim_end_matches(']') + } else { + part + }; + + let coord_parts: Vec<&str> = coord_part.split(',').collect(); + if coord_parts.len() != 2 { + return Err("Each coordinate must have exactly 2 parts".to_string()); + } + + let lat = coord_parts[0] + .trim() + .parse::() + .map_err(|_| "Invalid latitude")?; + let long = coord_parts[1] + .trim() + .parse::() + .map_err(|_| "Invalid longitude")?; + + coords.push(Coord { lat, long }); + } + + Ok(coords) +} From 47463ca673fb518228deb0f3118b52e859db60a5 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Mon, 28 Jul 2025 22:21:11 +0100 Subject: [PATCH 09/12] Update documentation - partial update --- API_DOCUMENTATION.md | 4 +++- README.md | 40 +++++++++++++++++++++++++++++++++------- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index 8eeb850..f133fed 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -124,6 +124,8 @@ Each major system is implemented as a Bevy plugin: 2. Clone repository 3. Run `cargo build` to compile 4. Run `cargo run` to start application -5. Set environment variable `OPENROUTER_API_KEY` for AI features +5. **For AI features**: Edit `src/workspace/ui.rs` and replace `"!!! YOUR TOKEN HERE !!!"` with your OpenRouter API key from [openrouter.ai](https://openrouter.ai/) + +The application will function without an API key, but AI-powered chat and analysis features will be unavailable. This documentation provides a high-level overview of the system architecture. Detailed API documentation for each module follows in the subsequent sections. diff --git a/README.md b/README.md index 051f140..cfc6204 100644 --- a/README.md +++ b/README.md @@ -24,14 +24,22 @@ To use the program in its current state: - Add, remove, and reorder layers (vector, raster, and real-time data). - Fetch OpenStreetMap data dynamically using **Overpass Turbo**. -### **πŸ“Š Geospatial Analysis** *(Planned)* -- Query building and road data based on custom filters. -- Measure areas, distances, and proximity between features. -- Overlay datasets such as solar potential, pollution, or transit accessibility. +### **πŸ€– AI-Powered Analysis** +- Interactive chat interface for geospatial queries +- Natural language commands for spatial analysis (nearby features, distances, etc.) +- Automated feature summarization and insights +- Context-aware responses based on current map selection -### **πŸ› οΈ Workspace & Project Management** *(Next working on)* +### **πŸ› οΈ Workspace & Project Management** - Save and load workspaces with selected areas and layers. -- Export workspaces for future analysis or collaboration. +- Persistent workspace state and configuration. +- Request history and data caching. + +### **πŸ“Š Geospatial Analysis** +- Query building and road data based on custom filters. +- Measure areas, distances, and proximity between features. +- Feature counting and spatial statistics. +- Bounding box and polygon-based queries. ### **πŸ”Œ Data Integration** *(Planned)* - Import/Export **GeoJSON**, **Shapefiles**, and other GIS formats. @@ -81,16 +89,34 @@ cd map-rs cargo build # or 'cargo run', to run the app ``` +### **AI Chat Setup (Optional)** +To use the AI-powered chat features: + +1. Get an API key from [OpenRouter](https://openrouter.ai/) +2. Edit `src/workspace/ui.rs` and replace the line: + ```rust + workspace.llm_agent.set_token("!!! YOUR TOKEN HERE !!!"); + ``` + with your actual API key: + ```rust + workspace.llm_agent.set_token("your-actual-api-key-here"); + ``` +3. Rebuild the application: `cargo build` + +**Note**: The application works without an API key, but AI chat functionality will be unavailable. + --- ## **Roadmap** - βœ… **Basic Map Navigation & Selection** - βœ… **Overpass Turbo Data Fetching** +- βœ… **AI-Powered Chat & Spatial Analysis** +- βœ… **Workspace Management** - ⏳ **Layer System (WIP)** - ⏳ **Attribute Table & Metadata Display** - ⏳ **Custom Styling & Visualization** - ⏳ **GeoJSON & Shapefile Support** -- ⏳ **Geospatial Analysis Tools** +- ⏳ **Advanced Geospatial Analysis Tools** --- From f678f262200ed66e9545e25a924a25e4a9369773 Mon Sep 17 00:00:00 2001 From: Samuel Oldham Date: Tue, 29 Jul 2025 11:46:46 +0100 Subject: [PATCH 10/12] Added LLM support. --- API_DOCUMENTATION.md | 2 -- README.md | 42 +++++++++++-------------------- src/llm/client.rs | 10 ++++---- src/workspace/commands.rs | 30 ++++++++++++++-------- src/workspace/ui.rs | 3 +++ src/workspace/worker.rs | 53 ++------------------------------------- 6 files changed, 44 insertions(+), 96 deletions(-) diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md index f133fed..a853dbe 100644 --- a/API_DOCUMENTATION.md +++ b/API_DOCUMENTATION.md @@ -126,6 +126,4 @@ Each major system is implemented as a Bevy plugin: 4. Run `cargo run` to start application 5. **For AI features**: Edit `src/workspace/ui.rs` and replace `"!!! YOUR TOKEN HERE !!!"` with your OpenRouter API key from [openrouter.ai](https://openrouter.ai/) -The application will function without an API key, but AI-powered chat and analysis features will be unavailable. - This documentation provides a high-level overview of the system architecture. Detailed API documentation for each module follows in the subsequent sections. diff --git a/README.md b/README.md index cfc6204..ed47574 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,19 @@ A fast, open-source GIS tool for urban planning, spatial analysis, and geospatia ## How to Use the Program To use the program in its current state: - -1. Select the **Selection Tool** (second icon from the left). -2. Use it to select an area on the map. -3. On the left side, choose the type of **Overpass request** you want to perform from the dropdown menu. -4. At the top, select the **Workspace**. +1. Edit `src/workspace/ui.rs` and replace `"!!! YOUR TOKEN HERE !!!"` with your OpenRouter API key from [openrouter.ai](https://openrouter.ai/api). This will change very soon to just getting prompted in the app to input your token :) +2. Select the **Selection Tool** (second icon from the left). +3. Use it to select an area on the map. +4. On the left side, choose the type of **Overpass request** you want to perform from the dropdown menu. +5. At the top, select the **Workspace**. - **Note:** Loading may take a few seconds depending on the size of the response. -5. Once the data is loaded, switch back to the **Cursor Tool** (which also acts as the info tool). -6. Click on a **house** or other **element**. -7. You can then change its color based on its properties. +From here you can either +**Analyse using llm** +For this simply ask what you want the llm to in the chat box on the right. Do note that it is containerised and will only be able to access data selected for the workspace. +**Change color** +6. Once the data is loaded, switch back to the **Cursor Tool** (which also acts as the info tool). +7. Click on a **house** or other **element**. +8. You can then change its color based on its properties. ## **Features** ### **πŸ—ΊοΈ Map Navigation & Selection** @@ -26,7 +30,7 @@ To use the program in its current state: ### **πŸ€– AI-Powered Analysis** - Interactive chat interface for geospatial queries -- Natural language commands for spatial analysis (nearby features, distances, etc.) +- Natural language commands for spatial analysis (population dencity, suitabilty of new houses, etc.) - Automated feature summarization and insights - Context-aware responses based on current map selection @@ -41,11 +45,6 @@ To use the program in its current state: - Feature counting and spatial statistics. - Bounding box and polygon-based queries. -### **πŸ”Œ Data Integration** *(Planned)* -- Import/Export **GeoJSON**, **Shapefiles**, and other GIS formats. -- Support for **WMS/WFS** layers (real-time weather, elevation, etc.). -- Generate heatmaps and custom visualizations. - --- ## **Building from source** *(WIP - Currently for Developers Only)* @@ -89,7 +88,7 @@ cd map-rs cargo build # or 'cargo run', to run the app ``` -### **AI Chat Setup (Optional)** +### **AI Chat Setup** To use the AI-powered chat features: 1. Get an API key from [OpenRouter](https://openrouter.ai/) @@ -107,19 +106,6 @@ To use the AI-powered chat features: --- -## **Roadmap** -- βœ… **Basic Map Navigation & Selection** -- βœ… **Overpass Turbo Data Fetching** -- βœ… **AI-Powered Chat & Spatial Analysis** -- βœ… **Workspace Management** -- ⏳ **Layer System (WIP)** -- ⏳ **Attribute Table & Metadata Display** -- ⏳ **Custom Styling & Visualization** -- ⏳ **GeoJSON & Shapefile Support** -- ⏳ **Advanced Geospatial Analysis Tools** - ---- - ## **Contributing** Contributions are welcome! Feel free to fork the repository, submit issues, or suggest new features. diff --git a/src/llm/client.rs b/src/llm/client.rs index 32277f5..1e9de78 100644 --- a/src/llm/client.rs +++ b/src/llm/client.rs @@ -50,12 +50,12 @@ Your job is to answer user questions about geographic areas. Always follow this --- Commands --- Workspace-level commands (no parameters needed): -- rq: i β†’ General workspace info and stats +- rq: i β†’ General workspace info and stats including summery - rq: cnt β†’ Count all features in workspace +- rq: sm β†’ Summarize features in workspace Location-based commands (need coordinates): - rq: nb {lat,lon} r{radius} β†’ Nearby features within radius -- rq: sm {lat,lon} r{radius} β†’ Summarize features in area - rq: n {lat,lon} β†’ Nearest feature to point - rq: d {lat1,lon1} {lat2,lon2} β†’ Distance between two points @@ -82,9 +82,9 @@ Assistant: There are 18 nearby features within 500 meters, including residential --- -User: What's the population density at {51.5,-0.09}? +User: What's the population density? -Assistant: rq: sm {51.5,-0.09} r500 +Assistant: rq: sm {Wait wait for data} @@ -96,7 +96,7 @@ Assistant: The population density is 15 people per 100 mΒ² (or 15,000 per kmΒ²). User: What happens if I add 200 houses here? -Assistant: rq: sm {51.5,-0.09} r500 +Assistant: rq: sm {Wait wait for data} diff --git a/src/workspace/commands.rs b/src/workspace/commands.rs index 440d663..8343474 100644 --- a/src/workspace/commands.rs +++ b/src/workspace/commands.rs @@ -12,7 +12,16 @@ impl Workspace { if let Some(workspace) = &self.workspace { let area = workspace.get_area(); let selection = &workspace.selection; - return format!("Features: {count}, Area: {area:?}, Selection: {selection:?}"); + + let mut matches = Vec::new(); + for request in self.get_requests() { + let mut m: Vec = request.processed_data.iter().cloned().collect(); + matches.append(&mut m); + } + + return format!( + "Features: {count}, Area: {area:?}, Selection: {selection:?}, Summery of features: {matches:?}" + ); } format!("Features: {count}") } @@ -71,20 +80,21 @@ impl Workspace { matches } - // sm : Summarize features. ex: rq: sm {51.5,-0.09} r500 - pub fn summarize_features(&self, point: Coord, radius: f64) -> String { - let features = self.nearby_point(point, radius); - let count = features.len(); + // sm : Summarize features. + pub fn summarize_features(&self) -> String { let mut tags_count = std::collections::HashMap::new(); + let mut count = 0; + for request in self.get_requests() { + count += request.processed_data.size(); - for f in &features { - if let Some(props) = f.properties.as_object() { - for (k, _v) in props { - *tags_count.entry(k.clone()).or_insert(0) += 1; + for f in request.processed_data.iter() { + if let Some(props) = f.properties.as_object() { + for (k, _v) in props { + *tags_count.entry(k.clone()).or_insert(0) += 1; + } } } } - format!("Count: {count}, Tags Summary: {tags_count:?}") } diff --git a/src/workspace/ui.rs b/src/workspace/ui.rs index f3a4abb..2a56681 100644 --- a/src/workspace/ui.rs +++ b/src/workspace/ui.rs @@ -429,6 +429,9 @@ pub fn chat_box_ui( chat_state: Res, mut workspace: ResMut, ) { + if workspace.workspace.is_none() { + return; + } let ctx = contexts.ctx_mut(); let screen_rect = ctx.screen_rect(); diff --git a/src/workspace/worker.rs b/src/workspace/worker.rs index 01f4576..bdb9755 100644 --- a/src/workspace/worker.rs +++ b/src/workspace/worker.rs @@ -194,20 +194,8 @@ fn process_llm_request( } } "sm" => { - // Summarize features: rq: sm {51.5,-0.09} r500 - if let (Some(coord_str), Some(radius_str)) = - (parts.next(), parts.next()) - { - if let (Ok(coord), Ok(radius)) = - (parse_coord(coord_str), parse_radius(radius_str)) - { - workspace_clone.summarize_features(coord, radius) - } else { - "Invalid coordinate or radius format. Use: rq: sm {lat,lon} r".to_string() - } - } else { - "Missing parameters. Use: rq: sm {lat,lon} r".to_string() - } + // Summarize features: rq: sm + workspace_clone.summarize_features() } "gt" => { // Feature details by ID: rq: gt 123456 @@ -407,40 +395,3 @@ fn parse_bbox(bbox_str: &str) -> Result<(f64, f64, f64, f64), String> { Ok((min_lat, min_lon, max_lat, max_lon)) } - -fn parse_polygon(polygon_str: &str) -> Result, String> { - // Parse format: {[lat1,lon1],[lat2,lon2],[lat3,lon3]} - let polygon_str = polygon_str.trim_start_matches('{').trim_end_matches('}'); - let mut coords = Vec::new(); - - // Split by '],' to separate coordinate pairs - let parts: Vec<&str> = polygon_str.split("],[").collect(); - - for (i, part) in parts.iter().enumerate() { - let coord_part = if i == 0 { - part.trim_start_matches('[') - } else if i == parts.len() - 1 { - part.trim_end_matches(']') - } else { - part - }; - - let coord_parts: Vec<&str> = coord_part.split(',').collect(); - if coord_parts.len() != 2 { - return Err("Each coordinate must have exactly 2 parts".to_string()); - } - - let lat = coord_parts[0] - .trim() - .parse::() - .map_err(|_| "Invalid latitude")?; - let long = coord_parts[1] - .trim() - .parse::() - .map_err(|_| "Invalid longitude")?; - - coords.push(Coord { lat, long }); - } - - Ok(coords) -} From 170615eb50471b3b2ec9ad601f19b7363a8dad94 Mon Sep 17 00:00:00 2001 From: Samuel Oldham <77629938+SO9010@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:57:34 +0100 Subject: [PATCH 11/12] Update README.md to include showcase --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ed47574..f391590 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # **Urban Planning GIS App** -A fast, open-source GIS tool for urban planning, spatial analysis, and geospatial data visualization. This application allows users to select areas, fetch data from OpenStreetMap (Overpass Turbo), and analyze geographic data with a focus on urban development and sustainability. +A fast, open-source GIS tool for urban planning, spatial analysis, and geospatial data visualization. This application allows users to select areas, + +fetch data from OpenStreetMap (Overpass Turbo), and analyze geographic data with a focus on urban development and sustainability. + +## Showcase +https://github.com/user-attachments/assets/ecc4bf27-834c-4568-9ca9-af6da94acf6a ## How to Use the Program From 11c412420ac9d2cc4a8f578cb476d57e3a483ddf Mon Sep 17 00:00:00 2001 From: Samuel Oldham <77629938+SO9010@users.noreply.github.com> Date: Tue, 29 Jul 2025 11:58:39 +0100 Subject: [PATCH 12/12] Update README.md to improve description --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index f391590..17adf84 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # **Urban Planning GIS App** -A fast, open-source GIS tool for urban planning, spatial analysis, and geospatial data visualization. This application allows users to select areas, - -fetch data from OpenStreetMap (Overpass Turbo), and analyze geographic data with a focus on urban development and sustainability. +A fast, open-source GIS tool for urban planning, spatial analysis, and geospatial data visualization. This application allows users to select areas, fetch data from OpenStreetMap (Overpass Turbo), and analyze geographic data with a focus on urban development and sustainability. It utilises LLM chatbots to help the user analyse their workspace. ## Showcase https://github.com/user-attachments/assets/ecc4bf27-834c-4568-9ca9-af6da94acf6a