Initial commit

This commit is contained in:
torsten committed 2026-08-13 18:33:10 +02:00
commit b7d90b9afe
16245 files changed
+3396666

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
# TME Simulator Frontend
TME Simulator is a web application built with React.
## Getting Started
These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.
### Prerequisites
- Node.js
- npm
### Installation
1. Clone the repository
2. Navigate to the project directory
3. Install dependencies with npm i
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits. You will also see any lint errors in the console.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes. Your app is ready to be deployed!
## Deployment
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
## Contributing
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of conduct, and the process for submitting pull requests to us.
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
+45
View File
@@ -0,0 +1,45 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@reduxjs/toolkit": "^2.2.5",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@turf/turf": "^7.0.0",
"cra-template": "1.2.0",
"immer": "^10.1.1",
"leaflet-draw": "^1.0.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-leaflet": "^4.2.1",
"react-leaflet-draw": "^0.20.4",
"react-redux": "^9.1.2",
"react-router-dom": "^6.23.1",
"react-scripts": "^5.0.1",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "HOST=localhost react-scripts start",
"build": "react-scripts build"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+19
View File
@@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/RheinmetallLogo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Website for simulating soldiers in the Legatus Excon"
/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Legatus Simulator Software | Frontend</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
+15
View File
@@ -0,0 +1,15 @@
{
"short_name": "Legatus Simulator Software | Frontend",
"name": "Website for simulating soldiers in the Legatus Excon",
"icons": [
{
"src": "RheinmetallLogo.png",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
+13
View File
@@ -0,0 +1,13 @@
body {
margin: 0;
padding: 0;
}
html body {
width: 100%;
height: 100vh;
}
.leaflet-draw.leaflet-control {
display: none;
}
+27
View File
@@ -0,0 +1,27 @@
import React, { useState } from "react"
import './App.css';
import { useDispatch } from "react-redux";
import { useGetExerciseIds } from "./websocket/getExerciseIds";
import { ErrorPopup } from "./components/errorPopup";
import MapBox from './components/map'
import SideBar from "./components/sideBar";
import Popup from "./components/popup";
import { useGetPartyData } from "./websocket/getPartyData";
function App() {
const [createPopupOpen, setCreatePopupOpen] = useState(false)
const dispatch = useDispatch()
useGetExerciseIds(dispatch)
useGetPartyData(42, dispatch)
return (
<div>
<ErrorPopup />
<Popup createPopupState={createPopupOpen} setPopupState={setCreatePopupOpen} />
<SideBar createPopupStateChanger={setCreatePopupOpen} />
<MapBox />
</div>
);
}
export default App;
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Rheinmetall_Group_4c_positiv"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="4656.32px"
height="1268.483px" viewBox="0 0 4656.32 1268.483" enable-background="new 0 0 4656.32 1268.483" xml:space="preserve">
<path id="RHEINMETALL_1_" fill="#3E3D40" d="M955.22,1044.451h-48.742c-9.427,0-15.275-5.268-15.275-17.631V696.106
c0-44.62,16.423-66.955,65.189-66.955H1001c57.589,0,96.966,5.306,119.287,14.117c44.621,18.201,59.921,59.328,59.921,109.823
c0,50.53-17.064,92.805-66.399,113.364l79.911,144.521c3.505,6.453,8.193,14.091,8.193,21.15c0,5.887-4.083,9.981-8.773,10.575
c-5.294,1.184-46.446,2.925-58.773,2.925c-10.563,0-16.424-5.282-20.558-13.5l-76.949-150.963
c-9.972,0.593-20.583,0.593-32.294,0.593h-32.317v145.064C972.249,1038.604,965.19,1044.451,955.22,1044.451z M1006.911,817.121
c25.246,0,45.804-2.331,59.34-7.057c18.793-7.035,30.54-26.409,30.54-52.852c0-31.133-9.415-48.149-29.38-55.207
c-11.747-4.135-29.96-5.306-52.875-5.306h-22.915c-14.104,0-19.373,6.464-19.373,19.396v101.025H1006.911z M1328.826,1044.451
h-49.309c-9.416,0-15.277-5.268-15.277-17.631v-380.64c0-10.564,4.69-17.622,15.844-17.622h50.531
c11.143,0,14.671,6.479,14.671,17.054v148.025h160.958V646.181c0-10.564,4.69-17.622,16.485-17.622h49.89
c11.167,0,14.671,6.479,14.671,17.054v381.207c0,11.192-6.43,17.631-16.424,17.631h-48.766c-9.391,0-15.857-5.268-15.857-17.631
V864.701h-160.958v162.119C1345.287,1038.013,1338.821,1044.451,1328.826,1044.451z M1746.508,1043.885
c-41.128,0-62.857-16.448-62.857-62.856V693.775c0-44.066,19.978-64.624,62.857-64.624h176.197c11.191,0,16.473,3.541,16.473,15.869
v41.116c0,9.97-5.281,13.513-16.473,13.513h-139.191c-12.339,0-18.806,6.464-18.806,21.74v72.25h143.337
c10.574,0,16.423,3.516,16.423,15.832v41.152c0,9.983-5.849,14.079-16.423,14.079h-143.337v85.761
c0,15.855,5.887,22.916,22.323,22.916h145.706c10.551,0,16.437,2.924,16.437,15.843v40.55c0,10.007-5.318,14.114-16.437,14.114
H1746.508z M2091.141,1044.451h-49.321c-9.416,0-15.265-5.268-15.265-17.631v-380.64c0-10.564,4.689-17.622,15.857-17.622h50.505
c11.168,0,14.696,6.479,14.696,17.054v381.207C2107.614,1038.013,2101.161,1044.451,2091.141,1044.451z M2271.101,1044.451h-47
c-9.392,0-15.264-5.268-15.264-17.631V682.039c0-37.019,14.659-55.231,54.023-55.231h22.321c33.478,0,41.153,14.67,52.876,39.35
c94.557,193.856,111.599,233.231,129.812,277.272c-1.764-53.442-1.764-104.567-1.764-293.141c0-12.918,5.885-21.73,17.645-21.73
h44.065c11.748,0,15.832,8.231,15.832,19.979v353.64c0,38.759-21.717,45.817-45.792,45.817h-18.227
c-34.082,0-44.064-11.76-60.513-45.226c-105.145-215.004-114.522-237.34-132.75-280.787c0.593,40.497,1.173,80.454,1.173,304.838
C2287.538,1038.013,2281.084,1044.451,2271.101,1044.451z M2708.651,1044.451h-49.915c-9.415,0-14.091-6.438-14.091-17.631V686.136
c0-30.553,7.602-59.329,57.564-59.329h17.634c41.103,0,44.04,10.563,63.993,61.081c40.535,103.95,52.307,135.142,64.029,170.939
c12.906-36.98,22.335-65.782,62.871-172.111c16.435-43.448,22.31-59.909,66.966-59.909h19.953c47.619,0,50.543,23.481,50.543,59.329
v338.93c0,14.117-5.861,19.385-16.473,19.385h-49.371c-9.366,0-15.239-5.268-15.239-18.792c0-211.462,0.592-258.453,2.356-316.597
c-14.709,48.149-26.431,82.775-81.083,226.739c-9.391,24.642-15.831,34.626-36.39,34.626h-9.427
c-19.386,0-28.172-9.391-36.414-32.292c-52.271-140.981-65.758-177.407-83.392-229.073c1.765,58.144,1.765,108.651,1.765,318.374
C2724.532,1036.827,2720.422,1044.451,2708.651,1044.451z M3207.404,1043.885c-41.114,0-62.833-16.448-62.833-62.856V693.775
c0-44.066,19.966-64.624,62.833-64.624h176.223c11.155,0,16.437,3.541,16.437,15.869v41.116c0,9.97-5.281,13.513-16.437,13.513
h-139.228c-12.34,0-18.793,6.464-18.793,21.74v72.25h143.336c10.563,0,16.461,3.516,16.461,15.832v41.152
c0,9.983-5.898,14.079-16.461,14.079h-143.336v85.761c0,15.855,5.849,22.916,22.321,22.916h145.681
c10.551,0,16.475,2.924,16.475,15.843v40.55c0,10.007-5.331,14.114-16.475,14.114H3207.404z M3619.782,1044.451h-49.891
c-9.438,0-15.867-5.847-15.867-17.631V699.649h-76.049c-11.131,0-16.462-3.542-16.462-13.513v-41.116
c0-12.329,5.331-15.869,16.462-15.869h253.356c11.143,0,16.485,3.998,12.45,15.869l-14.451,41.116
c-3.084,9.847-8.34,13.513-19.532,13.513h-73.556v327.171C3636.243,1038.604,3629.208,1044.451,3619.782,1044.451z
M3799.841,1025.659c-4.678,14.118-8.194,19.967-19.978,19.967c-11.712,0-50.482-1.175-58.133-2.925
c-6.465-1.765-8.206-7.06-8.206-11.748c0-5.294,1.741-12.351,4.097-18.793l115.141-334.847
c11.735-34.044,20.534-50.505,62.859-50.505h18.792c39.364,0,45.236,15.289,56.418,48.754l112.783,335.414
c2.345,6.453,4.107,12.92,4.107,19.384c0,4.69-2.344,9.984-7.688,12.342c-7.032,2.925-41.09,2.925-58.13,2.925
c-11.725,0-15.869-5.849-20.545-19.967l-29.95-88.673h-142.757L3799.841,1025.659z M3953.196,868.257
c-32.329-103.395-42.338-136.291-51.714-170.965c-9.416,34.082-19.952,68.138-52.876,170.965H3953.196z M4199.952,1043.885
c-39.918,0-61.624-17.657-61.624-59.932V646.181c0-10.564,4.665-17.622,15.807-17.622h50.507c11.191,0,14.709,6.479,14.709,17.054
v306.021c0,14.093,5.873,21.744,21.717,21.744h117.781c10.526,0,16.4,2.924,16.4,15.843v40.55c0,10.007-5.874,14.114-16.4,14.114
H4199.952z M4481.024,1043.885c-39.969,0-61.688-17.657-61.688-59.932V646.181c0-10.564,4.678-17.622,15.845-17.622h50.555
c11.144,0,14.661,6.479,14.661,17.054v306.021c0,14.093,5.886,21.744,21.691,21.744h117.783c10.587,0,16.448,2.924,16.448,15.843
v40.55c0,10.007-5.861,14.114-16.448,14.114H4481.024z"/>
<g id="Bildmarke_1_">
<path fill="#007EC1" d="M880.132,1.214c0,0-798.58,355.603-798.604,355.627C16.053,385.876,0,429.485,0,460.211v375.901v420.176
c0,8.18,8.279,14.894,17.004,11.115l414.327-184.39c24.802-12.105,57.551-40.313,57.551-96.531V544.477L835.007,390.22
c35.896-16.966,61.266-64.08,61.266-126.321c0-0.123,0.013-210.587,0.013-251.395C896.286,2.474,889.228-2.463,880.132,1.214z"/>
<path fill="#00406E" d="M17.004,1267.403l414.327-184.39c24.802-12.105,57.551-40.313,57.551-96.531V544.477
c0,0-407.477,183.749-407.453,183.749h0.012C13.807,758.236,0.679,803.053,0,836.112v420.176
C0,1264.468,8.279,1271.182,17.004,1267.403z"/>
<path fill="#000824" d="M0,836.112c0.679-33.059,13.807-77.876,81.441-107.887h-0.012c-0.024,0,407.453-183.749,407.453-183.749
v72.347L81.441,768.269C20.804,791.009,0.679,835.113,0,868.17V836.112z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg fill="#000000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 24 24" xml:space="preserve">
<g id="group">
<path d="M24,15.9c0-2.8-1.5-5-3.7-6.1C21.3,8.8,22,7.5,22,6c0-2.8-2.2-5-5-5c-2.1,0-3.8,1.2-4.6,3c0,0,0,0,0,0c-0.1,0-0.3,0-0.4,0
c-0.1,0-0.3,0-0.4,0c0,0,0,0,0,0C10.8,2.2,9.1,1,7,1C4.2,1,2,3.2,2,6c0,1.5,0.7,2.8,1.7,3.8C1.5,10.9,0,13.2,0,15.9V20h5v3h14v-3h5
V15.9z M17,3c1.7,0,3,1.3,3,3c0,1.6-1.3,3-3,3c0-1.9-1.1-3.5-2.7-4.4c0,0,0,0,0,0C14.8,3.6,15.8,3,17,3z M13.4,4.2
C13.4,4.2,13.4,4.2,13.4,4.2C13.4,4.2,13.4,4.2,13.4,4.2z M15,9c0,1.7-1.3,3-3,3s-3-1.3-3-3s1.3-3,3-3S15,7.3,15,9z M10.6,4.2
C10.6,4.2,10.6,4.2,10.6,4.2C10.6,4.2,10.6,4.2,10.6,4.2z M7,3c1.2,0,2.2,0.6,2.7,1.6C8.1,5.5,7,7.1,7,9C5.3,9,4,7.7,4,6S5.3,3,7,3
z M5.1,18H2v-2.1C2,13.1,4.1,11,7,11v0c0,0,0,0,0,0c0.1,0,0.2,0,0.3,0c0,0,0,0,0,0c0.3,0.7,0.8,1.3,1.3,1.8
C6.7,13.8,5.4,15.7,5.1,18z M17,21H7v-2.1c0-2.8,2.2-4.9,5-4.9c2.9,0,5,2.1,5,4.9V21z M22,18h-3.1c-0.3-2.3-1.7-4.2-3.7-5.2
c0.6-0.5,1-1.1,1.3-1.8c0.1,0,0.2,0,0.4,0v0c2.9,0,5,2.1,5,4.9V18z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 12H18M12 6V18" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 337 B

@@ -0,0 +1,41 @@
import { useEffect, useCallback } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import '../styles/errorpopup.css'
import { exerciseError } from '../store/exerciseSlice'
import { participantError } from '../store/participantSlice';
import { partyDataError } from '../store/partySlice';
export function ErrorPopup() {
const message = useSelector((state) => state.participants.errorMessage || state.exercise.errorMessage || state.party.errorMessage)
const isError = useSelector((state) => state.participants.isError || state.exercise.isError || state.party.isError)
const dispatch = useDispatch();
const resetStates = useCallback(() => {
dispatch(exerciseError(''));
dispatch(participantError(''));
dispatch(partyDataError(''));
}, [dispatch]);
useEffect(() => {
let timer;
if (isError) {
timer = setTimeout(() => {
resetStates();
}, 5000); // Setzt den Timer auf 5 Sekunden
}
return () => clearTimeout(timer); // Bereinigt den Timer, wenn die Komponente unmountet wird oder isError sich ändert
}, [isError, dispatch, resetStates]);
return (
<>
{isError &&
<div className="popup-error">
<button className="popup-error-button" onClick={() => resetStates()}>X</button>
<p>{message}</p>
</div>
}
</>
)
}
+106
View File
@@ -0,0 +1,106 @@
import React, { useState } from "react";
import { MapContainer, TileLayer, FeatureGroup, Marker, Tooltip, Polyline } from 'react-leaflet';
import { EditControl } from "react-leaflet-draw";
import { connect, useDispatch } from "react-redux";
import L from 'leaflet';
import MapEvents from '../helper/mapEvent.js';
import '../styles/map.css';
import 'leaflet/dist/leaflet.css';
import 'leaflet-draw/dist/leaflet.draw.css';
import icon from 'leaflet/dist/images/marker-icon.png';
import iconShadow from 'leaflet/dist/images/marker-shadow.png';
import { setWalkingPath } from "../store/participantSlice.js";
import { completedDrawing } from "./participantList.js";
import { addLayers, deleteLayers } from "../store/layerSlice.js";
let DefaultIcon = L.icon({
iconUrl: icon,
shadowUrl: iconShadow,
iconSize: [25, 41],
iconAnchor: [12.5, 41]
});
L.Marker.prototype.options.icon = DefaultIcon;
function MapBox({ mapPosition, participants, currentParticipant, layers }) {
const [position, setPosition] = useState({ lat: 0, lng: 0 });
const [clickPosition, setClickPosition] = useState({ lat: 0, lng: 0 });
const dispatch = useDispatch();
return (
<div>
<p className="map-position-center">Center : {position.lat.toFixed(3)} | {position.lng.toFixed(3)}</p>
<p className="map-position-click">Clicked : {clickPosition.lat.toFixed(3)} | {clickPosition.lng.toFixed(3)}</p>
<MapContainer
key={mapPosition.toString()}
center={mapPosition}
zoom={14.5}
scrollWheelZoom={true}
attributionControl={false}
>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<FeatureGroup>
<EditControl key={currentParticipant}
onCreated={(e) => {
if (e.layerType === 'polyline') {
localStorage.setItem(currentParticipant, true)
dispatch(deleteLayers(currentParticipant))
dispatch(setWalkingPath({ equipmentNumber: currentParticipant, path: JSON.parse(JSON.stringify(e.layer.getLatLngs())) }))
completedDrawing()
var newObject = {
layers: [],
participant: currentParticipant
}
newObject.layers.push(JSON.parse(JSON.stringify(e.layer.getLatLngs())))
dispatch(addLayers(newObject))
e.layer.remove()
}
}}
position='topright'
edit={{
edit: false,
remove: false,
}}
draw={{
rectangle: false,
circle: false,
circlemarker: false,
polygon: false
}}
/>
</FeatureGroup>
{/* Display the participants on the map */}
{participants.map((participant, i) => (
<Marker key={i} position={[participant.currentPosition[0], participant.currentPosition[1]]}>
<Tooltip>{participant.equipmentNumber}</Tooltip>
</Marker>
))}
{layers.map((layer, i) => (
<Polyline key={i} positions={layer.layers} />
))}
<MapEvents setPosition={setPosition} mapPosition={mapPosition} setClickPosition={setClickPosition} />
</MapContainer>
</div>
);
}
const mapStateToProps = (state) => {
return {
mapPosition: state.map,
participants: state.participants.data,
currentParticipant: state.participants.currentParticipantWalkPathEq,
layers: state.layer
};
};
export default connect(mapStateToProps)(MapBox);
@@ -0,0 +1,134 @@
import React, { useState } from 'react';
import "../styles/participantList.css"
import logo from "../assets/group-svgrepo-com.svg"
import { useDispatch } from 'react-redux'
import { changeMapPosition } from '../store/mapSlice';
import { useSetAbcProtection, select, setCurrentWalkParticipant } from '../store/participantSlice';
import { useCalculation } from '../helper/walkingPathCaclulation';
import { sleep } from '../helper/sleep';
import { changeHoldingPathValue } from './sideBar';
let isFinishedDrawing = false;
export function completedDrawing() {
isFinishedDrawing = true;
}
export function startDrawing() {
isFinishedDrawing = false;
}
export default function ParticipantList(participant) {
const [buttonState, setButtonState] = useState(false);
const calculate = useCalculation()
const dispatch = useDispatch()
// Handels value of checkbox input
const useHandleChange = (e) => {
let checkStatus = participant.participant.abcProtection
dispatch(useSetAbcProtection({ equipmentNumber: participant.participant.equipmentNumber, newAbcProtection: !checkStatus }))
};
// Opens Collapsible
function buttonPressed() {
setButtonState(!buttonState);
}
async function drawingPath() {
dispatch(setCurrentWalkParticipant({ currentParticipantWalkPathEq: participant.participant.equipmentNumber }))
let drawButton = document.getElementsByClassName("leaflet-draw-draw-polyline")[0];
await sleep(10)
drawButton.click()
}
function patrouille() {
localStorage.setItem(participant.participant.equipmentNumber.toString(), false)
calculate(participant.participant.path, participant.participant.equipmentNumber, participant.participant, "Patrouille")
startDrawing()
}
function oneTimeUsePath() {
localStorage.setItem(participant.participant.equipmentNumber.toString(), false)
calculate(participant.participant.path, participant.participant.equipmentNumber, participant.participant, "OTUP")
startDrawing()
}
function hold() {
localStorage.setItem(participant.participant.equipmentNumber.toString(), false)
calculate(participant.participant.path, participant.participant.equipmentNumber, participant.participant, "Hold")
startDrawing()
changeHoldingPathValue(true)
}
return (
<div className='collapsible-div'>
<button className={buttonState ? 'collapsible-active' : 'collapsible'} onClick={buttonPressed}>
<img src={logo} alt="Group" className='group-icon' />
<div className='participant-text'>
<p style={{ color: participant.participant.party }} > {participant.participant.equipmentNumber} | {participant.participant.type}</p>
</div>
</button>
{buttonState &&
<div className="content">
{/* Table of Participants Information */}
<table className='participants-table'>
<tbody>
<tr>
<td>
<h4>Group:</h4>
</td>
<td>{participant.participant.party}</td>
</tr>
<tr>
<td>
<h4>Typ:</h4>
</td>
<td>{participant.participant.type}</td>
</tr>
<tr>
<td>
<h4>Equip. No.:</h4>
</td>
<td>{participant.participant.equipmentNumber}</td>
</tr>
<tr>
<td>
<h4>Position:</h4>
</td>
<td>{participant.participant.currentPosition[0].toFixed(3)} | {participant.participant.currentPosition[1].toFixed(3)}</td>
</tr>
<tr>
<td>
<h4>ABC Protection:</h4>
</td>
<td><input type="checkbox" value={participant.participant.abcProtection} onChange={useHandleChange}></input></td>
</tr>
<tr>
<td>
<h4>Status:</h4>
</td>
<td>{participant.participant.status}</td>
</tr>
</tbody>
</table>
{/* Buttons for the Participants */}
<div className='collapsible-button-div'>
<button onClick={() => dispatch(changeMapPosition(participant.participant.currentPosition))} className="collapsible-button">GO TO</button>
<button className='collapsible-button' onClick={() => dispatch(select({ participant: participant.participant }))}>SET POS</button>
<button className='collapsible-button' onClick={() => drawingPath()}>EMULATE RUN</button>
{isFinishedDrawing && (
<div className='choose-popup'>
<button className='collapsible-button' onClick={() => patrouille()}>Patrouille</button>
<button className='collapsible-button' onClick={() => oneTimeUsePath()}>One time use path</button>
<button className='collapsible-button' onClick={() => hold()}>Hold Path</button>
</div>
)}
</div>
</div>
}
</div>
);
}
+123
View File
@@ -0,0 +1,123 @@
import React, { useCallback, useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux'
import { useCheckIfParticipantIsValid } from '../websocket/checkIfParticipantIsValid'
import "../styles/popup.css"
import { useGetPartyData } from '../websocket/getPartyData';
import { clearPartyData } from '../store/partySlice';
import { setCurrentExerciseId } from '../store/exerciseSlice';
export default function Popup({ createPopupState, setPopupState }) {
const [equipmentNumber, setEquipmentNumber] = useState('');
const [selectedExerciseId, setSelectedExerciseId] = useState('');
const [isExerciseIdSelected, setIsExerciseIdSelected] = useState(false);
const [partyId, setPartyId] = useState(2);
const partyList = useSelector((state) => state.party.partyData);
const dispatch = useDispatch()
const excerciseIds = useSelector((state) => state.exercise.exerciseIds);
// Add participant to the store
function useHandleClick() {
useCheckIfParticipantIsValid(equipmentNumber, selectedExerciseId, dispatch, partyList, partyId);
dispatch(clearPartyData([]))
setIsExerciseIdSelected(false)
}
function useHandleExerciseIdSubmit() {
useGetPartyData(selectedExerciseId, dispatch)
setIsExerciseIdSelected(!isExerciseIdSelected)
setPartyId(2)
dispatch(setCurrentExerciseId(selectedExerciseId))
setPopupState(false)
}
function handlePartyNameChange(event) {
setPartyId(event.target.value)
}
// Handle input change
const handleInputChange = (event) => {
setEquipmentNumber(event.target.value);
};
// Handle exerciseId change
const handleExerciseIdChange = (event) => {
setSelectedExerciseId(event.target.value);
};
// Close popup when ESC key is pressed
const handleKeyDown = useCallback((event) => {
if (event.keyCode === 27) {
setPopupState(false);
}
}, [setPopupState]);
// Add event listener when popup is open
useEffect(() => {
if (createPopupState) {
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("keydown", handleKeyDown);
};
}
}, [createPopupState, handleKeyDown]);
return (
<>
{createPopupState && (
<div className='popup-blur'>
<div className="popup">
<p className="popup-text">SELECT EXERCISE ID</p>
</div>
<div className='select-party'>
<form className='select-party-form'>
<select value={selectedExerciseId} onChange={handleExerciseIdChange} className='select-party-form-select'>
{excerciseIds.map((exerciseId) => (
<option key={exerciseId} value={exerciseId}>{exerciseId}</option>
))}
</select>
</form>
<div className="select-party-text">ID :</div>
<div className="select-party-dropdown"></div>
</div>
<div className="popup-buttons">
<button className="popup-cancel" onClick={() => setPopupState(false)}>Cancel</button>
<button onClick={useHandleExerciseIdSubmit} className="popup-submit">Submit</button>
</div>
</div>
)}
{isExerciseIdSelected && (
<div className='popup-blur'>
<div className="popup">
<p className="popup-text">ADD A PARTICIPANT</p>
</div>
<div className="select-party">
<form className='select-party-form'>
<select id="group" name="group" className='select-party-form-select' onChange={handlePartyNameChange}>
{partyList.map((item, index) => (
<option key={index} value={item.id} style={{ color: item.color }}>{item.party}</option>
))}
</select>
</form>
<div className="select-party-text">Group :</div>
<div className="select-party-dropdown"></div>
</div>
<div className='select-number'>
<input className='select-number-inputfield' type="number" value={equipmentNumber} onChange={handleInputChange}></input>
<div className="select-number-text">Number :</div>
<div className="select-number-inputfield-div"></div>
</div>
<div className="popup-buttons">
<button className="popup-cancel" onClick={() => setIsExerciseIdSelected(false)}>Cancel</button>
<button className="popup-submit" onClick={useHandleClick}>Submit</button>
</div>
</div>
)}
</>
)
}
+119
View File
@@ -0,0 +1,119 @@
import React, { useState } from 'react';
import "../styles/sideBar.css";
import logo from "../assets/Rheinmetall_Logo_2021.svg";
import ParticipantList from './participantList';
import { useDispatch, useSelector } from "react-redux";
import { getAllParticipants } from "../websocket/getAllParticipants";
import { setWalkingPath } from '../store/participantSlice';
import { generateRandomPath } from '../helper/generateRandomPath';
import { useCalculation } from '../helper/walkingPathCaclulation';
import { addLayers } from '../store/layerSlice';
import { sleep } from '../helper/sleep';
import { getStateFromLocalStorage, saveStateToLocalStorage } from '../helper/localStorageUsage';
let isHoldingPaths = false;
export function changeHoldingPathValue(value) {
isHoldingPaths = value;
}
export default function SideBar({ createPopupStateChanger }) {
const [hasParticipants, setHasParticipants] = useState(false)
const [startedStressTest, setStartedStressTest] = useState(false)
const participantList = useSelector((state) => state.participants.data);
const partyList = useSelector(state => state.party.partyData)
const excerciseId = useSelector(state => state.exercise.currentExerciseId) ? 0 : 42
const calculate = useCalculation()
const dispatch = useDispatch();
function useStartStressTest() {
const lowerRight = [53.019, 8.995]
const topLeft = [53.128, 8.730]
for (var participant of participantList) {
var newObject = {
layers: [],
participant: participant
}
var path = generateRandomPath(lowerRight, topLeft, participant.currentPosition);
newObject.layers.push(path)
localStorage.setItem(participant.equipmentNumber.toString(), false)
dispatch(setWalkingPath({ equipmentNumber: participant.equipmentNumber, path: path }))
dispatch(addLayers(newObject))
calculate(path, participant.equipmentNumber, participant, "OTUP")
}
setHasParticipants(false)
setStartedStressTest(true)
}
async function useGetAllParticipants() {
getAllParticipants(excerciseId || 42, dispatch, partyList)
await sleep(500)
setHasParticipants(true)
}
function startAllPaths() {
var pathObjectArray = [];
for (var singleParticipant of participantList) {
var pathObject = {
equipmentNumber: singleParticipant.equipmentNumber,
path: singleParticipant.path
}
if (singleParticipant.path && singleParticipant.path.length > 0) {
localStorage.setItem(singleParticipant.equipmentNumber.toString(), false)
calculate(singleParticipant.path, singleParticipant.equipmentNumber, singleParticipant, "OTUP")
pathObjectArray.push(pathObject)
}
}
saveStateToLocalStorage("paths", pathObjectArray)
getStateFromLocalStorage("paths")
changeHoldingPathValue(false)
}
return (
<div className="sidebar">
<div className="sidebar-header">
<img src={logo} alt="Rheinmetall Logo" className='logo' />
</div>
<div className="participant-list">
<p className="participant-list-text">Participant list</p>
<div className="addButton-div" onClick={() => createPopupStateChanger(true)}>
<div className="addButton"><p className="addButton-text">+</p></div>
</div>
</div>
<div className="sidebar-content">
{
participantList.map((participant, i) => {
return <ParticipantList key={i} participant={participant} participantList={participantList} />
})
}
</div>
<div className='sidebar-footer'>
<button onClick={useGetAllParticipants}>Get all Participants</button>
{hasParticipants &&
<button onClick={useStartStressTest}>Start Stress Test</button>
}
{startedStressTest &&
<div className='sidebar-startStop-container'>
<button>Start</button>
<button>Pause</button>
<button>Stop</button>
</div>
}
{isHoldingPaths > 0 && (
<div className='startingpath-popup'>
<button className='collapsible-button' onClick={() => startAllPaths()}>Start all Paths</button>
</div>
)}
</div>
</div>
);
}
+126
View File
@@ -0,0 +1,126 @@
export const participantData = [
{
equipmentNumber: 1054,
currentPosition: [53.0580229, 8.8972232],
abcProtection: true,
gear: [
{
weapon: "AK-74",
munition: "7.62x39mm AP"
}, {
weapon: "AK-74",
munition: "7.62x39mm AP"
}
],
party: "Blue",
symbol: "--",
type: "Soldat/G",
status: "Wounded",
selected: false,
walkingPath: []
},
{
equipmentNumber: 1024,
currentPosition: [53.0510229, 8.8902232],
abcProtection: true,
gear: [
{
weapon: "AK-74",
munition: "7.62x39mm AP"
}, {
weapon: "AK-74",
munition: "7.62x39mm AP"
}
],
party: "Blue",
symbol: "--",
type: "Soldat/G",
status: "Wounded",
selected: false,
walkingPath: []
},
{
equipmentNumber: 1014,
currentPosition: [53.0520229, 8.8932232],
abcProtection: true,
gear: [
{
weapon: "AK-74",
munition: "7.62x39mm AP"
}, {
weapon: "AK-74",
munition: "7.62x39mm AP"
}
],
party: "Red",
symbol: "--",
type: "Soldat/G",
status: "Wounded",
selected: false,
walkingPath: [],
walkMode: ""
},
{
equipmentNumber: 1088,
currentPosition: [53.0550229, 8.8942232],
abcProtection: true,
gear: [
{
weapon: "AK-74",
munition: "7.62x39mm AP"
}, {
weapon: "AK-74",
munition: "7.62x39mm AP"
}
],
party: "Pink",
symbol: "--",
type: "Soldat/G",
status: "Wounded",
selected: false,
walkingPath: [],
walkMode: ""
},
{
equipmentNumber: 1034,
currentPosition: [53.0540229, 8.8972232],
abcProtection: true,
gear: [
{
weapon: "AK-74",
munition: "7.62x39mm AP"
}, {
weapon: "AK-74",
munition: "7.62x39mm AP"
}
],
party: "Orange",
symbol: "--",
type: "Soldat/G",
status: "Wounded",
selected: false,
walkingPath: [],
walkMode: ""
},
]
export const partyData = [
{
id:2,
party:"RED",
color:"#eb0016"
},
{
id:3,
party:"BLUE",
color:"#1721a6"
},
{
id:4,
party:"GREEN",
color:"#00ff00"
}
]
@@ -0,0 +1,18 @@
export function generateRandomPath(lowerRight, topLeft,currentPosition) {
const path = [
{
lat: currentPosition[0],
lng: currentPosition[1],
},
{
lat: Math.random() * (topLeft[0] - lowerRight[0]) + lowerRight[0],
lng: Math.random() * (topLeft[1] - lowerRight[1]) + lowerRight[1],
},
{
lat: Math.random() * (topLeft[0] - lowerRight[0]) + lowerRight[0],
lng: Math.random() * (topLeft[1] - lowerRight[1]) + lowerRight[1],
},
]
return path;
}
@@ -0,0 +1,13 @@
export function saveStateToLocalStorage(item, pathObject) {
localStorage.setItem(
item,
JSON.stringify(pathObject)
);
}
export function getStateFromLocalStorage(item) {
let data = localStorage.getItem(item);
if (data !== undefined) {
console.log(data);
}
};
+43
View File
@@ -0,0 +1,43 @@
import { useEffect } from 'react';
import { useMap } from 'react-leaflet';
import { useDispatch, useSelector } from 'react-redux';
import { useSetPosition as SetParticipantPosition, deselect } from '../store/participantSlice';
export default function MapEvents({ setPosition, mapPosition, setClickPosition }) {
const dispatch = useDispatch()
const map = useMap();
const currentParticipant = useSelector((state) => state.participants.currentParticipant)
// Set the positions
useEffect(() => {
if (map) {
const onMove = () => {
setPosition(map.getCenter());
};
const onClick = (e) => {
if (currentParticipant === "") {
return
}
setClickPosition(e.latlng);
dispatch(SetParticipantPosition({ equipmentNumber: currentParticipant.equipmentNumber, newPosition: [e.latlng.lat, e.latlng.lng], exerciseId: currentParticipant.exerciseId }))
dispatch(deselect({ currentParticipant: "" }))
};
map.on('move', onMove);
map.on('click', onClick);
return () => {
map.off('move', onMove);
map.off('click', onClick);
};
}
}, [dispatch, map, mapPosition, setClickPosition, setPosition, currentParticipant]);
return null;
}
+3
View File
@@ -0,0 +1,3 @@
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
@@ -0,0 +1,63 @@
import { useSetPosition as setPosition, walkingState } from "../store/participantSlice";
import { useDispatch } from "react-redux";
import { sleep } from "./sleep";
export function useCalculation() {
const dispatch = useDispatch();
return async function (latLngs, equipmentNumber, participant, walkMode) {
var speed = 0.00003;
var currentPosition = participant.currentPosition;
if (latLngs.length === 0) {
return;
}
while (localStorage.getItem(equipmentNumber.toString()) === "false") {
if (walkMode === "Patrouille") {
for (var i = 0; i < latLngs.length; i++) {
currentPosition = await moveToTarget(latLngs[i], currentPosition, speed, dispatch, equipmentNumber);
}
// Rückwärts durch den Pfad
for (i = latLngs.length - 1; i >= 0; i--) {
currentPosition = await moveToTarget(latLngs[i], currentPosition, speed, dispatch, equipmentNumber);
}
} else if (walkMode === "OTUP") {
for (i = 0; i < latLngs.length; i++) {
currentPosition = await moveToTarget(latLngs[i], currentPosition, speed, dispatch, equipmentNumber);
}
break;
} else if (walkMode === "Hold") {
break;
}
}
dispatch(walkingState({ equipmentNumber: equipmentNumber, isWalking: false, walkingMode: " " }))
}
}
async function moveToTarget(target, currentPosition, speed, dispatch, equipmentNumber) {
var targetLat = target.lat;
var targetLng = target.lng;
while (localStorage.getItem(equipmentNumber.toString()) === "false") {
var diffLat = targetLat - currentPosition[0];
var diffLng = targetLng - currentPosition[1];
var distance = Math.sqrt(diffLat * diffLat + diffLng * diffLng);
if (distance < speed) {
currentPosition = [targetLat, targetLng];
break;
}
// Berechne den Anteil der Bewegung in Richtung des Ziels
var ratio = speed / distance;
var moveLat = diffLat * ratio;
var moveLng = diffLng * ratio;
// Aktualisiere die aktuelle Position
currentPosition = [currentPosition[0] + moveLat, currentPosition[1] + moveLng];
dispatch(setPosition({ equipmentNumber: equipmentNumber, newPosition: currentPosition, exerciseId: 42 }));
// Warte eine Sekunde bevor die Position erneut aktualisiert wird
await sleep(300);
}
return currentPosition;
}
View File
Whitespace-only changes.
+42
View File
@@ -0,0 +1,42 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { configureStore } from "@reduxjs/toolkit";
import { Provider } from "react-redux";
import participantReducer from './store/participantSlice'
import mapReducer from './store/mapSlice'
import partyReducer from './store/partySlice'
import exerciseReducer from './store/exerciseSlice'
import layerReducer from './store/layerSlice'
const router = createBrowserRouter([
{
path: "/",
element: <App />,
}
]);
const store = configureStore({
reducer: {
participants: participantReducer,
map: mapReducer,
party: partyReducer,
exercise: exerciseReducer,
layer: layerReducer,
}
});
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
</React.StrictMode>
);
reportWebVitals();
+13
View File
@@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;
@@ -0,0 +1,25 @@
import { createSlice } from '@reduxjs/toolkit'
const excerciseId = createSlice({
name: "exercise",
initialState: {exerciseIds : [], currentExerciseId: 0, isError : false, errorMessage : "" },
reducers: {
setExcerciseId: (state, action) => {
for(var excerciseId in action.payload) {
state.exerciseIds[excerciseId] = action.payload[excerciseId]
}
},
setCurrentExerciseId: (state, action) => {
state.currentExerciseId = action.payload
},
exerciseError: (state, action) => {
state.isError = action.payload.isError;
state.errorMessage = action.payload.errorMessage;
}
}
})
export const { setExcerciseId, setCurrentExerciseId, exerciseError } = excerciseId.actions
export default excerciseId.reducer
+22
View File
@@ -0,0 +1,22 @@
import { createSlice } from "@reduxjs/toolkit";
const layerSlice = createSlice({
name: "layer",
initialState: [],
reducers: {
addLayers: (state, action) => {
state.push(action.payload)
},
deleteLayers: (state, action) => {
for(var layerIndex in state) {
if (state[layerIndex].participant === action.payload) {
state.splice(layerIndex, 1)
}
}
}
}
})
export const { addLayers, deleteLayers } = layerSlice.actions
export default layerSlice.reducer
+16
View File
@@ -0,0 +1,16 @@
import { createSlice } from "@reduxjs/toolkit";
const mapSlice = createSlice({
name: "map",
initialState: [53.0580229, 8.8972232],
reducers: {
changeMapPosition: (state, action) => {
state[0] = action.payload[0]
state[1] = action.payload[1]
}
}
})
export const { changeMapPosition } = mapSlice.actions
export default mapSlice.reducer
@@ -0,0 +1,86 @@
import { createSlice } from "@reduxjs/toolkit";
import { original } from 'immer'
import { useSendPositionChange } from "../websocket/sendPositionChange";
import { useChangeAbcProtection } from "../websocket/changeAbcProtection"
import { useDispatch as dispatch } from "react-redux";
const participantSlice = createSlice({
name: "participants",
initialState: { data: [], currentParticipant: "", currentParticipantWalkPathEq: 0, isError: false, errorMessage: "" },
reducers: {
useSetPosition: (state, action) => {
for (var i = 0; i < original(state.data).length; i++) {
if (original(state.data)[i].equipmentNumber === action.payload.equipmentNumber) {
state.data[i].currentPosition = action.payload.newPosition
}
}
useSendPositionChange(action.payload.equipmentNumber, action.payload.newPosition, action.payload.exerciseId ,dispatch)
},
useSetAbcProtection: (state, action) => {
var exerciseId = 0;
for (var i = 0; i < original(state.data).length; i++) {
if (original(state.data)[i].equipmentNumber === action.payload.equipmentNumber) {
state.data[i].abcProtection = action.payload.newAbcProtection
exerciseId = state.data[i].exerciseId;
}
}
useChangeAbcProtection(action.payload.equipmentNumber, action.payload.newAbcProtection, exerciseId, dispatch)
},
select: (state, action) => {
for (var i = 0; i < original(state.data).length; i++) {
if (original(state.data)[i].equipmentNumber === action.payload.participant.equipmentNumber) {
state.currentParticipant = action.payload.participant;
}
}
},
deselect: (state, action) => {
state.currentParticipant = "";
},
addParticipant: (state, action) => {
for (var i = 0; i < original(state.data).length; i++) {
if (original(state.data)[i].equipmentNumber === action.payload.equipmentNumber) {
return
}
}
state.data.push(action.payload)
},
setCurrentWalkParticipant: (state, action) => {
state.currentParticipantWalkPathEq = action.payload.currentParticipantWalkPathEq
},
setWalkingPath: (state, action) => {
for (var i = 0; i < original(state.data).length; i++) {
if (original(state.data)[i].equipmentNumber === action.payload.equipmentNumber) {
state.data[i].path = action.payload.path
}
}
},
walkingState: (state, action) => {
for (var i = 0; i < original(state.data).length; i++) {
if (original(state.data)[i].equipmentNumber === action.payload.equipmentNumber) {
state.data[i].isWalking = action.payload.isWalking
state.data[i].walkingMode = action.payload.walkingMode
}
}
},
participantError: (state, action) => {
state.isError = action.payload.isError;
state.errorMessage = action.payload.errorMessage;
},
}
})
export const {
useSetPosition,
useSetAbcProtection,
select,
deselect,
addParticipant,
setCurrentWalkParticipant,
setWalkingPath,
walkingState,
participantError,
} = participantSlice.actions
export default participantSlice.reducer
+33
View File
@@ -0,0 +1,33 @@
import { createSlice } from "@reduxjs/toolkit";
const partySlice = createSlice({
name: "party",
initialState: { partyData: [], isError: false, errorMessage: "" },
reducers: {
setPartyData: (state, action) => {
var found = false;
for(var partyObject of state.partyData) {
if (partyObject["party"] === action.payload["party"]) {
found = true;
}
}
if(!found) {
state.partyData.push(action.payload)
}
},
clearPartyData: (state, action) => {
state = action.payload
},
partyDataError: (state, action) => {
state.isError = action.payload.isError;
state.errorMessage = action.payload.errorMessage;
},
}
})
export const { setPartyData, clearPartyData, partyDataError } = partySlice.actions
export default partySlice.reducer
+38
View File
@@ -0,0 +1,38 @@
.popup-error {
position: absolute;
width: 40%;
height: 20px;
top: 5%;
left: 50%;
background-color: white;
color: red;
z-index: 3;
border-radius: 25px;
transform: translate(-50%, -50%);
padding: 20px;
text-align: center;
}
.popup-error-button {
position: absolute;
left: 95%;
border-radius: 25px;
border: 1px solid #007EC1;
background-color: #007EC1;
color: #ffffff;
padding: 5px 10px;
}
.popup-error-button:hover {
border: 1px solid #0073b1;
background-color: #0073b1;
color: #ffffff;
padding: 5px 10px;
}
.popup-error-button:disabled,
.popup-error-button[disabled]{
border: 1px solid #007EC1;
background-color: #007EC1;
color: #666666;
}
+121
View File
@@ -0,0 +1,121 @@
.leaflet-container {
position: relative;
width: 80%;
height: 100vh;
margin-right: 0;
margin-left: auto;
outline-style: none;
}
.leaflet-pane,
.leaflet-control,
.leaflet-top,
.leaflet-bottom {
z-index: 0 !important;
}
.map-position-center {
position: absolute;
right: 10px;
z-index: 1;
margin: 10px;
padding: 10px;
border-radius: 15px;
background-color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.map-position-click {
position: absolute;
top: 48px;
right: 10px;
z-index: 1;
margin: 10px;
padding: 10px;
border-radius: 15px;
background-color: white;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
@media only screen and (max-width: 1100px) {
.leaflet-container {
width: 60%;
height: 100vh;
}
}
/* Media queries for responsive design */
@media only screen and (max-width: 1024px) {
.leaflet-container {
width: 60%;
height: 100vh;
}
}
@media only screen and (max-width: 768px) {
.leaflet-container {
position: relative;
width: 100%;
height: 50vh;
}
.map-position-center {
font-size: 10px;
top: 50%;
right: 5px;
margin: 5px;
padding: 5px;
}
.map-position-click {
font-size: 10px;
top: 53%;
right: 5px;
margin: 5px;
padding: 5px;
}
}
@media only screen and (max-width: 480px) {
.leaflet-container {
position: relative;
width: 100%;
height: 50vh;
}
.map-position-center {
top: 50%;
right: 5px;
margin: 5px;
padding: 5px;
}
.map-position-click {
top: 53%;
right: 5px;
margin: 5px;
padding: 5px;
}
}
@media only screen and (max-width: 320px) {
.leaflet-container {
position: relative;
width: 100%;
height: 50vh;
}
.map-position-center {
top: 50%;
right: 5px;
margin: 5px;
padding: 5px;
}
.map-position-click {
top: 53%;
right: 5px;
margin: 5px;
padding: 5px;
}
}
@@ -0,0 +1,161 @@
.collapsible {
display: flex;
align-items: center;
background: #F4F4F4;
color: #AAAAAA;
cursor: pointer;
height: 36px;
width: 100%;
border: none;
text-align: left;
outline: none;
border-radius: 25px;
font-weight: 600;
font-size: 16px;
font-family: 'Inter';
font-style: normal;
line-height: 19px;
color: #AAAAAA;
padding-left: 20px;
margin-bottom: 5px;
}
.active,
.collapsible:hover {
background-color: #e4e4e4;
}
.content {
height: auto;
overflow: hidden;
background-color: #F4F4F4;
padding: 0 18px;
margin-bottom: 5px;
border-radius: 0px 0px 25px 25px;
font-size: 12px;
}
.collapsible-active {
display: flex;
align-items: center;
background: #F4F4F4;
color: #AAAAAA;
cursor: pointer;
height: 36px;
width: 100%;
border: none;
text-align: left;
outline: none;
border-radius: 25px 25px 0px 0px;
font-weight: 600;
font-size: 16px;
font-family: 'Inter';
font-style: normal;
line-height: 19px;
color: #AAAAAA;
flex-grow: 1;
padding-left: 20px;
}
.group-icon {
width: 16px;
height: 16px;
margin-right: 10px;
}
.participant-text {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
margin-right: 10px;
font-size: 12px;
color: #050505;
}
.participant-text-selected {
display: flex;
align-self: flex-end;
flex-direction: row;
justify-content: center;
font-size: 12px;
color: green;
}
p {
margin: 2px;
}
.participant-table {
width: 100%;
height: 100%;
}
h4 {
margin: 0px;
}
td {
height: 25px;
}
.collapsible-button-div {
width: 100%;
height: 50%;
border-radius: 25px;
z-index: 3;
display: flex;
justify-content: space-between;
align-items: center;
gap: 5px;
}
.collapsible-button {
display: inline-block;
width: 100%;
border-radius: 25px;
background: #007EC1;
border: none;
outline: none;
color: white;
font-size: 8px;
font-family: 'Cascadia';
font-weight: 600;
cursor: pointer;
margin-bottom: 5px;
padding: 5px;
}
.collapsible-button:hover {
background-color: #005a8c;
}
.choose-popup {
position: fixed;
width: 10%;
height: 8%;
top: 10%;
left: 50%;
background-color: white;
color: red;
z-index: 3;
border-radius: 25px;
transform: translate(-50%, -50%);
padding: 20px;
text-align: center;
}
.startingpath-popup {
position: fixed;
width: 10%;
height: auto;
top: 90%;
left: 50%;
background-color: white;
color: red;
z-index: 3;
border-radius: 25px;
transform: translate(-50%, -50%);
padding: 20px;
text-align: center;
}
+318
View File
@@ -0,0 +1,318 @@
.popup {
position: fixed;
width: 20%;
height: 25%;
top: 50%;
left: 50%;
background-color: white;
z-index: 3;
border-radius: 25px;
transform: translate(-50%, -50%);
padding: 20px;
}
.popup-blur {
position: absolute;
height: 100%;
width: 100%;
z-index: 2;
backdrop-filter: blur(3px);
}
.popup-text {
position: absolute;
top: 5%;
left: 50%;
transform: translate(-50%, 0);
font-size: 20px;
}
.select-party {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, 0%);
width: 12%;
height: 3%;
background: #D9D9D9;
opacity: 0.5;
border-radius: 25px;
z-index: 3;
}
.select-party-dropdown {
width: 35%;
height: 100%;
background: #D9D9D9;
opacity: 0.4;
box-shadow: 2px 0px 5px rgba(0, 0, 0, 0.5);
border-radius: 25px;
z-index: 4;
border: none;
appearance: none;
text-align: right;
}
.select-party-text {
position: absolute;
width: 42px;
height: 15px;
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-size: 12px;
line-height: 15px;
color: #000000;
top: 50%;
left: 10px;
transform: translateY(-50%);
z-index: 5;
}
.select-number-inputfield {
position: absolute;
width: 55%;
height: 80%;
margin-top: 1%;
left: 40%;
background-color: #D9D9D9;
border: none;
border-radius: 25px;
}
.select-number-inputfield:focus {
outline: none;
}
.select-party-form-select {
position: absolute;
width: 60%;
height: 100%;
left: 40%;
border: none;
background-color: #D9D9D9;
border-radius: 25px;
color: #000000;
}
.select-number {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, 0);
width: 12%;
height: 3%;
background: #D9D9D9;
opacity: 0.5;
border-radius: 25px;
z-index: 4;
}
.select-number-inputfield-div {
width: 35%;
height: 100%;
background: #D9D9D9;
opacity: 0.5;
box-shadow: 2px 0px 5px rgba(0, 0, 0, 0.5);
border-radius: 25px;
border: none;
text-align: right;
z-index: 4;
}
.select-number-text {
position: absolute;
width: 53px;
height: 15px;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-size: 12px;
line-height: 15px;
color: #000000;
z-index: 5;
}
.popup-buttons {
position: absolute;
width: 12%;
height: 32px;
left: 50%;
top: 55%;
transform: translate(-50%, 0%);
background: white;
border-radius: 25px;
z-index: 3;
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.popup-buttons button {
width: 45%;
height: 25px;
border-radius: 25px;
background: #007EC1;
border: none;
outline: none;
color: white;
font-size: 12px;
font-family: 'Cascadia';
cursor: pointer;
}
.popup-cancel {
border-radius: 25px;
}
.popup-submit {
border-radius: 25px;
}
.popup-buttons button:hover,
.popup-buttons button:focus {
background-color: #005a8c;
}
textarea:focus,
input:focus {
color: #000000;
}
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type=number] {
-moz-appearance: textfield;
appearance: textfield;
}
/* Media queries for responsive design */
@media only screen and (max-width: 1024px) {
.popup {
width: 40%;
height: 25%;
}
.select-party,
.select-number {
width: 20%;
height: 4%;
}
.popup-buttons {
width: 20%;
height: 15px;
}
}
@media only screen and (max-width: 768px) {
.popup {
width: 40%;
height: 25%;
}
.select-party,
.select-number {
width: 25%;
height: 5%;
}
.popup-buttons {
width: 25%;
height: 15px;
}
}
@media only screen and (max-width: 480px) {
.popup {
width: 50%;
height: 25%;
}
.select-party,
.select-number {
margin-top: 5px;
width: 35%;
height: 3%;
}
.popup-buttons {
width: 35%;
height: 15px;
}
}
@media only screen and (max-width: 320px) {
.popup {
width: 50%;
height: 25%;
}
.select-party,
.select-number {
width: 40%;
height: 3%;
}
.popup-buttons {
width: 40%;
height: 25px;
}
.popup-text {
font-size: 18px;
}
.select-party-text,
.select-number-text {
font-size: 10px;
}
.popup-buttons button {
font-size: 10px;
width: 30%;
height: 15px;
}
}
@media only screen and (max-height: 600px) {
.popup {
width: 50%;
height: 25%;
}
.select-party,
.select-number {
width: 40%;
height: 3%;
}
.popup-buttons {
width: 40%;
height: 25px;
}
.popup-text {
font-size: 18px;
}
.select-party-text,
.select-number-text {
font-size: 10px;
}
.popup-buttons button {
font-size: 10px;
}
}
+211
View File
@@ -0,0 +1,211 @@
.sidebar {
position: absolute;
width: 20%;
height: 100vh;
background: #FFFFFF;
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
display: flex;
flex-direction: column;
align-items: center;
}
.participant-list {
width: 80%;
height: 36px;
background: #F4F4F4;
border-radius: 25px;
display: flex;
align-items: center;
}
.participant-list-text {
font-weight: 600;
font-size: 90%;
font-family: 'Inter';
font-style: normal;
line-height: 19px;
color: #777777;
flex-grow: 1;
padding-left: 20px;
}
.addButton-div {
width: 15%;
height: 36px;
background: #007EC1;
box-shadow: -2px 1px 7.6px rgba(0, 0, 0, 0.25);
border-radius: 25px;
display: flex;
justify-content: center;
align-items: center;
}
.addButton {
color: white;
font-size: 16px;
display: inline-block;
}
.sidebar-content {
width: 80%;
height: 87%;
flex-direction: column;
align-items: center;
margin-top: 5px;
margin-bottom: 20px;
overflow-y: auto;
}
.logo {
margin-top: 20px;
margin-left: 0.5cm;
margin-right: 0.5cm;
width: 150px;
height: auto;
margin-bottom: 20px;
}
@media only screen and (max-width: 1100px) {
.sidebar {
width: 40%;
height: 100vh;
}
.participant-list,
.sidebar-content {
width: 90%;
}
.logo {
width: 120px;
}
}
@media only screen and (max-width: 1024px) {
.sidebar {
width: 40%;
height: 100vh;
}
.participant-list,
.sidebar-content {
width: 90%;
}
.logo {
width: 120px;
}
}
@media only screen and (max-width: 768px) {
.sidebar {
position: relative;
width: 100%;
height: 50vh;
}
.participant-list,
.sidebar-content {
width: 90%;
}
.logo {
width: 100px;
}
.addButton-div {
width: 10%;
}
}
@media only screen and (max-width: 480px) {
.sidebar {
position: relative;
width: 100%;
height: 50vh;
}
.participant-list,
.sidebar-content {
width: 90%;
}
.logo {
width: 80px;
}
.addButton-div {
width: 15%;
}
}
@media only screen and (max-width: 320px) {
.sidebar {
position: relative;
width: 100%;
height: 50vh;
}
.participant-list,
.sidebar-content {
width: 100%;
}
.logo {
width: 60px;
}
.addButton-div {
width: 20%;
}
.addButton {
font-size: 12px;
}
}
.sidebar-footer {
height: 3%;
align-content: space-between;
margin-bottom: 5px;
}
.sidebar-footer button {
margin-left: 5px;
margin-right: 5px;
height: 100%;
border-radius: 25px;
background: #007EC1;
border: none;
outline: none;
color: white;
font-size: 12px;
font-family: 'Cascadia';
cursor: pointer;
}
.sidebar-footer button:hover,
.sidebar-footer button:focus {
background-color: #005a8c;
}
.sidebar-startStop-container {
position: fixed;
width: 30%;
height: 7.5%;
background-color: #FFFFFF;
z-index: 5;
top: 5%;
left: 50%;
transform: translate(-50%, -50%);
border-radius: 25px;
display: flex;
justify-content: space-between;
align-items: center;
}
.sidebar-startStop-container button {
width: 30%;
height: 50%;
}
@@ -0,0 +1,28 @@
import { participantError } from "../store/participantSlice";
/**
* Verwendet einen WebSocket, um die ABC-Schutzinformationen eines Teilnehmers zu ändern.
*
* Diese Funktion stellt eine Verbindung zu einem WebSocket-Server her und sendet eine Anfrage,
* um die ABC-Schutzinformationen eines bestimmten Teilnehmers zu ändern. Nach dem Senden der Anfrage
* wird die Verbindung sofort geschlossen. Im Fehlerfall wird ein Fehlerzustand über Redux dispatcht.
*
* @param {string} equipmentNumber - Die Ausrüstungsnummer des Teilnehmers, dessen ABC-Schutzinformationen geändert werden sollen.
* @param {boolean} abcProtection - Der neue Zustand des ABC-Schutzes für den Teilnehmer (true für aktiviert, false für deaktiviert).
* @param {string} exerciseId - Die ID der Übung, zu der der Teilnehmer gehört.
* @param {function} dispatch - Die Redux Dispatch-Funktion, um den Fehlerzustand zu aktualisieren, falls ein Fehler auftritt.
*/
export function useChangeAbcProtection(equipmentNumber, abcProtection, exerciseId, dispatch) {
const socket = new WebSocket('ws://localhost:6969/participantHandler');
socket.onopen = function open() {
var req = JSON.stringify({
type: 'changeAbcProtection',
excerciseId: parseInt(exerciseId),
participantData: { "participantId": parseInt(equipmentNumber), "abcProtection": abcProtection }
})
socket.send(req);
socket.close()
};
socket.onerror = () => {
dispatch(participantError({ isError: true, errorMessage: "ERROR: There was an error with the participant websocket!" }))
};
}
@@ -0,0 +1,49 @@
import { addParticipant, participantError } from "../store/participantSlice";
/**
* Überprüft, ob ein Teilnehmer für eine bestimmte Übung gültig ist, indem eine WebSocket-Verbindung verwendet wird.
*
* Diese Funktion initiiert eine WebSocket-Verbindung zu einem Server und sendet eine Anfrage, um zu überprüfen,
* ob ein Teilnehmer für eine bestimmte Übung gültig ist. Die Anfrage enthält die Übungs-ID und die Teilnehmer-ID.
* Bei einem Fehler in der WebSocket-Verbindung wird ein Fehlerzustand über die `participantError`-Aktion im Redux-Store
* dispatcht. Nach dem Öffnen der Verbindung wird die Anfrage gesendet. Bei Erhalt einer Nachricht vom Server (die Antwort
* auf die Gültigkeitsprüfung) wird die Antwort verarbeitet. Die Informationen über die
* Zugehörigkeit des Teilnehmers beinhaltet die PartyId, die dann weiter verwendet werden können.
*
* @param {string} equipmentNumber - Die ID des Teilnehmers, dessen Gültigkeit überprüft werden soll.
* @param {string} exerciseId - Die ID der Übung, für die die Gültigkeit des Teilnehmers überprüft werden soll.
* @param {function} dispatch - Die Redux Dispatch-Funktion, die verwendet wird, um Aktionen im Redux-Store zu dispatchen.
* @param {Array<Object>} partyList - Eine Liste von Parteien, die in der Überprüfung verwendet wird, um Details zum Teilnehmer zu ermitteln.
* @param {string} _partyId - Eine vorläufige Partei-ID, die in der Funktion verwendet wird (der genaue Verwendungszweck ist aus dem Auszug nicht ersichtlich).
*/
export function useCheckIfParticipantIsValid(equipmentNumber, exerciseId, dispatch, partyList, _partyId) {
const socket = new WebSocket('ws://localhost:6969/participantHandler');
socket.onerror = (error) => {
dispatch(participantError({ isError: true, errorMessage: "ERROR: There was an error with the participant websocket!" }))
};
socket.onopen = function open() {
var req = JSON.stringify({
type: 'checkIfParticipantIsValid',
excerciseId: parseInt(exerciseId),
participantData: { "participantId": parseInt(equipmentNumber) }
})
socket.send(req);
};
socket.onmessage = function incoming(event) {
const participant = JSON.parse(event.data);
var partyName = ""
var partyId = ""
for(var party of partyList) {
if(party.id === participant.partyId) {
partyName = party.party;
partyId = party.id
}
}
if (participant.valid && partyId === parseInt(_partyId)) {
dispatch(addParticipant({ equipmentNumber: participant.equipmentNumber, currentPosition: [53.0580229, 8.8972232], abcProtection: false, type: participant.tacticalIdentifier, party: partyName, exerciseId: exerciseId, isWalking: false, walkingMode: "", path: []}));
} else {
dispatch(participantError({isError: true, errorMessage: "Participant is not valid! Group or Equipment Number could be wrong!"}))
}
socket.close();
};
}
@@ -0,0 +1,56 @@
import { addParticipant, participantError } from "../store/participantSlice";
let req = ""
export function getAllParticipants(exerciseId, dispatch, partyList) {
var socket = new WebSocket('ws://localhost:6969/participantHandler');
socket.onerror = (error) => {
dispatch(participantError({ isError: true, errorMessage: "ERROR: There was an error with the participant websocket!" }))
};
socket.onopen = function open() {
req = JSON.stringify(
{type: "getAllParticipants", excerciseId: 42}
)
socket.send(req);
};
socket.onmessage = function incoming(event) {
const participants = JSON.parse(event.data);
var blueCount = 0;
var redCount = 0;
socket.onmessage = () => {}
for(var participant of participants) {
var partyName = ""
for (var party of partyList) {
if (party.id === participant.partyId) {
partyName = party.party;
}
}
if (participant.valid) {
if(participant.partyId === 2 ) {
dispatch(addParticipant({ equipmentNumber: participant.equipmentNumber, currentPosition: [53.044 + 0.0005 * blueCount , 8.814], abcProtection: false, type: participant.tacticalIdentifier, party: partyName, exerciseId: exerciseId, isWalking: false, walkingMode: "", path: [] }));
req = JSON.stringify({
type: 'changePosition',
excerciseId: parseInt(exerciseId),
participantData: { "participantId": parseInt(participant.equipmentNumber), "positions": [{"lat": 53.044 + 0.0005 * blueCount, "lng": 8.814}] }
})
socket.send(req);
blueCount++;
} else if(participant.partyId === 3) {
dispatch(addParticipant({ equipmentNumber: participant.equipmentNumber, currentPosition: [53.044 + 0.0005 * redCount , 8.959], abcProtection: false, type: participant.tacticalIdentifier, party: partyName, exerciseId: exerciseId, isWalking: false, walkingMode: "", path: [] }));
req = JSON.stringify({
type: 'changePosition',
excerciseId: parseInt(exerciseId),
participantData: { "participantId": parseInt(participant.equipmentNumber), "positions": [{"lat": 53.044 + 0.0005 * redCount, "lng": 8.959}] }
})
socket.send(req);
redCount++
}
}
}
};
}
@@ -0,0 +1,30 @@
import { setExcerciseId, exerciseError } from "../store/exerciseSlice";
/**
* Verwendet einen WebSocket, um eine Liste von Übungs-IDs vom Server zu erhalten.
*
* Diese Funktion stellt eine Verbindung zu einem WebSocket-Server her und wartet auf eine Nachricht,
* die eine Liste von Übungs-IDs enthält. Sobald die Nachricht empfangen wird, wird die Liste der IDs
* in einem Redux-Store gespeichert, indem die `setExcerciseId`-Aktion dispatched wird. Nach dem Empfang
* der Daten wird die WebSocket-Verbindung geschlossen. Im Falle eines Fehlers beim Empfangen der Daten
* wird ein Fehlerzustand über die `exerciseError`-Aktion im Redux-Store gesetzt.
*
* @param {function} dispatch - Die Redux Dispatch-Funktion, die verwendet wird, um Aktionen im Redux-Store zu dispatchen.
*/
export function useGetExerciseIds(dispatch) {
const socket = new WebSocket('ws://localhost:6969/exerciseIds');
const exerciseIds = [];
socket.onmessage = function incoming(event) {
for (var exerciseId of JSON.parse(event.data)) {
exerciseIds.push(exerciseId);
}
dispatch(setExcerciseId(exerciseIds))
socket.close();
};
socket.onerror = () => {
dispatch(exerciseError({ isError: true, errorMessage: "ERROR: There was an error with the exerciseIds websocket!" }))
};
}
@@ -0,0 +1,32 @@
import { partyDataError, setPartyData } from '../store/partySlice.js';
/**
* Stellt eine WebSocket-Verbindung her, um Parteidaten basierend auf einer Übungs-ID zu erhalten.
*
* Diese Funktion initiiert eine WebSocket-Verbindung zu einem vorgegebenen Server und sendet eine Anfrage
* mit der Übungs-ID, um spezifische Partydaten zu erhalten. Sobald die Verbindung geöffnet ist, wird die
* Übungs-ID an den Server gesendet. Bei Empfang der Daten (über die `onmessage`-Ereignisbehandlung) werden
* die empfangenen Parteidaten durch Dispatchen der `setPartyData`-Aktion im Redux-Store gespeichert.
* Im Falle eines Verbindungsfehlers wird ein Fehlerzustand durch Dispatchen der `partyDataError`-Aktion
* im Redux-Store gesetzt. Nach erfolgreichem Empfang der Daten wird die WebSocket-Verbindung geschlossen.
*
* @param {string} exerciseId - Die ID der Übung, für die Parteidaten abgerufen werden sollen.
* @param {function} dispatch - Die Redux Dispatch-Funktion, die verwendet wird, um Aktionen im Redux-Store zu dispatchen.
*/
export function useGetPartyData(exerciseId, dispatch) {
const socket = new WebSocket('ws://localhost:6969/partyData');
socket.onopen = function open() {
socket.send(JSON.stringify({
excerciseId: parseInt(exerciseId),
}));
};
socket.onmessage = function incoming(event) {
for (var party of JSON.parse(event.data)) {
dispatch(setPartyData(party))
}
socket.close();
};
socket.onerror = (error) => {
dispatch(partyDataError({ isError: true, errorMessage: "ERROR: There was an error with the partyData websocket!" }))
};
}
@@ -0,0 +1,26 @@
/**
* Verwendet einen WebSocket, um eine Positionsänderung eines Teilnehmers zu senden.
*
* Diese Funktion stellt eine Verbindung zu einem WebSocket-Server her und sendet eine Anfrage,
* um die Position eines Teilnehmers zu aktualisieren. Die Anfrage enthält die Übungs-ID, die Ausrüstungsnummer
* des Teilnehmers und die neue geographische Position. Nach dem Senden der Anfrage wird die Verbindung
* sofort geschlossen. Im Fehlerfall wird ein Fehlerzustand über Redux dispatcht, um die Anwendung
* über das Problem zu informieren.
*
* @param {string} equipmentNumber - Die Ausrüstungsnummer des Teilnehmers, dessen Position geändert werden soll.
* @param {Array<number>} geoPosition - Ein Array mit zwei Elementen, wobei das erste Element die Breitengrad- und das zweite Element die Längengradkoordinate ist.
* @param {string} exerciseId - Die ID der Übung, zu der der Teilnehmer gehört.
* @param {function} dispatch - Die Redux Dispatch-Funktion, um den Fehlerzustand zu aktualisieren, falls ein Fehler auftritt.
*/
export function useSendPositionChange(equipmentNumber, geoPosition, exerciseId) {
const socket = new WebSocket('ws://localhost:6969/participantHandler');
socket.onopen = function open() {
var req = JSON.stringify({
type: 'changePosition',
excerciseId: parseInt(exerciseId),
participantData: { "participantId": parseInt(equipmentNumber), "positions": [{"lat" : geoPosition[0], "lng": geoPosition[1]}] }
})
socket.send(req);
socket.close()
};
}