feat: added laser code change, target move, and delete

Note: deleted lasers are not removed from table and keep being drawn. Also added a cooler looking server page
This commit is contained in:
Davide Passoni 2025-01-30 16:20:31 +01:00
parent cc902aec04
commit 9525982161
19 changed files with 844 additions and 218 deletions

View File

@ -432,10 +432,10 @@ private:
};
/* Shine a laser with a specific code */
class Laser : public Command
class FireLaser : public Command
{
public:
Laser(unsigned int ID, unsigned int code, Coords destination, function<void(void)> callback = []() {}) :
FireLaser(unsigned int ID, unsigned int code, Coords destination, function<void(void)> callback = []() {}) :
Command(callback),
ID(ID),
destination(destination),
@ -453,10 +453,10 @@ private:
};
/* Shine a infrared light */
class Infrared : public Command
class FireInfrared : public Command
{
public:
Infrared(unsigned int ID, Coords destination, function<void(void)> callback = []() {}) :
FireInfrared(unsigned int ID, Coords destination, function<void(void)> callback = []() {}) :
Command(callback),
ID(ID),
destination(destination)
@ -470,3 +470,58 @@ private:
const unsigned int ID;
const Coords destination;
};
/* Change a laser code */
class SetLaserCode : public Command
{
public:
SetLaserCode(unsigned int spotID, unsigned int code, function<void(void)> callback = []() {}) :
Command(callback),
spotID(spotID),
code(code)
{
priority = CommandPriority::LOW;
};
virtual string getString();
virtual unsigned int getLoad() { return 5; }
private:
const unsigned int spotID;
const unsigned int code;
};
/* Delete a spot code */
class DeleteSpot : public Command
{
public:
DeleteSpot(unsigned int spotID, function<void(void)> callback = []() {}) :
Command(callback),
spotID(spotID)
{
priority = CommandPriority::LOW;
};
virtual string getString();
virtual unsigned int getLoad() { return 5; }
private:
const unsigned int spotID;
};
/* Move spot to a new target */
class MoveSpot : public Command
{
public:
MoveSpot(unsigned int spotID, Coords destination, function<void(void)> callback = []() {}) :
Command(callback),
spotID(spotID),
destination(destination)
{
priority = CommandPriority::LOW;
};
virtual string getString();
virtual unsigned int getLoad() { return 5; }
private:
const unsigned int spotID;
const Coords destination;
};

View File

@ -259,8 +259,8 @@ string Explosion::getString()
return commandSS.str();
}
/* Laser command */
string Laser::getString()
/* FireLaser command */
string FireLaser::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
@ -272,8 +272,8 @@ string Laser::getString()
return commandSS.str();
}
/* Infrared command */
string Infrared::getString()
/* FireInfrared command */
string FireInfrared::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
@ -282,4 +282,37 @@ string Infrared::getString()
<< destination.lat << ", "
<< destination.lng;
return commandSS.str();
}
/* SetLaserCode command */
string SetLaserCode::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
commandSS << "Olympus.setLaserCode, "
<< spotID << ", "
<< code;
return commandSS.str();
}
/* MoveSpot command */
string MoveSpot::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
commandSS << "Olympus.moveSpot, "
<< spotID << ", "
<< destination.lat << ", "
<< destination.lng;
return commandSS.str();
}
/* DeleteSpot command */
string DeleteSpot::getString()
{
std::ostringstream commandSS;
commandSS.precision(10);
commandSS << "Olympus.deleteSpot, "
<< spotID;
return commandSS.str();
}

View File

@ -705,9 +705,9 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
Coords loc; loc.lat = lat; loc.lng = lng;
unsigned int code = value[L"code"].as_integer();
log("Adding laser with code " + to_string(code) + " from unit " + unit->getUnitName() + " to (" + to_string(lat) + ", " + to_string(lng) + ")");
log("Firing laser with code " + to_string(code) + " from unit " + unit->getUnitName() + " to (" + to_string(lat) + ", " + to_string(lng) + ")");
command = dynamic_cast<Command*>(new Laser(ID, code, loc));
command = dynamic_cast<Command*>(new FireLaser(ID, code, loc));
}
}
/************************/
@ -719,13 +719,41 @@ void Scheduler::handleRequest(string key, json::value value, string username, js
double lat = value[L"location"][L"lat"].as_double();
double lng = value[L"location"][L"lng"].as_double();
Coords loc; loc.lat = lat; loc.lng = lng;
log("Adding infrared from unit " + unit->getUnitName() + " to (" + to_string(lat) + ", " + to_string(lng) + ")");
command = dynamic_cast<Command*>(new Infrared(ID, loc));
log("Firing infrared from unit " + unit->getUnitName() + " to (" + to_string(lat) + ", " + to_string(lng) + ")");
command = dynamic_cast<Command*>(new FireInfrared(ID, loc));
}
}
/************************/
else if (key.compare("setLaserCode") == 0)
{
unsigned int spotID = value[L"spotID"].as_integer();
unsigned int code = value[L"code"].as_integer();
log("Setting laser code " + to_string(code) + " to spot with ID " + to_string(spotID));
command = dynamic_cast<Command*>(new SetLaserCode(spotID, code));
}
/************************/
else if (key.compare("moveSpot") == 0)
{
unsigned int spotID = value[L"spotID"].as_integer();
double lat = value[L"location"][L"lat"].as_double();
double lng = value[L"location"][L"lng"].as_double();
Coords loc; loc.lat = lat; loc.lng = lng;
log("Moving spot with ID " + to_string(spotID) + " to (" + to_string(lat) + ", " + to_string(lng) + ")");
command = dynamic_cast<Command*>(new MoveSpot(spotID, loc));
}
/************************/
else if (key.compare("deleteSpot") == 0)
{
unsigned int spotID = value[L"spotID"].as_integer();
log("Deleting spot with ID " + to_string(spotID));
command = dynamic_cast<Command*>(new DeleteSpot(spotID));
}
/************************/
else if (key.compare("setCommandModeOptions") == 0)
{
setCommandModeOptions(value);

View File

@ -9,17 +9,32 @@
"preview": "vite preview"
},
"dependencies": {
"chart.js": "^4.4.7",
"react-chartjs-2": "^5.3.0",
"react-circular-progressbar": "^2.1.0",
"react-clock": "^5.1.0"
},
"devDependencies": {
"@eslint/js": "^9.6.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@tanem/svg-injector": "^10.1.68",
"@turf/clusters": "^7.1.0",
"@turf/turf": "^6.5.0",
"@types/dom-webcodecs": "^0.1.12",
"@types/leaflet": "^1.9.8",
"@types/node": "^22.5.1",
"@types/react": "^18.2.66",
"@types/react-dom": "^18.2.22",
"@types/react-leaflet": "^3.0.0",
"@types/turf": "^3.5.32",
"@typescript-eslint/parser": "^7.14.1",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.19",
"buffer": "^6.0.3",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
@ -28,26 +43,6 @@
"eslint-plugin-react-refresh": "^0.4.6",
"eslint-plugin-readable-tailwind": "^1.5.2",
"globals": "^15.7.0",
"postcss": "^8.4.38",
"prettier": "^3.3.2",
"tailwindcss": "^3.4.3",
"typescript-eslint": "^7.14.1",
"vite": "^5.2.0",
"vite-plugin-externals": "^0.6.2",
"vite-plugin-file": "^1.0.5",
"web-audio-peak-meter": "^3.1.0",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/free-brands-svg-icons": "^6.5.2",
"@fortawesome/free-regular-svg-icons": "^6.6.0",
"@fortawesome/free-solid-svg-icons": "^6.5.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@tanem/svg-injector": "^10.1.68",
"@turf/turf": "^6.5.0",
"@types/dom-webcodecs": "^0.1.12",
"@types/leaflet": "^1.9.8",
"@types/react-leaflet": "^3.0.0",
"@types/turf": "^3.5.32",
"buffer": "^6.0.3",
"js-sha256": "^0.11.0",
"jsstore": "^4.8.2",
"leaflet": "^1.9.4",
@ -55,11 +50,19 @@
"leaflet-path-drag": "^1.9.5",
"magvar": "^1.1.5",
"opus-decoder": "^0.7.6",
"postcss": "^8.4.38",
"prettier": "^3.3.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-icons": "^5.0.1",
"react-leaflet": "^4.2.1",
"tailwindcss": "^3.4.3",
"turf": "^3.0.14",
"usng": "^0.3.0"
"typescript-eslint": "^7.14.1",
"usng": "^0.3.0",
"vite": "^5.2.0",
"vite-plugin-externals": "^0.6.2",
"vite-plugin-file": "^1.0.5",
"web-audio-peak-meter": "^3.1.0"
}
}

View File

@ -496,6 +496,7 @@ export const DELETE_CYCLE_TIME = 0.05;
export const DELETE_SLOW_THRESHOLD = 50;
export const GROUPING_ZOOM_TRANSITION = 13;
export const SPOTS_EDIT_ZOOM_TRANSITION = 14;
export const MAX_SHOTS_SCATTER = 3;
export const MAX_SHOTS_INTENSITY = 3;

View File

@ -69,7 +69,7 @@ import { SmokeMarker } from "./markers/smokemarker";
/* Register the handler for the box selection */
L.Map.addInitHook("addHandler", "boxSelect", BoxSelect);
initDraggablePath(L); // TODO: breaks app when compiled
initDraggablePath(L);
export class Map extends L.Map {
/* Options */

View File

@ -1,21 +0,0 @@
import { DivIcon, LatLng } from "leaflet";
import { CustomMarker } from "./custommarker";
export class DestinationPreviewHandle extends CustomMarker {
constructor(latlng: LatLng) {
super(latlng, { interactive: true, draggable: true });
}
createIcon() {
this.setIcon(
new DivIcon({
iconSize: [18, 18],
iconAnchor: [9, 9],
className: "leaflet-destination-preview-handle-marker",
})
);
var el = document.createElement("div");
el.classList.add("ol-destination-preview-handle-icon");
this.getElement()?.appendChild(el);
}
}

View File

@ -1,4 +1,4 @@
import { Marker, LatLng, DivIcon, DomEvent, Map } from "leaflet";
import { Marker, LatLng, DivIcon, Map } from "leaflet";
export class SpotEditMarker extends Marker {
#textValue: string;
@ -6,14 +6,28 @@ export class SpotEditMarker extends Marker {
#rotationAngle: number; // Rotation angle in radians
#previousValue: string;
constructor(latlng: LatLng, textValue: string, rotationAngle: number = 0) {
onValueUpdated: (value: number) => void = () => {};
onDeleteButtonClicked: () => void = () => {};
/**
* Constructor for SpotEditMarker
* @param {LatLng} latlng - The geographical position of the marker.
* @param {string} textValue - The initial text value to display.
* @param {number} rotationAngle - The initial rotation angle in radians.
*/
constructor(latlng: LatLng, textValue: string, rotationAngle: number = 0, type: string) {
super(latlng, {
icon: new DivIcon({
className: "leaflet-spot-input-marker",
html: `<div class="container">
html:
type === "laser"
? `<div class="container">
<input class="input"/>
<div class="text">${textValue}</div>
<div class="delete">X</div>
</div>`
: `<div class="container">
<div class="delete">X</div>
</div>`,
}),
});
@ -23,21 +37,34 @@ export class SpotEditMarker extends Marker {
this.#previousValue = textValue;
}
/**
* Called when the marker is added to the map.
* @param {Map} map - The map instance.
* @returns {this} - The current instance of SpotEditMarker.
*/
onAdd(map: Map): this {
super.onAdd(map);
const element = this.getElement();
if (element) {
const text = element.querySelector(".text");
const button = element.querySelector(".delete");
const container = element.querySelector(".container") as HTMLDivElement;
const button = element.querySelector(".delete") as HTMLDivElement;
const input = element.querySelector(".input") as HTMLInputElement;
// Add click event listener to toggle edit mode
text?.addEventListener("mousedown", (ev) => this.#toggleEditMode(ev));
text?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
text?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
container?.addEventListener("mousedown", (ev) => {
ev.stopPropagation();
ev.preventDefault();
this.#setEditMode(ev, !this.#isEditable);
});
container?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
container?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
// Add click event listener to delete spot
button?.addEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
button?.addEventListener("mousedown", (ev) => {
ev.stopPropagation();
ev.preventDefault();
this.#onButtonClicked(ev);
});
button?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
button?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
@ -45,46 +72,62 @@ export class SpotEditMarker extends Marker {
input?.addEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
input?.addEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
input?.addEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
input?.addEventListener("blur", (ev) => this.#toggleEditMode(ev));
input?.addEventListener("keydown", (ev) => this.#acceptInput(ev));
input?.addEventListener("blur", (ev) => this.#setEditMode(ev, false));
input?.addEventListener("keydown", (ev) => this.#onKeyDown(ev));
input?.addEventListener("input", (ev) => this.#validateInput(ev));
}
return this;
}
/**
* Called when the marker is removed from the map.
* @param {Map} map - The map instance.
* @returns {this} - The current instance of SpotEditMarker.
*/
onRemove(map: Map): this {
super.onRemove(map);
const element = this.getElement();
if (element) {
const text = element.querySelector(".text");
const button = element.querySelector(".delete");
const input = element.querySelector(".input");
const container = element.querySelector(".container") as HTMLDivElement;
const button = element.querySelector(".delete") as HTMLDivElement;
const input = element.querySelector(".input") as HTMLInputElement;
// Add click event listener to toggle edit mode
text?.removeEventListener("mousedown", (ev) => this.#toggleEditMode(ev));
text?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
text?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
// Remove click event listener to toggle edit mode
container?.removeEventListener("mousedown", (ev) => {
ev.stopPropagation();
ev.preventDefault();
this.#setEditMode(ev, !this.#isEditable);
});
container?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
container?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
// Add click event listener to delete spot
button?.removeEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
// Remove click event listener to delete spot
button?.removeEventListener("mousedown", (ev) => {
ev.stopPropagation();
ev.preventDefault();
this.#onButtonClicked(ev);
});
button?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
button?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
// Add click event listener to input spot
// Remove click event listener to input spot
input?.removeEventListener("mousedown", (ev) => this.#stopEventPropagation(ev));
input?.removeEventListener("mouseup", (ev) => this.#stopEventPropagation(ev));
input?.removeEventListener("dblclick", (ev) => this.#stopEventPropagation(ev));
input?.removeEventListener("blur", (ev) => this.#toggleEditMode(ev));
input?.removeEventListener("keydown", (ev) => this.#acceptInput(ev));
input?.removeEventListener("blur", (ev) => this.#setEditMode(ev, false));
input?.removeEventListener("keydown", (ev) => this.#onKeyDown(ev));
input?.removeEventListener("input", (ev) => this.#validateInput(ev));
}
return this;
}
// Method to set the text value
/**
* Sets the text value of the marker.
* @param {string} textValue - The new text value.
*/
setTextValue(textValue: string) {
this.#textValue = textValue;
const element = this.getElement();
@ -94,12 +137,18 @@ export class SpotEditMarker extends Marker {
}
}
// Method to get the text value
/**
* Gets the text value of the marker.
* @returns {string} - The current text value.
*/
getTextValue() {
return this.#textValue;
}
// Method to set the rotation angle in radians
/**
* Sets the rotation angle of the marker in radians.
* @param {number} angle - The new rotation angle in radians.
*/
setRotationAngle(angle: number) {
this.#rotationAngle = angle;
if (!this.#isEditable) {
@ -107,12 +156,17 @@ export class SpotEditMarker extends Marker {
}
}
// Method to get the rotation angle in radians
/**
* Gets the rotation angle of the marker in radians.
* @returns {number} - The current rotation angle in radians.
*/
getRotationAngle() {
return this.#rotationAngle;
}
// Method to update the rotation angle to ensure the text is always readable
/**
* Updates the rotation angle to ensure the text is always readable.
*/
#updateRotation() {
const element = this.getElement();
if (element) {
@ -129,11 +183,12 @@ export class SpotEditMarker extends Marker {
}
}
#toggleEditMode(ev) {
this.#isEditable = !this.#isEditable;
ev.stopPropagation();
ev.preventDefault();
/**
* Toggles the edit mode of the marker.
* @param {Event} ev - The event object.
*/
#setEditMode(ev: Event, editable: boolean) {
this.#isEditable = editable;
const element = this.getElement();
if (element) {
@ -167,23 +222,43 @@ export class SpotEditMarker extends Marker {
}
}
#stopEventPropagation(ev) {
/**
* Stops the event propagation.
* @param {Event} ev - The event object.
*/
#stopEventPropagation(ev: Event) {
ev.stopPropagation();
}
#validateInput(ev) {
/**
* Validates the input value.
* @param {Event} ev - The event object.
*/
#validateInput(ev: Event) {
const element = this.getElement();
if (element) {
const input = ev.target as HTMLInputElement;
const text = element.querySelector(".text");
// Validate the input value
// Validate the input value (must be a valid laser code)
const value = input.value;
// Check if the value is a partial valid input
// Conditions for partial validity:
// 1. The first digit is 1.
// 2. The first digit is 1 and the second digit is between 1 and 7.
// 3. The first digit is 1, the second digit is between 1 and 7, and the third digit is between 1 and 8.
// 4. The first digit is 1, the second digit is between 1 and 7, the third digit is between 1 and 8, and the fourth digit is between 1 and 8.
const isPartialValid = /^[1]$|^[1][1-7]$|^[1][1-7][1-8]$|^[1][1-7][1-8][1-8]$/.test(value);
// Check if the value is a complete valid input
// Conditions for complete validity:
// 1. The input is a four-digit number where:
// - The first digit is 1.
// - The second digit is between 1 and 7.
// - The third digit is between 1 and 8.
// - The fourth digit is between 1 and 8.
// 2. The number is between 1111 and 1788.
const isValid = /^[1][1-7][1-8][1-8]$/.test(value) && Number(value) <= 1788;
if (isPartialValid || isValid) {
@ -197,6 +272,9 @@ export class SpotEditMarker extends Marker {
}
}
/**
* Handles invalid input by causing a small red flash around the input element.
*/
#errorFunction() {
const element = this.getElement();
if (element) {
@ -210,7 +288,59 @@ export class SpotEditMarker extends Marker {
}
}
/**
* Handles the key down event.
* @param {Event} ev - The keyboard event object.
*/
#onKeyDown(ev) {
if (ev.key === "Enter") this.#acceptInput(ev);
else if (ev.key === "Escape") this.#setEditMode(ev, false);
}
/**
* Accepts the input value when the Enter key is pressed.
* @param {Event} ev - The keyboard event object.
*/
#acceptInput(ev) {
if (ev.key === "Enter") this.#toggleEditMode(ev);
const element = this.getElement();
if (element) {
const input = element.querySelector(".input") as HTMLInputElement;
if (input) {
// Validate the input value (must be a valid laser code)
const value = Number(input.value);
if (value >= 1111 && value <= 1788) {
this.#setEditMode(ev, false);
this.onValueUpdated(value);
this.#successFunction();
} else {
this.#errorFunction();
}
}
}
}
/**
* Handles the button click event.
* @param {Event} ev - The event object.
*/
#onButtonClicked(ev: Event) {
if (this.#isEditable) this.#acceptInput(ev);
else this.onDeleteButtonClicked();
}
/**
* Handles valid input by causing a green flash around the container.
*/
#successFunction() {
const element = this.getElement();
if (element) {
const container = element.querySelector(".container") as HTMLDivElement;
if (container) {
container.classList.add("success-flash");
setTimeout(() => {
container.classList.remove("success-flash");
}, 900); // Duration of the flash effect (3 flashes, 300ms each)
}
}
}
}

View File

@ -4,7 +4,8 @@ import { CustomMarker } from "./custommarker";
export class SpotMarker extends CustomMarker {
constructor(latlng: LatLngExpression, options?: MarkerOptions) {
super(latlng, options);
this.options.interactive = false;
this.options.interactive = true;
this.options.draggable = true;
this.setZIndexOffset(9999);
}

View File

@ -1,3 +1,4 @@
/* Container for the spot input marker */
.leaflet-spot-input-marker {
text-align: center;
display: flex !important;
@ -5,6 +6,7 @@
align-items: center;
}
/* Delete button styles */
.leaflet-spot-input-marker .delete {
background-color: darkred;
color: #fffd;
@ -17,10 +19,12 @@
align-content: center;
}
/* Delete button hover effect */
.leaflet-spot-input-marker .delete:hover {
background-color: lightcoral;
}
/* Container for the spot input marker content */
.leaflet-spot-input-marker .container {
width: fit-content;
display: flex;
@ -31,14 +35,17 @@
font-size: 13px;
column-gap: 6px;
align-content: center;
border: 2px solid transparent;
}
/* Text inside the spot input marker */
.leaflet-spot-input-marker .text {
margin-left: 12px;
margin-top: auto;
margin-bottom: auto;
}
/* Input field inside the spot input marker */
.leaflet-spot-input-marker .input {
display: none;
color: white;
@ -50,25 +57,32 @@
outline: none; /* Remove default outline */
font-size: 13px;
width: 80px;
border-top-left-radius: 999px;
border-bottom-left-radius: 999px;
}
/* Input field focus effect */
.leaflet-spot-input-marker .input:focus {
border-color: #777; /* Border color on focus */
box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); /* Shadow on focus */
}
/* Hide input field inside the container */
.leaflet-spot-input-marker .container .input {
display: none;
}
/* Hide text in edit mode */
.leaflet-spot-input-marker .container .text.edit-mode {
display: none;
}
/* Error flash animation for input field */
.leaflet-spot-input-marker .input.error-flash {
animation: error-flash 0.3s;
}
/* Keyframes for error flash animation */
@keyframes error-flash {
0% {
border-color: #555;
@ -81,10 +95,27 @@
}
}
/* Edit mode styles for delete button */
.leaflet-spot-input-marker .delete.edit-mode {
background-color: green;
}
/* Edit mode hover effect for delete button */
.leaflet-spot-input-marker .delete.edit-mode:hover {
background-color: lightgreen;
}
/* Success flash animation for container */
.leaflet-spot-input-marker .container.success-flash {
animation: success-flash 0.3s 3;
}
/* Keyframes for success flash animation */
@keyframes success-flash {
0%, 100% {
border-color: transparent;
}
33%, 66% {
border-color: green;
}
}

View File

@ -70,11 +70,26 @@ export class MissionManager {
updateSpots(data: SpotsData) {
for (let idx in data.spots) {
const spotID = Number(idx);
const spot = data.spots[idx];
if (this.#spots[spotID] === undefined) {
const spot = data.spots[idx];
this.#spots[spotID] = new Spot(spotID, spot.type, new LatLng(spot.targetPosition.lat, spot.targetPosition.lng), spot.sourceUnitID, spot.code);
} else {
if (spot.type === "laser")
this.#spots[spotID].setCode(spot.code ?? 0)
this.#spots[spotID].setTargetPosition( new LatLng(spot.targetPosition.lat, spot.targetPosition.lng));
}
}
/* Iterate the existing spots and remove all spots that where deleted */
for (let idx in this.#spots) {
if (data.spots[idx] === undefined) {
delete this.#spots[idx];
}
}
}
getSpotByID(spotID: number) {
return this.#spots[spotID];
}
/** Update airbase information

View File

@ -179,7 +179,7 @@ export class OlympusApp {
const urlParams = new URLSearchParams(window.location.search);
const server = urlParams.get("server");
if (server == null) {
if (!server) {
this.setState(OlympusState.IDLE);
/* If no profile exists already with that name, create it from scratch from the defaults */
if (this.getProfile() === null) this.saveProfile();
@ -342,20 +342,20 @@ export class OlympusApp {
}
startServerMode() {
ConfigLoadedEvent.on((config) => {
this.getAudioManager().start();
Object.values(config.controllers).forEach((controllerOptions) => {
if (controllerOptions.type.toLowerCase() === "awacs") {
this.getControllerManager().addController(
new AWACSController(
{ frequency: controllerOptions.frequency, modulation: controllerOptions.modulation },
controllerOptions.coalition,
controllerOptions.callsign
)
);
}
});
});
//ConfigLoadedEvent.on((config) => {
// this.getAudioManager().start();
//
// Object.values(config.controllers).forEach((controllerOptions) => {
// if (controllerOptions.type.toLowerCase() === "awacs") {
// this.getControllerManager().addController(
// new AWACSController(
// { frequency: controllerOptions.frequency, modulation: controllerOptions.modulation },
// controllerOptions.coalition,
// controllerOptions.callsign
// )
// );
// }
// });
//});
}
}

View File

@ -14,7 +14,18 @@ import {
emissionsCountermeasures,
reactionsToThreat,
} from "../constants/constants";
import { AirbasesData, BullseyesData, CommandModeOptions, GeneralSettings, MissionData, Radio, ServerRequestOptions, ServerStatus, SpotsData, TACAN } from "../interfaces";
import {
AirbasesData,
BullseyesData,
CommandModeOptions,
GeneralSettings,
MissionData,
Radio,
ServerRequestOptions,
ServerStatus,
SpotsData,
TACAN,
} from "../interfaces";
import { MapOptionsChangedEvent, ServerStatusUpdatedEvent, WrongCredentialsEvent } from "../events";
export class ServerManager {
@ -40,13 +51,15 @@ export class ServerManager {
this.#lastUpdateTimes[BULLSEYE_URI] = Date.now();
this.#lastUpdateTimes[MISSION_URI] = Date.now();
getApp().getShortcutManager().addShortcut("togglePause", {
label: "Pause data update",
keyUpCallback: () => {
this.setPaused(!this.getPaused());
},
code: "Enter"
})
getApp()
.getShortcutManager()
.addShortcut("togglePause", {
label: "Pause data update",
keyUpCallback: () => {
this.setPaused(!this.getPaused());
},
code: "Enter",
});
MapOptionsChangedEvent.on((mapOptions) => {
/* TODO if (this.#updateMode === "normal" && mapOptions.AWACSMode) {
@ -56,7 +69,7 @@ export class ServerManager {
this.#updateMode = "normal";
this.startUpdate();
} */
})
});
}
setUsername(newUsername: string) {
@ -117,8 +130,10 @@ export class ServerManager {
if (xmlHttp.responseType == "arraybuffer") this.#lastUpdateTimes[uri] = callback(xmlHttp.response);
else {
/* Check if the response headers contain the enabled command modes and set them */
if (xmlHttp.getResponseHeader("X-Enabled-Command-Modes"))
getApp().getMissionManager().setEnabledCommandModes(xmlHttp.getResponseHeader("X-Enabled-Command-Modes")?.split(",") ??[])
if (xmlHttp.getResponseHeader("X-Enabled-Command-Modes"))
getApp()
.getMissionManager()
.setEnabledCommandModes(xmlHttp.getResponseHeader("X-Enabled-Command-Modes")?.split(",") ?? []);
const result = JSON.parse(xmlHttp.responseText);
this.#lastUpdateTimes[uri] = callback(result);
@ -534,14 +549,29 @@ export class ServerManager {
this.PUT(data, callback);
}
setCommandModeOptions(
commandModeOptions: CommandModeOptions,
callback: CallableFunction = () => {}
) {
setCommandModeOptions(commandModeOptions: CommandModeOptions, callback: CallableFunction = () => {}) {
var data = { setCommandModeOptions: commandModeOptions };
this.PUT(data, callback);
}
setLaserCode(spotID: number, code: number, callback: CallableFunction = () => {}) {
var command = { spotID: spotID, code: code };
var data = { setLaserCode: command };
this.PUT(data, callback);
}
moveSpot(spotID: number, latlng: LatLng, callback: CallableFunction = () => {}) {
var command = { spotID: spotID, location: latlng };
var data = { moveSpot: command };
this.PUT(data, callback);
}
deleteSpot(spotID: number, callback: CallableFunction = () => {}) {
var command = { spotID: spotID };
var data = { deleteSpot: command };
this.PUT(data, callback);
}
reloadDatabases(callback: CallableFunction = () => {}) {
var data = { reloadDatabases: {} };
this.PUT(data, callback);
@ -615,41 +645,44 @@ export class ServerManager {
);
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer, false);
return time;
}, false);
}
}, this.#updateMode === "normal"? 250: 2000)
);
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getWeapons((buffer: ArrayBuffer) => {
var time = getApp().getWeaponsManager()?.update(buffer, false);
return time;
}, false);
}
}, this.#updateMode === "normal"? 250: 2000)
window.setInterval(
() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer, false);
return time;
}, false);
}
},
this.#updateMode === "normal" ? 250 : 2000
)
);
this.#intervals.push(
window.setInterval(
() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer, true);
this.getWeapons((buffer: ArrayBuffer) => {
var time = getApp().getWeaponsManager()?.update(buffer, false);
return time;
}, true);
}, false);
}
},
5000
this.#updateMode === "normal" ? 250 : 2000
)
);
this.#intervals.push(
window.setInterval(() => {
if (!this.getPaused() && getApp().getMissionManager().getCommandModeOptions().commandMode != NONE) {
this.getUnits((buffer: ArrayBuffer) => {
var time = getApp().getUnitsManager()?.update(buffer, true);
return time;
}, true);
}
}, 5000)
);
// Mission clock and elapsed time
this.#intervals.push(
window.setInterval(() => {

View File

@ -4,12 +4,26 @@ import { ServerStatus } from "../interfaces";
import { FaCheck, FaXmark } from "react-icons/fa6";
import { zeroAppend } from "../other/utils";
import { colors } from "../constants/constants";
import { CircularProgressbar, buildStyles } from "react-circular-progressbar";
import "react-circular-progressbar/dist/styles.css";
import { Line } from "react-chartjs-2";
import { Chart, LineElement, LinearScale, Title, CategoryScale, Legend, PointElement } from "chart.js";
Chart.register(LineElement, LinearScale, Title, CategoryScale, Legend, PointElement);
export function ServerOverlay() {
const [serverStatus, setServerStatus] = useState({} as ServerStatus);
const [loadData, setLoadData] = useState<number[]>([]);
const [frameRateData, setFrameRateData] = useState<number[]>([]);
const [timeLabels, setTimeLabels] = useState<string[]>([]);
useEffect(() => {
ServerStatusUpdatedEvent.on((status) => setServerStatus(status));
ServerStatusUpdatedEvent.on((status) => {
setServerStatus(status);
setLoadData((prevData) => [...prevData, status.load].slice(-300));
setFrameRateData((prevData) => [...prevData, status.frameRate].slice(-300));
setTimeLabels((prevLabels) => [...prevLabels, new Date().toLocaleTimeString()].slice(-300));
});
}, []);
let loadColor = colors.OLYMPUS_GREEN;
@ -20,9 +34,9 @@ export function ServerOverlay() {
if (serverStatus.frameRate < 30) frameRateColor = colors.OLYMPUS_RED;
else if (serverStatus.frameRate >= 30 && serverStatus.frameRate < 60) frameRateColor = colors.OLYMPUS_ORANGE;
const MThours = serverStatus.missionTime? serverStatus.missionTime.h: 0;
const MTminutes = serverStatus.missionTime? serverStatus.missionTime.m: 0;
const MTseconds = serverStatus.missionTime? serverStatus.missionTime.s: 0;
const MThours = serverStatus.missionTime ? serverStatus.missionTime.h : 0;
const MTminutes = serverStatus.missionTime ? serverStatus.missionTime.m : 0;
const MTseconds = serverStatus.missionTime ? serverStatus.missionTime.s : 0;
const EThours = Math.floor((serverStatus.elapsedTime ?? 0) / 3600);
const ETminutes = Math.floor((serverStatus.elapsedTime ?? 0) / 60) % 60;
@ -31,43 +45,136 @@ export function ServerOverlay() {
let MTtimeString = `${zeroAppend(MThours, 2)}:${zeroAppend(MTminutes, 2)}:${zeroAppend(MTseconds, 2)}`;
let ETtimeString = `${zeroAppend(EThours, 2)}:${zeroAppend(ETminutes, 2)}:${zeroAppend(ETseconds, 2)}`;
const missionTime = new Date();
missionTime.setHours(MThours);
missionTime.setMinutes(MTminutes);
missionTime.setSeconds(MTseconds);
const data = {
labels: timeLabels,
datasets: [
{
label: "Server Load",
data: loadData,
borderColor: colors.LIGHT_BLUE,
borderWidth: 2,
fill: false,
pointRadius: 0,
yAxisID: "y-load", // Specify the y-axis for load
},
{
label: "Server Framerate",
data: frameRateData,
borderColor: colors.WHITE,
borderWidth: 2,
fill: false,
pointRadius: 0,
yAxisID: "y-framerate", // Specify the y-axis for framerate
},
],
};
const options = {
animation: false,
responsive: true,
scales: {
x: {
type: "category",
labels: timeLabels,
},
"y-framerate": {
type: "linear",
position: "left",
beginAtZero: true,
max: 120, // Max value for framerate
},
"y-load": {
type: "linear",
position: "right",
beginAtZero: true,
max: 2000, // Max value for load
grid: {
drawOnChartArea: false, // Only want the grid lines for one axis to show up
},
},
},
plugins: {
legend: {
display: true,
position: "top",
},
},
};
return (
<div
className={`
absolute left-0 top-0 z-50 h-full w-full flex-col bg-olympus-900 p-5
absolute left-0 top-0 z-50 flex h-full w-full flex-col bg-black
bg-opacity-80 p-5 backdrop-blur-sm animate-fadeIn
`}
>
<div className="flex-col content-center">
<h2 className="mb-10 text-3xl font-bold text-white">DCS Olympus server</h2>
<div className="flex flex-col">
<div className="flex gap-5 text-white">
<div className="w-64">Connected to DCS:</div>
<div>{serverStatus.connected? <FaCheck className={`
text-xl text-green-500
`}/> : <FaXmark className={`text-xl text-red-500`}/>}</div>
<div
className={`
m-auto flex w-3/4 max-w-4xl flex-col items-center rounded-lg bg-white
bg-opacity-10 p-5 shadow-lg animate-slideIn
`}
>
<h2 className="mb-5 text-4xl font-bold text-white drop-shadow-lg">DCS Olympus Server</h2>
<div className="flex w-full gap-12">
<div className="flex w-72 flex-col gap-4 text-lg text-white">
<div className="flex justify-between">
<div className="font-semibold">Connected to DCS:</div>
<div>{serverStatus.connected ? <FaCheck className={`
text-2xl text-green-500
`} /> : <FaXmark className={`text-2xl text-red-500`} />}</div>
</div>
<div className="flex justify-between">
<div className="font-semibold">Elapsed time:</div>
<div>{ETtimeString}</div>
</div>
<div className="flex justify-between">
<div className="font-semibold">Mission local time:</div>
<div>{MTtimeString}</div>
</div>
</div>
<div className="flex gap-5 text-white">
<div className="w-64">Server load:</div>
<div style={{color: loadColor}}>{serverStatus.load}</div>
</div>
<div className="flex gap-5 text-white">
<div className="w-64">Server framerate:</div>
<div style={{color: frameRateColor}}>{serverStatus.frameRate} fps</div>
</div>
<div className="flex gap-5 text-white">
<div className="w-64">Elapsed time:</div>
<div>{ETtimeString}</div>
</div>
<div className="flex gap-5 text-white">
<div className="w-64">Mission local time:</div>
<div>{MTtimeString}</div>
<div className="flex items-center justify-between gap-5 text-white">
<div className="flex flex-col items-center">
<div className="mb-2 font-semibold">Load</div>
<div className="h-24 w-24">
<CircularProgressbar
value={serverStatus.load}
maxValue={2000}
text={`${serverStatus.load}`}
styles={buildStyles({
textColor: loadColor,
pathColor: loadColor,
trailColor: "rgba(255, 255, 255, 0.2)",
})}
/>
</div>
</div>
<div className="flex flex-col items-center">
<div className="mb-2 font-semibold">Framerate</div>
<div className="h-24 w-24">
<CircularProgressbar
value={serverStatus.frameRate}
maxValue={120}
text={`${serverStatus.frameRate} fps`}
styles={buildStyles({
textColor: frameRateColor,
pathColor: frameRateColor,
trailColor: "rgba(255, 255, 255, 0.2)",
})}
/>
</div>
</div>
</div>
</div>
<div className="mt-10 w-full flex-1">
{/* @ts-ignore */}
<Line data={data} options={options} />
</div>
</div>
<img src="images/olympus-500x500.png" className={`
absolute right-4 top-4 ml-auto flex h-24
`}></img>
</div>
);
}

View File

@ -121,4 +121,32 @@ input[type="range"]:focus::-moz-range-thumb {
100% {
width: 200px;
}
}
}
.bouncing-ball {
position: relative;
width: 100px;
height: 100px;
background-color: transparent;
border-radius: 50%;
overflow: hidden;
animation: bounce 2s infinite;
}
.ball-logo {
width: 100%;
height: 100%;
object-fit: cover;
}
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-150px);
}
60% {
transform: translateY(-75px);
}
}

View File

@ -8,13 +8,7 @@ import { MainMenu } from "./panels/mainmenu";
import { SideBar } from "./panels/sidebar";
import { OptionsMenu } from "./panels/optionsmenu";
import { MapHiddenTypes, MapOptions } from "../types/types";
import {
NO_SUBSTATE,
OlympusState,
OlympusSubState,
OptionsSubstate,
UnitControlSubState
} from "../constants/constants";
import { NO_SUBSTATE, OlympusState, OlympusSubState, OptionsSubstate, UnitControlSubState } from "../constants/constants";
import { getApp, setupApp } from "../olympusapp";
import { LoginModal } from "./modals/loginmodal";
@ -30,7 +24,7 @@ import { ProtectionPromptModal } from "./modals/protectionpromptmodal";
import { KeybindModal } from "./modals/keybindmodal";
import { UnitExplosionMenu } from "./panels/unitexplosionmenu";
import { JTACMenu } from "./panels/jtacmenu";
import { AppStateChangedEvent } from "../events";
import { AppStateChangedEvent, ServerStatusUpdatedEvent } from "../events";
import { GameMasterMenu } from "./panels/gamemastermenu";
import { InfoBar } from "./panels/infobar";
import { HotGroupBar } from "./panels/hotgroupsbar";
@ -41,6 +35,7 @@ import { AWACSMenu } from "./panels/awacsmenu";
import { ServerOverlay } from "./serveroverlay";
import { ImportExportModal } from "./modals/importexportmodal";
import { WarningModal } from "./modals/warningmodal";
import { ServerStatus } from "../interfaces";
export type OlympusUIState = {
mainMenuVisible: boolean;
@ -57,12 +52,19 @@ export type OlympusUIState = {
export function UI() {
const [appState, setAppState] = useState(OlympusState.NOT_INITIALIZED);
const [appSubState, setAppSubState] = useState(NO_SUBSTATE as OlympusSubState);
const [serverStatus, setServerStatus] = useState({} as ServerStatus);
const [connectedOnce, setConnectedOnce] = useState(false);
useEffect(() => {
AppStateChangedEvent.on((state, subState) => {
setAppState(state);
setAppSubState(subState);
});
ServerStatusUpdatedEvent.on((status) => {
// If we connected at least once, record it
if (status.connected) setConnectedOnce(true);
setServerStatus(status);
});
}, []);
useEffect(() => {
@ -76,11 +78,7 @@ export function UI() {
font-sans
`}
>
{appState !== OlympusState.SERVER && (
<>
<Header />
</>
)}
{appState !== OlympusState.SERVER && <Header />}
<div className="flex h-full w-full flex-row-reverse">
{appState === OlympusState.SERVER && <ServerOverlay />}
@ -138,6 +136,45 @@ export function UI() {
</>
)}
</div>
{!serverStatus.connected && appState !== OlympusState.LOGIN && (
<div
className={`
absolute left-0 top-0 z-50 flex h-screen w-screen items-center
justify-center bg-gray-900 bg-opacity-80 text-white backdrop-blur-sm
`}
>
<div className="flex flex-col items-center justify-center gap-4">
<div className="bouncing-ball">
<img
src="images/olympus-500x500.png"
alt="Olympus Logo"
className={`ball-logo`}
/>
</div>
{!connectedOnce && <div>Establishing connection</div>}
{connectedOnce && <div>Connection lost</div>}
{!connectedOnce && <div className="text-gray-400">Trying to connect with the server, please wait...</div>}
{connectedOnce && (
<div className="text-gray-400">
Try reloading this page. However, this usually means that you internet connection is down, or that the server is offline, paused, or loading a
new mission.
</div>
)}
</div>
</div>
)}
{appState === OlympusState.NOT_INITIALIZED && (
<div
className={`
absolute left-0 top-0 z-50 flex h-screen w-screen items-center
justify-center bg-gray-900 text-white
`}
>
Loading...
</div>
)}
</div>
);
}

View File

@ -47,6 +47,7 @@ import {
TRAIL_LENGTH,
colors,
UnitState,
SPOTS_EDIT_ZOOM_TRANSITION,
} from "../constants/constants";
import { DataExtractor } from "../server/dataextractor";
import { Weapon } from "../weapon/weapon";
@ -71,6 +72,7 @@ import { ArrowMarker } from "../map/markers/arrowmarker";
import { Spot } from "../mission/spot";
import { SpotEditMarker } from "../map/markers/spoteditmarker";
import { SpotMarker } from "../map/markers/spotmarker";
import { get } from "http";
const bearingStrings = ["north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west", "north"];
@ -180,7 +182,7 @@ export abstract class Unit extends CustomMarker {
#racetrackAnchorMarkers: CoalitionAreaHandle[] = [new CoalitionAreaHandle(new LatLng(0, 0)), new CoalitionAreaHandle(new LatLng(0, 0))];
#racetrackArrow: ArrowMarker = new ArrowMarker(new LatLng(0, 0));
#inhibitRacetrackDraw: boolean = false;
#spots: { [key: number]: Polyline } = {};
#spotLines: { [key: number]: Polyline } = {};
#spotEditMarkers: { [key: number]: SpotEditMarker } = {};
#spotMarkers: { [key: number]: SpotMarker } = {};
@ -1867,39 +1869,150 @@ export abstract class Unit extends CustomMarker {
}
#drawSpots() {
// Iterate over all spots and draw lines, edit markers, and markers
Object.values(getApp().getMissionManager().getSpots()).forEach((spot: Spot) => {
if (spot.getSourceUnitID() === this.ID) {
const spotBearing = deg2rad(bearing(this.getPosition().lat, this.getPosition().lng, spot.getTargetPosition().lat, spot.getTargetPosition().lng, false));
const spotDistance = this.getPosition().distanceTo(spot.getTargetPosition());
const midPosition = bearingAndDistanceToLatLng(this.getPosition().lat, this.getPosition().lng, spotBearing, spotDistance / 2);
if (this.#spots[spot.getID()] === undefined) {
this.#spots[spot.getID()] = new Polyline([this.getPosition(), spot.getTargetPosition()], {
color: spot.getType() === "laser" ? colors.BLUE_VIOLET : colors.DARK_RED,
dashArray: "1, 8",
});
this.#spots[spot.getID()].addTo(getApp().getMap());
this.#spotEditMarkers[spot.getID()] = new SpotEditMarker(midPosition, `${spot.getCode() ?? ""}`);
this.#spotEditMarkers[spot.getID()].addTo(getApp().getMap());
this.#spotEditMarkers[spot.getID()].setRotationAngle(spotBearing + Math.PI / 2);
this.#spotMarkers[spot.getID()] = new SpotMarker(spot.getTargetPosition());
} else {
if (!getApp().getMap().hasLayer(this.#spots[spot.getID()])) this.#spots[spot.getID()].addTo(getApp().getMap());
if (!getApp().getMap().hasLayer(this.#spotEditMarkers[spot.getID()])) this.#spotEditMarkers[spot.getID()].addTo(getApp().getMap());
if (!getApp().getMap().hasLayer(this.#spotMarkers[spot.getID()])) this.#spotMarkers[spot.getID()].addTo(getApp().getMap());
this.#spots[spot.getID()].setLatLngs([this.getPosition(), spot.getTargetPosition()]);
this.#spotEditMarkers[spot.getID()].setLatLng(midPosition);
this.#spotMarkers[spot.getID()].setLatLng(spot.getTargetPosition());
// Draw the spot line
this.#drawSpotLine(spot, spotBearing);
// Draw the spot edit marker if the map is zoomed in enough
if (getApp().getMap().getZoom() >= SPOTS_EDIT_ZOOM_TRANSITION) {
// Draw the spot edit marker
this.#drawSpotEditMarker(spot, midPosition, spotBearing);
}
this.#spotEditMarkers[spot.getID()].setRotationAngle(spotBearing + Math.PI / 2);
this.#spotEditMarkers[spot.getID()].setTextValue(`${spot.getCode() ?? ""}`);
// Draw the spot marker
this.#drawSpotMarker(spot);
}
});
}
/**
* Draws or updates the spot line.
* @param {Spot} spot - The spot object.
* @param {number} spotBearing - The bearing of the spot.
*/
#drawSpotLine(spot: Spot, spotBearing: number) {
if (this.#spotLines[spot.getID()] === undefined) {
// Create a new polyline for the spot
this.#spotLines[spot.getID()] = new Polyline([this.getPosition(), spot.getTargetPosition()], {
color: spot.getType() === "laser" ? colors.BLUE_VIOLET : colors.DARK_RED,
dashArray: "1, 8",
});
this.#spotLines[spot.getID()].addTo(getApp().getMap());
} else {
// Update the existing polyline
if (!getApp().getMap().hasLayer(this.#spotLines[spot.getID()])) this.#spotLines[spot.getID()].addTo(getApp().getMap());
this.#spotLines[spot.getID()].setLatLngs([this.getPosition(), spot.getTargetPosition()]);
}
/* Iterate all existing lines and remove those associated to a spot that no longer exists */
Object.keys(this.#spotLines).forEach((spotID) => {
if (getApp().getMissionManager().getSpotByID(Number(spotID)) === undefined) {
getApp().getMap().removeLayer(this.#spotLines[spotID]);
delete this.#spotLines[spotID];
}
});
}
/**
* Draws or updates the spot edit marker.
* @param {Spot} spot - The spot object.
* @param {LatLng} midPosition - The midpoint position between the unit and the spot.
* @param {number} spotBearing - The bearing of the spot.
*/
#drawSpotEditMarker(spot: Spot, midPosition: LatLng, spotBearing: number) {
if (this.#spotEditMarkers[spot.getID()] === undefined) {
// Create a new spot edit marker
this.#spotEditMarkers[spot.getID()] = new SpotEditMarker(midPosition, `${spot.getCode() ?? ""}`, 0, spot.getType());
this.#spotEditMarkers[spot.getID()].addTo(getApp().getMap());
this.#spotEditMarkers[spot.getID()].setRotationAngle(spotBearing + Math.PI / 2);
this.#spotEditMarkers[spot.getID()].onDeleteButtonClicked = () => {
getApp().getServerManager().deleteSpot(spot.getID());
};
this.#spotEditMarkers[spot.getID()].onValueUpdated = (value) => {
getApp().getServerManager().setLaserCode(spot.getID(), value);
};
} else {
// Update the existing spot edit marker
if (!getApp().getMap().hasLayer(this.#spotEditMarkers[spot.getID()])) this.#spotEditMarkers[spot.getID()].addTo(getApp().getMap());
this.#spotEditMarkers[spot.getID()].setLatLng(midPosition);
}
this.#spotEditMarkers[spot.getID()].setRotationAngle(spotBearing + Math.PI / 2);
this.#spotEditMarkers[spot.getID()].setTextValue(`${spot.getCode() ?? ""}`);
/* Iterate all existing edit markers and remove those associated to a spot that no longer exists */
Object.keys(this.#spotEditMarkers).forEach((spotID) => {
if (getApp().getMissionManager().getSpotByID(Number(spotID)) === undefined) {
getApp().getMap().removeLayer(this.#spotEditMarkers[spotID]);
delete this.#spotEditMarkers[spotID];
}
});
}
/**
* Draws or updates the spot marker.
* @param {Spot} spot - The spot object.
*/
#drawSpotMarker(spot: Spot) {
if (this.#spotMarkers[spot.getID()] === undefined) {
// Create a new spot marker
this.#spotMarkers[spot.getID()] = new SpotMarker(spot.getTargetPosition());
this.#spotMarkers[spot.getID()].addTo(getApp().getMap());
this.#spotMarkers[spot.getID()].on("dragstart", (event) => {
event.target.options["freeze"] = true;
});
this.#spotMarkers[spot.getID()].on("dragend", (event) => {
getApp().getServerManager().moveSpot(spot.getID(), event.target.getLatLng());
event.target.options["freeze"] = false;
});
} else {
// Update the existing spot marker
if (!getApp().getMap().hasLayer(this.#spotMarkers[spot.getID()])) this.#spotMarkers[spot.getID()].addTo(getApp().getMap());
var frozen = this.#spotMarkers[spot.getID()].options["freeze"];
if (!frozen) {
this.#spotMarkers[spot.getID()].setLatLng(spot.getTargetPosition());
}
}
/* Iterate all existing markers and remove those associated to a spot that no longer exists */
Object.keys(this.#spotMarkers).forEach((spotID) => {
if (getApp().getMissionManager().getSpotByID(Number(spotID)) === undefined) {
getApp().getMap().removeLayer(this.#spotMarkers[spotID]);
delete this.#spotMarkers[spotID];
}
});
}
#clearSpots() {
Object.values(this.#spots).forEach((spot) => getApp().getMap().removeLayer(spot));
// Clear all spot lines, edit markers, and markers
this.#clearSpotLines();
this.#clearSpotEditMarkers();
this.#clearSpotMarkers();
}
/**
* Clears all spot lines from the map.
*/
#clearSpotLines() {
Object.values(this.#spotLines).forEach((spot) => getApp().getMap().removeLayer(spot));
}
/**
* Clears all spot edit markers from the map.
*/
#clearSpotEditMarkers() {
Object.values(this.#spotEditMarkers).forEach((spotEditMarker) => getApp().getMap().removeLayer(spotEditMarker));
}
/**
* Clears all spot markers from the map.
*/
#clearSpotMarkers() {
Object.values(this.#spotMarkers).forEach((spotMarker) => getApp().getMap().removeLayer(spotMarker));
}
@ -2093,6 +2206,11 @@ export abstract class Unit extends CustomMarker {
#onZoom(e: any) {
if (this.checkZoomRedraw()) this.#redrawMarker();
this.#updateMarker();
// Clear the spot edit markers if the map is zoomed out too much
if (getApp().getMap().getZoom() < SPOTS_EDIT_ZOOM_TRANSITION) {
this.#clearSpotEditMarkers();
}
}
}

View File

@ -301,7 +301,7 @@ export class UnitsManager {
this.#requestDetectionUpdate = false;
}
/* Update the detection lines of all the units. This code is handled by the UnitsManager since it must be run both when the detected OR the detecting unit is updated */
/* Update all the lines of all the selected units. This code is handled by the UnitsManager since, for example, it must be run both when the detected OR the detecting unit is updated */
for (let ID in this.#units) {
if (this.#units[ID].getSelected()) this.#units[ID].drawLines();
}

View File

@ -615,6 +615,33 @@ function Olympus.fireInfrared(ID, lat, lng)
end
end
-- Set new laser code
function Olympus.setLaserCode(spotID, code)
local spot = Olympus.spots[spotID]
if spot ~= nil and spot.type == "laser" then
spot.object:setCode(code)
spot.code = code
end
end
-- Move spot to a new location
function Olympus.moveSpot(spotID, lat, lng)
local spot = Olympus.spots[spotID]
if spot ~= nil then
spot.object:setPoint(coord.LLtoLO(lat, lng, 0))
spot.targetPosition = {lat = lat, lng = lng}
end
end
-- Remove the spot
function Olympus.deleteSpot(spotID)
local spot = Olympus.spots[spotID]
if spot ~= nil then
spot.object:destroy()
Olympus.spots[spotID] = nil
end
end
-- Spawns a new unit or group
-- Spawn table contains the following parameters
-- category: (string), either Aircraft, Helicopter, GroundUnit or NavyUnit