mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
bridge bevy and react-map-gl to exchange gps (#6)
* isolate ipc pattern * shuffle logic in main for readability, remove unused webview message observer * renders react map --------- Co-authored-by: geoffsee <>
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -8669,6 +8669,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"bevy",
|
||||
"bevy_asset_loader",
|
||||
"bevy_flurx",
|
||||
"bevy_flurx_ipc",
|
||||
"bevy_kira_audio",
|
||||
"bevy_webview_wry",
|
||||
"components",
|
||||
@@ -8676,6 +8678,8 @@ dependencies = [
|
||||
"image",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"systems",
|
||||
"tokio",
|
||||
"wasm-bindgen",
|
||||
|
16
bun.lock
16
bun.lock
@@ -23,9 +23,11 @@
|
||||
"@chakra-ui/react": "^3.21.1",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@eslint/js": "^9.29.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"bevy_flurx_api": "^0.1.0",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
@@ -287,6 +289,8 @@
|
||||
|
||||
"@types/geojson-vt": ["@types/geojson-vt@3.2.5", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g=="],
|
||||
|
||||
"@types/js-cookie": ["@types/js-cookie@3.0.6", "", {}, "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
|
||||
|
||||
"@types/mapbox__point-geometry": ["@types/mapbox__point-geometry@0.1.4", "", {}, "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA=="],
|
||||
@@ -485,6 +489,8 @@
|
||||
|
||||
"base-map": ["base-map@workspace:packages/base-map"],
|
||||
|
||||
"bevy_flurx_api": ["bevy_flurx_api@0.1.0", "", { "dependencies": { "global": "^4.4.0", "pnpm": "^9.15.0" } }, "sha512-h74CxORqMa7iwTGTPe5ItkABEf7ZQIRlUSbeYPSBkF1SXnFCAaeF7FE4FklFmpRCKTYhryqVez02h75QACr45g=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
|
||||
@@ -521,6 +527,8 @@
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
"dom-walk": ["dom-walk@0.1.2", "", {}, "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w=="],
|
||||
|
||||
"earcut": ["earcut@3.0.1", "", {}, "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw=="],
|
||||
|
||||
"error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="],
|
||||
@@ -589,6 +597,8 @@
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
"global": ["global@4.4.0", "", { "dependencies": { "min-document": "^2.19.0", "process": "^0.11.10" } }, "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w=="],
|
||||
|
||||
"globals": ["globals@16.3.0", "", {}, "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ=="],
|
||||
|
||||
"globrex": ["globrex@0.1.2", "", {}, "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg=="],
|
||||
@@ -667,6 +677,8 @@
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
|
||||
|
||||
"min-document": ["min-document@2.19.0", "", { "dependencies": { "dom-walk": "^0.1.0" } }, "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
@@ -707,12 +719,16 @@
|
||||
|
||||
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
||||
"pnpm": ["pnpm@9.15.9", "", { "bin": { "pnpm": "bin/pnpm.cjs", "pnpx": "bin/pnpx.cjs" } }, "sha512-aARhQYk8ZvrQHAeSMRKOmvuJ74fiaR1p5NQO7iKJiClf1GghgbrlW1hBjDolO95lpQXsfF+UA+zlzDzTfc8lMQ=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"potpack": ["potpack@2.0.0", "", {}, "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
"process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||
|
||||
"protocol-buffers-schema": ["protocol-buffers-schema@3.6.0", "", {}, "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw=="],
|
||||
|
||||
"proxy-compare": ["proxy-compare@3.0.1", "", {}, "sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q=="],
|
||||
|
@@ -6,8 +6,6 @@ authors = ["seemueller-io <git@github.geoffsee>"]
|
||||
edition = "2021"
|
||||
exclude = ["dist", "build", "assets", "credits"]
|
||||
|
||||
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
|
||||
@@ -63,16 +61,8 @@ systems = { path = "../systems" }
|
||||
components = { path = "../components" }
|
||||
wasm-bindgen = "0.2"
|
||||
web-sys = { version = "0.3.53", features = ["Document", "Element", "HtmlElement", "Window"] }
|
||||
|
||||
# Platform-specific tokio features
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.0", features = ["rt", "rt-multi-thread"] }
|
||||
image = "0.25"
|
||||
winit = "0.30"
|
||||
bevy_webview_wry = { version = "0.4", default-features = false, features = ["api"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
tokio = { version = "1.0", features = ["rt"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
# keep the following in sync with Bevy's dependencies
|
||||
winit = { version = "0.30", default-features = false }
|
||||
@@ -80,5 +70,17 @@ image = { version = "0.25", default-features = false }
|
||||
## This greatly improves WGPU's performance due to its heavy use of trace! calls
|
||||
log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] }
|
||||
|
||||
# Platform-specific tokio features
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1.0", features = ["rt", "rt-multi-thread"] }
|
||||
image = "0.25"
|
||||
winit = "0.30"
|
||||
bevy_webview_wry = { version = "0.4", default-features = false, features = ["api"] }
|
||||
bevy_flurx = "0.11"
|
||||
bevy_flurx_ipc = "0.4.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
tokio = { version = "1.0", features = ["rt"] }
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "1"
|
||||
|
@@ -14,31 +14,6 @@ use winit::window::Icon;
|
||||
use bevy_webview_wry::WebviewWryPlugin;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
App::new()
|
||||
.insert_resource(ClearColor(Color::NONE))
|
||||
.add_plugins(
|
||||
DefaultPlugins
|
||||
.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
// Bind to canvas included in `index.html`
|
||||
canvas: Some("#yachtpit-canvas".to_owned()),
|
||||
fit_canvas_to_parent: true,
|
||||
// Tells wasm not to override default event handling, like F5 and Ctrl+R
|
||||
prevent_default_event_handling: false,
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
})
|
||||
.set(AssetPlugin {
|
||||
meta_check: AssetMetaCheck::Never,
|
||||
..default()
|
||||
}),
|
||||
)
|
||||
.add_plugins(GamePlugin)
|
||||
.add_systems(Startup, set_window_icon)
|
||||
.run();
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
App::new()
|
||||
.insert_resource(ClearColor(Color::NONE))
|
||||
@@ -65,7 +40,33 @@ fn main() {
|
||||
.add_plugins(WebviewWryPlugin::default())
|
||||
.run();
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
App::new()
|
||||
.insert_resource(ClearColor(Color::NONE))
|
||||
.add_plugins(
|
||||
DefaultPlugins
|
||||
.set(WindowPlugin {
|
||||
primary_window: Some(Window {
|
||||
// Bind to canvas included in `index.html`
|
||||
canvas: Some("#yachtpit-canvas".to_owned()),
|
||||
fit_canvas_to_parent: true,
|
||||
// Tells wasm not to override default event handling, like F5 and Ctrl+R
|
||||
prevent_default_event_handling: false,
|
||||
..default()
|
||||
}),
|
||||
..default()
|
||||
})
|
||||
.set(AssetPlugin {
|
||||
meta_check: AssetMetaCheck::Never,
|
||||
..default()
|
||||
}),
|
||||
)
|
||||
.add_plugins(GamePlugin)
|
||||
.add_systems(Startup, set_window_icon)
|
||||
.run();
|
||||
|
||||
}
|
||||
|
||||
// Sets the icon on windows and X11
|
||||
fn set_window_icon(
|
||||
windows: NonSend<WinitWindows>,
|
||||
|
@@ -2,7 +2,9 @@ use bevy::prelude::*;
|
||||
use bevy::render::view::RenderLayers;
|
||||
use bevy::window::Window;
|
||||
use std::collections::HashMap;
|
||||
use bevy_flurx::prelude::*;
|
||||
use bevy_webview_wry::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
/// Render layer for GPS map entities to isolate them from other cameras
|
||||
|
||||
|
||||
@@ -12,6 +14,38 @@ use bevy_webview_wry::prelude::*;
|
||||
/// Render layer for GPS map entities to isolate them from other cameras
|
||||
const GPS_MAP_LAYER: usize = 1;
|
||||
|
||||
/// GPS position data
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct GpsPosition {
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
pub zoom: u8,
|
||||
}
|
||||
|
||||
/// Vessel position and status data
|
||||
#[derive(Serialize, Debug, Clone)]
|
||||
pub struct VesselStatus {
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
pub heading: f64,
|
||||
pub speed: f64,
|
||||
}
|
||||
|
||||
/// Map view change parameters
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct MapViewParams {
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
pub zoom: u8,
|
||||
}
|
||||
|
||||
/// Authentication parameters
|
||||
#[derive(Deserialize, Debug, Clone)]
|
||||
pub struct AuthParams {
|
||||
pub authenticated: bool,
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
/// Component to mark the GPS map window
|
||||
#[derive(Component)]
|
||||
pub struct GpsMapWindow;
|
||||
@@ -32,6 +66,10 @@ pub struct GpsMapState {
|
||||
pub center_lon: f64,
|
||||
pub zoom_level: u8,
|
||||
pub tile_cache: HashMap<String, Handle<Image>>,
|
||||
pub vessel_lat: f64,
|
||||
pub vessel_lon: f64,
|
||||
pub vessel_heading: f64,
|
||||
pub vessel_speed: f64,
|
||||
}
|
||||
|
||||
impl GpsMapState {
|
||||
@@ -42,6 +80,10 @@ impl GpsMapState {
|
||||
center_lon: -1.4497,
|
||||
zoom_level: 10,
|
||||
tile_cache: HashMap::new(),
|
||||
vessel_lat: 43.6377, // Default vessel position
|
||||
vessel_lon: -1.4497,
|
||||
vessel_heading: 0.0,
|
||||
vessel_speed: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +94,11 @@ pub struct GpsMapPlugin;
|
||||
impl Plugin for GpsMapPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<GpsMapState>()
|
||||
.add_systems(Update, (handle_gps_map_window_events, update_map_tiles));
|
||||
.add_systems(Update, (
|
||||
handle_gps_map_window_events,
|
||||
update_map_tiles,
|
||||
send_periodic_gps_updates,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,9 +230,128 @@ pub fn spawn_gps_map_window(commands: &mut Commands, gps_map_state: &mut ResMut<
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn spawn_gps_webview(commands: &mut Commands, gps_map_state: &mut ResMut<GpsMapState>) {
|
||||
if let Some(win) = gps_map_state.window_id {
|
||||
commands.entity(win).insert(Webview::Uri(WebviewUri::relative_local(
|
||||
// Using the build output of the base-map package
|
||||
"packages/base-map/dist/index.html",
|
||||
)));
|
||||
|
||||
|
||||
commands.entity(win).insert((
|
||||
IpcHandlers::new([
|
||||
navigation_clicked,
|
||||
search_clicked,
|
||||
map_view_changed,
|
||||
auth_status_changed,
|
||||
get_map_init,
|
||||
get_vessel_status
|
||||
]),
|
||||
Webview::Uri(WebviewUri::relative_local(
|
||||
// Using the build output of the base-map package
|
||||
"packages/base-map/dist/index.html",
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// GPS Map IPC Commands using bevy_flurx_ipc
|
||||
|
||||
/// Handle navigation button click
|
||||
#[command]
|
||||
fn navigation_clicked(
|
||||
WebviewEntity(_entity): WebviewEntity,
|
||||
) -> Action<(), ()> {
|
||||
once::run(|_: In<()>| {
|
||||
info!("Navigation button clicked in React");
|
||||
// Handle navigation logic here
|
||||
}).into()
|
||||
}
|
||||
|
||||
/// Handle search button click
|
||||
#[command]
|
||||
fn search_clicked(
|
||||
WebviewEntity(_entity): WebviewEntity,
|
||||
) -> Action<(), ()> {
|
||||
once::run(|_: In<()>| {
|
||||
info!("Search button clicked in React");
|
||||
// Handle search logic here
|
||||
}).into()
|
||||
}
|
||||
|
||||
/// Handle map view change
|
||||
#[command]
|
||||
fn map_view_changed(
|
||||
In(params): In<MapViewParams>,
|
||||
WebviewEntity(_entity): WebviewEntity,
|
||||
) -> Action<(f64, f64, u8), ()> {
|
||||
once::run(|In((latitude, longitude, zoom)): In<(f64, f64, u8)>, mut gps_map_state: ResMut<GpsMapState>| {
|
||||
info!("Map view changed: lat={}, lon={}, zoom={}", latitude, longitude, zoom);
|
||||
gps_map_state.center_lat = latitude;
|
||||
gps_map_state.center_lon = longitude;
|
||||
gps_map_state.zoom_level = zoom;
|
||||
}).with((params.latitude, params.longitude, params.zoom)).into()
|
||||
}
|
||||
|
||||
/// Handle authentication status change
|
||||
#[command]
|
||||
fn auth_status_changed(
|
||||
In(params): In<AuthParams>,
|
||||
WebviewEntity(_entity): WebviewEntity,
|
||||
) -> Action<(bool, Option<String>), ()> {
|
||||
once::run(|In((authenticated, token)): In<(bool, Option<String>)>| {
|
||||
info!("Auth status changed: authenticated={}, token={:?}", authenticated, token);
|
||||
// Handle authentication status change
|
||||
}).with((params.authenticated, params.token)).into()
|
||||
}
|
||||
|
||||
/// Get map initialization data
|
||||
#[command]
|
||||
async fn get_map_init(
|
||||
WebviewEntity(_entity): WebviewEntity,
|
||||
task: ReactorTask,
|
||||
) -> GpsPosition {
|
||||
task.will(Update, once::run(|gps_map_state: Res<GpsMapState>| {
|
||||
GpsPosition {
|
||||
latitude: gps_map_state.center_lat,
|
||||
longitude: gps_map_state.center_lon,
|
||||
zoom: gps_map_state.zoom_level,
|
||||
}
|
||||
})).await
|
||||
}
|
||||
|
||||
/// Get current vessel status
|
||||
#[command]
|
||||
async fn get_vessel_status(
|
||||
WebviewEntity(_entity): WebviewEntity,
|
||||
task: ReactorTask,
|
||||
) -> VesselStatus {
|
||||
task.will(Update, once::run(|gps_map_state: Res<GpsMapState>| {
|
||||
VesselStatus {
|
||||
latitude: gps_map_state.vessel_lat,
|
||||
longitude: gps_map_state.vessel_lon,
|
||||
heading: gps_map_state.vessel_heading,
|
||||
speed: gps_map_state.vessel_speed,
|
||||
}
|
||||
})).await
|
||||
}
|
||||
|
||||
/// System to send periodic GPS updates for testing
|
||||
fn send_periodic_gps_updates(
|
||||
mut gps_map_state: ResMut<GpsMapState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
// 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;
|
||||
|
||||
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;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -3,7 +3,7 @@
|
||||
"private": true,
|
||||
"workspaces": ["packages/*"],
|
||||
"scripts": {
|
||||
"build-and-deploy-map": "cd packages/base-map && npm run build && cd ../.. && mkdir -p crates/yachtpit/assets/ui/packages/base-map/dist && cp -r packages/base-map/dist/* crates/yachtpit/assets/ui/packages/base-map/dist/ && cp -r packages/base-map/dist/assets crates/yachtpit/assets/ui/"
|
||||
"build-and-deploy-map": "cd packages/base-map && npm run build && cd ../.. && mkdir -p crates/yachtpit/assets/ui/packages/base-map/dist && cp -r packages/base-map/dist/* crates/yachtpit/assets/ui/packages/base-map/dist/"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
|
@@ -33,6 +33,7 @@
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.34.1",
|
||||
"vite": "^7.0.0",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"bevy_flurx_api": "^0.1.0"
|
||||
}
|
||||
}
|
||||
|
@@ -1,28 +1,156 @@
|
||||
import Map from 'react-map-gl/mapbox'; // ↔ v5+ uses this import path
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import {Box, Button, HStack} from '@chakra-ui/react';
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
|
||||
// public key
|
||||
const key =
|
||||
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
||||
|
||||
|
||||
// Types for bevy_flurx_ipc communication
|
||||
interface GpsPosition {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
interface VesselStatus {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
heading: number;
|
||||
speed: number;
|
||||
}
|
||||
|
||||
interface MapViewParams {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
interface AuthParams {
|
||||
authenticated: boolean;
|
||||
token: string | null;
|
||||
}
|
||||
|
||||
function App() {
|
||||
|
||||
// Map state that can be updated from Rust
|
||||
const [mapView, setMapView] = useState({
|
||||
longitude: -122.4,
|
||||
latitude: 37.8,
|
||||
zoom: 14
|
||||
});
|
||||
|
||||
// Button click handlers
|
||||
const handleNavigationClick = useCallback(async () => {
|
||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||
try {
|
||||
await (window as any).__FLURX__.invoke("navigation_clicked");
|
||||
console.log('Navigation clicked');
|
||||
} catch (error) {
|
||||
console.error('Failed to invoke navigation_clicked:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
|
||||
const handleSearchClick = useCallback(async () => {
|
||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||
try {
|
||||
await (window as any).__FLURX__.invoke("search_clicked");
|
||||
console.log('Search clicked');
|
||||
} catch (error) {
|
||||
console.error('Failed to invoke search_clicked:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMapViewChange = useCallback(async (evt: any) => {
|
||||
const { longitude, latitude, zoom } = evt.viewState;
|
||||
setMapView({ longitude, latitude, zoom });
|
||||
|
||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||
try {
|
||||
const mapViewParams: MapViewParams = {
|
||||
latitude,
|
||||
longitude,
|
||||
zoom
|
||||
};
|
||||
await (window as any).__FLURX__.invoke("map_view_changed", mapViewParams);
|
||||
console.log('Map view changed:', mapViewParams);
|
||||
} catch (error) {
|
||||
console.error('Failed to invoke map_view_changed:', error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Poll for vessel status updates
|
||||
useEffect(() => {
|
||||
const pollVesselStatus = async () => {
|
||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||
try {
|
||||
const vesselStatus: VesselStatus = await (window as any).__FLURX__.invoke("get_vessel_status");
|
||||
console.log('Vessel status:', vesselStatus);
|
||||
// You can update vessel position on map here if needed
|
||||
} catch (error) {
|
||||
console.error('Failed to get vessel status:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Poll every 5 seconds
|
||||
const interval = setInterval(pollVesselStatus, 5000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
// Initialize map with data from Rust
|
||||
useEffect(() => {
|
||||
const initializeMap = async () => {
|
||||
if (typeof window !== 'undefined' && (window as any).__FLURX__) {
|
||||
try {
|
||||
const mapInit: GpsPosition = await (window as any).__FLURX__.invoke("get_map_init");
|
||||
console.log('Map initialization data:', mapInit);
|
||||
setMapView({
|
||||
latitude: mapInit.latitude,
|
||||
longitude: mapInit.longitude,
|
||||
zoom: mapInit.zoom
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to get map initialization data:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initializeMap();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
/* Full-screen wrapper — fills the viewport and becomes the positioning context */
|
||||
<Box w="100vw" h="100vh" position="relative" overflow="hidden">
|
||||
{/* Button bar — absolutely positioned inside the wrapper */}
|
||||
<HStack position="absolute" top={4} right={4} zIndex={1}>
|
||||
<Button colorScheme="blue" size="sm" variant="solid">
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
onClick={handleNavigationClick}
|
||||
>
|
||||
Navigation
|
||||
</Button>
|
||||
<Button colorScheme="teal" size="sm" variant="solid">
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
onClick={handleSearchClick}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
</HStack>
|
||||
<Map
|
||||
mapboxAccessToken={atob(key)}
|
||||
initialViewState={{longitude: -122.4, latitude: 37.8, zoom: 14}}
|
||||
initialViewState={mapView}
|
||||
onMove={handleMapViewChange}
|
||||
mapStyle="mapbox://styles/mapbox/dark-v11"
|
||||
reuseMaps
|
||||
attributionControl={false}
|
||||
|
Reference in New Issue
Block a user