mirror of
https://github.com/seemueller-io/yachtpit.git
synced 2025-09-08 22:46:45 +00:00
Integrate browser geolocation API (#9)
* 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`. * give map a custom style * shift towards rust exclusivity * `build.rs` streamlines map build. Added an axum server with the map assets embedded. * update readmes * base-map api retrieves geolocation from the navigator of the browser * make map standalone wry that pulls assets from the server to simulate behavior in bevy * wip wasm * wasm build fixed * fix path ref to assets --------- Co-authored-by: geoffsee <>
This commit is contained in:
24
crates/base-map/.gitignore
vendored
Normal file
24
crates/base-map/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
93
crates/base-map/Cargo.toml
Normal file
93
crates/base-map/Cargo.toml
Normal file
@@ -0,0 +1,93 @@
|
||||
[package]
|
||||
name = "base-map"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Rust back‑end + Vite front‑end"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
# Tell Cargo to run our build script
|
||||
build = "build.rs"
|
||||
|
||||
# ────────────────────────────────────────────────
|
||||
# Dependencies you actually need in Rust code
|
||||
# (pick only the ones you want)
|
||||
# ────────────────────────────────────────────────
|
||||
[dependencies]
|
||||
# Web server framework (swap for actix‑web, warp, etc.)
|
||||
axum = { version = "0.7", optional = true, default-features = false, features = ["macros", "tokio", "http1", "json"] }
|
||||
tokio = { version = "1.46.0", features = ["full"], optional = true}
|
||||
tower-http = { version = "0.6", features = ["full"] }
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = "0.3.16"
|
||||
anyhow = "1"
|
||||
axum-embed = "0.1.0"
|
||||
wry = { version = "=0.51.2", features = ["protocol", "os-webview"] }
|
||||
tao = { version = "0.34"}
|
||||
web-sys = { version = "0.3.77", features = [
|
||||
"console",
|
||||
"Geolocation",
|
||||
"Navigator",
|
||||
"Window",
|
||||
"Document",
|
||||
"Element",
|
||||
"Position",
|
||||
"PositionOptions",
|
||||
"PositionError",
|
||||
"Coordinates"
|
||||
] }
|
||||
# Static‑file embedding helper (compile assets straight into the binary).
|
||||
# If you prefer reading from disk at runtime, delete this.
|
||||
rust-embed = { version = "8", optional = true }
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────
|
||||
# Dev‑only dependencies (examples, tests, benches)
|
||||
# ────────────────────────────────────────────────
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
|
||||
|
||||
# ────────────────────────────────────────────────
|
||||
# Features let you strip out things you don’t want
|
||||
# by disabling default features in your workspace
|
||||
# ────────────────────────────────────────────────
|
||||
[features]
|
||||
default = ["server", "embed-assets"]
|
||||
|
||||
# Feature that pulls in the server stack
|
||||
server = ["axum", "tokio"]
|
||||
|
||||
# Feature that embeds Vite’s dist/ into the binary
|
||||
embed-assets = ["rust-embed"]
|
||||
|
||||
# ────────────────────────────────────────────────
|
||||
# Tell Cargo what NOT to publish to crates.io
|
||||
# ────────────────────────────────────────────────
|
||||
[package.metadata]
|
||||
# Keep your node_modules and dist/ out of the crate
|
||||
# published to crates.io (they’re huge and useless there)
|
||||
exclude = [
|
||||
"map/node_modules/**",
|
||||
"map/dist/**"
|
||||
]
|
||||
|
||||
# ────────────────────────────────────────────────
|
||||
# Optional: workspace setup
|
||||
# ────────────────────────────────────────────────
|
||||
# If this lives inside a workspace, remove [workspace] here
|
||||
# and put these paths in the root Cargo.toml instead.
|
||||
|
||||
[lib]
|
||||
# Default name is fine; no need to set anything unless you want cdylib, etc.
|
||||
path = "src/lib.rs"
|
||||
|
||||
# Optional: multiple binaries live in src/bin/
|
||||
[[bin]]
|
||||
name = "server" # produces target/release/server
|
||||
path = "src/main.rs"
|
||||
|
||||
# Optional: multiple binaries live in src/bin/
|
||||
[[bin]]
|
||||
name = "app" # produces target/release/server
|
||||
path = "src/app.rs"
|
22
crates/base-map/README.md
Normal file
22
crates/base-map/README.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# YachtPit Base Map Crate
|
||||
This crate is intended to provide a production build system and independent rust development environment for geospatial maps within yachtpit.
|
||||
|
||||
## Overview
|
||||
// TODO
|
||||
|
||||
## Features
|
||||
// TODO
|
||||
|
||||
## Usage
|
||||
// TODO
|
||||
```shell
|
||||
cargo run --bin server
|
||||
cargo run --bin app
|
||||
#Error: User denied Geolocation
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
// TODO
|
||||
|
||||
## Architecture
|
||||
// TODO
|
120
crates/base-map/build.rs
Normal file
120
crates/base-map/build.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
// build.rs
|
||||
use std::{
|
||||
env,
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
///
|
||||
/// {
|
||||
/// "name": "yachtpit",
|
||||
/// "private": true,
|
||||
/// "workspaces": ["packages/*"],
|
||||
/// "scripts": {
|
||||
/// "build-and-deploy-map": "cd crates/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/",
|
||||
/// "postinstall": "npm run build-and-deploy-map"
|
||||
/// },
|
||||
/// "devDependencies": {
|
||||
/// "@types/bun": "latest"
|
||||
/// },
|
||||
/// "peerDependencies": {
|
||||
/// "typescript": "^5"
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
|
||||
|
||||
fn main() {
|
||||
// ---------- 0. Locate the map front‑end ----------
|
||||
let map_dir = Path::new("map");
|
||||
assert!(
|
||||
map_dir.join("package.json").exists(),
|
||||
"Expected `map/package.json` – is your map front‑end in `map/`?"
|
||||
);
|
||||
|
||||
// ---------- 1. Tell Cargo when to rerun ----------
|
||||
println!("cargo:rerun-if-changed={}", map_dir.join("package.json").display());
|
||||
println!("cargo:rerun-if-changed={}", map_dir.join("vite.config.ts").display());
|
||||
println!("cargo:rerun-if-changed={}", map_dir.join("vite.config.js").display());
|
||||
println!("cargo:rerun-if-changed={}", map_dir.join("src").display());
|
||||
|
||||
// ---------- 2. Install & build ----------
|
||||
run("npm", &["install"], map_dir);
|
||||
run("npm", &["run", "build"], map_dir); // assumes "build": "tsc -b && vite build" in package.json
|
||||
|
||||
// ---------- 3. Copy artefacts following build-and-deploy-map script ----------
|
||||
let dist = map_dir.join("dist");
|
||||
|
||||
let crate_asset_output = out_static_dir();
|
||||
|
||||
// Create target directory: crates/yachtpit/assets/ui/packages/base-map/dist
|
||||
let base_map_dest = Path::new("../yachtpit/assets/ui/packages/base-map/dist");
|
||||
if base_map_dest.exists() {
|
||||
fs::remove_dir_all(&base_map_dest).expect("clearing old base-map dist failed");
|
||||
}
|
||||
|
||||
if crate_asset_output.exists() {
|
||||
fs::remove_dir_all(&crate_asset_output).expect("clearing old base-map dist failed");
|
||||
}
|
||||
|
||||
fs::create_dir_all(&base_map_dest).unwrap();
|
||||
fs::create_dir_all(&crate_asset_output).unwrap();
|
||||
|
||||
// Copy dist/* to crates/yachtpit/assets/ui/packages/base-map/dist/
|
||||
copy_dir(&dist, &base_map_dest).expect("copying base-map dist/ failed");
|
||||
copy_dir(&dist, &crate_asset_output).expect("crate asset output failed");
|
||||
|
||||
// Copy dist/assets to crates/yachtpit/assets/ui/assets (if assets exist)
|
||||
let dist_assets = dist.join("assets");
|
||||
if dist_assets.exists() {
|
||||
let ui_assets_dest = Path::new("../yachtpit/assets/ui/assets");
|
||||
if ui_assets_dest.exists() {
|
||||
fs::remove_dir_all(&ui_assets_dest).expect("clearing old ui assets failed");
|
||||
}
|
||||
fs::create_dir_all(&ui_assets_dest).unwrap();
|
||||
copy_dir(&dist_assets, &ui_assets_dest).expect("copying assets to ui/ failed");
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
fn run(cmd: &str, args: &[&str], cwd: &Path) {
|
||||
let ok = Command::new(cmd)
|
||||
.args(args)
|
||||
.current_dir(cwd)
|
||||
.status()
|
||||
.unwrap_or_else(|_| panic!("failed to spawn `{cmd}`; is it on PATH?"))
|
||||
.success();
|
||||
if !ok {
|
||||
panic!("command `{cmd} {}` failed", args.join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
/// `$OUT_DIR/../../../static` → a sibling of your crate root inside `target/*/static`.
|
||||
fn out_static_dir() -> PathBuf {
|
||||
let out_dir = env::var("OUT_DIR").expect("OUT_DIR env var is set by Cargo");
|
||||
PathBuf::from(out_dir)
|
||||
.parent() // build/
|
||||
.and_then(|p| p.parent()) // <hash>/
|
||||
.and_then(|p| p.parent()) // release/
|
||||
.map(|p| p.join("static"))
|
||||
.expect("could not compute static dir")
|
||||
}
|
||||
|
||||
/// Recursively copy a directory.
|
||||
fn copy_dir(src: &Path, dst: &Path) -> std::io::Result<()> {
|
||||
for entry in fs::read_dir(src)? {
|
||||
let entry = entry?;
|
||||
let ty = entry.file_type()?;
|
||||
let dst_path = dst.join(entry.file_name());
|
||||
if ty.is_dir() {
|
||||
fs::create_dir_all(&dst_path)?;
|
||||
copy_dir(&entry.path(), &dst_path)?;
|
||||
} else {
|
||||
fs::copy(entry.path(), dst_path)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
69
crates/base-map/map/README.md
Normal file
69
crates/base-map/map/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
...tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
23
crates/base-map/map/eslint.config.js
Normal file
23
crates/base-map/map/eslint.config.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { globalIgnores } from 'eslint/config'
|
||||
|
||||
export default tseslint.config([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
13
crates/base-map/map/index.html
Normal file
13
crates/base-map/map/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
5585
crates/base-map/map/package-lock.json
generated
Normal file
5585
crates/base-map/map/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
42
crates/base-map/map/package.json
Normal file
42
crates/base-map/map/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "base-map",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint ",
|
||||
"preview": "vite preview",
|
||||
"background-server": "(cd ../ && cargo run &)"
|
||||
},
|
||||
"dependencies": {
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-icons": "^5.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"eslint": "^9.29.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.2.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"mapbox-gl": "^3.13.0",
|
||||
"react-map-gl": "^8.0.4",
|
||||
"typescript": "~5.8.3",
|
||||
"typescript-eslint": "^8.34.1",
|
||||
"vite": "^7.0.0",
|
||||
"@tauri-apps/plugin-geolocation": "^2.3.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"bevy_flurx_api": "^0.1.0",
|
||||
"geojson": "^0.5.0"
|
||||
}
|
||||
}
|
1
crates/base-map/map/public/vite.svg
Normal file
1
crates/base-map/map/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
238
crates/base-map/map/src/App.tsx
Normal file
238
crates/base-map/map/src/App.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
// import Map from 'react-map-gl/mapbox';
|
||||
// import {Source, Layer} from 'react-map-gl/maplibre';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import {Box, Button, HStack, Input} from '@chakra-ui/react';
|
||||
import {useCallback, useEffect, useState} from "react";
|
||||
import MapNext from "@/MapNext.tsx";
|
||||
// import type {FeatureCollection} from 'geojson';
|
||||
// import type {CircleLayerSpecification} from "mapbox-gl";
|
||||
|
||||
// public key
|
||||
const key =
|
||||
'cGsuZXlKMUlqb2laMlZ2Wm1aelpXVWlMQ0poSWpvaVkycDFOalo0YkdWNk1EUTRjRE41YjJnNFp6VjNNelp6YXlKOS56LUtzS1l0X3VGUGdCSDYwQUFBNFNn';
|
||||
|
||||
|
||||
|
||||
// const vesselLayerStyle: CircleLayerSpecification = {
|
||||
// id: 'vessel',
|
||||
// type: 'circle',
|
||||
// paint: {
|
||||
// 'circle-radius': 8,
|
||||
// 'circle-color': '#ff4444',
|
||||
// 'circle-stroke-width': 2,
|
||||
// 'circle-stroke-color': '#ffffff'
|
||||
// },
|
||||
// source: ''
|
||||
// };
|
||||
|
||||
// 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() {
|
||||
|
||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||
|
||||
// Map state that can be updated from Rust
|
||||
// const [mapView, setMapView] = useState({
|
||||
// longitude: -122.4,
|
||||
// latitude: 37.8,
|
||||
// zoom: 14
|
||||
// });
|
||||
|
||||
// Vessel position state
|
||||
// const [vesselPosition, setVesselPosition] = useState<VesselStatus | null>(null);
|
||||
|
||||
// Create vessel geojson data
|
||||
// const vesselGeojson: FeatureCollection = {
|
||||
// type: 'FeatureCollection',
|
||||
// features: vesselPosition ? [
|
||||
// {
|
||||
// type: 'Feature',
|
||||
// geometry: {
|
||||
// type: 'Point',
|
||||
// coordinates: [vesselPosition.longitude, vesselPosition.latitude]
|
||||
// },
|
||||
// properties: {
|
||||
// title: 'Vessel Position',
|
||||
// heading: vesselPosition.heading,
|
||||
// speed: vesselPosition.speed
|
||||
// }
|
||||
// }
|
||||
// ] : []
|
||||
// };
|
||||
|
||||
|
||||
// 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 () => {
|
||||
setIsSearchOpen(!isSearchOpen);
|
||||
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);
|
||||
// setVesselPosition(vesselStatus);
|
||||
} catch (error) {
|
||||
console.error('Failed to get vessel status:', error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Poll every 5 seconds
|
||||
const interval = setInterval(pollVesselStatus, 5000);
|
||||
// Also poll immediately
|
||||
pollVesselStatus();
|
||||
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}>
|
||||
<Box
|
||||
display="flex"
|
||||
alignItems="center"
|
||||
>
|
||||
<Button
|
||||
colorScheme="teal"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
onClick={handleSearchClick}
|
||||
mr={2}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
{isSearchOpen && <Box
|
||||
w="200px"
|
||||
transition="all 0.3s"
|
||||
transform={`translateX(${isSearchOpen ? "0" : "100%"})`}
|
||||
opacity={isSearchOpen ? 1 : 0}
|
||||
color="white"
|
||||
>
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
size="sm"
|
||||
_placeholder={{
|
||||
color: "#d1cfcf"
|
||||
}}
|
||||
/>
|
||||
</Box>}
|
||||
</Box>
|
||||
<Button
|
||||
colorScheme="blue"
|
||||
size="sm"
|
||||
variant="solid"
|
||||
onClick={handleNavigationClick}
|
||||
>
|
||||
Layer
|
||||
</Button>
|
||||
</HStack>
|
||||
<MapNext mapboxPublicKey={atob(key)}/>
|
||||
{/*<Map*/}
|
||||
{/* mapboxAccessToken={atob(key)}*/}
|
||||
{/* initialViewState={mapView}*/}
|
||||
{/* onMove={handleMapViewChange}*/}
|
||||
{/* mapStyle="mapbox://styles/mapbox/dark-v11"*/}
|
||||
{/* reuseMaps*/}
|
||||
{/* attributionControl={false}*/}
|
||||
{/* style={{width: '100%', height: '100%'}} // let the wrapper dictate size*/}
|
||||
{/*>*/}
|
||||
{/* /!*{vesselPosition && (*!/*/}
|
||||
{/* /!* <Source id="vessel-data" type="geojson" data={vesselGeojson}>*!/*/}
|
||||
{/* /!* <Layer {...vesselLayerStyle} />*!/*/}
|
||||
{/* /!* </Source>*!/*/}
|
||||
{/* /!*)}*!/*/}
|
||||
{/*</Map>*/}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
115
crates/base-map/map/src/MapNext.tsx
Normal file
115
crates/base-map/map/src/MapNext.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import {useState, useMemo} from 'react';
|
||||
import Map, {
|
||||
Marker,
|
||||
Popup,
|
||||
NavigationControl,
|
||||
FullscreenControl,
|
||||
ScaleControl,
|
||||
GeolocateControl
|
||||
} from 'react-map-gl/mapbox';
|
||||
|
||||
import ControlPanel from './control-panel.tsx';
|
||||
import Pin from './pin.tsx';
|
||||
|
||||
import PORTS from './test_data/nautical-base-data.json';
|
||||
import {Box} from "@chakra-ui/react";
|
||||
|
||||
|
||||
export default function MapNext(props: any = {mapboxPublicKey: ""} as any) {
|
||||
const [popupInfo, setPopupInfo] = useState(null);
|
||||
|
||||
const pins = useMemo(
|
||||
() =>
|
||||
PORTS.map((city, index) => (
|
||||
<Marker
|
||||
key={`marker-${index}`}
|
||||
longitude={city.longitude}
|
||||
latitude={city.latitude}
|
||||
anchor="bottom"
|
||||
onClick={e => {
|
||||
// If we let the click event propagates to the map, it will immediately close the popup
|
||||
// with `closeOnClick: true`
|
||||
e.originalEvent.stopPropagation();
|
||||
/*
|
||||
src/MapNext.tsx:34:38 - error TS2345: Argument of type '{ city: string; population: string; image: string; state: string; latitude: number; longitude: number; }' is not assignable to parameter of type 'SetStateAction<null>'.
|
||||
Type '{ city: string; population: string; image: string; state: string; latitude: number; longitude: number; }' provides no match for the signature '(prevState: null): null'.
|
||||
*/
|
||||
// @ts-ignore
|
||||
setPopupInfo(city);
|
||||
}}
|
||||
>
|
||||
<Pin />
|
||||
</Marker>
|
||||
)),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Map
|
||||
initialViewState={{
|
||||
latitude: 40,
|
||||
longitude: -100,
|
||||
zoom: 3.5,
|
||||
bearing: 0,
|
||||
pitch: 0
|
||||
}}
|
||||
|
||||
mapStyle="mapbox://styles/geoffsee/cmd1qz39x01ga01qv5acea02y"
|
||||
mapboxAccessToken={props.mapboxPublicKey}
|
||||
style={{position: "fixed", width: '100%', height: '100%', bottom: 0, top: 0, left: 0, right: 0}}
|
||||
>
|
||||
<GeolocateControl showUserHeading={true} showUserLocation={true} position="top-left" />
|
||||
<FullscreenControl position="top-left" />
|
||||
<NavigationControl position="top-left" />
|
||||
<ScaleControl />
|
||||
|
||||
{pins}
|
||||
|
||||
{popupInfo && (
|
||||
<Popup
|
||||
anchor="top"
|
||||
/*
|
||||
src/MapNext.tsx:66:53 - error TS2339: Property 'longitude' does not exist on type 'never'.
|
||||
|
||||
66 longitude={Number(popupInfo.longitude)}
|
||||
*/
|
||||
// @ts-ignore
|
||||
longitude={Number(popupInfo.longitude)}
|
||||
/*
|
||||
src/MapNext.tsx:67:52 - error TS2339: Property 'latitude' does not exist on type 'never'.
|
||||
|
||||
67 latitude={Number(popupInfo.latitude)}
|
||||
~~~~~~~~
|
||||
*/
|
||||
// @ts-ignore
|
||||
latitude={Number(popupInfo.latitude)}
|
||||
onClose={() => setPopupInfo(null)}
|
||||
>
|
||||
<div>
|
||||
{/*src/MapNext.tsx:71:40 - error TS2339: Property 'city' does not exist on type 'never'.
|
||||
|
||||
71 {popupInfo.city}, {popupInfo.state} |{' '}
|
||||
~~~~*/}
|
||||
|
||||
{/*@ts-ignore*/}{/*@ts-ignore*/}
|
||||
{popupInfo.city},{popupInfo.state}
|
||||
{/*@ts-ignore*/}
|
||||
<a
|
||||
target="_new"
|
||||
|
||||
href={`http://en.wikipedia.org/w/index.php?title=Special:Search&search=${(popupInfo as any).city}, ${(popupInfo as any).state}`}
|
||||
>
|
||||
Wikipedia
|
||||
</a>
|
||||
</div>
|
||||
{/*@ts-ignore*/}
|
||||
<img width="100%" src={popupInfo.image} />
|
||||
</Popup>
|
||||
)}
|
||||
</Map>
|
||||
|
||||
<ControlPanel />
|
||||
</Box>
|
||||
);
|
||||
}
|
1
crates/base-map/map/src/assets/react.svg
Normal file
1
crates/base-map/map/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
After Width: | Height: | Size: 4.0 KiB |
108
crates/base-map/map/src/components/ui/color-mode.tsx
Normal file
108
crates/base-map/map/src/components/ui/color-mode.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
"use client"
|
||||
|
||||
import type { IconButtonProps, SpanProps } from "@chakra-ui/react"
|
||||
import { ClientOnly, IconButton, Skeleton, Span } from "@chakra-ui/react"
|
||||
import { ThemeProvider, useTheme } from "next-themes"
|
||||
import type { ThemeProviderProps } from "next-themes"
|
||||
import * as React from "react"
|
||||
import { LuMoon, LuSun } from "react-icons/lu"
|
||||
|
||||
export interface ColorModeProviderProps extends ThemeProviderProps {}
|
||||
|
||||
export function ColorModeProvider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ThemeProvider attribute="class" disableTransitionOnChange {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export type ColorMode = "light" | "dark"
|
||||
|
||||
export interface UseColorModeReturn {
|
||||
colorMode: ColorMode
|
||||
setColorMode: (colorMode: ColorMode) => void
|
||||
toggleColorMode: () => void
|
||||
}
|
||||
|
||||
export function useColorMode(): UseColorModeReturn {
|
||||
const { resolvedTheme, setTheme, forcedTheme } = useTheme()
|
||||
const colorMode = forcedTheme || resolvedTheme
|
||||
const toggleColorMode = () => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}
|
||||
return {
|
||||
colorMode: colorMode as ColorMode,
|
||||
setColorMode: setTheme,
|
||||
toggleColorMode,
|
||||
}
|
||||
}
|
||||
|
||||
export function useColorModeValue<T>(light: T, dark: T) {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "dark" ? dark : light
|
||||
}
|
||||
|
||||
export function ColorModeIcon() {
|
||||
const { colorMode } = useColorMode()
|
||||
return colorMode === "dark" ? <LuMoon /> : <LuSun />
|
||||
}
|
||||
|
||||
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
|
||||
|
||||
export const ColorModeButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
ColorModeButtonProps
|
||||
>(function ColorModeButton(props, ref) {
|
||||
const { toggleColorMode } = useColorMode()
|
||||
return (
|
||||
<ClientOnly fallback={<Skeleton boxSize="8" />}>
|
||||
<IconButton
|
||||
onClick={toggleColorMode}
|
||||
variant="ghost"
|
||||
aria-label="Toggle color mode"
|
||||
size="sm"
|
||||
ref={ref}
|
||||
{...props}
|
||||
css={{
|
||||
_icon: {
|
||||
width: "5",
|
||||
height: "5",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ColorModeIcon />
|
||||
</IconButton>
|
||||
</ClientOnly>
|
||||
)
|
||||
})
|
||||
|
||||
export const LightMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||
function LightMode(props, ref) {
|
||||
return (
|
||||
<Span
|
||||
color="fg"
|
||||
display="contents"
|
||||
className="chakra-theme light"
|
||||
colorPalette="gray"
|
||||
colorScheme="light"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
export const DarkMode = React.forwardRef<HTMLSpanElement, SpanProps>(
|
||||
function DarkMode(props, ref) {
|
||||
return (
|
||||
<Span
|
||||
color="fg"
|
||||
display="contents"
|
||||
className="chakra-theme dark"
|
||||
colorPalette="gray"
|
||||
colorScheme="dark"
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
},
|
||||
)
|
15
crates/base-map/map/src/components/ui/provider.tsx
Normal file
15
crates/base-map/map/src/components/ui/provider.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client"
|
||||
|
||||
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
|
||||
import {
|
||||
ColorModeProvider,
|
||||
type ColorModeProviderProps,
|
||||
} from "./color-mode.tsx"
|
||||
|
||||
export function Provider(props: ColorModeProviderProps) {
|
||||
return (
|
||||
<ChakraProvider value={defaultSystem}>
|
||||
<ColorModeProvider {...props} />
|
||||
</ChakraProvider>
|
||||
)
|
||||
}
|
43
crates/base-map/map/src/components/ui/toaster.tsx
Normal file
43
crates/base-map/map/src/components/ui/toaster.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
Toaster as ChakraToaster,
|
||||
Portal,
|
||||
Spinner,
|
||||
Stack,
|
||||
Toast,
|
||||
createToaster,
|
||||
} from "@chakra-ui/react"
|
||||
|
||||
export const toaster = createToaster({
|
||||
placement: "bottom-end",
|
||||
pauseOnPageIdle: true,
|
||||
})
|
||||
|
||||
export const Toaster = () => {
|
||||
return (
|
||||
<Portal>
|
||||
<ChakraToaster toaster={toaster} insetInline={{ mdDown: "4" }}>
|
||||
{(toast) => (
|
||||
<Toast.Root width={{ md: "sm" }}>
|
||||
{toast.type === "loading" ? (
|
||||
<Spinner size="sm" color="blue.solid" />
|
||||
) : (
|
||||
<Toast.Indicator />
|
||||
)}
|
||||
<Stack gap="1" flex="1" maxWidth="100%">
|
||||
{toast.title && <Toast.Title>{toast.title}</Toast.Title>}
|
||||
{toast.description && (
|
||||
<Toast.Description>{toast.description}</Toast.Description>
|
||||
)}
|
||||
</Stack>
|
||||
{toast.action && (
|
||||
<Toast.ActionTrigger>{toast.action.label}</Toast.ActionTrigger>
|
||||
)}
|
||||
{toast.closable && <Toast.CloseTrigger />}
|
||||
</Toast.Root>
|
||||
)}
|
||||
</ChakraToaster>
|
||||
</Portal>
|
||||
)
|
||||
}
|
46
crates/base-map/map/src/components/ui/tooltip.tsx
Normal file
46
crates/base-map/map/src/components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
|
||||
import * as React from "react"
|
||||
|
||||
export interface TooltipProps extends ChakraTooltip.RootProps {
|
||||
showArrow?: boolean
|
||||
portalled?: boolean
|
||||
portalRef?: React.RefObject<HTMLElement>
|
||||
content: React.ReactNode
|
||||
contentProps?: ChakraTooltip.ContentProps
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
|
||||
function Tooltip(props, ref) {
|
||||
const {
|
||||
showArrow,
|
||||
children,
|
||||
disabled,
|
||||
portalled = true,
|
||||
content,
|
||||
contentProps,
|
||||
portalRef,
|
||||
...rest
|
||||
} = props
|
||||
|
||||
if (disabled) return children
|
||||
|
||||
return (
|
||||
<ChakraTooltip.Root {...rest}>
|
||||
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
|
||||
<Portal disabled={!portalled} container={portalRef}>
|
||||
<ChakraTooltip.Positioner>
|
||||
<ChakraTooltip.Content ref={ref} {...contentProps}>
|
||||
{showArrow && (
|
||||
<ChakraTooltip.Arrow>
|
||||
<ChakraTooltip.ArrowTip />
|
||||
</ChakraTooltip.Arrow>
|
||||
)}
|
||||
{content}
|
||||
</ChakraTooltip.Content>
|
||||
</ChakraTooltip.Positioner>
|
||||
</Portal>
|
||||
</ChakraTooltip.Root>
|
||||
)
|
||||
},
|
||||
)
|
29
crates/base-map/map/src/control-panel.tsx
Normal file
29
crates/base-map/map/src/control-panel.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import * as React from 'react';
|
||||
|
||||
function ControlPanel() {
|
||||
return (
|
||||
<div className="control-panel">
|
||||
<h3>Marker, Popup, NavigationControl and FullscreenControl </h3>
|
||||
<p>
|
||||
Map showing top 20 most populated cities of the United States. Click on a marker to learn
|
||||
more.
|
||||
</p>
|
||||
<p>
|
||||
Data source:{' '}
|
||||
<a href="https://en.wikipedia.org/wiki/List_of_United_States_cities_by_population">
|
||||
Wikipedia
|
||||
</a>
|
||||
</p>
|
||||
<div className="source-link">
|
||||
<a
|
||||
href="https://github.com/visgl/react-map-gl/tree/8.0-release/examples/mapbox/controls"
|
||||
target="_new"
|
||||
>
|
||||
View Code ↗
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(ControlPanel);
|
12
crates/base-map/map/src/main.tsx
Normal file
12
crates/base-map/map/src/main.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import {StrictMode} from 'react'
|
||||
import {createRoot} from 'react-dom/client'
|
||||
import App from './App.tsx'
|
||||
import {Provider} from "@/components/ui/provider.tsx"
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<Provider>
|
||||
<App/>
|
||||
</Provider>
|
||||
</StrictMode>,
|
||||
)
|
21
crates/base-map/map/src/pin.tsx
Normal file
21
crates/base-map/map/src/pin.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
|
||||
const ICON = `M20.2,15.7L20.2,15.7c1.1-1.6,1.8-3.6,1.8-5.7c0-5.6-4.5-10-10-10S2,4.5,2,10c0,2,0.6,3.9,1.6,5.4c0,0.1,0.1,0.2,0.2,0.3
|
||||
c0,0,0.1,0.1,0.1,0.2c0.2,0.3,0.4,0.6,0.7,0.9c2.6,3.1,7.4,7.6,7.4,7.6s4.8-4.5,7.4-7.5c0.2-0.3,0.5-0.6,0.7-0.9
|
||||
C20.1,15.8,20.2,15.8,20.2,15.7z`;
|
||||
|
||||
const pinStyle = {
|
||||
cursor: 'pointer',
|
||||
fill: '#d00',
|
||||
stroke: 'none'
|
||||
};
|
||||
|
||||
function Pin({size = 20}) {
|
||||
return (
|
||||
<svg height={size} viewBox="0 0 24 24" style={pinStyle}>
|
||||
<path d={ICON} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(Pin);
|
22
crates/base-map/map/src/test_data/cities.json
Normal file
22
crates/base-map/map/src/test_data/cities.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{"city":"New York","population":"8,175,133","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Above_Gotham.jpg/240px-Above_Gotham.jpg","state":"New York","latitude":40.6643,"longitude":-73.9385},
|
||||
{"city":"Los Angeles","population":"3,792,621","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/5/57/LA_Skyline_Mountains2.jpg/240px-LA_Skyline_Mountains2.jpg","state":"California","latitude":34.0194,"longitude":-118.4108},
|
||||
{"city":"Chicago","population":"2,695,598","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/8/85/2008-06-10_3000x1000_chicago_skyline.jpg/240px-2008-06-10_3000x1000_chicago_skyline.jpg","state":"Illinois","latitude":41.8376,"longitude":-87.6818},
|
||||
{"city":"Houston","population":"2,100,263","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/6/60/Aerial_views_of_the_Houston%2C_Texas%2C_28005u.jpg/240px-Aerial_views_of_the_Houston%2C_Texas%2C_28005u.jpg","state":"Texas","latitude":29.7805,"longitude":-95.3863},
|
||||
{"city":"Phoenix","population":"1,445,632","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Downtown_Phoenix_Aerial_Looking_Northeast.jpg/207px-Downtown_Phoenix_Aerial_Looking_Northeast.jpg","state":"Arizona","latitude":33.5722,"longitude":-112.0880},
|
||||
{"city":"Philadelphia","population":"1,526,006","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Philly_skyline.jpg/240px-Philly_skyline.jpg","state":"Pennsylvania","latitude":40.0094,"longitude":-75.1333},
|
||||
{"city":"San Antonio","population":"1,327,407","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Downtown_San_Antonio_View.JPG/240px-Downtown_San_Antonio_View.JPG","state":"Texas","latitude":29.4724,"longitude":-98.5251},
|
||||
{"city":"San Diego","population":"1,307,402","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/5/53/US_Navy_110604-N-NS602-574_Navy_and_Marine_Corps_personnel%2C_along_with_community_leaders_from_the_greater_San_Diego_area_come_together_to_commemora.jpg/240px-US_Navy_110604-N-NS602-574_Navy_and_Marine_Corps_personnel%2C_along_with_community_leaders_from_the_greater_San_Diego_area_come_together_to_commemora.jpg","state":"California","latitude":32.8153,"longitude":-117.1350},
|
||||
{"city":"Dallas","population":"1,197,816","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/a/ab/Dallas_skyline_daytime.jpg/240px-Dallas_skyline_daytime.jpg","state":"Texas","latitude":32.7757,"longitude":-96.7967},
|
||||
{"city":"San Jose","population":"945,942","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/1/1e/Downtown_San_Jose_skyline.PNG/240px-Downtown_San_Jose_skyline.PNG","state":"California","latitude":37.2969,"longitude":-121.8193},
|
||||
{"city":"Austin","population":"790,390","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/9/97/Austin2012-12-01.JPG/240px-Austin2012-12-01.JPG","state":"Texas","latitude":30.3072,"longitude":-97.7560},
|
||||
{"city":"Jacksonville","population":"821,784","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/Skyline_of_Jacksonville_FL%2C_South_view_20160706_1.jpg/240px-Skyline_of_Jacksonville_FL%2C_South_view_20160706_1.jpg","state":"Florida","latitude":30.3370,"longitude":-81.6613},
|
||||
{"city":"San Francisco","population":"805,235","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/San_Francisco_skyline_from_Coit_Tower.jpg/240px-San_Francisco_skyline_from_Coit_Tower.jpg","state":"California","latitude":37.7751,"longitude":-122.4193},
|
||||
{"city":"Columbus","population":"787,033","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/f/fc/Columbus-ohio-skyline-panorama.jpg/240px-Columbus-ohio-skyline-panorama.jpg","state":"Ohio","latitude":39.9848,"longitude":-82.9850},
|
||||
{"city":"Indianapolis","population":"820,445","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/1/16/Downtown_indy_from_parking_garage_zoom.JPG/213px-Downtown_indy_from_parking_garage_zoom.JPG","state":"Indiana","latitude":39.7767,"longitude":-86.1459},
|
||||
{"city":"Fort Worth","population":"741,206","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/d/db/FortWorthTexasSkylineW.jpg/240px-FortWorthTexasSkylineW.jpg","state":"Texas","latitude":32.7795,"longitude":-97.3463},
|
||||
{"city":"Charlotte","population":"731,424","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/7/7d/Charlotte_skyline45647.jpg/222px-Charlotte_skyline45647.jpg","state":"North Carolina","latitude":35.2087,"longitude":-80.8307},
|
||||
{"city":"Seattle","population":"608,660","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/3/36/SeattleI5Skyline.jpg/240px-SeattleI5Skyline.jpg","state":"Washington","latitude":47.6205,"longitude":-122.3509},
|
||||
{"city":"Denver","population":"600,158","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/0/0b/DenverCP.JPG/240px-DenverCP.JPG","state":"Colorado","latitude":39.7618,"longitude":-104.8806},
|
||||
{"city":"El Paso","population":"649,121","image":"http://upload.wikimedia.org/wikipedia/commons/thumb/6/6d/Downtown_El_Paso_at_sunset.jpeg/240px-Downtown_El_Paso_at_sunset.jpeg","state":"Texas","latitude":31.8484,"longitude":-106.4270}
|
||||
]
|
22
crates/base-map/map/src/test_data/nautical-base-data.json
Normal file
22
crates/base-map/map/src/test_data/nautical-base-data.json
Normal file
@@ -0,0 +1,22 @@
|
||||
[
|
||||
{"city":"New York","population":"8,335,897","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/b/b9/Above_Gotham.jpg/240px-Above_Gotham.jpg","state":"New York","latitude":40.7128,"longitude":-74.0060},
|
||||
{"city":"Los Angeles","population":"3,822,238","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/5/57/LA_Skyline_Mountains2.jpg/240px-LA_Skyline_Mountains2.jpg","state":"California","latitude":34.0522,"longitude":-118.2437},
|
||||
{"city":"Long Beach","population":"456,062","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Long_Beach_skyline_from_Shoreline_Village.jpg/240px-Long_Beach_skyline_from_Shoreline_Village.jpg","state":"California","latitude":33.7701,"longitude":-118.1937},
|
||||
{"city":"Seattle","population":"749,256","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/36/SeattleI5Skyline.jpg/240px-SeattleI5Skyline.jpg","state":"Washington","latitude":47.6062,"longitude":-122.3321},
|
||||
{"city":"San Francisco","population":"808,437","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/6/6a/San_Francisco_skyline_from_Coit_Tower.jpg/240px-San_Francisco_skyline_from_Coit_Tower.jpg","state":"California","latitude":37.7749,"longitude":-122.4194},
|
||||
{"city":"San Diego","population":"1,386,932","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/US_Navy_110604-N-NS602-574_Navy_and_Marine_Corps_personnel%2C_along_with_community_leaders_from_the_greater_San_Diego_area_come_together_to_commemora.jpg/240px-US_Navy_110604-N-NS602-574_Navy_and_Marine_Corps_personnel%2C_along_with_community_leaders_from_the_greater_San_Diego_area_come_together_to_commemora.jpg","state":"California","latitude":32.7157,"longitude":-117.1611},
|
||||
{"city":"Norfolk","population":"235,089","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/6/6e/Norfolk_Skyline_from_Portsmouth.jpg/240px-Norfolk_Skyline_from_Portsmouth.jpg","state":"Virginia","latitude":36.8508,"longitude":-76.2859},
|
||||
{"city":"Miami","population":"449,514","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4b/Miami_skyline_201807_cat.jpg/240px-Miami_skyline_201807_cat.jpg","state":"Florida","latitude":25.7617,"longitude":-80.1918},
|
||||
{"city":"Boston","population":"675,647","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/1/1b/Boston_skyline_and_Boston_Harbor.jpg/240px-Boston_skyline_and_Boston_Harbor.jpg","state":"Massachusetts","latitude":42.3601,"longitude":-71.0589},
|
||||
{"city":"Baltimore","population":"585,708","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/3/3b/Baltimore_Skyline.jpg/240px-Baltimore_Skyline.jpg","state":"Maryland","latitude":39.2904,"longitude":-76.6122},
|
||||
{"city":"Charleston","population":"151,612","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/Charleston_SC_Skyline.jpg/240px-Charleston_SC_Skyline.jpg","state":"South Carolina","latitude":32.7765,"longitude":-79.9311},
|
||||
{"city":"Savannah","population":"147,780","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/40/Savannah_GA%2C_River_Street.jpg/240px-Savannah_GA%2C_River_Street.jpg","state":"Georgia","latitude":32.0809,"longitude":-81.0912},
|
||||
{"city":"Tampa","population":"403,364","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4d/Tampa_skyline_from_South%2C_2022.jpg/240px-Tampa_skyline_from_South%2C_2022.jpg","state":"Florida","latitude":27.9506,"longitude":-82.4572},
|
||||
{"city":"Mobile","population":"187,041","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/7/70/Mobile_skyline_from_Mobile_River.jpg/240px-Mobile_skyline_from_Mobile_River.jpg","state":"Alabama","latitude":30.6954,"longitude":-88.0399},
|
||||
{"city":"Anchorage","population":"288,121","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/5/55/Anchorage_skyline_and_susitna.jpg/240px-Anchorage_skyline_and_susitna.jpg","state":"Alaska","latitude":61.2181,"longitude":-149.9003},
|
||||
{"city":"Portland","population":"68,408","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/4/48/Portland_Maine_skyline.jpg/240px-Portland_Maine_skyline.jpg","state":"Maine","latitude":43.6591,"longitude":-70.2568},
|
||||
{"city":"Honolulu","population":"349,547","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Honolulu_and_Diamond_Head.jpg/240px-Honolulu_and_Diamond_Head.jpg","state":"Hawaii","latitude":21.3069,"longitude":-157.8583},
|
||||
{"city":"New Orleans","population":"376,971","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/New_Orleans_skyline_sunset_Dec_28_2021_PANO_DSC07177-07179.jpg/240px-New_Orleans_skyline_sunset_Dec_28_2021_PANO_DSC07177-07179.jpg","state":"Louisiana","latitude":29.9511,"longitude":-90.0715},
|
||||
{"city":"Jacksonville","population":"971,319","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/f/f3/Skyline_of_Jacksonville_FL%2C_South_view_20160706_1.jpg/240px-Skyline_of_Jacksonville_FL%2C_South_view_20160706_1.jpg","state":"Florida","latitude":30.3322,"longitude":-81.6557},
|
||||
{"city":"Houston","population":"2,302,878","image":"https://upload.wikimedia.org/wikipedia/commons/thumb/6/60/Aerial_views_of_the_Houston%2C_Texas%2C_28005u.jpg/240px-Aerial_views_of_the_Houston%2C_Texas%2C_28005u.jpg","state":"Texas","latitude":29.7604,"longitude":-95.3698}
|
||||
]
|
1
crates/base-map/map/src/vite-env.d.ts
vendored
Normal file
1
crates/base-map/map/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
31
crates/base-map/map/tsconfig.app.json
Normal file
31
crates/base-map/map/tsconfig.app.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
7
crates/base-map/map/tsconfig.json
Normal file
7
crates/base-map/map/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json"},
|
||||
{ "path": "./tsconfig.node.json"}
|
||||
],
|
||||
}
|
27
crates/base-map/map/tsconfig.node.json
Normal file
27
crates/base-map/map/tsconfig.node.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
8
crates/base-map/map/vite.config.ts
Normal file
8
crates/base-map/map/vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react-swc'
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths()],
|
||||
})
|
73
crates/base-map/src/app.rs
Normal file
73
crates/base-map/src/app.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use tao::{
|
||||
event::{Event, WindowEvent},
|
||||
event_loop::{ControlFlow, EventLoop},
|
||||
window::WindowBuilder,
|
||||
};
|
||||
use tao::platform::macos::WindowBuilderExtMacOS;
|
||||
use tower_http::follow_redirect::policy::PolicyExt;
|
||||
use tower_http::ServiceExt;
|
||||
use wry::WebViewBuilder;
|
||||
|
||||
fn main() -> wry::Result<()> {
|
||||
let event_loop = EventLoop::new();
|
||||
let window = WindowBuilder::new()
|
||||
.with_title("YachtPit Map")
|
||||
.build(&event_loop).unwrap();
|
||||
|
||||
let builder = WebViewBuilder::new()
|
||||
.with_url("http://localhost:8080/geolocate")
|
||||
.with_new_window_req_handler(|e| {
|
||||
println!("NewWindow: {e:?}");
|
||||
true
|
||||
})
|
||||
.with_ipc_handler(|e| {
|
||||
println!("IPC: {e:?}");
|
||||
})
|
||||
.with_drag_drop_handler(|e| {
|
||||
match e {
|
||||
wry::DragDropEvent::Enter { paths, position } => {
|
||||
println!("DragEnter: {position:?} {paths:?} ")
|
||||
}
|
||||
wry::DragDropEvent::Over { position } => println!("DragOver: {position:?} "),
|
||||
wry::DragDropEvent::Drop { paths, position } => {
|
||||
println!("DragDrop: {position:?} {paths:?} ")
|
||||
}
|
||||
wry::DragDropEvent::Leave => println!("DragLeave"),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
#[cfg(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "ios",
|
||||
target_os = "android"
|
||||
))]
|
||||
let _webview = builder.build(&window)?;
|
||||
#[cfg(not(any(
|
||||
target_os = "windows",
|
||||
target_os = "macos",
|
||||
target_os = "ios",
|
||||
target_os = "android"
|
||||
)))]
|
||||
let _webview = {
|
||||
use tao::platform::unix::WindowExtUnix;
|
||||
use wry::WebViewBuilderExtUnix;
|
||||
let vbox = window.default_vbox().unwrap();
|
||||
builder.build_gtk(vbox)?
|
||||
};
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
*control_flow = ControlFlow::Wait;
|
||||
|
||||
if let Event::WindowEvent {
|
||||
event: WindowEvent::CloseRequested,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
*control_flow = ControlFlow::Exit;
|
||||
}
|
||||
});
|
||||
}
|
216
crates/base-map/src/geolocate.rs
Normal file
216
crates/base-map/src/geolocate.rs
Normal file
@@ -0,0 +1,216 @@
|
||||
use axum::response::{Html, IntoResponse};
|
||||
|
||||
pub async fn geolocate() -> impl IntoResponse {
|
||||
Html(
|
||||
r#"
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head><meta charset="utf-8"><title>Geo Demo</title></head>
|
||||
<body>
|
||||
<div style="font-family: Arial, sans-serif; padding: 20px;">
|
||||
<h2>Location Service</h2>
|
||||
<div id="status"></div>
|
||||
<pre id="out"></pre>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
const out = document.getElementById('out');
|
||||
const status = document.getElementById('status');
|
||||
|
||||
// Persist / reuse a per‑browser UUID
|
||||
let id = localStorage.getItem('browser_id');
|
||||
if (!id) {
|
||||
id = crypto.randomUUID();
|
||||
localStorage.setItem('browser_id', id);
|
||||
}
|
||||
|
||||
async function checkLocationPermission() {
|
||||
if (!navigator.geolocation) {
|
||||
status.innerHTML = '<p style="color: red;">Geolocation is not supported by this browser.</p>';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!navigator.permissions) {
|
||||
// Fallback for browsers without Permissions API
|
||||
return requestLocationDirectly();
|
||||
}
|
||||
|
||||
try {
|
||||
const permission = await navigator.permissions.query({name: 'geolocation'});
|
||||
|
||||
switch(permission.state) {
|
||||
case 'granted':
|
||||
status.innerHTML = '<p style="color: green;">Location permission granted. Getting location...</p>';
|
||||
return getCurrentLocation();
|
||||
case 'denied':
|
||||
status.innerHTML = '<p style="color: red;">Location permission denied. Please enable location access in your browser settings and refresh the page.</p>';
|
||||
return false;
|
||||
case 'prompt':
|
||||
status.innerHTML = '<p style="color: orange;">Requesting location permission...</p>';
|
||||
return requestLocationDirectly();
|
||||
default:
|
||||
return requestLocationDirectly();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking permission:', error);
|
||||
return requestLocationDirectly();
|
||||
}
|
||||
}
|
||||
|
||||
function requestLocationDirectly() {
|
||||
status.innerHTML = '<p>Requesting location access...</p>';
|
||||
return getCurrentLocation();
|
||||
}
|
||||
|
||||
function getCurrentLocation() {
|
||||
return new Promise((resolve, reject) => {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
async pos => {
|
||||
const payload = {
|
||||
id,
|
||||
lat: pos.coords.latitude,
|
||||
lon: pos.coords.longitude,
|
||||
accuracy: pos.coords.accuracy,
|
||||
timestamp: pos.timestamp
|
||||
};
|
||||
|
||||
out.textContent = JSON.stringify(payload, null, 2);
|
||||
status.innerHTML = '<p style="color: green;">Location obtained successfully!</p>';
|
||||
|
||||
try {
|
||||
await fetch('/geolocate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
status.innerHTML += '<p style="color: green;">Location sent to server.</p>';
|
||||
} catch (fetchError) {
|
||||
status.innerHTML += `<p style="color: orange;">Warning: Could not send location to server: ${fetchError.message}</p>`;
|
||||
}
|
||||
|
||||
resolve(true);
|
||||
},
|
||||
err => {
|
||||
handleLocationError(err);
|
||||
reject(err);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 15000,
|
||||
maximumAge: 60000
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function handleLocationError(err) {
|
||||
let errorMessage = '';
|
||||
let color = 'red';
|
||||
|
||||
switch(err.code) {
|
||||
case err.PERMISSION_DENIED:
|
||||
errorMessage = 'Location access denied. Please enable location access in your browser settings and refresh the page.';
|
||||
break;
|
||||
case err.POSITION_UNAVAILABLE:
|
||||
errorMessage = 'Location information is unavailable. Please check your GPS/location services.';
|
||||
color = 'orange';
|
||||
break;
|
||||
case err.TIMEOUT:
|
||||
errorMessage = 'Location request timed out. Please refresh the page to try again.';
|
||||
color = 'orange';
|
||||
break;
|
||||
default:
|
||||
errorMessage = `Unknown error occurred: ${err.message}`;
|
||||
break;
|
||||
}
|
||||
|
||||
status.innerHTML = `<p style="color: ${color};">Error: ${errorMessage}</p>`;
|
||||
out.textContent = `Error: ${errorMessage}`;
|
||||
}
|
||||
|
||||
// Start the location check when page loads
|
||||
checkLocationPermission();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"#,
|
||||
)
|
||||
}
|
||||
|
||||
// v2
|
||||
// pub async fn geolocate() -> impl IntoResponse {
|
||||
// Html(
|
||||
// r#"
|
||||
// <!doctype html>
|
||||
// <html lang="en">
|
||||
// <head><meta charset="utf-8"><title>Geo Demo</title></head>
|
||||
// <body>
|
||||
// <pre id="out"></pre>
|
||||
//
|
||||
// <script type="module">
|
||||
// let position_var = undefined;
|
||||
// const out = document.getElementById('out');
|
||||
//
|
||||
// // Persist / reuse a per‑browser UUID
|
||||
// let id = localStorage.getItem('browser_id');
|
||||
// if (!id) {
|
||||
// id = crypto.randomUUID();
|
||||
// localStorage.setItem('browser_id', id);
|
||||
// }
|
||||
//
|
||||
// if (!navigator.geolocation) {
|
||||
// out.textContent = 'Geolocation not supported';
|
||||
// } else {
|
||||
//
|
||||
// navigator.geolocation.getCurrentPosition(
|
||||
// async pos => {
|
||||
// const payload = {
|
||||
// id,
|
||||
// lat: pos.coords.latitude,
|
||||
// lon: pos.coords.longitude
|
||||
// };
|
||||
// position_var = JSON.stringify(payload, null, 2);
|
||||
// out.textContent = JSON.stringify(payload, null, 2);
|
||||
// await fetch('/geolocate', { // <-- new route
|
||||
// method: 'POST',
|
||||
// headers: { 'Content-Type': 'application/json' },
|
||||
// body: JSON.stringify(payload)
|
||||
// });
|
||||
// },
|
||||
// err => out.textContent = `Error: ${err.message}`
|
||||
// );
|
||||
// }
|
||||
// </script>
|
||||
// </body>
|
||||
// </html>
|
||||
// "#,
|
||||
// )
|
||||
// }
|
||||
|
||||
// v1
|
||||
// pub async fn geolocate() -> impl IntoResponse {
|
||||
// // A minimal page that asks only after the user clicks.
|
||||
// Html(r#"
|
||||
// <!doctype html>
|
||||
// <html lang="en">
|
||||
// <head><meta charset="utf-8"><title>Geo Demo</title></head>
|
||||
// <body>
|
||||
// <pre id="out"></pre>
|
||||
//
|
||||
// <script>
|
||||
// console.log('Hello from the browser');
|
||||
// const out = document.getElementById('out');
|
||||
// if (!navigator.geolocation) {
|
||||
// out.textContent = 'Geolocation not supported';
|
||||
// } else {
|
||||
// navigator.geolocation.getCurrentPosition(
|
||||
// pos => out.textContent =
|
||||
// `Lat${pos.coords.latitude}, Lon${pos.coords.longitude}`,
|
||||
// err => out.textContent = `Error: ${err.message}`
|
||||
// );
|
||||
// }
|
||||
// </script>
|
||||
// </body>
|
||||
// </html>
|
||||
// "#)
|
||||
// }
|
33
crates/base-map/src/lib.rs
Normal file
33
crates/base-map/src/lib.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
mod geolocate;
|
||||
mod app;
|
||||
|
||||
use axum::response::IntoResponse;
|
||||
use axum::routing::post;
|
||||
// src/lib.rs
|
||||
use axum::{routing::get, Json, Router};
|
||||
use serde::Deserialize;
|
||||
use tower_http::trace::TraceLayer;
|
||||
|
||||
// ===== JSON coming back from the browser =====
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct LocationPayload {
|
||||
id: String,
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
}
|
||||
|
||||
// ===== POST /api/location handler =====
|
||||
async fn receive_location(axum::Json(p): Json<LocationPayload>) -> impl IntoResponse {
|
||||
println!("Got location: {p:?}");
|
||||
axum::http::StatusCode::OK
|
||||
}
|
||||
|
||||
|
||||
// a helper for integration tests or other binaries
|
||||
pub fn build_router() -> Router {
|
||||
Router::new()
|
||||
.route("/status", get(|| async { "OK" }))
|
||||
.route("/geolocate", get(geolocate::geolocate))
|
||||
.route("/geolocate", post(receive_location))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
}
|
32
crates/base-map/src/main.rs
Normal file
32
crates/base-map/src/main.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
use axum_embed::ServeEmbed;
|
||||
use base_map::build_router;
|
||||
use rust_embed::RustEmbed;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
#[derive(RustEmbed, Clone)]
|
||||
#[folder = "map/dist/"]
|
||||
struct Assets;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(tracing::Level::INFO)
|
||||
.init();
|
||||
let listener = TcpListener::bind("127.0.0.1:8080").await?;
|
||||
let local_address = listener.local_addr()?;
|
||||
tracing::info!("Server listening on http://{}", local_address);
|
||||
|
||||
async fn fallback(uri: axum::http::Uri) -> (axum::http::StatusCode, String) {
|
||||
(axum::http::StatusCode::NOT_FOUND, format!("No route for {uri}"))
|
||||
}
|
||||
|
||||
let serve_assets = ServeEmbed::<Assets>::new();
|
||||
let router = build_router();
|
||||
let app = router
|
||||
.nest_service("/", serve_assets)
|
||||
.fallback(fallback);
|
||||
|
||||
axum::serve(listener, app).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
28
crates/base-map/tsconfig.json
Normal file
28
crates/base-map/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
@@ -21,3 +21,18 @@ datalink = { path = "../datalink" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
datalink-provider = { path = "../datalink-provider" }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
wasm-bindgen = { workspace = true }
|
||||
web-sys = { version = "0.3.77", features = [
|
||||
"console",
|
||||
"Geolocation",
|
||||
"Navigator",
|
||||
"Window",
|
||||
"Document",
|
||||
"Element",
|
||||
"Position",
|
||||
"PositionOptions",
|
||||
"PositionError",
|
||||
"Coordinates"
|
||||
] }
|
115
crates/systems/src/geo_plugin.rs
Normal file
115
crates/systems/src/geo_plugin.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
// src/geo_plugin.rs
|
||||
use bevy::prelude::*;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::{closure::Closure, JsCast};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use web_sys::window;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct UserLocation {
|
||||
pub lat: f64,
|
||||
pub lon: f64,
|
||||
pub accuracy: f64,
|
||||
pub fresh: bool,
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct LocationData {
|
||||
pub data: Arc<Mutex<Option<(f64, f64, f64)>>>,
|
||||
}
|
||||
|
||||
impl Default for LocationData {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
data: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GeoPlugin;
|
||||
impl Plugin for GeoPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<UserLocation>()
|
||||
.init_resource::<LocationData>();
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
app.add_systems(Startup, request_location)
|
||||
.add_systems(Update, update_location);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn request_location(location_data: Res<LocationData>) {
|
||||
let window = match window() {
|
||||
Some(w) => w,
|
||||
None => {
|
||||
warn!("No window object available");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let geo = match window.navigator().geolocation() {
|
||||
Ok(g) => g,
|
||||
Err(_) => {
|
||||
warn!("Geolocation not available");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let data_clone = location_data.data.clone();
|
||||
|
||||
let success = Closure::<dyn FnMut(web_sys::Position)>::new(move |pos: web_sys::Position| {
|
||||
let c: web_sys::Coordinates = pos.coords();
|
||||
if let Ok(mut data) = data_clone.lock() {
|
||||
*data = Some((c.latitude(), c.longitude(), c.accuracy()));
|
||||
}
|
||||
});
|
||||
|
||||
let error = Closure::<dyn FnMut(web_sys::PositionError)>::new(move |err: web_sys::PositionError| {
|
||||
match err.code() {
|
||||
1 => {
|
||||
warn!("Geolocation permission denied. This may be due to:");
|
||||
warn!(" - User denied location access");
|
||||
warn!(" - Insecure connection (HTTP instead of HTTPS)");
|
||||
warn!(" - Browser security settings");
|
||||
warn!(" Consider serving over HTTPS for geolocation access");
|
||||
},
|
||||
2 => {
|
||||
warn!("Geolocation position unavailable: {}", err.message());
|
||||
},
|
||||
3 => {
|
||||
warn!("Geolocation timeout: {}", err.message());
|
||||
},
|
||||
_ => {
|
||||
warn!("Geolocation error: {} (code: {})", err.message(), err.code());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// watch_position keeps updating; get_current_position is one‑shot
|
||||
match geo.watch_position_with_error_callback(success.as_ref().unchecked_ref(), Some(error.as_ref().unchecked_ref())) {
|
||||
Ok(_) => {
|
||||
success.forget(); // leak the closure so it lives forever
|
||||
error.forget(); // leak the error closure too
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to start watching position: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn update_location(mut loc: ResMut<UserLocation>, location_data: Res<LocationData>) {
|
||||
if let Ok(mut data) = location_data.data.lock() {
|
||||
if let Some((lat, lon, accuracy)) = data.take() {
|
||||
loc.lat = lat;
|
||||
loc.lon = lon;
|
||||
loc.accuracy = accuracy;
|
||||
loc.fresh = true;
|
||||
}
|
||||
}
|
||||
}
|
@@ -5,6 +5,7 @@ mod vessel;
|
||||
mod ais;
|
||||
mod gps;
|
||||
mod radar;
|
||||
mod geo_plugin;
|
||||
|
||||
// Re-export components from the components crate
|
||||
pub use components::{
|
||||
@@ -16,3 +17,5 @@ pub use components::{
|
||||
|
||||
pub use world::player::{get_vessel_systems, setup_instrument_cluster_system, PlayerPlugin};
|
||||
pub use vessel::vessel_systems::{create_vessel_systems, AisSystem, GpsSystem, RadarSystem, SystemInteraction, SystemStatus, VesselSystem};
|
||||
|
||||
pub use geo_plugin::GeoPlugin;
|
@@ -59,8 +59,19 @@ rand = { version = "0.8.3" }
|
||||
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"] }
|
||||
wasm-bindgen = { workspace = true }
|
||||
web-sys = { version = "0.3", features = [
|
||||
"console",
|
||||
"Geolocation",
|
||||
"Navigator",
|
||||
"Window",
|
||||
"Document",
|
||||
"Element",
|
||||
"Position",
|
||||
"PositionOptions",
|
||||
"PositionError",
|
||||
"Coordinates"
|
||||
] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
@@ -78,9 +89,12 @@ winit = "0.30"
|
||||
bevy_webview_wry = { version = "0.4", default-features = false, features = ["api"] }
|
||||
bevy_flurx = "0.11"
|
||||
bevy_flurx_ipc = "0.4.0"
|
||||
# (run `cargo tree | grep wry` and use the version you see for bevy_webview_wry)
|
||||
wry = { version = "=0.51.2", optional = true, features = ["os-webview"] } # GPS support for native platforms - placeholder for future real GPS implementation
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
tokio = { version = "1.0", features = ["rt"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
[build-dependencies]
|
||||
embed-resource = "1"
|
||||
|
@@ -51,3 +51,9 @@ body {
|
||||
#bevy {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#yachtpit-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
@@ -3,6 +3,7 @@
|
||||
|
||||
mod core;
|
||||
mod ui;
|
||||
mod services;
|
||||
|
||||
use bevy::app::App;
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -11,7 +12,10 @@ 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};
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use systems::GeoPlugin;
|
||||
|
||||
// See https://bevy-cheatbook.github.io/programming/states.html
|
||||
#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
|
||||
@@ -41,13 +45,19 @@ impl Plugin for GamePlugin {
|
||||
LoadingPlugin,
|
||||
MenuPlugin,
|
||||
GpsMapPlugin,
|
||||
GpsServicePlugin,
|
||||
ActionsPlugin,
|
||||
SystemManagerPlugin,
|
||||
PlayerPlugin,
|
||||
))
|
||||
|
||||
|
||||
.add_systems(OnEnter(GameState::Playing), (setup_instrument_cluster, initialize_vessel_systems));
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
app.add_plugins(GeoPlugin);
|
||||
}
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
app.add_plugins((
|
||||
|
@@ -41,29 +41,38 @@ fn main() {
|
||||
.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,
|
||||
{
|
||||
// Add console logging for WASM debugging
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
info!("Starting WASM Bevy application");
|
||||
|
||||
App::new()
|
||||
.insert_resource(ClearColor(Color::srgb(0.1, 0.1, 0.1))) // Dark gray background instead of transparent
|
||||
.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()
|
||||
}),
|
||||
..default()
|
||||
})
|
||||
.set(AssetPlugin {
|
||||
meta_check: AssetMetaCheck::Never,
|
||||
..default()
|
||||
}),
|
||||
)
|
||||
.add_plugins(GamePlugin)
|
||||
.add_systems(Startup, set_window_icon)
|
||||
.run();
|
||||
)
|
||||
.add_plugins(GamePlugin)
|
||||
.add_systems(Startup, || {
|
||||
info!("WASM Bevy startup system running");
|
||||
})
|
||||
.run();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
237
crates/yachtpit/src/services/gps_service.rs
Normal file
237
crates/yachtpit/src/services/gps_service.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
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>) {
|
||||
if !gps_service.is_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use Bevy's time instead of std::time for WASM compatibility
|
||||
let current_time = time.elapsed_secs_f64();
|
||||
|
||||
// Only try to get GPS every 5 seconds to avoid overwhelming the browser
|
||||
if current_time - 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: current_time,
|
||||
};
|
||||
|
||||
gps_service.update_position(gps_data.clone());
|
||||
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::*;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn test_gps_data_update_wasm() {
|
||||
let mut gps_service = GpsService::new();
|
||||
|
||||
// Use a mock timestamp for WASM testing
|
||||
let timestamp = 1234567890.0;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
3
crates/yachtpit/src/services/mod.rs
Normal file
3
crates/yachtpit/src/services/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod gps_service;
|
||||
|
||||
pub use gps_service::*;
|
@@ -2,14 +2,14 @@ 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
|
||||
|
||||
use crate::services::{GpsService, GpsData};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use bevy_flurx::prelude::*;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use bevy_webview_wry::prelude::*;
|
||||
use web_sys::window;
|
||||
|
||||
/// Render layer for GPS map entities to isolate them from other cameras
|
||||
const GPS_MAP_LAYER: usize = 1;
|
||||
@@ -97,8 +97,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,18 +183,26 @@ fn spawn_placeholder_map(commands: &mut Commands, _asset_server: &Res<AssetServe
|
||||
|
||||
/// Function to spawn the GPS map window
|
||||
pub fn spawn_gps_map_window(commands: &mut Commands, gps_map_state: &mut ResMut<GpsMapState>) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
spawn_webview(commands, gps_map_state);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
web_sys::window().unwrap().document().unwrap().create_element("div").unwrap().set_id("bevy-gps-map");
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn spawn_webview(commands: &mut Commands, gps_map_state: &mut ResMut<GpsMapState>) {
|
||||
if gps_map_state.window_id.is_some() {
|
||||
info!("GPS map window already open");
|
||||
return;
|
||||
}
|
||||
|
||||
info!("Spawning GPS map window");
|
||||
|
||||
// Create a new window for the GPS map
|
||||
let window_entity = commands
|
||||
.spawn((
|
||||
Window {
|
||||
title: "GPS Navigation - OpenStreetMap".to_string(),
|
||||
title: "GPS".to_string(),
|
||||
resolution: (800.0, 600.0).into(),
|
||||
position: bevy::window::WindowPosition::Centered(
|
||||
bevy::window::MonitorSelection::Current,
|
||||
@@ -204,6 +213,8 @@ pub fn spawn_gps_map_window(commands: &mut Commands, gps_map_state: &mut ResMut<
|
||||
))
|
||||
.id();
|
||||
|
||||
gps_map_state.window_id = Some(window_entity);
|
||||
|
||||
// Create a camera for the map window
|
||||
commands.spawn((
|
||||
Camera2d,
|
||||
@@ -217,29 +228,25 @@ pub fn spawn_gps_map_window(commands: &mut Commands, gps_map_state: &mut ResMut<
|
||||
GpsMapWindow,
|
||||
));
|
||||
|
||||
gps_map_state.window_id = Some(window_entity);
|
||||
|
||||
|
||||
info!("GPS map window spawned with entity: {:?}", window_entity);
|
||||
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
spawn_gps_webview(commands, gps_map_state);
|
||||
}
|
||||
|
||||
#[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((
|
||||
IpcHandlers::new([
|
||||
navigation_clicked,
|
||||
search_clicked,
|
||||
map_view_changed,
|
||||
auth_status_changed,
|
||||
get_map_init,
|
||||
get_vessel_status
|
||||
ipc_commands::navigation_clicked,
|
||||
ipc_commands::search_clicked,
|
||||
ipc_commands::map_view_changed,
|
||||
ipc_commands::auth_status_changed,
|
||||
ipc_commands::get_map_init,
|
||||
ipc_commands::get_vessel_status
|
||||
]),
|
||||
Webview::Uri(WebviewUri::relative_local(
|
||||
// Using the build output of the base-map package
|
||||
@@ -249,109 +256,122 @@ fn spawn_gps_webview(commands: &mut Commands, gps_map_state: &mut ResMut<GpsMapS
|
||||
}
|
||||
}
|
||||
|
||||
// GPS Map IPC Commands using bevy_flurx_ipc
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod ipc_commands {
|
||||
use super::*;
|
||||
|
||||
/// 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()
|
||||
// GPS Map IPC Commands using bevy_flurx_ipc
|
||||
|
||||
/// Handle navigation button click
|
||||
#[command]
|
||||
pub 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]
|
||||
pub 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]
|
||||
pub 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]
|
||||
pub 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]
|
||||
pub 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]
|
||||
pub 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
|
||||
}
|
||||
}
|
||||
|
||||
/// 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()
|
||||
/// 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");
|
||||
}
|
||||
|
||||
/// 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(
|
||||
/// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,18 +13,28 @@ pub struct LoadingPlugin;
|
||||
/// If interested, take a look at <https://bevy-cheatbook.github.io/features/assets.html>
|
||||
impl Plugin for LoadingPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_loading_state(
|
||||
LoadingState::new(GameState::Loading)
|
||||
.continue_to_state(GameState::Playing)
|
||||
// .load_collection::<AudioAssets>()
|
||||
// .load_collection::<TextureAssets>(),
|
||||
);
|
||||
// Temporarily bypass asset loading and go directly to Playing state
|
||||
app.add_systems(Startup, || {
|
||||
info!("LoadingPlugin: Starting up, transitioning to Playing state");
|
||||
});
|
||||
|
||||
app.add_systems(Update, transition_to_playing.run_if(in_state(GameState::Loading)));
|
||||
|
||||
// Add a system to hide the loading indicator when transitioning to the Playing state
|
||||
app.add_systems(OnEnter(GameState::Playing), hide_loading_indicator);
|
||||
|
||||
// Add debug systems to track state transitions
|
||||
app.add_systems(OnEnter(GameState::Loading), || info!("Entered Loading state"));
|
||||
app.add_systems(OnExit(GameState::Loading), || info!("Exiting Loading state"));
|
||||
app.add_systems(OnEnter(GameState::Playing), || info!("Entered Playing state"));
|
||||
}
|
||||
}
|
||||
|
||||
fn transition_to_playing(mut next_state: ResMut<NextState<GameState>>) {
|
||||
info!("Transitioning from Loading to Playing state");
|
||||
next_state.set(GameState::Playing);
|
||||
}
|
||||
|
||||
/// Hides the loading indicator when transitioning to the Playing state
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn hide_loading_indicator() {
|
||||
|
Reference in New Issue
Block a user