Add GPS service and nautical base city data

- Implement `GpsService` with methods for position updates and enabling/disabling GPS.
- Introduce test data for nautical base cities with key attributes like population, coordinates, and images.
- Update dependencies in `bun.lock` with required packages such as `geojson`.
This commit is contained in:
geoffsee
2025-07-10 11:27:44 -04:00
parent 4b3dd2a1c3
commit 9da9fa777c
14 changed files with 618 additions and 84 deletions

View File

@@ -60,7 +60,7 @@ webbrowser = { version = "1", features = ["hardened"] }
systems = { path = "../systems" }
components = { path = "../components" }
wasm-bindgen = "0.2"
web-sys = { version = "0.3.53", features = ["Document", "Element", "HtmlElement", "Window"] }
web-sys = { version = "0.3.53", features = ["Document", "Element", "HtmlElement", "Window", "Navigator", "Geolocation", "Position", "PositionOptions", "PositionError", "Coordinates"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
@@ -78,6 +78,7 @@ winit = "0.30"
bevy_webview_wry = { version = "0.4", default-features = false, features = ["api"] }
bevy_flurx = "0.11"
bevy_flurx_ipc = "0.4.0"
# GPS support for native platforms - placeholder for future real GPS implementation
[target.'cfg(target_arch = "wasm32")'.dependencies]
tokio = { version = "1.0", features = ["rt"] }

View File

@@ -3,6 +3,7 @@
mod core;
mod ui;
mod services;
use bevy::app::App;
#[cfg(debug_assertions)]
@@ -11,6 +12,7 @@ use bevy::prelude::*;
use crate::core::{ActionsPlugin, SystemManagerPlugin};
use crate::core::system_manager::SystemManager;
use crate::ui::{LoadingPlugin, MenuPlugin, GpsMapPlugin};
use crate::services::GpsServicePlugin;
use systems::{PlayerPlugin, setup_instrument_cluster, get_vessel_systems};
// See https://bevy-cheatbook.github.io/programming/states.html
@@ -41,11 +43,12 @@ impl Plugin for GamePlugin {
LoadingPlugin,
MenuPlugin,
GpsMapPlugin,
GpsServicePlugin,
ActionsPlugin,
SystemManagerPlugin,
PlayerPlugin,
))
.add_systems(OnEnter(GameState::Playing), (setup_instrument_cluster, initialize_vessel_systems));
#[cfg(debug_assertions)]

View File

@@ -0,0 +1,211 @@
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GpsData {
pub latitude: f64,
pub longitude: f64,
pub altitude: Option<f64>,
pub accuracy: Option<f64>,
pub heading: Option<f64>,
pub speed: Option<f64>,
pub timestamp: f64,
}
#[derive(Resource, Default)]
pub struct GpsService {
pub current_position: Option<GpsData>,
pub is_enabled: bool,
pub last_update: f64,
}
impl GpsService {
pub fn new() -> Self {
Self {
current_position: None,
is_enabled: false,
last_update: 0.0,
}
}
pub fn enable(&mut self) {
self.is_enabled = true;
info!("GPS service enabled");
}
pub fn disable(&mut self) {
self.is_enabled = false;
info!("GPS service disabled");
}
pub fn update_position(&mut self, gps_data: GpsData) {
self.current_position = Some(gps_data.clone());
self.last_update = gps_data.timestamp;
info!("GPS position updated: lat={:.6}, lon={:.6}", gps_data.latitude, gps_data.longitude);
}
pub fn get_current_position(&self) -> Option<&GpsData> {
self.current_position.as_ref()
}
}
// Native GPS implementation - Mock implementation for demonstration
// TODO: Replace with real GPS hardware access (e.g., using gpsd, CoreLocation, etc.)
#[cfg(not(target_arch = "wasm32"))]
pub fn start_native_gps_tracking(mut gps_service: ResMut<GpsService>, time: Res<Time>) {
use std::time::{SystemTime, UNIX_EPOCH};
if !gps_service.is_enabled {
return;
}
// Mock GPS data that simulates realistic movement
// In a real implementation, this would read from GPS hardware
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs_f64();
// Only update every 2 seconds to simulate realistic GPS update rate
if timestamp - gps_service.last_update < 2.0 {
return;
}
// Simulate GPS coordinates around Monaco with realistic movement
let base_lat = 43.7384;
let base_lon = 7.4246;
let time_factor = time.elapsed_secs() * 0.1;
// Simulate a boat moving in a realistic pattern
let lat_offset = (time_factor.sin() * 0.002) as f64;
let lon_offset = (time_factor.cos() * 0.003) as f64;
let gps_data = GpsData {
latitude: base_lat + lat_offset,
longitude: base_lon + lon_offset,
altitude: Some(0.0), // Sea level
accuracy: Some(3.0), // 3 meter accuracy
heading: Some(((time_factor * 20.0) % 360.0) as f64),
speed: Some(5.2), // 5.2 knots
timestamp,
};
gps_service.update_position(gps_data);
}
// Web GPS implementation using geolocation API
// For web platforms, we'll use a simplified approach that requests position periodically
#[cfg(target_arch = "wasm32")]
pub fn start_web_gps_tracking(mut gps_service: ResMut<GpsService>, time: Res<Time>) {
use std::time::{SystemTime, UNIX_EPOCH};
if !gps_service.is_enabled {
return;
}
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs_f64();
// Only try to get GPS every 5 seconds to avoid overwhelming the browser
if timestamp - gps_service.last_update < 5.0 {
return;
}
// For now, use mock data for web as well
// TODO: Implement proper web geolocation API integration using channels or events
let time_factor = time.elapsed_secs() * 0.1;
let base_lat = 43.7384;
let base_lon = 7.4246;
let lat_offset = (time_factor.sin() * 0.001) as f64;
let lon_offset = (time_factor.cos() * 0.002) as f64;
let gps_data = GpsData {
latitude: base_lat + lat_offset,
longitude: base_lon + lon_offset,
altitude: Some(0.0),
accuracy: Some(5.0), // Slightly less accurate on web
heading: Some(((time_factor * 15.0) % 360.0) as f64),
speed: Some(4.8), // Slightly different speed for web
timestamp,
};
gps_service.update_position(gps_data);
info!("Web GPS position updated: lat={:.6}, lon={:.6}", gps_data.latitude, gps_data.longitude);
}
pub struct GpsServicePlugin;
impl Plugin for GpsServicePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<GpsService>()
.add_systems(Update, (
#[cfg(not(target_arch = "wasm32"))]
start_native_gps_tracking,
#[cfg(target_arch = "wasm32")]
start_web_gps_tracking,
));
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn test_gps_service_initialization() {
let mut gps_service = GpsService::new();
assert!(!gps_service.is_enabled);
assert!(gps_service.current_position.is_none());
gps_service.enable();
assert!(gps_service.is_enabled);
}
#[test]
fn test_gps_data_update() {
let mut gps_service = GpsService::new();
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs_f64();
let test_gps_data = GpsData {
latitude: 43.7384,
longitude: 7.4246,
altitude: Some(0.0),
accuracy: Some(3.0),
heading: Some(45.0),
speed: Some(5.2),
timestamp,
};
gps_service.update_position(test_gps_data.clone());
let current_pos = gps_service.get_current_position().unwrap();
assert_eq!(current_pos.latitude, 43.7384);
assert_eq!(current_pos.longitude, 7.4246);
assert_eq!(current_pos.speed, Some(5.2));
assert_eq!(current_pos.heading, Some(45.0));
}
#[test]
fn test_gps_service_enable_disable() {
let mut gps_service = GpsService::new();
// Test initial state
assert!(!gps_service.is_enabled);
// Test enable
gps_service.enable();
assert!(gps_service.is_enabled);
// Test disable
gps_service.disable();
assert!(!gps_service.is_enabled);
}
}

View File

@@ -0,0 +1,3 @@
pub mod gps_service;
pub use gps_service::*;

View File

@@ -5,6 +5,7 @@ use std::collections::HashMap;
use bevy_flurx::prelude::*;
use bevy_webview_wry::prelude::*;
use serde::{Deserialize, Serialize};
use crate::services::{GpsService, GpsData};
/// Render layer for GPS map entities to isolate them from other cameras
@@ -97,8 +98,9 @@ impl Plugin for GpsMapPlugin {
.add_systems(Update, (
handle_gps_map_window_events,
update_map_tiles,
send_periodic_gps_updates,
));
update_gps_from_service,
))
.add_systems(Startup, enable_gps_service);
}
}
@@ -330,28 +332,36 @@ async fn get_vessel_status(
})).await
}
/// System to send periodic GPS updates for testing
fn send_periodic_gps_updates(
/// System to enable GPS service on startup
fn enable_gps_service(mut gps_service: ResMut<GpsService>) {
gps_service.enable();
info!("GPS service enabled for map tracking");
}
/// System to update GPS map state from GPS service
fn update_gps_from_service(
mut gps_map_state: ResMut<GpsMapState>,
time: Res<Time>,
gps_service: Res<GpsService>,
) {
// Update vessel position every frame for testing
if time.delta_secs() > 0.0 {
// Simulate slight movement around Monaco
let base_lat = 43.6377;
let base_lon = -1.4497;
let offset = (time.elapsed_secs().sin() * 0.001) as f64;
if let Some(gps_data) = gps_service.get_current_position() {
// Update vessel position from real GPS data
gps_map_state.vessel_lat = gps_data.latitude;
gps_map_state.vessel_lon = gps_data.longitude;
gps_map_state.vessel_lat = base_lat + offset;
gps_map_state.vessel_lon = base_lon + offset * 0.5;
gps_map_state.vessel_speed = 5.0 + (time.elapsed_secs().cos() * 2.0) as f64;
gps_map_state.vessel_heading = ((time.elapsed_secs() * 10.0) % 360.0) as f64;
// Update speed and heading if available
if let Some(speed) = gps_data.speed {
gps_map_state.vessel_speed = speed;
}
if let Some(heading) = gps_data.heading {
gps_map_state.vessel_heading = heading;
}
// React side can poll for updates using get_vessel_status command
if time.elapsed_secs() as u32 % 5 == 0 && time.delta_secs() < 0.1 {
info!("Vessel position updated: lat={:.4}, lon={:.4}, speed={:.1}, heading={:.1}",
gps_map_state.vessel_lat, gps_map_state.vessel_lon,
gps_map_state.vessel_speed, gps_map_state.vessel_heading);
// Also update map center to follow vessel if this is the first GPS fix
if gps_map_state.center_lat == 43.6377 && gps_map_state.center_lon == -1.4497 {
gps_map_state.center_lat = gps_data.latitude;
gps_map_state.center_lon = gps_data.longitude;
info!("Map centered on GPS position: lat={:.6}, lon={:.6}",
gps_data.latitude, gps_data.longitude);
}
}
}