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:
Geoff Seemueller
2025-07-08 17:44:37 -04:00
committed by GitHub
parent 59c0474bf9
commit 92e5cfb21c
8 changed files with 366 additions and 49 deletions

4
Cargo.lock generated
View File

@@ -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",

View File

@@ -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=="],

View File

@@ -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"

View File

@@ -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>,

View File

@@ -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);
}
}
}

View File

@@ -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"

View File

@@ -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"
}
}

View File

@@ -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}
@@ -32,4 +160,4 @@ function App() {
);
}
export default App;
export default App;