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

Binary file not shown.
Binary file not shown.
+17
View File
@@ -0,0 +1,17 @@
module legatus-rme/backend
go 1.21.5
require github.com/gorilla/websocket v1.5.1
require (
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)
+22
View File
@@ -0,0 +1,22 @@
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+21
View File
@@ -0,0 +1,21 @@
package main
import (
serviceportsinformation "legatus-rme/backend/servicePortsInformation"
typedata "legatus-rme/backend/typeData"
"legatus-rme/backend/websocket"
)
func main() {
servicePortsInformationSocket := websocket.OpenWebsocket("servicePortsInformation", []int{7300}).(serviceportsinformation.ServicePortsInformation)
exerciseServicePortMap, exerciseIds := servicePortsInformationSocket.GetPorts()
typeDataSocket := websocket.OpenWebsocket("typeBaseData", []int{exerciseServicePortMap["0-ParticipantManagement1"]}).(typedata.TypeData)
typeData := typeDataSocket.GetTypeData()
websocket.OpenWebsocketServer(websocket.Server{
ExerciseIds: exerciseIds,
ExerciseServicePortMap: exerciseServicePortMap,
TypeData: typeData,
})
}
@@ -0,0 +1,37 @@
package participanthandler
import "time"
// ChangeAbcProtection ändert den ABC-Schutzstatus eines Geräts.
// equipmentNumber: Die Nummer des Geräts, dessen ABC-Schutzstatus geändert werden soll.
// abcProtection: Der neue Status des ABC-Schutzes (true für aktiv, false für inaktiv).
// Rückgabewert: Gibt einen Fehler zurück, falls die Änderung fehlschlägt.
func (c *ParticipantHandler) ChangeAbcProtection(equipmentNumber int, abcProtection bool) error {
// Erstellen einer Anfrage zur Änderung des ABC-Schutzstatus
req := AbcProtectionChangeRequest{
Data: struct {
Header struct {
EquipmentNumber int `json:"equipmentNumber"`
TimeStamp int `json:"timeStamp"`
} `json:"header"`
NbcProtectionActive bool `json:"nbcProtectionActive"`
}{
Header: struct {
EquipmentNumber int `json:"equipmentNumber"`
TimeStamp int `json:"timeStamp"`
}{
EquipmentNumber: equipmentNumber,
TimeStamp: int(time.Now().UnixMilli()),
},
NbcProtectionActive: abcProtection,
},
TypeID: 7,
}
// Senden der Anfrage über den MariusSocket
if err := c.MariusSocket.WriteJSON(req); err != nil {
return err
}
return nil
}
@@ -0,0 +1,62 @@
package participanthandler
import (
"log"
"math"
"time"
)
// degreesToRadians konvertiert Grad in Radiant.
// degrees: Der Winkel in Grad.
// Rückgabewert: Der Winkel in Radiant.
func degreesToRadians(degrees float64) float64 {
return degrees * (math.Pi / 180)
}
// ChangePosition ändert die Position eines Geräts.
// equipmentNumber: Die Nummer des Geräts, dessen Position geändert werden soll.
// geoPosition: Die neue geografische Position des Geräts.
// Rückgabewert: Gibt einen Fehler zurück, falls die Änderung fehlschlägt.
func (c *ParticipantHandler) ChangePosition(equipmentNumber int, geoPosition *GeoPosition) error {
log.Println(equipmentNumber, geoPosition)
// Erstellen einer Anfrage zur Änderung der Position
req := PositionChangeRequest{
Data: struct {
Header struct {
EquipmentNumber int `json:"equipmentNumber"`
TimeStamp int `json:"timeStamp"`
} `json:"header"`
GeoPosition struct {
Lon float64 `json:"lon"`
Lat float64 `json:"lat"`
Z float64 `json:"z"`
} `json:"geoPosition"`
}{
Header: struct {
EquipmentNumber int `json:"equipmentNumber"`
TimeStamp int `json:"timeStamp"`
}{
EquipmentNumber: equipmentNumber,
TimeStamp: int(time.Now().UnixMilli()),
},
GeoPosition: struct {
Lon float64 `json:"lon"`
Lat float64 `json:"lat"`
Z float64 `json:"z"`
}{
Lon: degreesToRadians(geoPosition.Lng),
Lat: degreesToRadians(geoPosition.Lat),
Z: 0.0,
},
},
TypeID: 21,
}
// Senden der Anfrage über den MariusSocket
if err := c.MariusSocket.WriteJSON(req); err != nil {
return err
}
return nil
}
@@ -0,0 +1,55 @@
package participanthandler
import "log"
// CheckIfParticipantIsValid überprüft, ob ein Teilnehmer gültig ist.
// equipmentNumber: Die Nummer des Geräts, dessen Gültigkeit überprüft werden soll.
// Rückgabewert: Gibt eine Struktur zurück, die die Gültigkeit des Teilnehmers und seine Daten enthält.
func (c *ParticipantHandler) CheckIfParticipantIsValid(equipmentNumber int) CheckIfParticipantIsValidReturn {
// Erstellen einer Anfrage zur Überprüfung der Teilnehmergültigkeit
req := CheckIfParticipantIsValidRequest{Type: "baseDlRequest", UUID: "UUID"}
// Senden der Anfrage über das ParticipantManagement
c.ParticipantManagement.WriteJSON(req)
var res CheckIfParticipantIsValidResponse
for {
// Lesen der Antwort vom ParticipantManagement
err := c.ParticipantManagement.ReadJSON(&res)
if err != nil {
log.Fatal("CheckIfParticipantIsValidRequest - Error getting participant data:", err)
}
// Überprüfen, ob die Antwort den erwarteten Typ hat
if res.Type == "participantBaseData" {
break
}
}
var participantData *CheckIfParticipantIsValidReturn
// Durchlaufen der Teilnehmerdaten, um den passenden Teilnehmer zu finden
for _, participant := range res.Data {
if participant.EquipmentNumber == equipmentNumber {
participantData = &CheckIfParticipantIsValidReturn{
TacticalIdentifier: participant.TacticalIdentifier,
Designator: participant.Designator,
EquipmentNumber: participant.EquipmentNumber,
Valid: true,
PartyId: participant.PartyID,
}
}
}
// Falls kein passender Teilnehmer gefunden wurde, wird eine ungültige Struktur zurückgegeben
if participantData == nil {
participantData = &CheckIfParticipantIsValidReturn{
TacticalIdentifier: "",
Designator: "",
EquipmentNumber: 0,
Valid: false,
PartyId: 0,
}
}
return *participantData
}
@@ -0,0 +1,43 @@
package participanthandler
import "log"
func (c *ParticipantHandler) GetAllParticipants() []*CheckIfParticipantIsValidReturn {
// Erstellen einer Anfrage zur Überprüfung der Teilnehmergültigkeit
req := CheckIfParticipantIsValidRequest{Type: "baseDlRequest", UUID: "UUID"}
// Senden der Anfrage über das ParticipantManagement
c.ParticipantManagement.WriteJSON(req)
var res CheckIfParticipantIsValidResponse
for {
// Lesen der Antwort vom ParticipantManagement
err := c.ParticipantManagement.ReadJSON(&res)
if err != nil {
log.Fatal("CheckIfParticipantIsValidRequest - Error getting participant data:", err)
}
// Überprüfen, ob die Antwort den erwarteten Typ hat
if res.Type == "participantBaseData" {
break
}
}
var participantData []*CheckIfParticipantIsValidReturn
// Durchlaufen der Teilnehmerdaten, um den passenden Teilnehmer zu finden
for _, participant := range res.Data {
if participant.PartyID == 2 || participant.PartyID == 3 || participant.PartyID == 4 {
if participant.EquipmentNumber != 0 {
participantData = append(participantData, &CheckIfParticipantIsValidReturn{
TacticalIdentifier: participant.TacticalIdentifier,
Designator: participant.Designator,
EquipmentNumber: participant.EquipmentNumber,
Valid: true,
PartyId: participant.PartyID,
})
}
}
}
return participantData
}
@@ -0,0 +1,85 @@
package participanthandler
import (
"github.com/gorilla/websocket"
)
// ParticipantHandler verwaltet die Verbindungen und Anfragen für Teilnehmer.
type ParticipantHandler struct {
ParticipantManagement *websocket.Conn // Verbindung zum Participant Management
MariusSocket *websocket.Conn // Verbindung zum Marius Socket
}
// CheckIfParticipantIsValidRequest repräsentiert eine Anfrage zur Überprüfung der Gültigkeit eines Teilnehmers.
type CheckIfParticipantIsValidRequest struct {
Type string `json:"type"` // Typ der Anfrage
UUID string `json:"uuid"` // Eindeutige Identifikationsnummer der Anfrage
}
// CheckIfParticipantIsValidResponse repräsentiert die Antwort auf eine Anfrage zur Überprüfung der Gültigkeit eines Teilnehmers.
type CheckIfParticipantIsValidResponse struct {
Data []struct {
AssignState int `json:"assignState"` // Zuweisungsstatus
Designator string `json:"designator"` // Designator des Teilnehmers
EquipmentNumber int `json:"equipmentNumber"` // Gerätenummer
ExternalID int `json:"externalId"` // Externe ID
HierarchyNumber int `json:"hierarchyNumber"` // Hierarchienummer
ID int `json:"id"` // ID des Teilnehmers
Identification string `json:"identification"` // Identifikation
Info string `json:"info"` // Zusätzliche Informationen
IsDeleted bool `json:"isDeleted"` // Löschstatus
IsSimulated bool `json:"isSimulated"` // Simulationsstatus
Name string `json:"name"` // Name des Teilnehmers
PartyID int `json:"partyId"` // Party ID
Properties string `json:"properties"` // Eigenschaften
SuperiorID int `json:"superiorId"` // ID des Vorgesetzten
Symbol string `json:"symbol"` // Symbol
TacticalIdentifier string `json:"tacticalIdentifier"` // Taktischer Identifikator
TypeID int `json:"typeId"` // Typ ID
} `json:"data"` // Liste der Teilnehmerdaten
Type string `json:"type"` // Typ der Antwort
UUID string `json:"uuid"` // Eindeutige Identifikationsnummer der Antwort
}
// CheckIfParticipantIsValidReturn repräsentiert die Rückgabewerte der Überprüfung der Gültigkeit eines Teilnehmers.
type CheckIfParticipantIsValidReturn struct {
TacticalIdentifier string `json:"tacticalIdentifier"` // Taktischer Identifikator
Designator string `json:"designator"` // Designator des Teilnehmers
EquipmentNumber int `json:"equipmentNumber"` // Gerätenummer
Valid bool `json:"valid"` // Gültigkeitsstatus
PartyId int `json:"partyId"` // Party ID
}
// AbcProtectionChangeRequest repräsentiert eine Anfrage zur Änderung des ABC-Schutzstatus.
type AbcProtectionChangeRequest struct {
Data struct {
Header struct {
EquipmentNumber int `json:"equipmentNumber"` // Gerätenummer
TimeStamp int `json:"timeStamp"` // Zeitstempel
} `json:"header"`
NbcProtectionActive bool `json:"nbcProtectionActive"` // Status des ABC-Schutzes
} `json:"data"`
TypeID int `json:"typeId"` // Typ ID der Anfrage
}
// GeoPosition repräsentiert eine geografische Position.
type GeoPosition struct {
Lat float64 // Breitengrad
Lng float64 // Längengrad
}
// PositionChangeRequest repräsentiert eine Anfrage zur Änderung der Position eines Geräts.
type PositionChangeRequest struct {
Data struct {
Header struct {
EquipmentNumber int `json:"equipmentNumber"` // Gerätenummer
TimeStamp int `json:"timeStamp"` // Zeitstempel
} `json:"header"`
GeoPosition struct {
Lon float64 `json:"lon"` // Längengrad in Radiant
Lat float64 `json:"lat"` // Breitengrad in Radiant
Z float64 `json:"z"` // Höhe (standardmäßig 0.0)
} `json:"geoPosition"`
} `json:"data"`
TypeID int `json:"typeId"` // Typ ID der Anfrage
}
+27
View File
@@ -0,0 +1,27 @@
package partydata
import (
"log"
)
func (c *PartyData) GetPartyData() PartyDataResponse {
req := PartyDataRequest{Type: "baseDlRequest", UUID: "UUID"}
c.Websocket.WriteJSON(req)
var res PartyDataResponse
for {
err := c.Websocket.ReadJSON(&res)
if err != nil {
log.Fatal("PartyDataRequest - Error getting party data:", err)
}
if res.Type == "partyBaseData" {
break
}
}
c.Websocket.Close()
return res
}
+28
View File
@@ -0,0 +1,28 @@
package partydata
import (
"github.com/gorilla/websocket"
)
type PartyData struct {
Websocket *websocket.Conn
}
type PartyDataRequest struct {
Type string `json:"type"`
UUID string `json:"uuid"`
}
type PartyDataResponse struct {
Data []struct {
Color int `json:"color"`
FadingColor int `json:"fadingColor"`
HlaForceIdentifier int `json:"hlaForceIdentifier"`
ID int `json:"id"`
IsUsedBy bool `json:"isUsedBy"`
Name string `json:"name"`
RoleID int `json:"roleId"`
} `json:"data"`
Type string `json:"type"`
UUID string `json:"uuid"`
}
@@ -0,0 +1,37 @@
package serviceportsinformation
import (
"fmt"
"log"
)
func (c *ServicePortsInformation) GetPorts() (map[string]int, []int) {
req := ServicePortsInformationRequest{Type: "servicePortsInformationRequest", UUID: "UUID"}
c.Websocket.WriteJSON(req)
var res ServicePortsInformationResponse
err := c.Websocket.ReadJSON(&res)
if err != nil {
log.Fatal("ServicePortsInformationRequest - Error getting service ports information:", err)
}
var exerciseServicePortMap map[string]int = make(map[string]int)
for _, exercise := range res.Data {
for _, service := range exercise.Services {
for _, port := range service.Ports {
log.Printf("%d-%s%d\n", exercise.ExerciseID, service.Name, port.TypeID)
exerciseServicePortMap[fmt.Sprintf("%d-%s%d", exercise.ExerciseID, service.Name, port.TypeID)] = port.Number
}
}
}
var excerciseIds []int
for _, exercise := range res.Data {
excerciseIds = append(excerciseIds, exercise.ExerciseID)
}
c.Websocket.Close()
return exerciseServicePortMap, excerciseIds
}
@@ -0,0 +1,32 @@
package serviceportsinformation
import (
"github.com/gorilla/websocket"
)
type ServicePortsInformation struct {
Websocket *websocket.Conn
}
type ServicePortsInformationRequest struct {
Type string `json:"type"`
UUID string `json:"uuid"`
}
type ServicePortsInformationResponse struct {
Data []struct {
ExerciseID int `json:"exerciseId"`
ReplayPartitionID int `json:"replayPartitionId"`
Services []struct {
Name string `json:"name"`
NameAbbrevation string `json:"nameAbbrevation"`
Ports []struct {
Number int `json:"number"`
Properties string `json:"properties"`
TypeID int `json:"typeId"`
} `json:"ports"`
} `json:"services"`
} `json:"data"`
Type string `json:"type"`
UUID string `json:"uuid"`
}
+27
View File
@@ -0,0 +1,27 @@
package typedata
import (
"log"
)
func (c *TypeData) GetTypeData() TypeDataResponse {
req := TypeDataRequest{Type: "baseDlRequest", UUID: "UUID"}
c.Websocket.WriteJSON(req)
var res TypeDataResponse
for {
err := c.Websocket.ReadJSON(&res)
if err != nil {
log.Fatal("TypeDataRequest - Error getting type data:", err)
}
if res.Type == "typeBaseData" {
break
}
}
c.Websocket.Close()
return res
}
+31
View File
@@ -0,0 +1,31 @@
package typedata
import (
"github.com/gorilla/websocket"
)
type TypeData struct {
Websocket *websocket.Conn
}
type TypeDataRequest struct {
Type string `json:"type"`
UUID string `json:"uuid"`
}
type TypeDataResponse struct {
Data []struct {
DisEntities string `json:"disEntities"`
HierarchyLevel int `json:"hierarchyLevel"`
ID int `json:"id"`
IsDeleted bool `json:"isDeleted"`
Name string `json:"name"`
Properties string `json:"properties"`
RadioCount int `json:"radioCount"`
Symbol string `json:"symbol"`
TypeClassID int `json:"typeClassId"`
EquipmentTypeID int `json:"equipmentTypeId,omitempty"`
} `json:"data"`
Type string `json:"type"`
UUID string `json:"uuid"`
}
+17
View File
@@ -0,0 +1,17 @@
package websocket
import (
"log"
"net/http"
)
func (s *Server) exerciseIds(w http.ResponseWriter, r *http.Request) {
c, err := s.Upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Websocket Server - Error upgrading connection:", err)
return
}
defer c.Close()
c.WriteJSON(&s.ExerciseIds)
}
+41
View File
@@ -0,0 +1,41 @@
package websocket
import (
"fmt"
participanthandler "legatus-rme/backend/participantHandler"
partydata "legatus-rme/backend/partyData"
serviceportsinformation "legatus-rme/backend/servicePortsInformation"
typedata "legatus-rme/backend/typeData"
"log"
"net/url"
"github.com/gorilla/websocket"
)
func OpenWebsocket(websocketType string, port []int) interface{} {
var sockets []*websocket.Conn
for _, port := range port {
u := url.URL{Scheme: "ws", Host: fmt.Sprintf("localhost:%d", port)}
socket, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
log.Fatal("Error connecting to websocket:", err)
}
sockets = append(sockets, socket)
}
switch websocketType {
case "servicePortsInformation":
return serviceportsinformation.ServicePortsInformation{Websocket: sockets[0]}
case "typeBaseData":
return typedata.TypeData{Websocket: sockets[0]}
case "partyBaseData":
return partydata.PartyData{Websocket: sockets[0]}
case "participantHandler":
return participanthandler.ParticipantHandler{ParticipantManagement: sockets[0], MariusSocket: sockets[1]}
default:
log.Fatal("Invalid websocket type")
return nil
}
}
@@ -0,0 +1,20 @@
package websocket
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
func OpenWebsocketServer(s Server) {
s.Upgrader = &websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
http.HandleFunc("/ports", s.ports)
http.HandleFunc("/exerciseIds", s.exerciseIds)
http.HandleFunc("/partyData", s.partyData)
http.HandleFunc("/participantHandler", s.participantHandler)
log.Fatal("Websocket Server - Error while serving websockt server:", http.ListenAndServe("localhost:6969", nil))
}
@@ -0,0 +1,85 @@
package websocket
import (
"fmt"
participanthandler "legatus-rme/backend/participantHandler"
"log"
"net/http"
"github.com/go-playground/validator/v10"
)
func (s *Server) participantHandler(w http.ResponseWriter, r *http.Request) {
c, err := s.Upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Websocket Server - Error upgrading connection:", err)
return
}
defer c.Close()
for {
var req ParticipantHandlerWebsocketRequestWithoutData
if err := c.ReadJSON(&req); err != nil {
c.WriteJSON(&ErrorWebsocketResponse{
Error: err.Error(),
})
return
}
if req.Type != "getAllParticipants" {
req2 := ParticipantHandlerWebsocketRequest(req)
err = validator.New().Struct(req2)
if err != nil {
c.WriteJSON(&ErrorWebsocketResponse{
Error: err.Error(),
})
return
}
} else {
err = validator.New().Struct(req)
if err != nil {
c.WriteJSON(&ErrorWebsocketResponse{
Error: err.Error(),
})
return
}
}
var (
participantHandlerByExcersiseId participanthandler.ParticipantHandler
ok bool
)
if participantHandlerByExcersiseId, ok = s.ParticipantHandlers[req.ExcerciseId]; !ok {
participantHandlerByExcersiseId = OpenWebsocket("participantHandler", []int{s.ExerciseServicePortMap[fmt.Sprintf("%d-ParticipantManagement1", req.ExcerciseId)], req.ExcerciseId * 102}).(participanthandler.ParticipantHandler)
}
switch req.Type {
case "checkIfParticipantIsValid":
participantData := participantHandlerByExcersiseId.CheckIfParticipantIsValid(req.ParticipantData.ParticipantId)
c.WriteJSON(&participantData)
continue
case "getAllParticipants":
participantData := participantHandlerByExcersiseId.GetAllParticipants()
c.WriteJSON(&participantData)
continue
case "changeAbcProtection":
if err := participantHandlerByExcersiseId.ChangeAbcProtection(req.ParticipantData.ParticipantId, req.ParticipantData.AbcProtection); err != nil {
c.WriteJSON(&ErrorWebsocketResponse{
Error: err.Error(),
})
}
c.WriteJSON(req.ParticipantData)
continue
case "changePosition":
if err := participantHandlerByExcersiseId.ChangePosition(req.ParticipantData.ParticipantId, &participanthandler.GeoPosition{
Lat: req.ParticipantData.Positions[0].Lat,
Lng: req.ParticipantData.Positions[0].Lng,
}); err != nil {
c.WriteJSON(&ErrorWebsocketResponse{
Error: err.Error(),
})
}
continue
}
}
}
+68
View File
@@ -0,0 +1,68 @@
package websocket
import (
"fmt"
partydata "legatus-rme/backend/partyData"
"log"
"net/http"
"slices"
"github.com/go-playground/validator/v10"
)
func paintIt(color int) string {
hex := fmt.Sprintf("%06x", color+16777216)
return "#" + hex
}
func (s *Server) partyData(w http.ResponseWriter, r *http.Request) {
c, err := s.Upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Websocket Server - Error upgrading connection:", err)
return
}
defer c.Close()
for {
var req PartyDataWebsocketRequest
if err := c.ReadJSON(&req); err != nil {
c.WriteJSON(&ErrorWebsocketResponse{
Error: err.Error(),
})
}
err = validator.New().Struct(req)
if err != nil {
c.WriteJSON(&ErrorWebsocketResponse{
Error: err.Error(),
})
}
if slices.Contains(s.ExerciseIds, req.ExcerciseId) {
partyDataSocket := OpenWebsocket("partyBaseData", []int{s.ExerciseServicePortMap[fmt.Sprintf("%d-ParticipantManagement1", req.ExcerciseId)]}).(partydata.PartyData)
partyData := partyDataSocket.GetPartyData()
var res []PartyDataWebsocketResponse
for _, party := range partyData.Data {
if party.ID == 0 || party.ID == 1 {
continue
}
colorInHex := paintIt(party.Color)
res = append(res, PartyDataWebsocketResponse{
Id: party.ID,
Party: party.Name,
Color: colorInHex,
})
}
c.WriteJSON(&res)
} else {
c.WriteJSON(&ErrorWebsocketResponse{
Error: "Invalid exerciseId",
})
continue
}
}
}
+17
View File
@@ -0,0 +1,17 @@
package websocket
import (
"log"
"net/http"
)
func (s *Server) ports(w http.ResponseWriter, r *http.Request) {
c, err := s.Upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Websocket Server - Error upgrading connection:", err)
return
}
defer c.Close()
c.WriteJSON(&s.ExerciseServicePortMap)
}
+56
View File
@@ -0,0 +1,56 @@
package websocket
import (
participanthandler "legatus-rme/backend/participantHandler"
typedata "legatus-rme/backend/typeData"
"github.com/gorilla/websocket"
)
type Server struct {
ExerciseIds []int
ExerciseServicePortMap map[string]int
TypeData typedata.TypeDataResponse
ParticipantHandlers map[int]participanthandler.ParticipantHandler
Upgrader *websocket.Upgrader
}
type PartyDataWebsocketResponse struct {
Id int `json:"id"`
Party string `json:"party"`
Color string `json:"color"`
}
type PartyDataWebsocketRequest struct {
ExcerciseId int `json:"excerciseId" validate:"required"`
}
type ParticipantHandlerWebsocketRequestWithoutData struct {
Type string `json:"type" validate:"required"`
ExcerciseId int `json:"excerciseId" validate:"required"`
ParticipantData struct {
ParticipantId int `json:"participantId"`
Positions []struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
} `json:"positions"`
AbcProtection bool `json:"abcProtection"`
} `json:"participantData"`
}
type ParticipantHandlerWebsocketRequest struct {
Type string `json:"type" validate:"required"`
ExcerciseId int `json:"excerciseId" validate:"required"`
ParticipantData struct {
ParticipantId int `json:"participantId" validate:"required"`
Positions []struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
} `json:"positions"`
AbcProtection bool `json:"abcProtection"`
} `json:"participantData" validate:"required"`
}
type ErrorWebsocketResponse struct {
Error string `json:"error"`
}
+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()
};
}