diff --git a/README.md b/README.md index 1cf9774..c907641 100644 --- a/README.md +++ b/README.md @@ -145,10 +145,20 @@ discord_integration: false Pathfinder on the o7 Show +## Mapper Support + +Short Circuit supports consuming wormhole data from multiple instances of multiple mapper sources simultaneously: +- Tripwire. +- Eve Scout. + +TBD: +- pathfinder. +- eve-whmapper - waiting for public API. + +See [docs/MAPPER_MODULES.md](docs/MAPPER_MODULES.md) for details on the modular mapper architecture. + ## Future development -1. Add support for more 3rd party wormhole mapping tools. -2. Combine data from multiple sources (multiple Tripwire accounts, etc.). -3. Suggestions? +1. Suggestions? ## Contacts For any questions please contact Lenai Chelien. I accept PLEX, ISK, Exotic Dancers and ~~drugs~~ boosters. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..49c1c88 --- /dev/null +++ b/TODO.md @@ -0,0 +1,80 @@ +# TODO - Short Circuit Future Enhancements + +## Mapper System + +### Connection Deduplication + +Currently, if two mappers provide the same connection, it's added twice to the solar map. Future enhancement could: + +- Track connection source in metadata +- Deduplicate based on (source_system, dest_system, sig_ids) +- Show "confidence" level based on multiple sources confirming the same connection +- Allow users to see which mappers reported each connection + +### Multiple Mapper Instance Management + +The architecture supports multiple instances of the same mapper type (e.g., multiple Tripwire servers), but the UI currently only supports configuring one instance of each type. + +To fully support multiple instances: + +1. **UI Enhancement**: Create a table window interface for managing mapper configurations + - Add/remove/edit mapper instances + - Enable/disable individual instances + - Test connectivity for each instance + +2. **Configuration Storage**: Update QSettings to store list of mapper configurations + - Each configuration includes: type, name, URL, credentials, enabled state + - Support multiple instances of same mapper type + +3. **Status Bar Improvement**: Rethink status bar to dynamically show all active mappers + - Current implementation hardcodes Tripwire and Eve Scout + - Should iterate over all registered sources + +## UI/UX Improvements + +### Dynamic Mapper Status Display + +Current status bar shows hardcoded Tripwire and Eve Scout connection counts. Should be refactored to: +- Dynamically display all active mapper sources +- Show connection count per source +- Indicate errors per source +- Update automatically when mappers are added/removed + +### Configuration Validation + +Current validation happens at mapper instantiation time. Consider: +- Validate configuration in UI before saving +- URL validation (format, reachability) +- Credential validation (test login) +- Provide immediate feedback to users + +## Performance + +### Parallel Mapper Fetching + +Currently mappers are called sequentially. Consider: +- Fetch from all mappers in parallel (ThreadPoolExecutor) +- Timeout per mapper to prevent one slow source blocking others +- Cancel all on user request + +### Caching + +- Cache mapper responses to reduce API calls +- Respect cache headers from APIs +- Allow manual refresh to bypass cache +- Show age of cached data + +## Code Quality + +### Mapper Interface Refinement + +- Consider removing `validate_config()` from mapper interface +- Move validation closer to UI/QSettings +- Simplify mapper interface to only what's essential + +### Testing + +- Add integration tests for mapper interactions +- Mock HTTP responses for deterministic testing +- Test error scenarios (network failures, auth failures, invalid data) +- Performance testing with large connection datasets diff --git a/docs/MAPPER_MODULES.md b/docs/MAPPER_MODULES.md new file mode 100644 index 0000000..a9b7bf2 --- /dev/null +++ b/docs/MAPPER_MODULES.md @@ -0,0 +1,356 @@ +# Modular Mapper Architecture + +## Overview + +Short Circuit now supports consuming wormhole connection data from multiple mapper sources simultaneously. This modular architecture allows you to: + +- Use multiple Tripwire servers at once (e.g., corp/alliance + public) +- Combine data from different mapping tools +- Easily add support for new mapping tools as they develop APIs + +## Architecture + +The modular mapper system consists of three main components: + +### 1. MapperSource (Base Class) + +`mapper_base.py` defines the interface that all mapper sources must implement: + +```python +class MapperSource(ABC): + @abstractmethod + def augment_map(self, solar_map: SolarMap) -> int: + """Add connections to the map. Returns connection count or -1 on error.""" + + @abstractmethod + def get_name(self) -> str: + """Return the mapper instance name.""" + + @abstractmethod + def get_config(self) -> Dict[str, str]: + """Return configuration as a dictionary.""" + + def validate_config(self) -> tuple[bool, Optional[str]]: + """Validate configuration. Returns (is_valid, error_message).""" +``` + +### 2. MapperRegistry + +`mapper_registry.py` manages multiple mapper sources: + +```python +registry = MapperRegistry() + +# Register sources +registry.register(Tripwire("user1", "pass1", "https://tripwire1.com", "Corp Tripwire")) +registry.register(Tripwire("user2", "pass2", "https://tripwire2.com", "Alliance Tripwire")) +registry.register(EveScout()) + +# Augment map from all sources +results = registry.augment_map(solar_map) +# Returns: {"Corp Tripwire": 15, "Alliance Tripwire": 23, "Eve Scout": 8} +``` + +### 3. Mapper Implementations + +Each mapper tool has its own implementation: + +- **Tripwire** (`tripwire.py`): Supports multiple instances with different URLs/credentials +- **Eve Scout** (`evescout.py`): Public API for Thera connections +- **Template** (`mapper_template.py`): Guide for adding new mappers + +## Supported Mappers + +### Tripwire + +**Status**: Fully supported with multiple instances + +**Configuration**: +- URL: Tripwire server URL +- Username: Account username +- Password: Account password +- Name: Instance identifier (e.g., "Corp Tripwire") + +**Example**: +```python +tripwire = Tripwire( + username="your_username", + password="your_password", + url="https://tripwire.eve-apps.com", + name="My Tripwire" +) +``` + +### Eve Scout + +**Status**: Fully supported + +**Configuration**: +- URL: API endpoint (default: https://api.eve-scout.com/v2/public/signatures) +- Name: Instance identifier + +**Example**: +```python +evescout = EveScout( + url="https://api.eve-scout.com/v2/public/signatures", + name="Eve Scout Thera" +) +``` + +### eve-whmapper + +**Status**: Not currently supported - no public API available + +**Investigation**: The eve-whmapper project (https://github.com/pfh59/eve-whmapper) is a C# Blazor web application that does not currently expose a public REST API for external consumption. It is designed as a self-hosted web application with internal services but no documented endpoints for retrieving wormhole connection data. + +**Future Support**: If eve-whmapper adds an API in the future, support can be easily added by: +1. Creating a new `WHMapper` class that inherits from `MapperSource` +2. Implementing the API client logic in `augment_map()` +3. Following the pattern in `mapper_template.py` + +## Adding a New Mapper + +To add support for a new wormhole mapping tool: + +### Step 1: Create a new mapper class + +Copy `mapper_template.py` to a new file (e.g., `mymapper.py`) and rename the class: + +```python +from .mapper_base import MapperSource + +class MyMapper(MapperSource): + def __init__(self, url: str, api_key: str, name: str = "My Mapper"): + self.url = url + self.api_key = api_key + self.name = name + self.eve_db = EveDb() +``` + +### Step 2: Implement augment_map() + +This is where you fetch data from the mapper's API and add connections: + +```python +def augment_map(self, solar_map: SolarMap) -> int: + try: + # Fetch data from API + response = requests.get( + f"{self.url}/api/connections", + headers={"Authorization": f"Bearer {self.api_key}"} + ) + + if response.status_code != 200: + return -1 + + data = response.json() + connections = 0 + + for conn in data['connections']: + # Extract connection details + source = conn['source_system_id'] + dest = conn['dest_system_id'] + sig_source = conn['source_sig'] + sig_dest = conn['dest_sig'] + wh_type = conn['wh_type'] + + # Determine wormhole properties + wh_size = self.eve_db.get_whsize_by_code(wh_type) + wh_life = WormholeTimespan.STABLE # Parse from API + wh_mass = WormholeMassspan.UNKNOWN # Parse from API + time_elapsed = 0.0 # Calculate from timestamp + + # Add to map + solar_map.add_connection( + source, dest, ConnectionType.WORMHOLE, + [sig_source, wh_type, sig_dest, 'K162', + wh_size, wh_life, wh_mass, time_elapsed] + ) + connections += 1 + + return connections + + except Exception as e: + Logger.error(f"Error fetching from {self.name}: {e}") + return -1 +``` + +### Step 3: Implement interface methods + +```python +def get_name(self) -> str: + return self.name + +def get_config(self) -> Dict[str, str]: + return { + 'url': self.url, + 'name': self.name, + 'api_key': '***' # Don't expose secrets + } + +def validate_config(self) -> tuple[bool, Optional[str]]: + if not self.url: + return False, "URL is required" + if not self.api_key: + return False, "API key is required" + return True, None +``` + +### Step 4: Register and use + +```python +# Create instance +my_mapper = MyMapper( + url="https://api.mymapper.com", + api_key="your_api_key", + name="My Corp Mapper" +) + +# Register with registry +registry = MapperRegistry() +registry.register(my_mapper) + +# Augment map +results = registry.augment_map(solar_map) +``` + +## Configuration Format + +Multiple mapper instances can be configured in the settings. Here's an example of the data structure: + +```python +mappers = [ + { + 'type': 'tripwire', + 'name': 'Corp Tripwire', + 'url': 'https://tripwire.corp.com', + 'username': 'user1', + 'password': 'pass1', + 'enabled': True + }, + { + 'type': 'tripwire', + 'name': 'Public Tripwire', + 'url': 'https://tripwire.eve-apps.com', + 'username': 'user2', + 'password': 'pass2', + 'enabled': True + }, + { + 'type': 'evescout', + 'name': 'Eve Scout', + 'url': 'https://api.eve-scout.com/v2/public/signatures', + 'enabled': True + } +] +``` + +## API Requirements for Mapper Tools + +For a mapping tool to be compatible with Short Circuit, it needs to provide: + +### Minimum Requirements + +1. **Read-only API endpoint** that returns wormhole connections +2. **Connection data** including: + - Source system ID + - Destination system ID + - Wormhole type (optional but recommended) + - Signature IDs (optional but recommended) + - Connection age/timestamp (optional) + - Wormhole life status (optional) + - Wormhole mass status (optional) + +### Authentication + +The API should support one of: +- Public unauthenticated access (like Eve Scout) +- Username/password authentication (like Tripwire) +- API key/token authentication +- OAuth2 authentication + +### Response Format + +Any format is acceptable (JSON, XML, etc.) as long as it can be parsed to extract the required connection data. + +### Example API Response + +```json +{ + "connections": [ + { + "source_system_id": 30000142, + "dest_system_id": 31000005, + "source_signature": "ABC-123", + "dest_signature": "XYZ-789", + "wormhole_type": "N110", + "life_status": "stable", + "mass_status": "stable", + "updated_at": "2026-02-14T12:00:00Z" + } + ] +} +``` + +## Benefits + +### For Users + +- **Aggregate data**: Combine connections from multiple sources for a complete picture +- **Redundancy**: If one mapper is down, others continue to work +- **Flexibility**: Use different mappers for different purposes (corp, alliance, public) +- **Community tools**: Easy to integrate new community mapping tools + +### For Developers + +- **Clear interface**: `MapperSource` defines exactly what needs to be implemented +- **Template available**: `mapper_template.py` provides a starting point +- **Well-documented**: Extensive documentation and examples +- **Tested pattern**: Tripwire and Eve Scout serve as reference implementations + +## Testing + +When adding a new mapper, create unit tests following the pattern in: +- `test_tripwire.py`: Tests for Tripwire implementation +- `test_tripwire_gate.py`: Integration tests + +Example test structure: + +```python +import unittest +from shortcircuit.model.mymapper import MyMapper +from shortcircuit.model.solarmap import SolarMap + +class TestMyMapper(unittest.TestCase): + def test_augment_map(self): + mapper = MyMapper(url="...", api_key="...") + solar_map = SolarMap(eve_db) + + result = mapper.augment_map(solar_map) + + self.assertGreaterEqual(result, 0) + + def test_get_name(self): + mapper = MyMapper(url="...", api_key="...", name="Test") + self.assertEqual(mapper.get_name(), "Test") +``` + +## Future Enhancements + +Potential improvements to the modular mapper system: + +1. **UI for managing multiple sources**: GUI for adding/removing/configuring mappers +2. **Connection deduplication**: Detect and merge duplicate connections from different sources +3. **Source prioritization**: Prefer data from certain sources when conflicts occur +4. **Connection metadata**: Track which source provided each connection +5. **Performance optimization**: Parallel fetching from multiple sources +6. **Rate limiting**: Respect API rate limits for each source +7. **Caching**: Cache mapper responses to reduce API calls + +## Questions? + +For questions or to propose adding support for a new mapper tool, please: +1. Check if the mapping tool has a public API +2. Review the documentation in this file +3. Look at `mapper_template.py` for implementation guidance +4. Open an issue on GitHub with details about the mapper tool diff --git a/docs/MODULE_ARCHITECTURE.md b/docs/MODULE_ARCHITECTURE.md new file mode 100644 index 0000000..c9672bf --- /dev/null +++ b/docs/MODULE_ARCHITECTURE.md @@ -0,0 +1,238 @@ +# Module Architecture + +## Overview + +This document describes how the different modules in Short Circuit work together to fetch wormhole connections from external mappers and calculate routes. + +## Control Flow for Fetching Wormhole Data + +### 1. User Interaction +``` +User clicks "Get Tripwire" button in GUI + ↓ +app.py: btn_trip_get_clicked() +``` + +### 2. Thread Initialization +``` +app.py starts worker_thread (separate thread to avoid blocking UI) + ↓ +NavProcessor.process() executes in worker thread +``` + +### 3. Map Setup +``` +NavProcessor.process() + ↓ +navigation.reset_chain() - creates fresh SolarMap + ↓ +navigation.setup_mappers() - configures mapper sources +``` + +### 4. Mapper Configuration +``` +Navigation.setup_mappers() + ↓ +Reads config from app_obj (MainWindow): + - Mapper configurations (type, url, credentials, enabled) + - Currently: single Tripwire + Eve Scout + - Future: multiple instances via table window interface + ↓ +Creates mapper instances: + - Tripwire(user, pass, url, name="Tripwire") + - EveScout(name="Eve Scout") if enabled + ↓ +Registers each mapper with MapperRegistry +``` + +### 5. Data Fetching +``` +Navigation.augment_map(solar_map) + ↓ +MapperRegistry.augment_map(solar_map) + ↓ +For each registered mapper: + - Calls mapper.augment_map(solar_map) + - Tripwire: Logs in, fetches /refresh.php, parses JSON + - EveScout: Fetches public API, parses JSON + - Each adds connections to solar_map + ↓ +Returns dict: {"Tripwire": 15, "Eve Scout": 8} +``` + +### 6. Result Processing +``` +NavProcessor receives results + ↓ +Calculates total_connections from all sources + ↓ +If total_connections > 0: + navigation.solar_map = solar_map (updates the map) + ↓ +Emits finished signal with (tripwire_count, evescout_count) +``` + +### 7. UI Update +``` +app.py receives finished signal + ↓ +worker_thread_done() handler updates: + - state_tripwire["connections"] + - state_evescout["connections"] + - Status bar displays + - Enables buttons again +``` + +## Module Responsibilities + +### app.py (MainWindow) +- **Role**: Main GUI application window +- **Responsibilities**: + - User interface and event handling + - Configuration storage (QSettings) + - Thread management for background tasks + - Status display updates +- **Key State**: + - Mapper configurations (currently: single Tripwire instance + Eve Scout) + - Future: should store multiple instances of multiple mapper types + - `state_tripwire`, `state_evescout` (connection counts, errors) + +### navigation.py (Navigation) +- **Role**: Orchestrates wormhole data fetching and pathfinding +- **Responsibilities**: + - Manages SolarMap instance + - Configures and manages MapperRegistry + - Provides pathfinding interface + - Route formatting and instructions +- **Key Methods**: + - `setup_mappers()`: Configures mappers from app config + - `augment_map()`: Fetches from all registered mappers + - `route()`: Calculates shortest path between systems + +### navprocessor.py (NavProcessor) +- **Role**: Worker thread processor for background tasks +- **Responsibilities**: + - Runs in separate thread to avoid blocking UI + - Coordinates map fetching workflow + - Aggregates results from multiple sources + - Signals completion to main thread +- **Threading**: Runs in `worker_thread`, emits `finished` signal + +### mapper_registry.py (MapperRegistry) +- **Role**: Registry for managing multiple mapper sources +- **Responsibilities**: + - Registers/unregisters mapper sources + - Iterates through all sources to fetch data + - Aggregates results from multiple mappers + - Handles individual source failures gracefully +- **Key Feature**: Allows combining data from multiple Tripwire servers, Eve Scout, etc. + +### mapper_base.py (MapperSource) +- **Role**: Abstract base class for mapper implementations +- **Interface**: + - `augment_map(solar_map)`: Add connections to map, return count + - `get_name()`: Return human-readable name + - `validate_config()`: Check if configuration is valid + +### tripwire.py (Tripwire) +- **Role**: Tripwire mapper implementation +- **Responsibilities**: + - Authenticate with Tripwire server + - Fetch wormhole connection data via /refresh.php + - Parse Tripwire JSON format + - Add connections to SolarMap +- **Authentication**: Session-based (POST to /login.php) +- **API**: /refresh.php with system_id parameter + +### evescout.py (EveScout) +- **Role**: Eve Scout Thera connections implementation +- **Responsibilities**: + - Fetch public Thera connection data + - Parse Eve Scout JSON format + - Add Thera connections to SolarMap +- **Authentication**: None (public API) +- **API**: https://api.eve-scout.com/v2/public/signatures + +### solarmap.py (SolarMap) +- **Role**: Graph representation of Eve solar system map +- **Responsibilities**: + - Stores systems and connections (gates + wormholes) + - Implements shortest path algorithm (Dijkstra) + - Handles connection weights based on security, wormhole size, etc. + - Applies restrictions (avoid lists, size limits, etc.) + +## Data Flow Diagram + +``` +┌─────────────┐ +│ app.py │ User clicks "Get Tripwire" +│ (MainWindow)│ +└──────┬──────┘ + │ starts + ↓ +┌─────────────────┐ +│ NavProcessor │ Worker Thread +│ (QThread) │ +└──────┬──────────┘ + │ calls + ↓ +┌─────────────────┐ +│ Navigation │ Orchestrator +└──────┬──────────┘ + │ uses + ↓ +┌─────────────────┐ +│ MapperRegistry │ Manages sources +└──────┬──────────┘ + │ iterates + ↓ +┌──────────────────────┐ +│ MapperSource │ Interface +│ ├─ Tripwire │ Implementations +│ └─ EveScout │ +└──────┬───────────────┘ + │ augments + ↓ +┌─────────────────┐ +│ SolarMap │ Graph structure +└─────────────────┘ +``` + +## Configuration Storage + +Short Circuit uses QSettings (Qt's configuration system) to store: + +- **Tripwire credentials**: `tripwire_url`, `tripwire_user`, `tripwire_pass` +- **Eve Scout enabled**: `evescout_enabled` (boolean) +- **Other settings**: Proxy, restrictions, avoidance lists, etc. + +Configuration is: +1. Loaded from QSettings in `app.py.__init__()` → `read_settings()` +2. Stored in MainWindow instance variables +3. Accessed by Navigation through `self.app_obj` reference +4. Saved back to QSettings when changed + +## Adding a New Mapper + +See `docs/MAPPER_MODULES.md` for detailed guide on implementing new mapper sources. + +## Threading Model + +- **Main Thread**: UI (app.py, MainWindow) + - Handles user interaction + - Updates display + - Cannot be blocked + +- **Worker Thread**: Data fetching (NavProcessor) + - Runs `NavProcessor.process()` + - Calls mappers (can block on network I/O) + - Emits signal when done + +This separation ensures the UI remains responsive while fetching data from external mappers. + +## Error Handling + +- **Individual mapper failures**: MapperRegistry continues with other sources +- **Network errors**: Each mapper returns -1 on failure +- **Authentication errors**: Logged and reported in UI status +- **Invalid data**: Gracefully skipped, logged for debugging diff --git a/src/shortcircuit/app.py b/src/shortcircuit/app.py index bc86b84..a57b2c7 100644 --- a/src/shortcircuit/app.py +++ b/src/shortcircuit/app.py @@ -860,7 +860,6 @@ def btn_trip_get_clicked(self): if not self.worker_thread.isRunning(): self.pushButton_trip_get.setEnabled(False) self.pushButton_find_path.setEnabled(False) - self.nav_processor.evescout_enable = self.state_evescout["enabled"] self.worker_thread.start() else: self.state_tripwire['error'] = "error. Process is already running." diff --git a/src/shortcircuit/model/evescout.py b/src/shortcircuit/model/evescout.py index de37c91..0d5cc6f 100644 --- a/src/shortcircuit/model/evescout.py +++ b/src/shortcircuit/model/evescout.py @@ -1,27 +1,33 @@ # evescout.py from datetime import datetime +from typing import Dict, Optional import requests from shortcircuit import USER_AGENT from .evedb import EveDb, WormholeSize, WormholeMassspan, WormholeTimespan from .logger import Logger +from .mapper_base import MapperSource from .solarmap import ConnectionType, SolarMap -class EveScout: +class EveScout(MapperSource): """ - Eve Scout Thera Connections + Eve Scout wormhole connections provider. + + Provides public wormhole connections to Thera and Turnur systems. """ TIMEOUT = 2 def __init__( self, url: str = 'https://api.eve-scout.com/v2/public/signatures', + name: str = "Eve Scout", ): self.eve_db = EveDb() self.evescout_url = url + self.name = name def augment_map(self, solar_map: SolarMap): """ @@ -106,3 +112,24 @@ def augment_map(self, solar_map: SolarMap): ) return connections + + def get_name(self) -> str: + """ + Get the name of this Eve Scout instance. + + Returns: + The name of this mapper source + """ + return self.name + + def validate_config(self) -> tuple[bool, Optional[str]]: + """ + Validate the Eve Scout configuration. + + Returns: + Tuple of (is_valid, error_message) + """ + if not self.evescout_url: + return False, "URL is required" + return True, None + diff --git a/src/shortcircuit/model/mapper_base.py b/src/shortcircuit/model/mapper_base.py new file mode 100644 index 0000000..0f559cf --- /dev/null +++ b/src/shortcircuit/model/mapper_base.py @@ -0,0 +1,49 @@ +# mapper_base.py + +from abc import ABC, abstractmethod +from typing import Optional + +from .solarmap import SolarMap + + +class MapperSource(ABC): + """ + Abstract base class for wormhole mapper data sources. + + This class defines the interface that all mapper sources (Tripwire, Eve Scout, etc.) + must implement to integrate with Short Circuit. The primary method is augment_map(), + which adds wormhole connections from the external mapper to the solar map. + """ + + @abstractmethod + def augment_map(self, solar_map: SolarMap) -> int: + """ + Augment the solar map with wormhole connections from this mapper source. + + Args: + solar_map: The SolarMap to augment with connections + + Returns: + Number of connections added on success, -1 on failure + """ + pass + + @abstractmethod + def get_name(self) -> str: + """ + Get the human-readable name of this mapper source. + + Returns: + The name of the mapper source (e.g., "Tripwire", "Eve Scout") + """ + pass + + def validate_config(self) -> tuple[bool, Optional[str]]: + """ + Validate the configuration of this mapper source. + + Returns: + Tuple of (is_valid, error_message). If valid, error_message is None. + """ + return True, None + diff --git a/src/shortcircuit/model/mapper_registry.py b/src/shortcircuit/model/mapper_registry.py new file mode 100644 index 0000000..6769cf2 --- /dev/null +++ b/src/shortcircuit/model/mapper_registry.py @@ -0,0 +1,103 @@ +# mapper_registry.py + +from typing import Dict, List, Optional + +from .logger import Logger +from .mapper_base import MapperSource +from .solarmap import SolarMap + + +class MapperRegistry: + """ + Registry for managing multiple mapper data sources. + + This class allows Short Circuit to consume data from multiple mapper instances + (e.g., multiple Tripwire servers, eve-whmapper instances, etc.) and combine + them into a single solar map. + """ + + def __init__(self): + self.sources: List[MapperSource] = [] + + def register(self, source: MapperSource): + """ + Register a new mapper source. + + Args: + source: The mapper source to register + """ + is_valid, error = source.validate_config() + if not is_valid: + Logger.warning( + f"Mapper source {source.get_name()} has invalid config: {error}" + ) + self.sources.append(source) + Logger.info(f"Registered mapper source: {source.get_name()}") + + def unregister(self, source: MapperSource): + """ + Unregister a mapper source. + + Args: + source: The mapper source to unregister + """ + if source in self.sources: + self.sources.remove(source) + Logger.info(f"Unregistered mapper source: {source.get_name()}") + + def clear(self): + """ + Clear all registered mapper sources. + """ + self.sources.clear() + Logger.info("Cleared all mapper sources") + + def augment_map(self, solar_map: SolarMap) -> Dict[str, int]: + """ + Augment the solar map with connections from all registered sources. + + Args: + solar_map: The SolarMap to augment + + Returns: + Dictionary mapping source names to connection counts. + Returns -1 for sources that failed. + """ + results = {} + for source in self.sources: + source_name = source.get_name() + Logger.info(f"Augmenting map from source: {source_name}") + try: + connections = source.augment_map(solar_map) + results[source_name] = connections + if connections >= 0: + Logger.info( + f"Added {connections} connections from {source_name}" + ) + else: + Logger.error(f"Failed to get connections from {source_name}") + except Exception as e: + Logger.error( + f"Exception while augmenting from {source_name}: {e}" + ) + results[source_name] = -1 + + return results + + def get_sources(self) -> List[MapperSource]: + """ + Get all registered mapper sources. + + Returns: + List of registered mapper sources + """ + return self.sources.copy() + + def get_source_count(self) -> int: + """ + Get the number of registered mapper sources. + + Returns: + Number of registered sources + """ + return len(self.sources) diff --git a/src/shortcircuit/model/navigation.py b/src/shortcircuit/model/navigation.py index 9044bdd..c5fe679 100644 --- a/src/shortcircuit/model/navigation.py +++ b/src/shortcircuit/model/navigation.py @@ -1,9 +1,10 @@ # navigation.py -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING, Dict, List from .evedb import EveDb, SystemDescription, WormholeMassspan, WormholeSize, WormholeTimespan from .evescout import EveScout +from .mapper_registry import MapperRegistry from .solarmap import ConnectionType, SolarMap from .tripwire import Tripwire @@ -13,7 +14,10 @@ class Navigation: """ - Navigation + Navigation - handles pathfinding and wormhole mapper integration. + + This class manages the solar map and integrates with multiple wormhole + mapping sources (Tripwire, Eve Scout, etc.) through the MapperRegistry. """ def __init__(self, app_obj: 'MainWindow', eve_db: EveDb): @@ -21,13 +25,14 @@ def __init__(self, app_obj: 'MainWindow', eve_db: EveDb): self.eve_db = eve_db self.solar_map = SolarMap(self.eve_db) - self.tripwire_obj = None + self.mapper_registry = MapperRegistry() self.tripwire_url = self.app_obj.tripwire_url self.tripwire_user = self.app_obj.tripwire_user self.tripwire_password = self.app_obj.tripwire_pass def reset_chain(self): + """Reset the solar map to its initial state.""" self.solar_map = SolarMap(self.eve_db) return self.solar_map @@ -37,6 +42,7 @@ def tripwire_set_login( user: str = None, password: str = None, ): + """Update Tripwire login credentials.""" if not url: url = self.app_obj.tripwire_url self.tripwire_url = url @@ -49,14 +55,43 @@ def tripwire_set_login( password = self.app_obj.tripwire_pass self.tripwire_password = password - def tripwire_augment(self, solar_map: SolarMap): - self.tripwire_obj = Tripwire( - self.tripwire_user, - self.tripwire_password, - self.tripwire_url, - ) - connections = self.tripwire_obj.augment_map(solar_map) - return connections + def setup_mappers(self): + """ + Configure mapper sources in the registry based on current settings. + + This method reads configuration from the app and sets up all enabled + mapper sources (Tripwire, Eve Scout, etc.) in the registry. + """ + # Clear existing mappers + self.mapper_registry.clear() + + # Add Tripwire if configured + if self.tripwire_url and self.tripwire_user and self.tripwire_password: + tripwire = Tripwire( + self.tripwire_user, + self.tripwire_password, + self.tripwire_url, + name="Tripwire" + ) + self.mapper_registry.register(tripwire) + + # Add Eve Scout if enabled + evescout_enabled = self.app_obj.state_evescout.get("enabled", False) + if evescout_enabled: + evescout = EveScout(name="Eve Scout") + self.mapper_registry.register(evescout) + + def augment_map(self, solar_map: SolarMap) -> Dict[str, int]: + """ + Augment the solar map from all registered mapper sources. + + Args: + solar_map: The solar map to augment + + Returns: + Dictionary mapping source names to connection counts + """ + return self.mapper_registry.augment_map(solar_map) # FIXME refactor neighbor info - weights @staticmethod diff --git a/src/shortcircuit/model/navprocessor.py b/src/shortcircuit/model/navprocessor.py index 5838944..e3d52f1 100644 --- a/src/shortcircuit/model/navprocessor.py +++ b/src/shortcircuit/model/navprocessor.py @@ -3,7 +3,7 @@ from PySide2 import QtCore -from .navigation import Navigation, evescout_augment +from .navigation import Navigation class NavProcessor(QtCore.QObject): @@ -15,7 +15,6 @@ class NavProcessor(QtCore.QObject): def __init__(self, nav: Navigation, parent=None): super().__init__(parent) - self.evescout_enable = False self.nav = nav def process(self): @@ -24,10 +23,24 @@ def process(self): debugpy.debug_this_thread() solar_map = self.nav.reset_chain() - connections = self.nav.tripwire_augment(solar_map) - evescout_connections = 0 - if self.evescout_enable: - evescout_connections = evescout_augment(solar_map) - if connections > 0 or evescout_connections > 0: + + # Setup mappers based on configuration + self.nav.setup_mappers() + + # Augment from all registered mappers + results = self.nav.augment_map(solar_map) + + # Calculate total connections from all sources + total_connections = sum(count for count in results.values() if count > 0) + + # For backward compatibility with UI, extract specific mapper counts + # TODO: rethink status bar to support dynamic list of mappers + # The UI currently expects (tripwire_connections, evescout_connections) + tripwire_connections = results.get("Tripwire", 0) + evescout_connections = results.get("Eve Scout", 0) + + # Update solar map if we got any connections + if total_connections > 0: self.nav.solar_map = solar_map - self.finished.emit(connections, evescout_connections) + + self.finished.emit(tripwire_connections, evescout_connections) diff --git a/src/shortcircuit/model/test_mapper_registry.py b/src/shortcircuit/model/test_mapper_registry.py new file mode 100644 index 0000000..4c9f2cf --- /dev/null +++ b/src/shortcircuit/model/test_mapper_registry.py @@ -0,0 +1,204 @@ +# test_mapper_registry.py + +import unittest +from typing import Dict, Optional +from unittest.mock import Mock + +from shortcircuit.model.mapper_base import MapperSource +from shortcircuit.model.mapper_registry import MapperRegistry + + +class MockSolarMap: + """Mock solar map for testing.""" + pass + + +class MockMapperSource(MapperSource): + """Mock mapper source for testing.""" + + def __init__(self, name: str, connections_to_return: int = 5): + self.name = name + self.connections_to_return = connections_to_return + self.augment_called = False + + def augment_map(self, solar_map) -> int: + self.augment_called = True + return self.connections_to_return + + def get_name(self) -> str: + return self.name + + def validate_config(self) -> tuple[bool, Optional[str]]: + return True, None + + +class TestMapperRegistry(unittest.TestCase): + """Test cases for MapperRegistry.""" + + def setUp(self): + """Set up test fixtures.""" + self.registry = MapperRegistry() + self.solar_map = MockSolarMap() + + def test_register_source(self): + """Test registering a mapper source.""" + source = MockMapperSource("Test Mapper") + self.registry.register(source) + + self.assertEqual(self.registry.get_source_count(), 1) + self.assertIn(source, self.registry.get_sources()) + + def test_register_multiple_sources(self): + """Test registering multiple mapper sources.""" + source1 = MockMapperSource("Mapper 1") + source2 = MockMapperSource("Mapper 2") + source3 = MockMapperSource("Mapper 3") + + self.registry.register(source1) + self.registry.register(source2) + self.registry.register(source3) + + self.assertEqual(self.registry.get_source_count(), 3) + + def test_unregister_source(self): + """Test unregistering a mapper source.""" + source1 = MockMapperSource("Mapper 1") + source2 = MockMapperSource("Mapper 2") + + self.registry.register(source1) + self.registry.register(source2) + self.assertEqual(self.registry.get_source_count(), 2) + + self.registry.unregister(source1) + self.assertEqual(self.registry.get_source_count(), 1) + self.assertNotIn(source1, self.registry.get_sources()) + self.assertIn(source2, self.registry.get_sources()) + + def test_clear_sources(self): + """Test clearing all mapper sources.""" + source1 = MockMapperSource("Mapper 1") + source2 = MockMapperSource("Mapper 2") + + self.registry.register(source1) + self.registry.register(source2) + self.assertEqual(self.registry.get_source_count(), 2) + + self.registry.clear() + self.assertEqual(self.registry.get_source_count(), 0) + + def test_augment_map_single_source(self): + """Test augmenting map from a single source.""" + source = MockMapperSource("Test Mapper", connections_to_return=10) + self.registry.register(source) + + results = self.registry.augment_map(self.solar_map) + + self.assertTrue(source.augment_called) + self.assertEqual(results["Test Mapper"], 10) + + def test_augment_map_multiple_sources(self): + """Test augmenting map from multiple sources.""" + source1 = MockMapperSource("Mapper 1", connections_to_return=5) + source2 = MockMapperSource("Mapper 2", connections_to_return=8) + source3 = MockMapperSource("Mapper 3", connections_to_return=12) + + self.registry.register(source1) + self.registry.register(source2) + self.registry.register(source3) + + results = self.registry.augment_map(self.solar_map) + + self.assertTrue(source1.augment_called) + self.assertTrue(source2.augment_called) + self.assertTrue(source3.augment_called) + + self.assertEqual(results["Mapper 1"], 5) + self.assertEqual(results["Mapper 2"], 8) + self.assertEqual(results["Mapper 3"], 12) + + def test_augment_map_with_failure(self): + """Test augmenting map when a source fails.""" + source1 = MockMapperSource("Good Mapper", connections_to_return=5) + source2 = MockMapperSource("Bad Mapper", connections_to_return=-1) + source3 = MockMapperSource("Another Good Mapper", connections_to_return=8) + + self.registry.register(source1) + self.registry.register(source2) + self.registry.register(source3) + + results = self.registry.augment_map(self.solar_map) + + self.assertEqual(results["Good Mapper"], 5) + self.assertEqual(results["Bad Mapper"], -1) + self.assertEqual(results["Another Good Mapper"], 8) + + def test_augment_map_with_exception(self): + """Test augmenting map when a source raises an exception.""" + class FailingMapperSource(MapperSource): + def augment_map(self, solar_map) -> int: + raise RuntimeError("Test exception") + + def get_name(self) -> str: + return "Failing Mapper" + + def validate_config(self) -> tuple[bool, Optional[str]]: + return True, None + + good_source = MockMapperSource("Good Mapper", connections_to_return=5) + failing_source = FailingMapperSource() + + self.registry.register(good_source) + self.registry.register(failing_source) + + results = self.registry.augment_map(self.solar_map) + + self.assertEqual(results["Good Mapper"], 5) + self.assertEqual(results["Failing Mapper"], -1) + + def test_get_sources_returns_copy(self): + """Test that get_sources returns a copy of the sources list.""" + source = MockMapperSource("Test Mapper") + self.registry.register(source) + + sources = self.registry.get_sources() + sources.clear() + + # Original registry should still have the source + self.assertEqual(self.registry.get_source_count(), 1) + + def test_empty_registry(self): + """Test operations on an empty registry.""" + results = self.registry.augment_map(self.solar_map) + + self.assertEqual(results, {}) + self.assertEqual(self.registry.get_source_count(), 0) + self.assertEqual(self.registry.get_sources(), []) + + def test_invalid_config_validation(self): + """Test that sources with invalid configurations can still be registered.""" + class InvalidConfigSource(MapperSource): + def augment_map(self, solar_map) -> int: + return 0 + + def get_name(self) -> str: + return "Invalid Config Source" + + def validate_config(self) -> tuple[bool, Optional[str]]: + return False, "URL is required" + + # Registry allows registration even with invalid config (just logs warning) + source = InvalidConfigSource() + self.registry.register(source) + + # Source is registered despite invalid config + self.assertEqual(self.registry.get_source_count(), 1) + + # But we can check validation ourselves + is_valid, error = source.validate_config() + self.assertFalse(is_valid) + self.assertEqual(error, "URL is required") + + +if __name__ == '__main__': + unittest.main() + diff --git a/src/shortcircuit/model/tripwire.py b/src/shortcircuit/model/tripwire.py index 351bb0d..df6db8e 100644 --- a/src/shortcircuit/model/tripwire.py +++ b/src/shortcircuit/model/tripwire.py @@ -9,6 +9,7 @@ from .evedb import EveDb, WormholeSize, WormholeMassspan, WormholeTimespan from .logger import Logger +from .mapper_base import MapperSource from .solarmap import ConnectionType, SolarMap from .utility.configuration import Configuration @@ -119,19 +120,20 @@ class TripwireChain(TypedDict): SignatureKey = Literal['initialID', 'secondaryID'] -class Tripwire: +class Tripwire(MapperSource): """ - Tripwire handler + Tripwire wormhole mapper client. """ WTYPE_UNKNOWN = '----' SIG_UNKNOWN = '-------' - def __init__(self, username: str, password: str, url: str): + def __init__(self, username: str, password: str, url: str, name: str = "Tripwire"): self.eve_db = EveDb() self.username = username self.password = password self.url = url + self.name = name self.session_requests = self.login() self.chain: TripwireChain = self._empty_chain() @@ -444,6 +446,30 @@ def augment_map(self, solar_map: SolarMap): return connections + def get_name(self) -> str: + """ + Get the name of this Tripwire instance. + + Returns: + The name of this mapper source + """ + return self.name + + def validate_config(self) -> tuple[bool, Optional[str]]: + """ + Validate the Tripwire configuration. + + Returns: + Tuple of (is_valid, error_message) + """ + if not self.url: + return False, "URL is required" + if not self.username: + return False, "Username is required" + if not self.password: + return False, "Password is required" + return True, None + @staticmethod def format_tripwire_wormhole_type(wtype): if not wtype or wtype == '' or wtype == '????':