diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..a853dbe --- /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. **For AI features**: Edit `src/workspace/ui.rs` and replace `"!!! YOUR TOKEN HERE !!!"` with your OpenRouter API key from [openrouter.ai](https://openrouter.ai/) + +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/README.md b/README.md index 051f140..17adf84 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,25 @@ # **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 ## 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** @@ -24,19 +31,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 (population dencity, suitabilty of new houses, 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. -### **🔌 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. +### **📊 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. --- @@ -81,16 +91,21 @@ cd map-rs cargo build # or 'cargo run', to run the app ``` ---- - -## **Roadmap** -- ✅ **Basic Map Navigation & Selection** -- ✅ **Overpass Turbo Data Fetching** -- ⏳ **Layer System (WIP)** -- ⏳ **Attribute Table & Metadata Display** -- ⏳ **Custom Styling & Visualization** -- ⏳ **GeoJSON & Shapefile Support** -- ⏳ **Geospatial Analysis Tools** +### **AI Chat Setup** +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. --- diff --git a/src/camera.rs b/src/camera.rs index d10ec1c..b792ff2 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, @@ -24,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 4f40257..4034671 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}, @@ -109,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 2320e68..d389ffb 100644 --- a/src/geojson/loader.rs +++ b/src/geojson/loader.rs @@ -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 0d7d697..1cc8edd 100644 --- a/src/interaction.rs +++ b/src/interaction.rs @@ -1,7 +1,27 @@ +//! # 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; -use crate::workspace::Workspace; pub struct InteractionSystemPlugin; @@ -11,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 new file mode 100644 index 0000000..1e9de78 --- /dev/null +++ b/src/llm/client.rs @@ -0,0 +1,174 @@ +use bevy::log; +use serde_json::json; + +use crate::llm::LlmResponse; + +// https://openrouter.ai/docs/api-reference/overview +use super::{Message, OpenrouterClient}; +// ============================================================================ +// LLM Request Command Specification +// ============================================================================ +// +// Use "rq:" for data requests. Format: +// 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 +// 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} +// +// Rules: +// - Points as {lat,lon}, radius as r (e.g., r200). +// - bbox: {minLat,minLon,maxLat,maxLon}. +// - Always prefix data requests with "rq:". +// +// Example: +// LLM -> rq: nb {51.5,-0.09} r200 +// App -> Returns JSON of features. +// +// ============================================================================ + +// 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. + +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 --- + +Workspace-level commands (no parameters needed): +- 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: n {lat,lon} → Nearest feature to point +- rq: d {lat1,lon1} {lat2,lon2} → Distance between two points + +Bounding box command: +- rq: bb {minLat,minLon,maxLat,maxLon} → Features in bounding box + +Feature-specific commands (need feature ID): +- rq: gt → Get full details for a feature +- rq: t → Get tag metadata for a feature + +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? + +Assistant: rq: sm + +{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 + +{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. + +"#; + +// 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!({ + "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) + }); + + // 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 + if let Ok(mut response) = self + .agent + .post(&self.url) + .header( + "Authorization", + format!("Bearer {}", self.token.as_ref().unwrap()), + ) + .send_json(body_clone) + { + 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/llm/mod.rs b/src/llm/mod.rs new file mode 100644 index 0000000..47c4eaf --- /dev/null +++ b/src/llm/mod.rs @@ -0,0 +1,79 @@ +//! # 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; + +use std::fmt::Display; + +pub use openrouter_types::*; +use serde::Serialize; +use ureq::Agent; + +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().build(); + let agent: Agent = config.into(); + OpenrouterClient { + agent, + url: url.to_string(), + token, + prompt: LLM_PROMPT.to_string(), + } + } + + 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/llm/openrouter_types.rs b/src/llm/openrouter_types.rs new file mode 100644 index 0000000..20c264c --- /dev/null +++ b/src/llm/openrouter_types.rs @@ -0,0 +1,46 @@ +use serde::{Deserialize, Serialize}; +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(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, +} diff --git a/src/main.rs b/src/main.rs index 519d0f4..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}, @@ -18,6 +44,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/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/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/commands.rs b/src/workspace/commands.rs new file mode 100644 index 0000000..8343474 --- /dev/null +++ b/src/workspace/commands.rs @@ -0,0 +1,216 @@ +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 { + let count = self.count_workspace(); + if let Some(workspace) = &self.workspace { + let area = workspace.get_area(); + let selection = &workspace.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}") + } + + // 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. + 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 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:?}") + } + + // 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 2fd6ce3..fc0d6e7 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}, @@ -10,8 +43,13 @@ use serde_json::Value; use worker::WorkspaceWorker; pub use workspace_types::*; -use crate::{geojson::MapFeature, overpass::OverpassClient}; +use crate::{ + geojson::MapFeature, + llm::{Message, OpenrouterClient}, + overpass::OverpassClient, +}; +mod commands; mod renderer; mod ui; mod worker; @@ -19,7 +57,7 @@ mod workspace_types; pub struct WorkspacePlugin; -#[derive(Resource)] +#[derive(Resource, Clone)] pub struct Workspace { pub workspace: Option, // (id, request) @@ -28,6 +66,8 @@ pub struct Workspace { // Request Clients: pub overpass_agent: OverpassClient, + pub llm_agent: OpenrouterClient, + // Add llm requests agent! } impl Default for Workspace { @@ -37,6 +77,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("https://openrouter.ai/api/v1/chat/completions", None), } } } @@ -55,6 +96,7 @@ pub struct WorkspaceData { last_modified: i64, requests: HashSet, properties: HashMap<(String, Value), Srgba>, + messages: Vec, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -66,6 +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, - last_query_date: i64, // When the OSM data was fetched } diff --git a/src/workspace/ui.rs b/src/workspace/ui.rs index 5dee3d6..2a56681 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,6 +21,41 @@ use crate::{ use super::{RequestType, WorkspaceRequest}; +// 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, +} + +#[derive(Clone)] +pub struct ChatMessage { + pub content: String, + pub is_user: bool, +} + pub fn workspace_actions_ui( mut tile_map_res: ResMut, mut contexts: EguiContexts, @@ -139,10 +175,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() {} }); + */ }); }); }); @@ -286,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 @@ -386,130 +424,287 @@ 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 +pub fn chat_box_ui( + mut contexts: EguiContexts, + chat_state: Res, + mut workspace: ResMut, +) { + if workspace.workspace.is_none() { + return; + } + let ctx = contexts.ctx_mut(); + let screen_rect = ctx.screen_rect(); - House Type Breakdown: - Show a pie chart or bar graph breaking down the area by different house types - (e.g., "Detached," "Semi-detached," "Apartments"). + 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 + 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() + { + if let Ok(mut inner) = chat_state.inner.lock() { + inner.chat_history.clear(); + } + } + }, + ); + }); - Area Size: - Display the total size (area) of a selected polygon or area (e.g., "Total area: 150,000 m²"). + ui.separator(); + + 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 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 { + 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("AI is thinking...") + .color(egui::Color32::GRAY), + ); + }); + } + } + }); - Population Density: - Show the population density for the selected area (e.g., people per km²). + ui.add_space(5.0); + ui.separator(); - Weather Analysis: - Include a weather summary for the selected area, - such as temperature, precipitation, or other relevant weather parameters. + // 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..."); + + 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) + && !inner.input_text.trim().is_empty() + && !inner.is_processing + { + 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, + }); - 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. + // Drop the lock before calling send_chat_message_background + drop(inner); + send_chat_message_background( + &chat_state, + &mut workspace, + user_message, + ); + } + } + }); + }); + }); + }); +} - Attribute Analysis: - A detailed breakdown of any attributes present in the area - (e.g., "Number of residential buildings," "Average household income"). +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 + }; -5. Layer Pickers + let text_color = egui::Color32::WHITE; - Add Layers: - Allow users to add additional data layers, such as road networks, land use, pollution levels, etc. + ui.horizontal(|ui| { + ui.add_space(10.0); + if !message.is_user { + ui.add_space(20.0); + } - Layer Toggle: - Let users enable/disable specific layers in the analysis. + 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), + ); + }); -UI Layout Suggestions: + ui.add(egui::Label::new(RichText::new(&message.content).color(text_color)).wrap()); + }); - Top Section: - A title or summary of the selected area (e.g., "Cambridge - Central Area"). + if message.is_user { + ui.add_space(10.0); + } + }); +} - Left Section: - A panel with sliders/dropdowns for custom styles, color by attribute, and thematic mapping options. +fn send_chat_message_background( + chat_state: &ChatState, + workspace: &mut Workspace, + user_message: String, +) { + workspace.llm_agent.set_token("!!! YOUR TOKEN HERE !!!"); + + // 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"); + } + } + } + _ => {} + } - Middle Section: - Interactive charts (pie charts, bar graphs, heatmaps) showing key statistics, - such as population density and area size. + // 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: {total_features}\n")); + context_info.push_str(&format!("Data layers: {}\n", requests.len())); + } else { + context_info.push_str("No workspace selected\n"); + } - 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(); + // Create the enhanced user message with context + let enhanced_message = if context_info.trim().is_empty() { + user_message.clone() + } else { + format!("CONTEXT:\n{context_info}\nUSER QUERY: {user_message}") + }; + if let Some(workspace) = workspace.workspace.as_mut() { + workspace.add_message("user", &enhanced_message); + } - let tilebox_width = 200.0; - let tilebox_height = screen_rect.height() - 40.0; + // Attempt to get response from LLM - let tilebox_pos = egui::pos2(screen_rect.width() - tilebox_width - 10.0, 30.0); + let request = WorkspaceRequest::new( + Uuid::new_v4().to_string(), + 1, + RequestType::OpenRouterRequest(), + Vec::new(), + ); + workspace.worker.queue_request(request); - // Number of items: - let mut item_count = 0; - for lists in workspace_res.get_rendered_requests().iter() { - item_count += lists.get_processed_data().size(); + // Clean up old messages + if let Ok(mut inner) = chat_state.inner.lock() { + while inner.chat_history.len() > 50 { + inner.chat_history.remove(0); } - 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 - )); - }); - }); - }); } } diff --git a/src/workspace/worker.rs b/src/workspace/worker.rs index 9bb4177..bdb9755 100644 --- a/src/workspace/worker.rs +++ b/src/workspace/worker.rs @@ -1,12 +1,13 @@ +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>>, @@ -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(); @@ -66,19 +71,28 @@ pub fn process_requests(mut commands: Commands, mut workspace: ResMut info!("No workspace found"); return; } - let client = workspace.overpass_agent.clone(); - // Clone the loaded_requests Arc instead of holding the MutexGuard let loaded_requests = workspace.loaded_requests.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) = 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(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) => {} } @@ -109,3 +123,275 @@ 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 + workspace_clone.summarize_features() + } + "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)) +} diff --git a/src/workspace/workspace_types.rs b/src/workspace/workspace_types.rs index abe9cb4..4395a51 100644 --- a/src/workspace/workspace_types.rs +++ b/src/workspace/workspace_types.rs @@ -7,18 +7,23 @@ 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}, + llm::Message, + 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}, 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 +31,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 +71,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(); @@ -85,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() @@ -142,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), } @@ -165,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() @@ -191,6 +223,7 @@ impl WorkspaceRequest { } } crate::workspace::RequestType::OpenMeteoRequest(_) => {} + crate::workspace::RequestType::OpenRouterRequest() => {} } } @@ -223,6 +256,7 @@ pub enum RequestType { // If we want to add more requests we can just add them here. OpenMeteoRequest(OpenMeteoRequest), OverpassTurboRequest(String), + OpenRouterRequest(), } impl std::fmt::Debug for RequestType { @@ -230,6 +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"), } } } @@ -276,6 +311,19 @@ impl WorkspaceData { last_modified: chrono::Utc::now().timestamp(), requests: HashSet::new(), properties: HashMap::new(), + messages: Vec::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(), + messages: Vec::new(), } } } @@ -392,6 +440,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.